home · contact · privacy
Change language to reflect map protection becoming world protection.
[plomrogue2] / rogue_chat.html
1 <!DOCTYPE html>
2 <html><head>
3 <style>
4   pre {
5       display: inline-block;
6   }
7   pre a {
8       color: white;
9   }
10 </style>
11 </head><body>
12 <div>
13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
16 </div>
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
19 <div>
20 keyboard input/control: <span id="keyboard_control"></span>
21 </div>
22 <h3>button controls for mouse players</h3>
23 <table style="float: left">
24   <tr>
25     <td style="text-align: right"><button id="move_upleft">up-left</button></td>
26     <td style="text-align: center"><button id="move_up">up</button></td>
27     <td><button id="move_upright">up-right</button></td>
28   </tr>
29   <tr>
30     <td style="text-align: right;"><button id="move_left">left</button></td>
31     <td stlye="text-align: center;">move</td>
32     <td><button id="move_right">right</button></td>
33   </tr>
34   <tr>
35     <td><button id="move_downleft">down-left</button></td>
36     <td style="text-align: center"><button id="move_down">down</button></td>
37     <td><button id="move_downright">down-right</button></td>
38   </tr>
39 </table>
40 <table>
41   <tr>
42     <td><button id="help">help</button></td>
43   </tr>
44   <tr>
45     <td><button id="switch_to_chat">chat mode</button><br /></td>
46   </tr>
47   <tr>
48     <td><button id="switch_to_study">study mode</button></td>
49     <td><button id="toggle_map_mode">toggle map view</button>
50   </tr>
51   <tr>
52     <td><button id="switch_to_play">play mode</button></td>
53     <td>
54       <button id="take_thing">pick up thing</button>
55       <button id="drop_thing">drop thing</button>
56       <button id="teleport">teleport</button>
57     </td>
58   </tr>
59   <tr>
60     <td><button id="switch_to_edit">map edit mode</button></td>
61     <td>
62       <button id="switch_to_write">change terrain</button>
63       <button id="flatten">flatten surroundings</button>
64       <button id="switch_to_annotate">annotate tile</button>
65       <button id="switch_to_portal">edit portal</button>
66       <button id="switch_to_name_thing">name thing</button>
67       <button id="switch_to_password">enter map edit password</button>
68     </td>
69   </tr>
70   <tr>
71     <td><button id="switch_to_admin_enter">admin mode</button></td>
72     <td>
73       <button id="switch_to_control_pw_type">change protection character password</button>
74       <button id="switch_to_control_tile_type">change protection areas</button>
75       <button id="switch_to_admin_thing_protect">change thing protection</button>
76       <button id="toggle_tile_draw">toggle protection character drawing</button>
77     </td>
78   <tr>
79   </tr>
80 </table>
81 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
82 <ul>
83 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
84 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
85 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
86 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
87 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
88 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
89 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
90 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
91 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
92 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
93 <li>help: <input id="key_help" type="text" value="h" />
94 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
95 <li>teleport: <input id="key_teleport" type="text" value="p" />
96 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
97 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
98 <li><input id="key_switch_to_chat" type="text" value="t" />
99 <li><input id="key_switch_to_play" type="text" value="p" />
100 <li><input id="key_switch_to_study" type="text" value="?" />
101 <li><input id="key_switch_to_edit" type="text" value="E" />
102 <li><input id="key_switch_to_write" type="text" value="m" />
103 <li><input id="key_switch_to_name_thing" type="text" value="N" />
104 <li><input id="key_switch_to_password" type="text" value="P" />
105 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
106 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
107 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
108 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
109 <li><input id="key_switch_to_annotate" type="text" value="M" />
110 <li><input id="key_switch_to_portal" type="text" value="T" />
111 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
112 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
113 </ul>
114 </div>
115 <script>
116 "use strict";
117 let websocket_location = "wss://plomlompom.com/rogue_chat/";
118 //let websocket_location = "ws://localhost:8000/";
119
120 let mode_helps = {
121     'play': {
122         'short': 'play',
123         'long': 'This mode allows you to interact with the map in various ways.'
124     },
125     'study': {
126         'short': 'study',
127         'long': 'This mode allows you to study the map and its tiles in detail.  Move the question mark over a tile, and the right half of the screen will show detailed information on it.  Toggle the map view to show or hide different information layers.'},
128     'edit': {
129         'short': 'world edit',
130         'long': 'This mode allows you to change the game world in various ways.  Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view.  You can edit a tile if you set the map edit password that matches its protection character.  The character "." marks the absence of protection:  Such tiles can always be edited.'
131     },
132     'name_thing': {
133         'short': 'name thing',
134         'long': 'Give name to/change name of thing here.'
135     },
136     'admin_thing_protect': {
137         'short': 'change thing protection',
138         'long': 'Change protection character for thing here.'
139     },
140     'write': {
141         'short': 'change terrain',
142         'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.'
143     },
144     'control_pw_type': {
145         'short': 'change protection character password',
146         'long': 'This mode is the first of two steps to change the password for a protection character.  First enter the protection character for which you want to change the password.'
147     },
148     'control_pw_pw': {
149         'short': 'change protection character password',
150         'long': 'This mode is the second of two steps to change the password for a protection character.  Enter the new password for the protection character you chose.'
151     },
152     'control_tile_type': {
153         'short': 'change tiles protection',
154         'long': 'This mode is the first of two steps to change tile protection areas on the map.  First enter the tile protection character you want to write.'
155     },
156     'control_tile_draw': {
157         'short': 'change tiles protection',
158         'long': 'This mode is the second of two steps to change tile protection areas on the map.  Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
159     },
160     'annotate': {
161         'short': 'annotate tile',
162         'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so).  Hit Return to leave.'
163     },
164     'portal': {
165         'short': 'edit portal',
166         'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so).  Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target.  Hit Return to leave.'
167     },
168     'chat': {
169         'short': 'chat',
170         'long': 'This mode allows you to engage in chit-chat with other users.  Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message.  Lines that start with a "/" are used for commands like:'
171     },
172     'login': {
173         'short': 'login',
174         'long': 'Enter your player name.'
175     },
176     'waiting_for_server': {
177         'short': 'waiting for server response',
178         'long': 'Waiting for a server response.'
179     },
180     'post_login_wait': {
181         'short': 'waiting for server response',
182         'long': 'Waiting for a server response.'
183     },
184     'password': {
185         'short': 'set world edit password',
186         'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world.  Hit return to confirm and leave.'
187     },
188     'admin_enter': {
189         'short': 'become admin',
190         'long': 'This mode allows you to become admin if you know an admin password.'
191     },
192     'admin': {
193         'short': 'admin',
194         'long': 'This mode allows you access to actions limited to administrators.'
195     }
196 }
197
198 let rows_selector = document.getElementById("n_rows");
199 let cols_selector = document.getElementById("n_cols");
200 let key_selectors = document.querySelectorAll('[id^="key_"]');
201
202 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
203     const action = key_switch_selector.id.slice("key_switch_to_".length);
204     key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
205 }
206
207 function restore_selector_value(selector) {
208     let stored_selection = window.localStorage.getItem(selector.id);
209     if (stored_selection) {
210         selector.value = stored_selection;
211     }
212 }
213 restore_selector_value(rows_selector);
214 restore_selector_value(cols_selector);
215 for (let key_selector of key_selectors) {
216     restore_selector_value(key_selector);
217 }
218
219 let terminal = {
220   foreground: 'white',
221   background: 'black',
222   initialize: function() {
223     this.rows = rows_selector.value;
224     this.cols = cols_selector.value;
225     this.pre_el = document.getElementById("terminal");
226     this.pre_el.style.color = this.foreground;
227     this.pre_el.style.backgroundColor = this.background;
228     this.content = [];
229       let line = []
230     for (let y = 0, x = 0; y <= this.rows; x++) {
231         if (x == this.cols) {
232             x = 0;
233             y += 1;
234             this.content.push(line);
235             line = [];
236             if (y == this.rows) {
237                 break;
238             }
239         }
240         line.push(' ');
241     }
242   },
243   blink_screen: function() {
244       this.pre_el.style.color = this.background;
245       this.pre_el.style.backgroundColor = this.foreground;
246       setTimeout(() => {
247           this.pre_el.style.color = this.foreground;
248           this.pre_el.style.backgroundColor = this.background;
249       }, 100);
250   },
251   refresh: function() {
252       function escapeHTML(str) {
253           return str.
254               replace(/&/g, '&amp;').
255               replace(/</g, '&lt;').
256               replace(/>/g, '&gt;').
257               replace(/'/g, '&apos;').
258               replace(/"/g, '&quot;');
259       };
260       let pre_content = '';
261       for (let y = 0; y < this.rows; y++) {
262           let line = this.content[y].join('');
263           let chunks = [];
264           if (y in tui.links) {
265               let start_x = 0;
266               for (let span of tui.links[y]) {
267                   chunks.push(escapeHTML(line.slice(start_x, span[0])));
268                   chunks.push('<a target="_blank" href="');
269                   chunks.push(escapeHTML(span[2]));
270                   chunks.push('">');
271                   chunks.push(escapeHTML(line.slice(span[0], span[1])));
272                   chunks.push('</a>');
273                   start_x = span[1];
274               }
275               chunks.push(escapeHTML(line.slice(start_x)));
276           } else {
277               chunks = [escapeHTML(line)];
278           }
279           for (const chunk of chunks) {
280               pre_content += chunk;
281           }
282           pre_content += '\n';
283       }
284       this.pre_el.innerHTML = pre_content;
285   },
286   write: function(start_y, start_x, msg) {
287       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
288           this.content[start_y][x] = msg[i];
289       }
290   },
291   drawBox: function(start_y, start_x, height, width) {
292     let end_y = start_y + height;
293     let end_x = start_x + width;
294     for (let y = start_y, x = start_x; y < this.rows; x++) {
295       if (x == end_x) {
296         x = start_x;
297         y += 1;
298         if (y == end_y) {
299             break;
300         }
301       };
302       this.content[y][x] = ' ';
303     }
304   },
305 }
306 terminal.initialize();
307
308 let parser = {
309   tokenize: function(str) {
310     let tokens = [];
311     let token = ''
312     let quoted = false;
313     let escaped = false;
314     for (let i = 0; i < str.length; i++) {
315       let c = str[i];
316       if (quoted) {
317         if (escaped) {
318           token += c;
319           escaped = false;
320         } else if (c == '\\') {
321           escaped = true;
322         } else if (c == '"') {
323           quoted = false
324         } else {
325           token += c;
326         }
327       } else if (c == '"') {
328         quoted = true
329       } else if (c === ' ') {
330         if (token.length > 0) {
331           tokens.push(token);
332           token = '';
333         }
334       } else {
335         token += c;
336       }
337     }
338     if (token.length > 0) {
339       tokens.push(token);
340     }
341     return tokens;
342   },
343   parse_yx: function(position_string) {
344     let coordinate_strings = position_string.split(',')
345     let position = [0, 0];
346     position[0] = parseInt(coordinate_strings[0].slice(2));
347     position[1] = parseInt(coordinate_strings[1].slice(2));
348     return position;
349   },
350 }
351
352 class Thing {
353     constructor(yx) {
354         this.position = yx;
355     }
356 }
357
358 let server = {
359     init: function(url) {
360         this.url = url;
361         this.websocket = new WebSocket(this.url);
362         this.websocket.onopen = function(event) {
363             server.connected = true;
364             game.thing_types = {};
365             game.terrains = {};
366             server.send(['TASKS']);
367             server.send(['TERRAINS']);
368             server.send(['THING_TYPES']);
369             tui.log_msg("@ server connected! :)");
370             tui.switch_mode('login');
371         };
372         this.websocket.onclose = function(event) {
373             server.connected = false;
374             tui.switch_mode('waiting_for_server');
375             tui.log_msg("@ server disconnected :(");
376         };
377             this.websocket.onmessage = this.handle_event;
378         },
379     reconnect_to: function(url) {
380         this.websocket.close();
381         this.init(url);
382     },
383     send: function(tokens) {
384         this.websocket.send(unparser.untokenize(tokens));
385     },
386     handle_event: function(event) {
387         let tokens = parser.tokenize(event.data);
388         if (tokens[0] === 'TURN') {
389             game.turn_complete = false;
390             explorer.empty_info_db();
391             game.things = {};
392             game.portals = {};
393             game.turn = parseInt(tokens[1]);
394         } else if (tokens[0] === 'THING') {
395             let t = game.get_thing(tokens[4], true);
396             t.position = parser.parse_yx(tokens[1]);
397             t.type_ = tokens[2];
398             t.protection = tokens[3];
399         } else if (tokens[0] === 'THING_NAME') {
400             let t = game.get_thing(tokens[1], false);
401             if (t) {
402                 t.name_ = tokens[2];
403             };
404         } else if (tokens[0] === 'THING_CHAR') {
405             let t = game.get_thing(tokens[1], false);
406             if (t) {
407                 t.player_char = tokens[2];
408             };
409         } else if (tokens[0] === 'TASKS') {
410             game.tasks = tokens[1].split(',');
411             tui.mode_write.legal = game.tasks.includes('WRITE');
412         } else if (tokens[0] === 'THING_TYPE') {
413             game.thing_types[tokens[1]] = tokens[2]
414         } else if (tokens[0] === 'TERRAIN') {
415             game.terrains[tokens[1]] = tokens[2]
416         } else if (tokens[0] === 'MAP') {
417             game.map_geometry = tokens[1];
418             tui.init_keys();
419             game.map_size = parser.parse_yx(tokens[2]);
420             game.map = tokens[3]
421         } else if (tokens[0] === 'FOV') {
422             game.fov = tokens[1]
423         } else if (tokens[0] === 'MAP_CONTROL') {
424             game.map_control = tokens[1]
425         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
426             game.turn_complete = true;
427             if (tui.mode.name == 'post_login_wait') {
428                 tui.switch_mode('play');
429             } else if (tui.mode.name == 'study') {
430                 explorer.query_info();
431             }
432             tui.full_refresh();
433         } else if (tokens[0] === 'CHAT') {
434              tui.log_msg('# ' + tokens[1], 1);
435         } else if (tokens[0] === 'PLAYER_ID') {
436             game.player_id = parseInt(tokens[1]);
437         } else if (tokens[0] === 'LOGIN_OK') {
438             this.send(['GET_GAMESTATE']);
439             tui.switch_mode('post_login_wait');
440         } else if (tokens[0] === 'ADMIN_OK') {
441             tui.is_admin = true;
442             tui.log_msg('@ you now have admin rights');
443             tui.switch_mode('admin');
444         } else if (tokens[0] === 'PORTAL') {
445             let position = parser.parse_yx(tokens[1]);
446             game.portals[position] = tokens[2];
447         } else if (tokens[0] === 'ANNOTATION_HINT') {
448             let position = parser.parse_yx(tokens[1]);
449             explorer.info_hints = explorer.info_hints.concat([position]);
450         } else if (tokens[0] === 'ANNOTATION') {
451             let position = parser.parse_yx(tokens[1]);
452             explorer.update_info_db(position, tokens[2]);
453             tui.full_refresh();
454         } else if (tokens[0] === 'UNHANDLED_INPUT') {
455             tui.log_msg('? unknown command');
456         } else if (tokens[0] === 'PLAY_ERROR') {
457             tui.log_msg('? ' + tokens[1]);
458             terminal.blink_screen();
459         } else if (tokens[0] === 'ARGUMENT_ERROR') {
460             tui.log_msg('? syntax error: ' + tokens[1]);
461         } else if (tokens[0] === 'GAME_ERROR') {
462             tui.log_msg('? game error: ' + tokens[1]);
463         } else if (tokens[0] === 'PONG') {
464             ;
465         } else {
466             tui.log_msg('? unhandled input: ' + event.data);
467         }
468     }
469 }
470
471 let unparser = {
472     quote: function(str) {
473         let quoted = ['"'];
474         for (let i = 0; i < str.length; i++) {
475             let c = str[i];
476             if (['"', '\\'].includes(c)) {
477                 quoted.push('\\');
478             };
479             quoted.push(c);
480         }
481         quoted.push('"');
482         return quoted.join('');
483     },
484     to_yx: function(yx_coordinate) {
485         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
486     },
487     untokenize: function(tokens) {
488         let quoted_tokens = [];
489         for (let token of tokens) {
490             quoted_tokens.push(this.quote(token));
491         }
492         return quoted_tokens.join(" ");
493     }
494 }
495
496 class Mode {
497     constructor(name, has_input_prompt=false, shows_info=false,
498                 is_intro=false, is_single_char_entry=false) {
499         this.name = name;
500         this.short_desc = mode_helps[name].short;
501         this.available_modes = [];
502         this.has_input_prompt = has_input_prompt;
503         this.shows_info= shows_info;
504         this.is_intro = is_intro;
505         this.help_intro = mode_helps[name].long;
506         this.is_single_char_entry = is_single_char_entry;
507         this.legal = true;
508     }
509     *iter_available_modes() {
510         for (let mode_name of this.available_modes) {
511             let mode = tui['mode_' + mode_name];
512             if (!mode.legal) {
513                 continue;
514             }
515             let key = tui.keys['switch_to_' + mode.name];
516             yield [mode, key]
517         }
518     }
519     list_available_modes() {
520         let msg = ''
521         if (this.available_modes.length > 0) {
522             msg += 'Other modes available from here:\n';
523             for (let [mode, key] of this.iter_available_modes()) {
524                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
525             }
526         }
527         return msg;
528     }
529     mode_switch_on_key(key_event) {
530         for (let [mode, key] of this.iter_available_modes()) {
531             if (key_event.key == key) {
532                 event.preventDefault();
533                 tui.switch_mode(mode.name);
534                 return true;
535             };
536         }
537         return false;
538     }
539 }
540 let tui = {
541   links: {},
542   log: [],
543   input_prompt: '> ',
544   input_lines: [],
545   window_width: terminal.cols / 2,
546   height_turn_line: 1,
547   height_mode_line: 1,
548   height_input: 1,
549   password: 'foo',
550   show_help: false,
551   is_admin: false,
552   tile_draw: false,
553   mode_waiting_for_server: new Mode('waiting_for_server',
554                                      false, false, true),
555   mode_login: new Mode('login', true, false, true),
556   mode_post_login_wait: new Mode('post_login_wait'),
557   mode_chat: new Mode('chat', true),
558   mode_annotate: new Mode('annotate', true, true),
559   mode_play: new Mode('play'),
560   mode_study: new Mode('study', false, true),
561   mode_write: new Mode('write', false, false, false, true),
562   mode_edit: new Mode('edit'),
563   mode_control_pw_type: new Mode('control_pw_type', true),
564   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
565   mode_portal: new Mode('portal', true, true),
566   mode_password: new Mode('password', true),
567   mode_name_thing: new Mode('name_thing', true, true),
568   mode_admin_enter: new Mode('admin_enter', true),
569   mode_admin: new Mode('admin'),
570   mode_control_pw_pw: new Mode('control_pw_pw', true),
571   mode_control_tile_type: new Mode('control_tile_type', true),
572   mode_control_tile_draw: new Mode('control_tile_draw'),
573   init: function() {
574       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
575       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
576       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
577                                          "control_tile_type", "chat",
578                                          "study", "play", "edit"]
579       this.mode_control_tile_draw.available_modes = ["admin_enter"]
580       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
581                                         "password", "chat", "study", "play",
582                                         "admin_enter"]
583       this.mode = this.mode_waiting_for_server;
584       this.inputEl = document.getElementById("input");
585       this.inputEl.focus();
586       this.recalc_input_lines();
587       this.height_header = this.height_turn_line + this.height_mode_line;
588       this.log_msg("@ waiting for server connection ...");
589       this.init_keys();
590   },
591   init_keys: function() {
592     this.keys = {};
593     for (let key_selector of key_selectors) {
594         this.keys[key_selector.id.slice(4)] = key_selector.value;
595     }
596     if (game.map_geometry == 'Square') {
597         this.movement_keys = {
598             [this.keys.square_move_up]: 'UP',
599             [this.keys.square_move_left]: 'LEFT',
600             [this.keys.square_move_down]: 'DOWN',
601             [this.keys.square_move_right]: 'RIGHT'
602         };
603         document.getElementById("move_upright").hidden = true;
604         document.getElementById("move_upleft").hidden = true;
605         document.getElementById("move_downright").hidden = true;
606         document.getElementById("move_downleft").hidden = true;
607         document.getElementById("move_up").hidden = false;
608         document.getElementById("move_down").hidden = false;
609     } else if (game.map_geometry == 'Hex') {
610         document.getElementById("move_upright").hidden = false;
611         document.getElementById("move_upleft").hidden = false;
612         document.getElementById("move_downright").hidden = false;
613         document.getElementById("move_downleft").hidden = false;
614         document.getElementById("move_up").hidden = true;
615         document.getElementById("move_down").hidden = true;
616         this.movement_keys = {
617             [this.keys.hex_move_upleft]: 'UPLEFT',
618             [this.keys.hex_move_upright]: 'UPRIGHT',
619             [this.keys.hex_move_right]: 'RIGHT',
620             [this.keys.hex_move_downright]: 'DOWNRIGHT',
621             [this.keys.hex_move_downleft]: 'DOWNLEFT',
622             [this.keys.hex_move_left]: 'LEFT'
623         };
624     };
625   },
626   switch_mode: function(mode_name) {
627     if (this.mode.name == 'control_tile_draw') {
628         tui.log_msg('@ finished tile protection drawing.')
629     }
630     this.tile_draw = false;
631     if (mode_name == 'admin_enter' && this.is_admin) {
632         mode_name = 'admin';
633     } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
634         let player_position = game.things[game.player_id].position;
635         let thing_id = null;
636         for (let t_id in game.things) {
637             if (t_id == game.player_id) {
638                 continue;
639             }
640             let t = game.things[t_id];
641             if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
642                 thing_id = t_id;
643                 break;
644             }
645         }
646         if (!thing_id) {
647             terminal.blink_screen();
648             this.log_msg('? not standing over thing');
649             return;
650         } else {
651             this.selected_thing_id = thing_id;
652         }
653     };
654     this.mode = this['mode_' + mode_name];
655     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
656         this.map_mode = 'protections';
657     } else if (this.mode.name != "edit") {
658         this.map_mode = 'terrain + things';
659     };
660     if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
661         this.inputEl.focus();
662     }
663     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
664         explorer.position = game.things[game.player_id].position;
665         if (this.mode.shows_info) {
666             explorer.query_info();
667         }
668     }
669     this.inputEl.value = "";
670     this.restore_input_values();
671     for (let el of document.getElementsByTagName("button")) {
672         el.disabled = true;
673     }
674     document.getElementById("help").disabled = false;
675     if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit' || this.mode.name == 'admin') {
676         for (const move_key of document.querySelectorAll('[id^="move_"]')) {
677             move_key.disabled = false;
678         }
679     }
680     if (!this.mode.is_intro && this.mode.name != 'play') {
681         document.getElementById("switch_to_play").disabled = false;
682     }
683     if (!this.mode.is_intro && this.mode.name != 'study') {
684         document.getElementById("switch_to_study").disabled = false;
685     }
686     if (!this.mode.is_intro && this.mode.name != 'chat') {
687         document.getElementById("switch_to_chat").disabled = false;
688     }
689     if (!this.mode.is_intro && this.mode.name != 'edit') {
690         document.getElementById("switch_to_edit").disabled = false;
691     }
692     if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
693         document.getElementById("switch_to_admin_enter").disabled = false;
694     }
695     if (this.mode.name == 'login') {
696         if (this.login_name) {
697             server.send(['LOGIN', this.login_name]);
698         } else {
699             this.log_msg("? need login name");
700         }
701     } else if (this.mode.name == 'play') {
702         if (game.tasks.includes('PICK_UP')) {
703             document.getElementById("take_thing").disabled = false;
704         }
705         if (game.tasks.includes('DROP')) {
706             document.getElementById("drop_thing").disabled = false;
707         }
708         if (game.tasks.includes('MOVE')) {
709         }
710         document.getElementById("teleport").disabled = false;
711     } else if (this.mode.name == 'edit') {
712         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
713             document.getElementById("flatten").disabled = false;
714         }
715         document.getElementById("switch_to_annotate").disabled = false;
716         document.getElementById("switch_to_write").disabled = false;
717         document.getElementById("switch_to_portal").disabled = false;
718         document.getElementById("switch_to_password").disabled = false;
719         document.getElementById("switch_to_name_thing").disabled = false;
720         document.getElementById("toggle_map_mode").disabled = false;
721     } else if (this.mode.name == 'admin') {
722         document.getElementById("switch_to_control_pw_type").disabled = false;
723         document.getElementById("switch_to_control_tile_type").disabled = false;
724         document.getElementById("switch_to_admin_thing_protect").disabled = false;
725     } else if (this.mode.name == 'study') {
726         document.getElementById("toggle_map_mode").disabled = false;
727     } else if (this.mode.is_single_char_entry) {
728         this.show_help = true;
729     } else if (this.mode.name == 'admin_enter') {
730         this.log_msg('@ enter admin password:')
731     } else if (this.mode.name == 'control_pw_type') {
732         this.log_msg('@ enter protection character for which you want to change the password:')
733     } else if (this.mode.name == 'control_tile_type') {
734         this.log_msg('@ enter protection character which you want to draw:')
735     } else if (this.mode.name == 'admin_thing_protect') {
736         this.log_msg('@ enter thing protection character:')
737     } else if (this.mode.name == 'control_pw_pw') {
738         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
739     } else if (this.mode.name == 'control_tile_draw') {
740         document.getElementById("toggle_tile_draw").disabled = false;
741         this.log_msg('@ can draw protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' +  this.keys.switch_to_admin_enter + '].')
742     }
743     this.full_refresh();
744   },
745   offset_links: function(offset, links) {
746       for (let y in links) {
747           let real_y = offset[0] + parseInt(y);
748           if (!this.links[real_y]) {
749               this.links[real_y] = [];
750           }
751           for (let link of links[y]) {
752               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
753               this.links[real_y].push(offset_link);
754           }
755       }
756   },
757   restore_input_values: function() {
758       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
759           let info = explorer.info_db[explorer.position];
760           if (info != "(none)") {
761               this.inputEl.value = info;
762           }
763       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
764           let portal = game.portals[explorer.position]
765           this.inputEl.value = portal;
766       } else if (this.mode.name == 'password') {
767           this.inputEl.value = this.password;
768       } else if (this.mode.name == 'name_thing') {
769           let t = game.get_thing(this.selected_thing_id);
770           if (t && t.name_) {
771               this.inputEl.value = t.name_;
772           }
773       } else if (this.mode.name == 'admin_thing_protect') {
774           let t = game.get_thing(this.selected_thing_id);
775           if (t && t.protection) {
776               this.inputEl.value = t.protection;
777           }
778       }
779   },
780   recalc_input_lines: function() {
781       if (this.mode.has_input_prompt) {
782           let _ = null;
783           [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
784       } else {
785           this.input_lines = [];
786       }
787       this.height_input = this.input_lines.length;
788   },
789   msg_into_lines_of_width: function(msg, width) {
790       function push_inner_link(y, end_x) {
791           if (!inner_links[y]) {
792               inner_links[y] = [];
793           };
794           inner_links[y].push([url_start_x, end_x, url]);
795       };
796       const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
797       let link_data = {};
798       let url_ends = [];
799       for (const match of matches) {
800           const url = match[0];
801           const url_start = match.index;
802           const url_end = match.index + match[0].length;
803           link_data[url_start] = url;
804           url_ends.push(url_end);
805       }
806       let url_start_x = 0;
807       let url = '';
808       let inner_links = {};
809       let in_link = false;
810       let chunk = "";
811       let lines = [];
812       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
813           if (x >= width || msg[i] == "\n") {
814               if (in_link) {
815                   push_inner_link(y, chunk.length);
816                   url_start_x = 0;
817               };
818               lines.push(chunk);
819               chunk = "";
820               x = 0;
821               if (msg[i] == "\n") {
822                   x -= 1;
823               };
824               y += 1;
825           };
826           if (msg[i] != "\n") {
827               chunk += msg[i];
828           };
829           if (i in link_data) {
830               url_start_x = x;
831               url = link_data[i];
832               in_link = true;
833           } else if (url_ends.includes(i)) {
834               push_inner_link(y, x);
835               in_link = false;
836           }
837       }
838       lines.push(chunk);
839       if (in_link) {
840           push_inner_link(lines.length - 1, chunk.length);
841       }
842       return [lines, inner_links];
843   },
844   log_msg: function(msg) {
845       this.log.push(msg);
846       while (this.log.length > 100) {
847         this.log.shift();
848       };
849       this.full_refresh();
850   },
851   draw_map: function() {
852     let map_lines_split = [];
853     let line = [];
854     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
855         if (j == game.map_size[1]) {
856             map_lines_split.push(line);
857             line = [];
858             j = 0;
859         };
860         if (this.map_mode == 'protections') {
861             line.push(game.map_control[i] + ' ');
862         } else {
863             line.push(game.map[i] + ' ');
864         }
865     };
866     map_lines_split.push(line);
867     if (this.map_mode == 'terrain + annotations') {
868         for (const coordinate of explorer.info_hints) {
869             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
870         }
871     } else if (this.map_mode == 'terrain + things') {
872         for (const p in game.portals) {
873             let coordinate = p.split(',')
874             let original = map_lines_split[coordinate[0]][coordinate[1]];
875             map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
876         }
877         let used_positions = [];
878         for (const thing_id in game.things) {
879             let t = game.things[thing_id];
880             let symbol = game.thing_types[t.type_];
881             let meta_char = ' ';
882             if (t.player_char) {
883                 meta_char = t.player_char;
884             }
885             if (used_positions.includes(t.position.toString())) {
886                 meta_char = '+';
887             };
888             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
889             used_positions.push(t.position.toString());
890         };
891     }
892     let player = game.things[game.player_id];
893     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
894         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
895     } else if (tui.map_mode != 'terrain + things') {
896         map_lines_split[player.position[0]][player.position[1]] = '??';
897     }
898     let map_lines = []
899     if (game.map_geometry == 'Square') {
900         for (let line_split of map_lines_split) {
901             map_lines.push(line_split.join(''));
902         };
903     } else if (game.map_geometry == 'Hex') {
904         let indent = 0
905         for (let line_split of map_lines_split) {
906             map_lines.push(' '.repeat(indent) + line_split.join(''));
907             if (indent == 0) {
908                 indent = 1;
909             } else {
910                 indent = 0;
911             };
912         };
913     }
914     let window_center = [terminal.rows / 2, this.window_width / 2];
915     let center_position = [player.position[0], player.position[1]];
916     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
917         center_position = [explorer.position[0], explorer.position[1]];
918     }
919     center_position[1] = center_position[1] * 2;
920     let offset = [center_position[0] - window_center[0],
921                   center_position[1] - window_center[1]]
922     if (game.map_geometry == 'Hex' && offset[0] % 2) {
923         offset[1] += 1;
924     };
925     let term_y = Math.max(0, -offset[0]);
926     let term_x = Math.max(0, -offset[1]);
927     let map_y = Math.max(0, offset[0]);
928     let map_x = Math.max(0, offset[1]);
929     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
930         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
931         terminal.write(term_y, term_x, to_draw);
932     }
933   },
934   draw_mode_line: function() {
935       let help = 'hit [' + this.keys.help + '] for help';
936       if (this.mode.has_input_prompt) {
937           help = 'enter /help for help';
938       }
939       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
940   },
941   draw_turn_line: function(n) {
942     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
943   },
944   draw_history: function() {
945       let log_display_lines = [];
946       let log_links = {};
947       let y_offset_in_log = 0;
948       for (let line of this.log) {
949           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
950                                                                     this.window_width)
951           log_display_lines = log_display_lines.concat(new_lines);
952           for (const y in link_data) {
953               const rel_y = y_offset_in_log + parseInt(y);
954               log_links[rel_y] = [];
955               for (let link of link_data[y]) {
956                   log_links[rel_y].push(link);
957               }
958           }
959           y_offset_in_log += new_lines.length;
960       };
961       let i = log_display_lines.length - 1;
962       for (let y = terminal.rows - 1 - this.height_input;
963            y >= this.height_header && i >= 0;
964            y--, i--) {
965           terminal.write(y, this.window_width, log_display_lines[i]);
966       }
967       for (const key of Object.keys(log_links)) {
968           if (parseInt(key) <= i) {
969               delete log_links[key];
970           }
971       }
972       let offset = [terminal.rows - this.height_input - log_display_lines.length,
973                     this.window_width];
974       this.offset_links(offset, log_links);
975   },
976   draw_info: function() {
977       let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
978                                                             this.window_width);
979       let offset = [this.height_header, this.window_width];
980       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
981         terminal.write(y, offset[1], lines[i]);
982       }
983       this.offset_links(offset, link_data);
984   },
985   draw_input: function() {
986     if (this.mode.has_input_prompt) {
987         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
988             terminal.write(y, this.window_width, this.input_lines[i]);
989         }
990     }
991   },
992   draw_help: function() {
993       let movement_keys_desc = '';
994       if (!this.mode.is_intro) {
995           movement_keys_desc = Object.keys(this.movement_keys).join(',');
996       }
997       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
998       if (this.mode.name == 'play') {
999           content += "Available actions:\n";
1000           if (game.tasks.includes('MOVE')) {
1001               content += "[" + movement_keys_desc + "] – move player\n";
1002           }
1003           if (game.tasks.includes('PICK_UP')) {
1004               content += "[" + this.keys.take_thing + "] – pick up thing\n";
1005           }
1006           if (game.tasks.includes('DROP')) {
1007               content += "[" + this.keys.drop_thing + "] – drop thing\n";
1008           }
1009           content += "[" + tui.keys.teleport + "] – teleport\n";
1010           content += '\n';
1011       } else if (this.mode.name == 'study') {
1012           content += "Available actions:\n";
1013           content += '[' + movement_keys_desc + '] – move question mark\n';
1014           content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1015           content += '\n';
1016       } else if (this.mode.name == 'edit') {
1017           content += "Available actions:\n";
1018           if (game.tasks.includes('MOVE')) {
1019               content += "[" + movement_keys_desc + "] – move player\n";
1020           }
1021           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1022               content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
1023           }
1024           content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1025           content += '\n';
1026       } else if (this.mode.name == 'control_tile_draw') {
1027           content += "Available actions:\n";
1028           content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
1029           content += '\n';
1030       } else if (this.mode.name == 'chat') {
1031           content += '/nick NAME – re-name yourself to NAME\n';
1032           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1033           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1034           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
1035           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1036       } else if (this.mode.name == 'admin') {
1037           content += "Available actions:\n";
1038           if (game.tasks.includes('MOVE')) {
1039               content += "[" + movement_keys_desc + "] – move player\n";
1040           }
1041           content += '\n';
1042       }
1043       content += this.mode.list_available_modes();
1044       let start_x = 0;
1045       if (!this.mode.has_input_prompt) {
1046           start_x = this.window_width
1047       }
1048       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1049       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1050       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1051           terminal.write(y, start_x, lines[i]);
1052       }
1053   },
1054   toggle_tile_draw: function() {
1055       if (tui.tile_draw) {
1056           tui.tile_draw = false;
1057       } else {
1058           tui.tile_draw = true;
1059       }
1060   },
1061   toggle_map_mode: function() {
1062       if (tui.map_mode == 'terrain only') {
1063           tui.map_mode = 'terrain + annotations';
1064       } else if (tui.map_mode == 'terrain + annotations') {
1065           tui.map_mode = 'terrain + things';
1066       } else if (tui.map_mode == 'terrain + things') {
1067           tui.map_mode = 'protections';
1068       } else if (tui.map_mode == 'protections') {
1069           tui.map_mode = 'terrain only';
1070       }
1071   },
1072   full_refresh: function() {
1073     this.links = {};
1074     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1075     this.recalc_input_lines();
1076     if (this.mode.is_intro) {
1077         this.draw_history();
1078         this.draw_input();
1079     } else {
1080         if (game.turn_complete) {
1081             this.draw_map();
1082             this.draw_turn_line();
1083         }
1084         this.draw_mode_line();
1085         if (this.mode.shows_info) {
1086           this.draw_info();
1087         } else {
1088           this.draw_history();
1089         }
1090         this.draw_input();
1091     }
1092     if (this.show_help) {
1093         this.draw_help();
1094     }
1095     terminal.refresh();
1096   }
1097 }
1098
1099 let game = {
1100     init: function() {
1101         this.things = {};
1102         this.turn = -1;
1103         this.map = "";
1104         this.map_control = "";
1105         this.map_size = [0,0];
1106         this.player_id = -1;
1107         this.portals = {};
1108         this.tasks = {};
1109     },
1110     get_thing: function(id_, create_if_not_found=false) {
1111         if (id_ in game.things) {
1112             return game.things[id_];
1113         } else if (create_if_not_found) {
1114             let t = new Thing([0,0]);
1115             game.things[id_] = t;
1116             return t;
1117         };
1118     },
1119     move: function(start_position, direction) {
1120         let target = [start_position[0], start_position[1]];
1121         if (direction == 'LEFT') {
1122             target[1] -= 1;
1123         } else if (direction == 'RIGHT') {
1124             target[1] += 1;
1125         } else if (game.map_geometry == 'Square') {
1126             if (direction == 'UP') {
1127                 target[0] -= 1;
1128             } else if (direction == 'DOWN') {
1129                 target[0] += 1;
1130             };
1131         } else if (game.map_geometry == 'Hex') {
1132             let start_indented = start_position[0] % 2;
1133             if (direction == 'UPLEFT') {
1134                 target[0] -= 1;
1135                 if (!start_indented) {
1136                     target[1] -= 1;
1137                 }
1138             } else if (direction == 'UPRIGHT') {
1139                 target[0] -= 1;
1140                 if (start_indented) {
1141                     target[1] += 1;
1142                 }
1143             } else if (direction == 'DOWNLEFT') {
1144                 target[0] += 1;
1145                 if (!start_indented) {
1146                     target[1] -= 1;
1147                 }
1148             } else if (direction == 'DOWNRIGHT') {
1149                 target[0] += 1;
1150                 if (start_indented) {
1151                     target[1] += 1;
1152                 }
1153             };
1154         };
1155         if (target[0] < 0 || target[1] < 0 ||
1156             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1157             return null;
1158         };
1159         return target;
1160     },
1161     teleport: function() {
1162         let player = this.get_thing(game.player_id);
1163         if (player.position in this.portals) {
1164             server.reconnect_to(this.portals[player.position]);
1165         } else {
1166             terminal.blink_screen();
1167             tui.log_msg('? not standing on portal')
1168         }
1169     }
1170 }
1171
1172 game.init();
1173 tui.init();
1174 tui.full_refresh();
1175 server.init(websocket_location);
1176
1177 let explorer = {
1178     position: [0,0],
1179     info_db: {},
1180     info_hints: [],
1181     move: function(direction) {
1182         let target = game.move(this.position, direction);
1183         if (target) {
1184             this.position = target
1185             if (tui.mode.shows_info) {
1186                 this.query_info();
1187             } else if (tui.tile_draw) {
1188                 this.send_tile_control_command();
1189             }
1190         } else {
1191             terminal.blink_screen();
1192         };
1193     },
1194     update_info_db: function(yx, str) {
1195         this.info_db[yx] = str;
1196         if (tui.mode.name == 'study') {
1197             tui.full_refresh();
1198         }
1199     },
1200     empty_info_db: function() {
1201         this.info_db = {};
1202         this.info_hints = [];
1203         if (tui.mode.name == 'study') {
1204             tui.full_refresh();
1205         }
1206     },
1207     query_info: function() {
1208         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1209     },
1210     get_info: function() {
1211         let info = "MAP VIEW: " + tui.map_mode + "\n";
1212         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1213         if (game.fov[position_i] != '.') {
1214             return info + 'outside field of view';
1215         };
1216         let terrain_char = game.map[position_i]
1217         let terrain_desc = '?'
1218         if (game.terrains[terrain_char]) {
1219             terrain_desc = game.terrains[terrain_char];
1220         };
1221         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1222         let protection = game.map_control[position_i];
1223         if (protection == '.') {
1224             protection = 'unprotected';
1225         };
1226         info += 'PROTECTION: ' + protection + '\n';
1227         for (let t_id in game.things) {
1228              let t = game.things[t_id];
1229              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1230                  let symbol = game.thing_types[t.type_];
1231                  let protection = t.protection;
1232                  if (protection == '.') {
1233                      protection = 'unprotected';
1234                  }
1235                  info += "THING: " + t.type_ + " / protection: " + protection + " / " + symbol;
1236                  if (t.player_char) {
1237                      info += t.player_char;
1238                  };
1239                  if (t.name_) {
1240                      info += " (" + t.name_ + ")";
1241                  }
1242                  info += "\n";
1243              }
1244         }
1245         if (this.position in game.portals) {
1246             info += "PORTAL: " + game.portals[this.position] + "\n";
1247         }
1248         if (this.position in this.info_db) {
1249             info += "ANNOTATIONS: " + this.info_db[this.position];
1250         } else {
1251             info += 'waiting …';
1252         }
1253         return info;
1254     },
1255     annotate: function(msg) {
1256         if (msg.length == 0) {
1257             msg = " ";  // triggers annotation deletion
1258         }
1259         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1260     },
1261     set_portal: function(msg) {
1262         if (msg.length == 0) {
1263             msg = " ";  // triggers portal deletion
1264         }
1265         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1266     },
1267     send_tile_control_command: function() {
1268         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1269     }
1270 }
1271
1272 tui.inputEl.addEventListener('input', (event) => {
1273     if (tui.mode.has_input_prompt) {
1274         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1275         if (tui.inputEl.value.length > max_length) {
1276             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1277         };
1278     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1279         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1280         tui.switch_mode('edit');
1281     }
1282     tui.full_refresh();
1283 }, false);
1284 document.onclick = function() {
1285     tui.show_help = false;
1286 };
1287 tui.inputEl.addEventListener('keydown', (event) => {
1288     tui.show_help = false;
1289     if (event.key == 'Enter') {
1290         event.preventDefault();
1291     }
1292     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1293         tui.show_help = true;
1294         tui.inputEl.value = "";
1295         tui.restore_input_values();
1296     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1297                && !tui.mode.is_single_char_entry) {
1298         tui.show_help = true;
1299     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1300         tui.login_name = tui.inputEl.value;
1301         server.send(['LOGIN', tui.inputEl.value]);
1302         tui.inputEl.value = "";
1303     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1304         if (tui.inputEl.value.length == 0) {
1305             tui.log_msg('@ aborted');
1306         } else {
1307             server.send(['SET_MAP_CONTROL_PASSWORD',
1308                         tui.tile_control_char, tui.inputEl.value]);
1309             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1310         }
1311         tui.switch_mode('admin');
1312     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1313         explorer.set_portal(tui.inputEl.value);
1314         tui.switch_mode('edit');
1315     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1316         if (tui.inputEl.value.length == 0) {
1317             tui.inputEl.value = " ";
1318         }
1319         server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1320                      tui.password]);
1321         tui.switch_mode('edit');
1322     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1323         explorer.annotate(tui.inputEl.value);
1324         tui.switch_mode('edit');
1325     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1326         if (tui.inputEl.value.length == 0) {
1327             tui.inputEl.value = " ";
1328         }
1329         tui.password = tui.inputEl.value
1330         tui.switch_mode('edit');
1331     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1332         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1333         tui.switch_mode('play');
1334     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1335         if (tui.inputEl.value.length != 1) {
1336             tui.log_msg('@ entered non-single-char, therefore aborted');
1337             tui.switch_mode('admin');
1338         } else {
1339             tui.tile_control_char = tui.inputEl.value[0];
1340             tui.switch_mode('control_pw_pw');
1341         }
1342     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1343         if (tui.inputEl.value.length != 1) {
1344             tui.log_msg('@ entered non-single-char, therefore aborted');
1345             tui.switch_mode('admin');
1346         } else {
1347             tui.tile_control_char = tui.inputEl.value[0];
1348             tui.switch_mode('control_tile_draw');
1349         }
1350     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1351         if (tui.inputEl.value.length != 1) {
1352             tui.log_msg('@ entered non-single-char, therefore aborted');
1353         } else {
1354             server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1355             tui.log_msg('@ sent new protection character for thing');
1356         }
1357         tui.switch_mode('admin');
1358     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1359         let tokens = parser.tokenize(tui.inputEl.value);
1360         if (tokens.length > 0 && tokens[0].length > 0) {
1361             if (tui.inputEl.value[0][0] == '/') {
1362                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1363                     tui.switch_mode('play');
1364                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1365                     tui.switch_mode('study');
1366                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1367                     tui.switch_mode('edit');
1368                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1369                     tui.switch_mode('admin_enter');
1370                 } else if (tokens[0].slice(1) == 'nick') {
1371                     if (tokens.length > 1) {
1372                         server.send(['NICK', tokens[1]]);
1373                     } else {
1374                         tui.log_msg('? need new name');
1375                     }
1376                 } else {
1377                     tui.log_msg('? unknown command');
1378                 }
1379             } else {
1380                     server.send(['ALL', tui.inputEl.value]);
1381             }
1382         } else if (tui.inputEl.valuelength > 0) {
1383                 server.send(['ALL', tui.inputEl.value]);
1384         }
1385         tui.inputEl.value = "";
1386     } else if (tui.mode.name == 'play') {
1387           if (tui.mode.mode_switch_on_key(event)) {
1388               null;
1389           } else if (event.key === tui.keys.take_thing
1390                      && game.tasks.includes('PICK_UP')) {
1391               server.send(["TASK:PICK_UP"]);
1392           } else if (event.key === tui.keys.drop_thing
1393                      && game.tasks.includes('DROP')) {
1394               server.send(["TASK:DROP"]);
1395           } else if (event.key in tui.movement_keys
1396                      && game.tasks.includes('MOVE')) {
1397               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1398           } else if (event.key === tui.keys.teleport) {
1399               game.teleport();
1400           };
1401     } else if (tui.mode.name == 'study') {
1402         if (tui.mode.mode_switch_on_key(event)) {
1403               null;
1404         } else if (event.key in tui.movement_keys) {
1405             explorer.move(tui.movement_keys[event.key]);
1406         } else if (event.key == tui.keys.toggle_map_mode) {
1407             tui.toggle_map_mode();
1408         };
1409     } else if (tui.mode.name == 'control_tile_draw') {
1410         if (tui.mode.mode_switch_on_key(event)) {
1411             null;
1412         } else if (event.key in tui.movement_keys) {
1413             explorer.move(tui.movement_keys[event.key]);
1414         } else if (event.key === tui.keys.toggle_tile_draw) {
1415             tui.toggle_tile_draw();
1416         };
1417     } else if (tui.mode.name == 'admin') {
1418         if (tui.mode.mode_switch_on_key(event)) {
1419               null;
1420         } else if (event.key in tui.movement_keys
1421                    && game.tasks.includes('MOVE')) {
1422             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1423         };
1424     } else if (tui.mode.name == 'edit') {
1425         if (tui.mode.mode_switch_on_key(event)) {
1426               null;
1427         } else if (event.key in tui.movement_keys
1428                    && game.tasks.includes('MOVE')) {
1429             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1430         } else if (event.key === tui.keys.flatten
1431                    && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1432             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1433         } else if (event.key == tui.keys.toggle_map_mode) {
1434             tui.toggle_map_mode();
1435         }
1436     }
1437     tui.full_refresh();
1438 }, false);
1439
1440 rows_selector.addEventListener('input', function() {
1441     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1442         return;
1443     }
1444     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1445     terminal.initialize();
1446     tui.full_refresh();
1447 }, false);
1448 cols_selector.addEventListener('input', function() {
1449     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1450         return;
1451     }
1452     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1453     terminal.initialize();
1454     tui.window_width = terminal.cols / 2,
1455     tui.full_refresh();
1456 }, false);
1457 for (let key_selector of key_selectors) {
1458     key_selector.addEventListener('input', function() {
1459         window.localStorage.setItem(key_selector.id, key_selector.value);
1460         tui.init_keys();
1461     }, false);
1462 }
1463 window.setInterval(function() {
1464     if (server.connected) {
1465         server.send(['PING']);
1466     } else {
1467         server.reconnect_to(server.url);
1468         tui.log_msg('@ attempting reconnect …')
1469     }
1470 }, 5000);
1471 window.setInterval(function() {
1472     let val = "?";
1473     if (document.activeElement == tui.inputEl) {
1474         val = "on (click outside terminal to change)";
1475     } else {
1476         val = "off (click into terminal to change)";
1477     };
1478     document.getElementById("keyboard_control").textContent = val;
1479 }, 100);
1480 document.getElementById("terminal").onclick = function() {
1481     tui.inputEl.focus();
1482 };
1483 document.getElementById("help").onclick = function() {
1484     tui.show_help = true;
1485     tui.full_refresh();
1486 };
1487 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1488     const mode = switchEl.id.slice("switch_to_".length);
1489     switchEl.onclick = function() {
1490         tui.switch_mode(mode);
1491         tui.full_refresh();
1492     }
1493 };
1494 document.getElementById("toggle_tile_draw").onclick = function() {
1495     tui.toggle_tile_draw();
1496 }
1497 document.getElementById("toggle_map_mode").onclick = function() {
1498     tui.toggle_map_mode();
1499     tui.full_refresh();
1500 };
1501 document.getElementById("take_thing").onclick = function() {
1502         server.send(['TASK:PICK_UP']);
1503 };
1504 document.getElementById("drop_thing").onclick = function() {
1505         server.send(['TASK:DROP']);
1506 };
1507 document.getElementById("flatten").onclick = function() {
1508     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1509 };
1510 document.getElementById("teleport").onclick = function() {
1511     game.teleport();
1512 };
1513 document.getElementById("move_upleft").onclick = function() {
1514     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1515         server.send(['TASK:MOVE', 'UPLEFT']);
1516     } else {
1517         explorer.move('UPLEFT');
1518     };
1519 };
1520 document.getElementById("move_left").onclick = function() {
1521     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1522         server.send(['TASK:MOVE', 'LEFT']);
1523     } else {
1524         explorer.move('LEFT');
1525     };
1526 };
1527 document.getElementById("move_downleft").onclick = function() {
1528     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1529         server.send(['TASK:MOVE', 'DOWNLEFT']);
1530     } else {
1531         explorer.move('DOWNLEFT');
1532     };
1533 };
1534 document.getElementById("move_down").onclick = function() {
1535     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1536         server.send(['TASK:MOVE', 'DOWN']);
1537     } else {
1538         explorer.move('DOWN');
1539     };
1540 };
1541 document.getElementById("move_up").onclick = function() {
1542     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1543         server.send(['TASK:MOVE', 'UP']);
1544     } else {
1545         explorer.move('UP');
1546     };
1547 };
1548 document.getElementById("move_upright").onclick = function() {
1549     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1550         server.send(['TASK:MOVE', 'UPRIGHT']);
1551     } else {
1552         explorer.move('UPRIGHT');
1553     };
1554 };
1555 document.getElementById("move_right").onclick = function() {
1556     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1557         server.send(['TASK:MOVE', 'RIGHT']);
1558     } else {
1559         explorer.move('RIGHT');
1560     };
1561 };
1562 document.getElementById("move_downright").onclick = function() {
1563     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1564         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1565     } else {
1566         explorer.move('DOWNRIGHT');
1567     };
1568 };
1569 </script>
1570 </body></html>