home · contact · privacy
Refactor parser code.
[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 <div style="position: relative; display: inline-block;">
18 <pre id="terminal"></pre>
19 <textarea id="input" style="position: absolute; left: 0; height: 100%; width: 100%; opacity: 0; z-index: -1;"></textarea>
20 </div>
21 <h3>button controls for hard-to-remember keybindings</h3>
22 <table id="move_table" style="float: left">
23   <tr>
24     <td style="text-align: right"><button id="hex_move_upleft"></button></td>
25     <td style="text-align: center"><button id="square_move_up"></button></td>
26     <td><button id="hex_move_upright"></button></td>
27   </tr>
28   <tr>
29     <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
30     <td stlye="text-align: center;">move</td>
31     <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
32   </tr>
33   <tr>
34     <td><button id="hex_move_downleft"></button></td>
35     <td style="text-align: center"><button id="square_move_down"></button></td>
36     <td><button id="hex_move_downright"></button></td>
37   </tr>
38 </table>
39 <table>
40   <tr>
41     <td><button id="help"></button></td>
42   </tr>
43   <tr>
44     <td><button id="switch_to_chat"></button><br /></td>
45   </tr>
46   <tr>
47     <td><button id="switch_to_study"></button></td>
48     <td><button id="toggle_map_mode"></button>
49   </tr>
50   <tr>
51     <td><button id="switch_to_play"></button></td>
52     <td>
53       <button id="switch_to_take_thing"></button>
54       <button id="switch_to_drop_thing"></button>
55       <button id="door"></button>
56       <button id="consume"></button>
57       <button id="dance"></button>
58       <button id="switch_to_command_thing"></button>
59       <button id="teleport"></button>
60       <button id="wear"></button>
61       <button id="spin"></button>
62     </td>
63   </tr>
64   <tr>
65     <td><button id="switch_to_edit"></button></td>
66     <td>
67       <button id="switch_to_write"></button>
68       <button id="flatten"></button>
69       <button id="install"></button>
70       <button id="switch_to_annotate"></button>
71       <button id="switch_to_portal"></button>
72       <button id="switch_to_name_thing"></button>
73       <button id="switch_to_password"></button>
74       <button id="switch_to_enter_face"></button>
75       <button id="switch_to_enter_design"></button>
76     </td>
77   </tr>
78   <tr>
79     <td><button id="switch_to_admin_enter"></button></td>
80     <td>
81       <button id="switch_to_control_pw_type"></button>
82       <button id="switch_to_control_tile_type"></button>
83       <button id="switch_to_admin_thing_protect"></button>
84       <button id="toggle_tile_draw"></button>
85     </td>
86   <tr>
87   </tr>
88 </table>
89 <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 />
90 <ul>
91 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
92 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
93 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
94 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
95 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
96 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
97 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
98 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
99 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
100 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
101 <li>help: <input id="key_help" type="text" value="h" />
102 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
103 <li>teleport: <input id="key_teleport" type="text" value="p" />
104 <li>spin: <input id="key_spin" type="text" value="S" />
105 <li>dance: <input id="key_dance" type="text" value="T" />
106 <li>open/close: <input id="key_door" type="text" value="D" />
107 <li>consume: <input id="key_consume" type="text" value="C" />
108 <li>install: <input id="key_install" type="text" value="I" />
109 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
110 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
111 <li><input id="key_switch_to_enter_face" type="text" value="f" />
112 <li><input id="key_switch_to_enter_design" type="text" value="D" />
113 <li><input id="key_switch_to_take_thing" type="text" value="z" />
114 <li><input id="key_switch_to_chat" type="text" value="t" />
115 <li><input id="key_switch_to_play" type="text" value="p" />
116 <li><input id="key_switch_to_study" type="text" value="?" />
117 <li><input id="key_switch_to_edit" type="text" value="E" />
118 <li><input id="key_switch_to_write" type="text" value="m" />
119 <li><input id="key_switch_to_name_thing" type="text" value="N" />
120 <li><input id="key_switch_to_command_thing" type="text" value="O" />
121 <li><input id="key_switch_to_password" type="text" value="P" />
122 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
123 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
124 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
125 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
126 <li><input id="key_switch_to_annotate" type="text" value="M" />
127 <li><input id="key_switch_to_portal" type="text" value="T" />
128 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
129 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
130 </ul>
131 </div>
132 <script>
133 "use strict";
134 let websocket_location = "wss://plomlompom.com/rogue_chat/";
135 //let websocket_location = "ws://localhost:8000/";
136
137 let mode_helps = {
138     'play': {
139         'short': 'play',
140         'intro': '',
141         'long': 'This mode allows you to interact with the map in various ways.'
142     },
143     'study': {
144         'short': 'study',
145         'intro': '',
146         '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.'},
147     'edit': {
148         'short': 'world edit',
149         'intro': '',
150         '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 world edit password that matches its protection character.  The character "." marks the absence of protection:  Such tiles can always be edited.'
151     },
152     'name_thing': {
153         'short': 'name thing',
154         'intro': '',
155         'long': 'Give name to/change name of carried thing.'
156     },
157     'command_thing': {
158         'short': 'command',
159         'intro': '',
160         'long': 'Enter a command to the thing you carry.  Enter nothing to return to play mode.'
161     },
162     'take_thing': {
163         'short': 'take',
164         'intro': 'Pick up a thing in reach by entering its index number.  Enter nothing to abort.',
165         'long': 'You see a list of things which you could pick up.  Enter the target thing\'s index, or, to leave, nothing.'
166     },
167     'drop_thing': {
168         'short': 'drop',
169         'intro': 'Enter number of direction to which you want to drop thing.',
170         'long': 'Drop currently carried thing by entering the target direction index.  Enter nothing to return to play mode..'
171     },
172     'admin_thing_protect': {
173         'short': 'change thing protection',
174         'intro': '@ enter thing protection character:',
175         'long': 'Change protection character for carried thing.'
176     },
177     'enter_face': {
178         'short': 'edit face',
179         'intro': '@ enter face line:',
180         'long': 'Draw your face as ASCII art.  The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom.  Eat cookies to extend the ASCII characters available for drawing.'
181     },
182     'enter_design': {
183         'short': 'edit design',
184         'intro': '@ enter design:',
185         'long': 'Enter design for carried thing as ASCII art.'
186     },
187     'write': {
188         'short': 'edit tile',
189         'intro': '',
190         '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.'
191     },
192     'control_pw_type': {
193         'short': 'change protection character password',
194         'intro': '@ enter protection character for which you want to change the password:',
195         '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.'
196     },
197     'control_pw_pw': {
198         'short': 'change protection character password',
199         'intro': '',
200         '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.'
201     },
202     'control_tile_type': {
203         'short': 'change tiles protection',
204         'intro': '@ enter protection character which you want to draw:',
205         '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.'
206     },
207     'control_tile_draw': {
208         'short': 'change tiles protection',
209         'intro': '',
210         '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.'
211     },
212     'annotate': {
213         'short': 'annotate tile',
214         'intro': '',
215         '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.'
216     },
217     'portal': {
218         'short': 'edit portal',
219         'intro': '',
220         '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.'
221     },
222     'chat': {
223         'short': 'chat',
224         'intro': '',
225         '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:\n\n/nick NAME – re-name yourself to NAME'
226     },
227     'login': {
228         'short': 'login',
229         'intro': '',
230         'long': 'Enter your player name.'
231     },
232     'waiting_for_server': {
233         'short': 'waiting for server response',
234         'intro': '@ waiting for server …',
235         'long': 'Waiting for a server response.'
236     },
237     'post_login_wait': {
238         'short': 'waiting for server response',
239         'intro': '',
240         'long': 'Waiting for a server response.'
241     },
242     'password': {
243         'short': 'set world edit password',
244         'intro': '',
245         '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.'
246     },
247     'admin_enter': {
248         'short': 'become admin',
249         'intro': '@ enter admin password:',
250         'long': 'This mode allows you to become admin if you know an admin password.'
251     },
252     'admin': {
253         'short': 'admin',
254         'intro': '',
255         'long': 'This mode allows you access to actions limited to administrators.'
256     }
257 }
258 let key_descriptions = {
259     'help': 'help',
260     'flatten': 'flatten surroundings',
261     'teleport': 'teleport',
262     'door': 'open/close',
263     'consume': 'consume',
264     'install': '(un-)install',
265     'wear': '(un-)wear',
266     'spin': 'spin',
267     'dance': 'dance',
268     'toggle_map_mode': 'toggle map view',
269     'toggle_tile_draw': 'toggle protection character drawing',
270     'hex_move_upleft': 'up-left',
271     'hex_move_upright': 'up-right',
272     'hex_move_right': 'right',
273     'hex_move_left': 'left',
274     'hex_move_downleft': 'down-left',
275     'hex_move_downright': 'down-right',
276     'square_move_up': 'up',
277     'square_move_left': 'left',
278     'square_move_down': 'down',
279     'square_move_right': 'right',
280 };
281 for (const mode_name of Object.keys(mode_helps)) {
282     key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
283 };
284
285 let rows_selector = document.getElementById("n_rows");
286 let cols_selector = document.getElementById("n_cols");
287 let key_selectors = document.querySelectorAll('[id^="key_"]');
288
289 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
290     const action = key_switch_selector.id.slice("key_switch_to_".length);
291     key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
292 }
293
294 function restore_selector_value(selector) {
295     let stored_selection = window.localStorage.getItem(selector.id);
296     if (stored_selection) {
297         selector.value = stored_selection;
298     }
299 }
300 restore_selector_value(rows_selector);
301 restore_selector_value(cols_selector);
302 for (let key_selector of key_selectors) {
303     restore_selector_value(key_selector);
304 }
305
306 function escapeHTML(str) {
307     return str.
308         replace(/&/g, '&amp;').
309         replace(/</g, '&lt;').
310         replace(/>/g, '&gt;').
311         replace(/'/g, '&apos;').
312         replace(/"/g, '&quot;');
313 };
314
315 let terminal = {
316   initialize: function() {
317     this.rows = rows_selector.value;
318     this.cols = cols_selector.value;
319     this.pre_el = document.getElementById("terminal");
320     this.set_default_colors();
321     this.apply_colors();
322     this.content = [];
323       let line = []
324     for (let y = 0, x = 0; y <= this.rows; x++) {
325         if (x == this.cols) {
326             x = 0;
327             y += 1;
328             this.content.push(line);
329             line = [];
330             if (y == this.rows) {
331                 break;
332             }
333         }
334         line.push(' ');
335     }
336   },
337   apply_colors: function() {
338     this.pre_el.style.color = this.foreground;
339     this.pre_el.style.backgroundColor = this.background;
340   },
341   set_default_colors: function() {
342       this.foreground = 'white';
343       this.background = 'black';
344       this.apply_colors();
345   },
346   set_random_colors: function() {
347       function rand(offset) {
348           return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
349       }
350       this.foreground = '#' + rand(159) + rand(159) + rand(159);
351       this.background = '#' + rand(0) + rand(0) + rand(0);
352       this.apply_colors();
353   },
354   blink_screen: function() {
355       this.pre_el.style.color = this.background;
356       this.pre_el.style.backgroundColor = this.foreground;
357       setTimeout(() => {
358           this.pre_el.style.color = this.foreground;
359           this.pre_el.style.backgroundColor = this.background;
360       }, 100);
361   },
362   refresh: function() {
363       let pre_content = '';
364       for (let y = 0; y < this.rows; y++) {
365           let line = this.content[y].join('');
366           let chunks = [];
367           if (y in tui.links) {
368               let start_x = 0;
369               for (let span of tui.links[y]) {
370                   chunks.push(escapeHTML(line.slice(start_x, span[0])));
371                   chunks.push('<a target="_blank" href="');
372                   chunks.push(escapeHTML(span[2]));
373                   chunks.push('">');
374                   chunks.push(escapeHTML(line.slice(span[0], span[1])));
375                   chunks.push('</a>');
376                   start_x = span[1];
377               }
378               chunks.push(escapeHTML(line.slice(start_x)));
379           } else {
380               chunks = [escapeHTML(line)];
381           }
382           for (const chunk of chunks) {
383               pre_content += chunk;
384           }
385           pre_content += '\n';
386       }
387       this.pre_el.innerHTML = pre_content;
388   },
389   write: function(start_y, start_x, msg) {
390       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
391           this.content[start_y][x] = msg[i];
392       }
393   },
394   drawBox: function(start_y, start_x, height, width) {
395     let end_y = start_y + height;
396     let end_x = start_x + width;
397     for (let y = start_y, x = start_x; y < this.rows; x++) {
398       if (x == end_x) {
399         x = start_x;
400         y += 1;
401         if (y == end_y) {
402             break;
403         }
404       };
405       this.content[y][x] = ' ';
406     }
407   },
408 }
409 terminal.initialize();
410
411 let parser = {
412   tokenize: function(str) {
413     let tokens = [];
414     let token = ''
415     let quoted = false;
416     let escaped = false;
417     for (let i = 0; i < str.length; i++) {
418       let c = str[i];
419       if (quoted) {
420         if (escaped) {
421           token += c;
422           escaped = false;
423         } else if (c == '\\') {
424           escaped = true;
425         } else if (c == '"') {
426           quoted = false
427         } else {
428           token += c;
429         }
430       } else if (c == '"') {
431         quoted = true
432       } else if (c === ' ') {
433         if (token.length > 0) {
434           tokens.push(token);
435           token = '';
436         }
437       } else {
438         token += c;
439       }
440     }
441     if (token.length > 0) {
442       tokens.push(token);
443     }
444     return tokens;
445   },
446   parse_yx: function(position_string) {
447     let coordinate_strings = position_string.split(',')
448     let position = [0, 0];
449     position[0] = parseInt(coordinate_strings[0].slice(2));
450     position[1] = parseInt(coordinate_strings[1].slice(2));
451     return position;
452   },
453 }
454
455 class Thing {
456     constructor(yx) {
457         this.position = yx;
458     }
459 }
460
461 let server = {
462     init: function(url) {
463         this.url = url;
464         this.websocket = new WebSocket(this.url);
465         this.websocket.onopen = function(event) {
466             game.thing_types = {};
467             game.terrains = {};
468             tui.is_admin = false;
469             server.send(['TASKS']);
470             server.send(['TERRAINS']);
471             server.send(['THING_TYPES']);
472             tui.log_msg("@ server connected! :)");
473             tui.switch_mode('login');
474         };
475         this.websocket.onclose = function(event) {
476             tui.switch_mode('waiting_for_server');
477             tui.log_msg("@ server disconnected :(");
478         };
479             this.websocket.onmessage = this.handle_event;
480         },
481     reconnect_to: function(url) {
482         this.websocket.close();
483         this.init(url);
484     },
485     send: function(tokens) {
486         this.websocket.send(unparser.untokenize(tokens));
487     },
488     handle_event: function(event) {
489         let tokens = parser.tokenize(event.data);
490         if (tokens[0] === 'TURN') {
491             game.turn_complete = false;
492         } else if (tokens[0] === 'OTHER_WIPE') {
493             game.portals_new = {};
494             explorer.annotations_new = {};
495             game.things_new = [];
496         } else if (tokens[0] === 'STATS') {
497             game.bladder_pressure_new = parseInt(tokens[1])
498             game.energy_new = parseInt(tokens[2])
499         } else if (tokens[0] === 'THING') {
500             let t = game.get_thing_temp(tokens[4], true);
501             t.position = parser.parse_yx(tokens[1]);
502             t.type_ = tokens[2];
503             t.protection = tokens[3];
504             t.portable = parseInt(tokens[5]);
505             t.commandable = parseInt(tokens[6]);
506         } else if (tokens[0] === 'THING_NAME') {
507             let t = game.get_thing_temp(tokens[1]);
508             t.name_ = tokens[2];
509         } else if (tokens[0] === 'THING_FACE') {
510             let t = game.get_thing_temp(tokens[1]);
511             t.face = tokens[2];
512         } else if (tokens[0] === 'THING_HAT') {
513             let t = game.get_thing_temp(tokens[1]);
514             t.hat = tokens[2];
515         } else if (tokens[0] === 'THING_DESIGN') {
516             let t = game.get_thing_temp(tokens[1]);
517             t.design = [parser.parse_yx(tokens[2]), tokens[3]];
518         } else if (tokens[0] === 'THING_CHAR') {
519             let t = game.get_thing_temp(tokens[1]);
520             t.thing_char = tokens[2];
521         } else if (tokens[0] === 'TASKS') {
522             game.tasks = tokens[1].split(',');
523             tui.mode_write.legal = game.tasks.includes('WRITE');
524             tui.mode_command_thing.legal = game.tasks.includes('WRITE');
525             tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
526             tui.mode_drop_thing.legal = game.tasks.includes('DROP');
527         } else if (tokens[0] === 'THING_TYPE') {
528             game.thing_types[tokens[1]] = tokens[2]
529         } else if (tokens[0] === 'THING_CARRYING') {
530             let t = game.get_thing_temp(tokens[1]);
531             t.carrying = game.get_thing_temp(tokens[2], false);
532         } else if (tokens[0] === 'THING_INSTALLED') {
533             let t = game.get_thing_temp(tokens[1]);
534             t.installed = true;
535         } else if (tokens[0] === 'TERRAIN') {
536             game.terrains[tokens[1]] = tokens[2]
537         } else if (tokens[0] === 'MAP') {
538             game.map_geometry_new = tokens[1];
539             game.map_size_new = parser.parse_yx(tokens[2]);
540             game.map_new = tokens[3]
541         } else if (tokens[0] === 'FOV') {
542             game.fov_new = tokens[1]
543         } else if (tokens[0] === 'MAP_CONTROL') {
544             game.map_control_new = tokens[1]
545         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
546             game.portals = game.portals_new;
547             game.map_geometry = game.map_geometry_new;
548             game.map_size = game.map_size_new;
549             game.map = game.map_new;
550             game.fov = game.fov_new;
551             tui.init_keys();
552             game.map_control = game.map_control_new;
553             explorer.annotations = explorer.annotations_new;
554             explorer.info_cached = false;
555             game.things = game.things_new;
556             game.player = game.things[game.player_id];
557             game.players_hat_chars = game.players_hat_chars_new;
558             game.bladder_pressure = game.bladder_pressure_new
559             game.energy = game.energy_new
560             game.turn_complete = true;
561             if (tui.mode.name == 'post_login_wait') {
562                 tui.switch_mode('play');
563             } else {
564                 tui.full_refresh();
565             }
566         } else if (tokens[0] === 'CHAT') {
567              tui.log_msg('# ' + tokens[1], 1);
568         } else if (tokens[0] === 'CHATFACE') {
569             tui.draw_face = tokens[1];
570             tui.full_refresh();
571         } else if (tokens[0] === 'REPLY') {
572              tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
573         } else if (tokens[0] === 'PLAYER_ID') {
574             game.player_id = parseInt(tokens[1]);
575         } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
576             game.players_hat_chars_new = tokens[1];
577         } else if (tokens[0] === 'LOGIN_OK') {
578             this.send(['GET_GAMESTATE']);
579             tui.switch_mode('post_login_wait');
580             tui.log_msg('@ welcome!')
581         } else if (tokens[0] === 'DEFAULT_COLORS') {
582             terminal.set_default_colors();
583         } else if (tokens[0] === 'RANDOM_COLORS') {
584             terminal.set_random_colors();
585         } else if (tokens[0] === 'ADMIN_OK') {
586             tui.is_admin = true;
587             tui.log_msg('@ you now have admin rights');
588             tui.switch_mode('admin');
589         } else if (tokens[0] === 'PORTAL') {
590             let position = parser.parse_yx(tokens[1]);
591             game.portals_new[position] = tokens[2];
592         } else if (tokens[0] === 'ANNOTATION') {
593             let position = parser.parse_yx(tokens[1]);
594             explorer.annotations_new[position] = tokens[2];
595         } else if (tokens[0] === 'UNHANDLED_INPUT') {
596             tui.log_msg('? unknown command');
597         } else if (tokens[0] === 'PLAY_ERROR') {
598             tui.log_msg('? ' + tokens[1]);
599             terminal.blink_screen();
600         } else if (tokens[0] === 'ARGUMENT_ERROR') {
601             tui.log_msg('? syntax error: ' + tokens[1]);
602         } else if (tokens[0] === 'GAME_ERROR') {
603             tui.log_msg('? game error: ' + tokens[1]);
604         } else if (tokens[0] === 'PONG') {
605             ;
606         } else {
607             tui.log_msg('? unhandled input: ' + event.data);
608         }
609     }
610 }
611
612 let unparser = {
613     quote: function(str) {
614         let quoted = ['"'];
615         for (let i = 0; i < str.length; i++) {
616             let c = str[i];
617             if (['"', '\\'].includes(c)) {
618                 quoted.push('\\');
619             };
620             quoted.push(c);
621         }
622         quoted.push('"');
623         return quoted.join('');
624     },
625     to_yx: function(yx_coordinate) {
626         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
627     },
628     untokenize: function(tokens) {
629         let quoted_tokens = [];
630         for (let token of tokens) {
631             quoted_tokens.push(this.quote(token));
632         }
633         return quoted_tokens.join(" ");
634     }
635 }
636
637 class Mode {
638     constructor(name, has_input_prompt=false, shows_info=false,
639                 is_intro=false, is_single_char_entry=false) {
640         this.name = name;
641         this.short_desc = mode_helps[name].short;
642         this.available_modes = [];
643         this.available_actions = [];
644         this.has_input_prompt = has_input_prompt;
645         this.shows_info= shows_info;
646         this.is_intro = is_intro;
647         this.help_intro = mode_helps[name].long;
648         this.intro_msg = mode_helps[name].intro;
649         this.is_single_char_entry = is_single_char_entry;
650         this.legal = true;
651     }
652     *iter_available_modes() {
653         for (let mode_name of this.available_modes) {
654             let mode = tui['mode_' + mode_name];
655             if (!mode.legal) {
656                 continue;
657             }
658             let key = tui.keys['switch_to_' + mode.name];
659             yield [mode, key]
660         }
661     }
662     list_available_modes() {
663         let msg = ''
664         if (this.available_modes.length > 0) {
665             msg += 'Other modes available from here:\n';
666             for (let [mode, key] of this.iter_available_modes()) {
667                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
668             }
669         }
670         return msg;
671     }
672     mode_switch_on_key(key_event) {
673         for (let [mode, key] of this.iter_available_modes()) {
674             if (key_event.key == key) {
675                 event.preventDefault();
676                 tui.switch_mode(mode.name);
677                 return true;
678             };
679         }
680         return false;
681     }
682 }
683 let tui = {
684   links: {},
685   log: [],
686   input_prompt: '> ',
687   input_lines: [],
688   height_turn_line: 1,
689   height_mode_line: 1,
690   height_input: 1,
691   password: 'foo',
692   show_help: false,
693   is_admin: false,
694   tile_draw: false,
695   mode_waiting_for_server: new Mode('waiting_for_server',
696                                      false, false, true),
697   mode_login: new Mode('login', true, false, true),
698   mode_post_login_wait: new Mode('post_login_wait'),
699   mode_chat: new Mode('chat', true),
700   mode_annotate: new Mode('annotate', true, true),
701   mode_play: new Mode('play'),
702   mode_study: new Mode('study', false, true),
703   mode_write: new Mode('write', false, false, false, true),
704   mode_edit: new Mode('edit'),
705   mode_control_pw_type: new Mode('control_pw_type', true),
706   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
707   mode_portal: new Mode('portal', true, true),
708   mode_password: new Mode('password', true),
709   mode_name_thing: new Mode('name_thing', true, true),
710   mode_command_thing: new Mode('command_thing', true),
711   mode_take_thing: new Mode('take_thing', true),
712   mode_drop_thing: new Mode('drop_thing', true),
713   mode_enter_face: new Mode('enter_face', true),
714   mode_enter_design: new Mode('enter_design', true),
715   mode_admin_enter: new Mode('admin_enter', true),
716   mode_admin: new Mode('admin'),
717   mode_control_pw_pw: new Mode('control_pw_pw', true),
718   mode_control_tile_type: new Mode('control_tile_type', true),
719   mode_control_tile_draw: new Mode('control_tile_draw'),
720   action_tasks: {
721       'flatten': 'FLATTEN_SURROUNDINGS',
722       'take_thing': 'PICK_UP',
723       'drop_thing': 'DROP',
724       'move': 'MOVE',
725       'door': 'DOOR',
726       'install': 'INSTALL',
727       'wear': 'WEAR',
728       'command': 'COMMAND',
729       'consume': 'INTOXICATE',
730       'spin': 'SPIN',
731       'dance': 'DANCE',
732   },
733   offset: [0,0],
734   map_lines: [],
735   ascii_draw_stage: 0,
736   full_ascii_draw: '',
737   selectables: [],
738   draw_face: false,
739   init: function() {
740       this.reset_screen_size();
741       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
742                                         "command_thing", "take_thing", "drop_thing"]
743       this.mode_play.available_actions = ["move", "teleport", "door", "consume",
744                                           "wear", "spin", "dance"];
745       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
746       this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
747       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
748                                          "control_tile_type", "chat",
749                                          "study", "play", "edit"]
750       this.mode_admin.available_actions = ["move", "toggle_map_mode"];
751       this.mode_control_tile_draw.available_modes = ["admin_enter"]
752       this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
753       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
754                                         "enter_design", "password", "chat", "study",
755                                         "play", "admin_enter", "enter_face"]
756       this.mode_edit.available_actions = ["move", "flatten", "install",
757                                           "toggle_map_mode"]
758       this.inputEl = document.getElementById("input");
759       this.switch_mode('waiting_for_server');
760       this.recalc_input_lines();
761       this.height_header = this.height_turn_line + this.height_mode_line;
762       this.init_keys();
763   },
764   reset_screen_size: function() {
765       this.left_window_width = Math.min(52, terminal.cols / 2);
766       this.right_window_width = terminal.cols - tui.left_window_width;
767   },
768   init_keys: function() {
769     document.getElementById("move_table").hidden = true;
770     this.keys = {};
771     for (let key_selector of key_selectors) {
772         this.keys[key_selector.id.slice(4)] = key_selector.value;
773     }
774     this.movement_keys = {};
775     let geometry_prefix = 'undefinedMapGeometry_';
776     if (game.map_geometry) {
777         geometry_prefix = game.map_geometry.toLowerCase() + '_';
778     }
779     for (const key_name of Object.keys(key_descriptions)) {
780         if (key_name.startsWith(geometry_prefix)) {
781             let direction = key_name.split('_')[2].toUpperCase();
782             let key = this.keys[key_name];
783             this.movement_keys[key] = direction;
784         }
785     };
786     for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
787         if (move_button.id.startsWith('key_')) {
788             continue;
789         }
790         move_button.hidden = true;
791     };
792     for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
793         document.getElementById("move_table").hidden = false;
794         move_button.hidden = false;
795     };
796     for (let el of document.getElementsByTagName("button")) {
797       let action_desc = key_descriptions[el.id];
798       let action_key = '[' + this.keys[el.id] + ']';
799       el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
800     }
801   },
802   task_action_on: function(action) {
803       return game.tasks.includes(this.action_tasks[action]);
804   },
805   switch_mode: function(mode_name) {
806
807     function fail(msg, return_mode='play') {
808         tui.log_msg('? ' + msg);
809         terminal.blink_screen();
810         tui.switch_mode(return_mode);
811     }
812
813     if (this.mode && this.mode.name == 'control_tile_draw') {
814         tui.log_msg('@ finished tile protection drawing.')
815     }
816     this.draw_face = false;
817     this.tile_draw = false;
818     this.ascii_draw_stage = 0;
819     this.full_ascii_draw = '';
820     if (mode_name == 'command_thing' && (!game.player.carrying
821                                          || !game.player.carrying.commandable)) {
822         return fail('not carrying anything commandable');
823     } else if (mode_name == 'name_thing' && !game.player.carrying) {
824         return fail('not carrying anything to re-name', 'edit');
825     } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
826         return fail('not carrying anything to protect')
827     } else if (mode_name == 'take_thing' && game.player.carrying) {
828         return fail('already carrying something');
829     } else if (mode_name == 'drop_thing' && !game.player.carrying) {
830         return fail('not carrying anything droppable');
831     } else if (mode_name == 'enter_design' && (!game.player.carrying
832                                                || !game.player.carrying.design)) {
833         return fail('not carrying designable to edit', 'edit');
834     }
835     if (mode_name == 'admin_enter' && this.is_admin) {
836         mode_name = 'admin';
837     };
838     this.mode = this['mode_' + mode_name];
839     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
840         this.map_mode = 'protections';
841     } else if (this.mode.name != "edit") {
842         this.map_mode = 'terrain + things';
843     };
844     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
845         explorer.position = game.player.position;
846     }
847     this.inputEl.value = "";
848     this.restore_input_values();
849     for (let el of document.getElementsByTagName("button")) {
850         el.disabled = true;
851     }
852     document.getElementById("help").disabled = false;
853     for (const action of this.mode.available_actions) {
854         if (["move", "move_explorer"].includes(action)) {
855             for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
856                 move_key.disabled = false;
857             }
858         } else if (Object.keys(this.action_tasks).includes(action)) {
859             if (this.task_action_on(action)) {
860                 document.getElementById(action).disabled = false;
861             }
862         } else {
863             document.getElementById(action).disabled = false;
864         };
865     }
866     for (const mode_name of this.mode.available_modes) {
867             document.getElementById('switch_to_' + mode_name).disabled = false;
868     }
869     if (this.mode.intro_msg.length > 0) {
870         this.log_msg(this.mode.intro_msg);
871     }
872     if (this.mode.name == 'login') {
873         if (this.login_name) {
874             server.send(['LOGIN', this.login_name]);
875         } else {
876             this.log_msg("? need login name");
877         }
878     } else if (this.mode.is_single_char_entry) {
879         this.show_help = true;
880     } else if (this.mode.name == 'take_thing') {
881         this.log_msg("Portable things in reach for pick-up:");
882         const y = game.player.position[0]
883         const x = game.player.position[1]
884         let directed_moves = {
885             'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
886         }
887         if (game.map_geometry == 'Square') {
888             directed_moves['UP'] = [-1, 0];
889             directed_moves['DOWN'] = [1, 0];
890         } else if (game.map_geometry == 'Hex') {
891             if (y % 2) {
892                 directed_moves['UPLEFT'] = [-1, 0];
893                 directed_moves['UPRIGHT'] = [-1, 1];
894                 directed_moves['DOWNLEFT'] = [1, 0];
895                 directed_moves['DOWNRIGHT'] = [1, 1];
896             } else {
897                 directed_moves['UPLEFT'] = [-1, -1];
898                 directed_moves['UPRIGHT'] = [-1, 0];
899                 directed_moves['DOWNLEFT'] = [1, -1];
900                 directed_moves['DOWNRIGHT'] = [1, 0];
901             }
902         }
903         let select_range = {};
904         for (const direction in directed_moves) {
905             const move = directed_moves[direction];
906             select_range[direction] = [y + move[0], x + move[1]];
907         }
908         this.selectables = [];
909         let directions = [];
910         for (const direction in select_range) {
911             for (const t_id in game.things) {
912                 const t = game.things[t_id];
913                 const position = select_range[direction];
914                 if (t.portable
915                     && t.position[0] == position[0]
916                     && t.position[1] == position[1]) {
917                     this.selectables.push(t_id);
918                     directions.push(direction);
919                 }
920             }
921         }
922         if (this.selectables.length == 0) {
923             this.log_msg('none');
924             terminal.blink_screen();
925             this.switch_mode('play');
926             return;
927         } else {
928             for (let [i, t_id] of this.selectables.entries()) {
929                 const t = game.things[t_id];
930                 const direction = directions[i];
931                 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
932             }
933         }
934     } else if (this.mode.name == 'drop_thing') {
935         this.log_msg('Direction to drop thing to:');
936         this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
937         for (let [i, direction] of this.selectables.entries()) {
938             this.log_msg(i + ': ' + direction);
939         };
940     } else if (this.mode.name == 'enter_design') {
941         if (game.player.carrying.type_ == 'Hat') {
942             this.log_msg('@ The design you enter must be '
943                          + game.player.carrying.design[0][0] + ' lines of max '
944                          + game.player.carrying.design[0][1] + ' characters width each');
945             this.log_msg('@ Legal characters: ' + game.players_hat_chars);
946             this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
947         } else {
948             this.log_msg('@ Width of first line determines maximum width for remaining design')
949             this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
950         }
951     } else if (this.mode.name == 'command_thing') {
952         server.send(['TASK:COMMAND', 'HELP']);
953     } else if (this.mode.name == 'control_pw_pw') {
954         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
955     } else if (this.mode.name == 'control_tile_draw') {
956         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 + '].')
957     }
958     this.full_refresh();
959   },
960   offset_links: function(offset, links) {
961       for (let y in links) {
962           let real_y = offset[0] + parseInt(y);
963           if (!this.links[real_y]) {
964               this.links[real_y] = [];
965           }
966           for (let link of links[y]) {
967               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
968               this.links[real_y].push(offset_link);
969           }
970       }
971   },
972   restore_input_values: function() {
973       if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
974           let info = explorer.annotations[explorer.position];
975           if (info != "(none)") {
976               this.inputEl.value = info;
977           }
978       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
979           let portal = game.portals[explorer.position]
980           this.inputEl.value = portal;
981       } else if (this.mode.name == 'password') {
982           this.inputEl.value = this.password;
983       } else if (this.mode.name == 'name_thing') {
984           if (game.player.carrying && game.player.carrying.name_) {
985               this.inputEl.value = game.player.carrying.name_;
986           }
987       } else if (this.mode.name == 'admin_thing_protect') {
988           if (game.player.carrying && game.player.carrying.protection) {
989               this.inputEl.value = game.player.carrying.protection;
990           }
991       } else if (this.mode.name == 'enter_face') {
992           const start = this.ascii_draw_stage * 6;
993           const end = (this.ascii_draw_stage + 1) * 6;
994           this.inputEl.value = game.player.face.slice(start, end);
995       } else if (this.mode.name == 'enter_design') {
996           const width = game.player.carrying.design[0][1];
997           const start = this.ascii_draw_stage * width;
998           const end = (this.ascii_draw_stage + 1) * width;
999           this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1000       }
1001   },
1002   recalc_input_lines: function() {
1003       if (this.mode.has_input_prompt) {
1004           let _ = null;
1005           [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1006       } else {
1007           this.input_lines = [];
1008       }
1009       this.height_input = this.input_lines.length;
1010   },
1011   msg_into_lines_of_width: function(msg, width) {
1012       function push_inner_link(y, end_x) {
1013           if (!inner_links[y]) {
1014               inner_links[y] = [];
1015           };
1016           inner_links[y].push([url_start_x, end_x, url]);
1017       };
1018       let link_data = {};
1019       let url_ends = [];
1020       const regexp = RegExp('https?://[^\\s]+', 'g');
1021       let match;
1022       while ((match = regexp.exec(msg)) !== null) {
1023           const url = match[0];
1024           const url_start = match.index;
1025           const url_end = match.index + match[0].length;
1026           link_data[url_start] = url;
1027           url_ends.push(url_end);
1028       }
1029       let url_start_x = 0;
1030       let url = '';
1031       let inner_links = {};
1032       let in_link = false;
1033       let chunk = "";
1034       let lines = [];
1035       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1036           if (x >= width || msg[i] == "\n") {
1037               if (in_link) {
1038                   push_inner_link(y, chunk.length);
1039                   url_start_x = 0;
1040                   if (url_ends[0] == i) {
1041                       in_link = false;
1042                       url_ends.shift();
1043                   }
1044               };
1045               lines.push(chunk);
1046               chunk = "";
1047               x = 0;
1048               if (msg[i] == "\n") {
1049                   x -= 1;
1050               };
1051               y += 1;
1052           };
1053           if (msg[i] != "\n") {
1054               chunk += msg[i];
1055           };
1056           if (i in link_data) {
1057               url_start_x = x;
1058               url = link_data[i];
1059               in_link = true;
1060           } else if (url_ends[0] == i) {
1061               url_ends.shift();
1062               push_inner_link(y, x);
1063               in_link = false;
1064           }
1065       }
1066       lines.push(chunk);
1067       if (in_link) {
1068           push_inner_link(lines.length - 1, chunk.length);
1069       }
1070       return [lines, inner_links];
1071   },
1072   log_msg: function(msg) {
1073       this.log.push(msg);
1074       while (this.log.length > 100) {
1075         this.log.shift();
1076       };
1077       this.full_refresh();
1078   },
1079   pick_selectable: function(task_name) {
1080       const i = parseInt(this.inputEl.value);
1081       if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1082           tui.log_msg('? invalid index, aborted');
1083       } else {
1084           server.send(['TASK:' + task_name, tui.selectables[i]]);
1085       }
1086       this.inputEl.value = "";
1087       this.switch_mode('play');
1088   },
1089     enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1090         if (with_size && this.ascii_draw_stage == 0) {
1091             width = this.inputEl.value.length;
1092             if (width > 36) {
1093                 this.log_msg('? wrong input length, must be max 36; try again');
1094                 return;
1095             }
1096             if (width != game.player.carrying.design[0][1]) {
1097                 game.player.carrying.design[1] = '';
1098                 game.player.carrying.design[0][1] = width;
1099             }
1100         } else if (this.inputEl.value.length > width) {
1101             this.log_msg('? wrong input length, must be max ' + width + '; try again');
1102             return;
1103         }
1104         this.log_msg('  ' + this.inputEl.value);
1105         if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1106           height = this.ascii_draw_stage;
1107         } else {
1108             if (with_size) {
1109                 height = this.ascii_draw_stage + 2;
1110             }
1111             while (this.inputEl.value.length < width) {
1112                 this.inputEl.value += ' ';
1113             }
1114             this.full_ascii_draw += this.inputEl.value;
1115         }
1116         if (with_size) {
1117             game.player.carrying.design[0][0] = height;
1118         }
1119         this.ascii_draw_stage += 1;
1120         if (this.ascii_draw_stage < height) {
1121             this.restore_input_values();
1122         } else {
1123             if (with_pw && with_size) {
1124                 server.send([command + '_SIZE',
1125                              unparser.to_yx(game.player.carrying.design[0]),
1126                              this.password]);
1127             }
1128             if (with_pw) {
1129                 server.send([command, this.full_ascii_draw, this.password]);
1130             } else {
1131                 server.send([command, this.full_ascii_draw]);
1132             }
1133             this.full_ascii_draw = '';
1134             this.ascii_draw_stage = 0;
1135             this.inputEl.value = '';
1136             this.switch_mode('edit');
1137         }
1138   },
1139   draw_map: function() {
1140     if (!game.turn_complete && this.map_lines.length == 0) {
1141         return;
1142     }
1143     if (game.turn_complete) {
1144         let map_lines_split = [];
1145         let line = [];
1146         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1147             if (j == game.map_size[1]) {
1148                 map_lines_split.push(line);
1149                 line = [];
1150                 j = 0;
1151             };
1152             if (this.map_mode == 'protections') {
1153                 line.push(game.map_control[i] + ' ');
1154             } else {
1155                 line.push(game.map[i] + ' ');
1156             }
1157         };
1158         map_lines_split.push(line);
1159         if (this.map_mode == 'terrain + annotations') {
1160             for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1161                 const yx = coordinate.split(',')
1162                 map_lines_split[yx[0]][yx[1]] = 'A ';
1163             }
1164         } else if (this.map_mode == 'terrain + things') {
1165             for (const p in game.portals) {
1166                 let coordinate = p.split(',')
1167                 let original = map_lines_split[coordinate[0]][coordinate[1]];
1168                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1169             }
1170             let used_positions = [];
1171             function draw_thing(t, used_positions) {
1172                 let symbol = game.thing_types[t.type_];
1173                 let meta_char = ' ';
1174                 if (t.thing_char) {
1175                     meta_char = t.thing_char;
1176                 }
1177                 if (used_positions.includes(t.position.toString())) {
1178                     meta_char = '+';
1179                 };
1180                 if (t.carrying) {
1181                     meta_char = '$';
1182                 }
1183                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1184                 used_positions.push(t.position.toString());
1185             }
1186             for (const thing_id in game.things) {
1187                 let t = game.things[thing_id];
1188                 if (t.type_ != 'Player') {
1189                     draw_thing(t, used_positions);
1190                 }
1191             };
1192             for (const thing_id in game.things) {
1193                 let t = game.things[thing_id];
1194                 if (t.type_ == 'Player') {
1195                     draw_thing(t, used_positions);
1196                 }
1197             };
1198         }
1199         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1200             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1201         } else if (tui.map_mode != 'terrain + things') {
1202             map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1203         }
1204         this.map_lines = []
1205         if (game.map_geometry == 'Square') {
1206             for (let line_split of map_lines_split) {
1207                 this.map_lines.push(line_split.join(''));
1208             };
1209         } else if (game.map_geometry == 'Hex') {
1210             let indent = 0
1211             for (let line_split of map_lines_split) {
1212                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1213                 if (indent == 0) {
1214                     indent = 1;
1215                 } else {
1216                     indent = 0;
1217                 };
1218             };
1219         }
1220         let window_center = [terminal.rows / 2, this.left_window_width / 2];
1221         let center_position = [game.player.position[0], game.player.position[1]];
1222         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1223             center_position = [explorer.position[0], explorer.position[1]];
1224         }
1225         center_position[1] = center_position[1] * 2;
1226         this.offset = [center_position[0] - window_center[0],
1227                        center_position[1] - window_center[1]]
1228         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1229             this.offset[1] += 1;
1230         };
1231     };
1232     let term_y = Math.max(0, -this.offset[0]);
1233     let term_x = Math.max(0, -this.offset[1]);
1234     let map_y = Math.max(0, this.offset[0]);
1235     let map_x = Math.max(0, this.offset[1]);
1236     for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1237         let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1238         terminal.write(term_y, term_x, to_draw);
1239     }
1240   },
1241   draw_names: function() {
1242       let players = [];
1243       for (const thing_id in game.things) {
1244            let t = game.things[thing_id];
1245            if (t.type_ == 'Player') {
1246                players.push(t);
1247            }
1248       };
1249       function compare(a, b) {
1250           if (a.name_.length > b.name_.length) {
1251               return -1;
1252           } else if (a.name_.length < b.name_.length) {
1253               return 1;
1254           } else {
1255               return 0;
1256           }
1257       }
1258       players.sort(compare);
1259       const shrink_offset = Math.max(0, (terminal.rows - tui.left_window_width / 2) / 2);
1260       let y = 0;
1261       for (const player of players) {
1262           let name = player.name_;
1263           const offset_y = y - shrink_offset;
1264           const max_len = Math.max(5, (tui.left_window_width / 2) - (offset_y * 2) - 8);
1265           if (name.length > max_len) {
1266               name = name.slice(0, max_len - 1) + '…';
1267           }
1268           terminal.write(y, 0, '@' + player.thing_char + ':' + name);
1269           y += 1;
1270           if (y >= terminal.rows) {
1271               break;
1272           }
1273       }
1274   },
1275   draw_face_popup: function() {
1276       const t = game.things[this.draw_face];
1277       if (!t || !t.face) {
1278           this.draw_face = false;
1279           return;
1280       }
1281       const start_x = tui.left_window_width - 10;
1282       function draw_body_part(body_part, end_y) {
1283           terminal.write(end_y - 3, start_x, '----------');
1284           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1285           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1286           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1287       }
1288       if (t.face) {
1289           draw_body_part(t.face, terminal.rows - 3);
1290       }
1291       if (t.hat) {
1292           draw_body_part(t.hat, terminal.rows - 6);
1293       }
1294       terminal.write(terminal.rows - 2, start_x, '----------');
1295       let name = t.name_;
1296       if (name.length > 7) {
1297           name = name.slice(0, 6) + '…';
1298       }
1299       terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1300   },
1301   draw_mode_line: function() {
1302       let help = 'hit [' + this.keys.help + '] for help';
1303       if (this.mode.has_input_prompt) {
1304           help = 'enter /help for help';
1305       }
1306       terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1307   },
1308   draw_stats_line: function(n) {
1309       terminal.write(0, this.left_window_width,
1310                      'ENERGY: ' + game.energy +
1311                      ' BLADDER: ' + game.bladder_pressure);
1312   },
1313   draw_history: function() {
1314       let log_display_lines = [];
1315       let log_links = {};
1316       let y_offset_in_log = 0;
1317       for (let line of this.log) {
1318           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1319                                                                     this.right_window_width)
1320           log_display_lines = log_display_lines.concat(new_lines);
1321           for (const y in link_data) {
1322               const rel_y = y_offset_in_log + parseInt(y);
1323               log_links[rel_y] = [];
1324               for (let link of link_data[y]) {
1325                   log_links[rel_y].push(link);
1326               }
1327           }
1328           y_offset_in_log += new_lines.length;
1329       };
1330       let i = log_display_lines.length - 1;
1331       for (let y = terminal.rows - 1 - this.height_input;
1332            y >= this.height_header && i >= 0;
1333            y--, i--) {
1334           terminal.write(y, this.left_window_width, log_display_lines[i]);
1335       }
1336       for (const key of Object.keys(log_links)) {
1337           if (parseInt(key) <= i) {
1338               delete log_links[key];
1339           }
1340       }
1341       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1342                     this.left_window_width];
1343       this.offset_links(offset, log_links);
1344   },
1345   draw_info: function() {
1346       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1347       let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1348       let offset = [this.height_header, this.left_window_width];
1349       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1350         terminal.write(y, offset[1], lines[i]);
1351       }
1352       this.offset_links(offset, link_data);
1353   },
1354   draw_input: function() {
1355     if (this.mode.has_input_prompt) {
1356         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1357             terminal.write(y, this.left_window_width, this.input_lines[i]);
1358         }
1359     }
1360   },
1361   draw_help: function() {
1362       let movement_keys_desc = '';
1363       if (!this.mode.is_intro) {
1364           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1365       }
1366       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1367       if (this.mode.available_actions.length > 0) {
1368           content += "Available actions:\n";
1369           for (let action of this.mode.available_actions) {
1370               if (Object.keys(this.action_tasks).includes(action)) {
1371                   if (!this.task_action_on(action)) {
1372                       continue;
1373                   }
1374               }
1375               if (action == 'move_explorer') {
1376                   action = 'move';
1377               }
1378               if (action == 'move') {
1379                   content += "[" + movement_keys_desc + "] – move\n"
1380               } else {
1381                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1382               }
1383           }
1384           content += '\n';
1385       }
1386       content += this.mode.list_available_modes();
1387       let start_x = 0;
1388       let lines = [];
1389       let _ = undefined;
1390       if (!this.mode.has_input_prompt) {
1391           start_x = this.left_window_width;
1392           this.draw_links = false;
1393           terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1394           [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1395       } else {
1396           start_x = 0;
1397           terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1398           [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1399       }
1400       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1401           terminal.write(y, start_x, lines[i]);
1402       }
1403   },
1404   toggle_tile_draw: function() {
1405       if (tui.tile_draw) {
1406           tui.tile_draw = false;
1407       } else {
1408           tui.tile_draw = true;
1409       }
1410   },
1411   toggle_map_mode: function() {
1412       if (tui.map_mode == 'terrain only') {
1413           tui.map_mode = 'terrain + annotations';
1414       } else if (tui.map_mode == 'terrain + annotations') {
1415           tui.map_mode = 'terrain + things';
1416       } else if (tui.map_mode == 'terrain + things') {
1417           tui.map_mode = 'protections';
1418       } else if (tui.map_mode == 'protections') {
1419           tui.map_mode = 'terrain only';
1420       }
1421   },
1422   full_refresh: function() {
1423     this.draw_links = true;
1424     this.links = {};
1425     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1426     this.recalc_input_lines();
1427     if (this.mode.is_intro) {
1428         this.draw_history();
1429         this.draw_input();
1430     } else {
1431         this.draw_map();
1432         this.draw_stats_line();
1433         this.draw_mode_line();
1434         if (this.mode.shows_info) {
1435           this.draw_info();
1436         } else {
1437           this.draw_history();
1438         }
1439         this.draw_input();
1440     }
1441     if (this.show_help) {
1442         this.draw_help();
1443     }
1444     if (['chat', 'play'].includes(this.mode.name)) {
1445         this.draw_names();
1446         if (this.draw_face) {
1447             this.draw_face_popup();
1448         }
1449     }
1450     if (!this.draw_links) {
1451         this.links = {};
1452     }
1453     terminal.refresh();
1454   }
1455 }
1456
1457 let game = {
1458     init: function() {
1459         this.turn = -1;
1460         this.player_id = -1;
1461         this.tasks = {};
1462         this.things = {};
1463         this.things_new = {};
1464         this.fov = "";
1465         this.fov_new = "";
1466         this.map = "";
1467         this.map_new = "";
1468         this.map_control = "";
1469         this.map_control_new = "";
1470         this.map_size = [0,0];
1471         this.map_size_new = [0,0];
1472         this.portals = {};
1473         this.portals_new = {};
1474         this.players_hat_chars = "";
1475         this.bladder_pressure = 0;
1476         this.bladder_pressure_new = 0;
1477     },
1478     get_thing_temp: function(id_, create_if_not_found=false) {
1479         if (id_ in game.things_new) {
1480             return game.things_new[id_];
1481         } else if (create_if_not_found) {
1482             let t = new Thing([0,0]);
1483             game.things_new[id_] = t;
1484             return t;
1485         };
1486     },
1487     get_thing: function(id_, create_if_not_found=false) {
1488         if (id_ in game.things) {
1489             return game.things[id_];
1490         };
1491     },
1492     move: function(start_position, direction) {
1493         let target = [start_position[0], start_position[1]];
1494         if (direction == 'LEFT') {
1495             target[1] -= 1;
1496         } else if (direction == 'RIGHT') {
1497             target[1] += 1;
1498         } else if (game.map_geometry == 'Square') {
1499             if (direction == 'UP') {
1500                 target[0] -= 1;
1501             } else if (direction == 'DOWN') {
1502                 target[0] += 1;
1503             };
1504         } else if (game.map_geometry == 'Hex') {
1505             let start_indented = start_position[0] % 2;
1506             if (direction == 'UPLEFT') {
1507                 target[0] -= 1;
1508                 if (!start_indented) {
1509                     target[1] -= 1;
1510                 }
1511             } else if (direction == 'UPRIGHT') {
1512                 target[0] -= 1;
1513                 if (start_indented) {
1514                     target[1] += 1;
1515                 }
1516             } else if (direction == 'DOWNLEFT') {
1517                 target[0] += 1;
1518                 if (!start_indented) {
1519                     target[1] -= 1;
1520                 }
1521             } else if (direction == 'DOWNRIGHT') {
1522                 target[0] += 1;
1523                 if (start_indented) {
1524                     target[1] += 1;
1525                 }
1526             };
1527         };
1528         if (target[0] < 0 || target[1] < 0 ||
1529             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1530             return null;
1531         };
1532         return target;
1533     },
1534     teleport: function() {
1535         if (game.player.position in this.portals) {
1536             server.reconnect_to(this.portals[game.player.position]);
1537         } else {
1538             terminal.blink_screen();
1539             tui.log_msg('? not standing on portal')
1540         }
1541     }
1542 }
1543
1544 game.init();
1545 tui.init();
1546 tui.full_refresh();
1547 server.init(websocket_location);
1548
1549 let explorer = {
1550     position: [0,0],
1551     annotations: {},
1552     annotations_new: {},
1553     info_cached: false,
1554     move: function(direction) {
1555         let target = game.move(this.position, direction);
1556         if (target) {
1557             this.position = target
1558             this.info_cached = false;
1559             if (tui.tile_draw) {
1560                 this.send_tile_control_command();
1561             }
1562         } else {
1563             terminal.blink_screen();
1564         };
1565     },
1566     get_info: function() {
1567         if (this.info_cached) {
1568             return this.info_cached;
1569         }
1570         let info_to_cache = '';
1571         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1572         if (game.fov[position_i] != '.') {
1573             info_to_cache += 'outside field of view';
1574         } else {
1575             for (let t_id in game.things) {
1576                  let t = game.things[t_id];
1577                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1578                      info_to_cache += this.get_thing_info(t, true);
1579                  }
1580             }
1581             let terrain_char = game.map[position_i]
1582             let terrain_desc = '?'
1583             if (game.terrains[terrain_char]) {
1584                 terrain_desc = game.terrains[terrain_char];
1585             };
1586             info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1587             let protection = game.map_control[position_i];
1588             if (protection != '.') {
1589                 info_to_cache += '/protection:' + protection;
1590             };
1591             info_to_cache += ')\n';
1592             if (this.position in game.portals) {
1593                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1594             }
1595             if (this.position in this.annotations) {
1596                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1597             }
1598         }
1599         this.info_cached = info_to_cache;
1600         return this.info_cached;
1601     },
1602     get_thing_info: function(t, detailed=false) {
1603         let info = '';
1604         if (detailed) {
1605             info += '- ';
1606         }
1607         info += game.thing_types[t.type_];
1608         if (t.thing_char) {
1609             info += t.thing_char;
1610         };
1611         if (t.name_) {
1612             info += ": " + t.name_;
1613         }
1614         info += ' (' + t.type_;
1615         if (t.installed) {
1616             info += "/installed";
1617         }
1618         if (t.type_ == 'Bottle') {
1619             if (t.thing_char == '_') {
1620                 info += '/empty';
1621             } else if (t.thing_char == '~') {
1622                 info += '/full';
1623             }
1624         }
1625         if (detailed) {
1626             const protection = t.protection;
1627             if (protection != '.') {
1628                 info += '/protection:' + protection;
1629             }
1630             info += ')\n';
1631             if (t.hat || t.face) {
1632                 info += '----------\n';
1633             }
1634             if (t.hat) {
1635                 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1636                 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1637                 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1638             }
1639             if (t.face) {
1640                 info += '| ' + t.face.slice(0, 6) + ' |\n';
1641                 info += '| ' + t.face.slice(6, 12) + ' |\n';
1642                 info += '| ' + t.face.slice(12, 18) + ' |\n';
1643                 info += '----------\n';
1644             }
1645             if (t.design) {
1646                 const line_length = t.design[0][1];
1647                 info += '-'.repeat(line_length + 4) + '\n';
1648                 let lines = ['']
1649                 if (line_length > 0) {
1650                     const regexp = RegExp('.{1,' + line_length + '}', 'g');
1651                     lines = t.design[1].match(regexp);
1652                 }
1653                 for (const line of lines) {
1654                     info += '| ' + line + ' |\n';
1655                 }
1656                 info += '-'.repeat(line_length + 4) + '\n';
1657             }
1658         } else {
1659             info += ')';
1660         }
1661         return info;
1662     },
1663     annotate: function(msg) {
1664         if (msg.length == 0) {
1665             msg = " ";  // triggers annotation deletion
1666         }
1667         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1668     },
1669     set_portal: function(msg) {
1670         if (msg.length == 0) {
1671             msg = " ";  // triggers portal deletion
1672         }
1673         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1674     },
1675     send_tile_control_command: function() {
1676         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1677     }
1678 }
1679
1680 tui.inputEl.addEventListener('input', (event) => {
1681     if (tui.mode.has_input_prompt) {
1682         let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1683         if (tui.inputEl.value.length > max_length) {
1684             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1685         };
1686     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1687         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1688         tui.switch_mode('edit');
1689     }
1690     tui.full_refresh();
1691 }, false);
1692 document.onclick = function() {
1693     if (!tui.mode.is_single_char_entry) {
1694         tui.show_help = false;
1695     }
1696 };
1697 tui.inputEl.addEventListener('keydown', (event) => {
1698     tui.show_help = false;
1699     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1700         event.preventDefault();
1701     }
1702     if ((!tui.mode.is_intro && event.key == 'Escape')
1703         || (tui.mode.has_input_prompt && event.key == 'Enter'
1704             && tui.inputEl.value.length == 0
1705             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1706                 'admin_enter'].includes(tui.mode.name))) {
1707         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1708             tui.log_msg('@ aborted');
1709         }
1710         tui.switch_mode('play');
1711     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1712         tui.show_help = true;
1713         tui.inputEl.value = "";
1714         tui.restore_input_values();
1715     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1716                && !tui.mode.is_single_char_entry) {
1717         tui.show_help = true;
1718     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1719         tui.login_name = tui.inputEl.value;
1720         server.send(['LOGIN', tui.inputEl.value]);
1721         tui.inputEl.value = "";
1722     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1723         tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1724     } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1725         if (game.player.carrying.type_ == 'Hat') {
1726             tui.enter_ascii_art('THING_DESIGN',
1727                                 game.player.carrying.design[0][0],
1728                                 game.player.carrying.design[0][1], true);
1729         } else {
1730             tui.enter_ascii_art('THING_DESIGN',
1731                                 game.player.carrying.design[0][0],
1732                                 game.player.carrying.design[0][1], true, true);
1733         }
1734     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1735         server.send(['TASK:COMMAND', tui.inputEl.value]);
1736         tui.inputEl.value = "";
1737     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1738         tui.pick_selectable('PICK_UP');
1739     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1740         tui.pick_selectable('DROP');
1741     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1742         if (tui.inputEl.value.length == 0) {
1743             tui.log_msg('@ aborted');
1744         } else {
1745             server.send(['SET_MAP_CONTROL_PASSWORD',
1746                         tui.tile_control_char, tui.inputEl.value]);
1747             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1748         }
1749         tui.switch_mode('admin');
1750     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1751         explorer.set_portal(tui.inputEl.value);
1752         tui.switch_mode('edit');
1753     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1754         if (tui.inputEl.value.length == 0) {
1755             tui.inputEl.value = " ";
1756         }
1757         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1758         tui.switch_mode('edit');
1759     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1760         explorer.annotate(tui.inputEl.value);
1761         tui.switch_mode('edit');
1762     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1763         if (tui.inputEl.value.length == 0) {
1764             tui.inputEl.value = " ";
1765         }
1766         tui.password = tui.inputEl.value
1767         tui.switch_mode('edit');
1768     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1769         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1770         tui.switch_mode('play');
1771     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1772         if (tui.inputEl.value.length != 1) {
1773             tui.log_msg('@ entered non-single-char, therefore aborted');
1774             tui.switch_mode('admin');
1775         } else {
1776             tui.tile_control_char = tui.inputEl.value[0];
1777             tui.switch_mode('control_pw_pw');
1778         }
1779     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1780         if (tui.inputEl.value.length != 1) {
1781             tui.log_msg('@ entered non-single-char, therefore aborted');
1782             tui.switch_mode('admin');
1783         } else {
1784             tui.tile_control_char = tui.inputEl.value[0];
1785             tui.switch_mode('control_tile_draw');
1786         }
1787     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1788         if (tui.inputEl.value.length != 1) {
1789             tui.log_msg('@ entered non-single-char, therefore aborted');
1790         } else {
1791             server.send(['THING_PROTECTION', tui.inputEl.value])
1792             tui.log_msg('@ sent new protection character for thing');
1793         }
1794         tui.switch_mode('admin');
1795     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1796         let tokens = parser.tokenize(tui.inputEl.value);
1797         if (tokens.length > 0 && tokens[0].length > 0) {
1798             if (tui.inputEl.value[0][0] == '/') {
1799                 if (tokens[0].slice(1) == 'nick') {
1800                     if (tokens.length > 1) {
1801                         server.send(['NICK', tokens[1]]);
1802                     } else {
1803                         tui.log_msg('? need new name');
1804                     }
1805                 } else {
1806                     tui.log_msg('? unknown command');
1807                 }
1808             } else {
1809                     server.send(['ALL', tui.inputEl.value]);
1810             }
1811         } else if (tui.inputEl.valuelength > 0) {
1812                 server.send(['ALL', tui.inputEl.value]);
1813         }
1814         tui.inputEl.value = "";
1815     } else if (tui.mode.name == 'play') {
1816           if (tui.mode.mode_switch_on_key(event)) {
1817               null;
1818           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1819               server.send(["TASK:INTOXICATE"]);
1820           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1821               server.send(["TASK:DOOR"]);
1822           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1823               server.send(["TASK:WEAR"]);
1824           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1825               server.send(["TASK:SPIN"]);
1826           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1827               server.send(["TASK:DANCE"]);
1828           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1829               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1830           } else if (event.key === tui.keys.teleport) {
1831               game.teleport();
1832           };
1833     } else if (tui.mode.name == 'study') {
1834         if (tui.mode.mode_switch_on_key(event)) {
1835               null;
1836         } else if (event.key in tui.movement_keys) {
1837             explorer.move(tui.movement_keys[event.key]);
1838         } else if (event.key == tui.keys.toggle_map_mode) {
1839             tui.toggle_map_mode();
1840         };
1841     } else if (tui.mode.name == 'control_tile_draw') {
1842         if (tui.mode.mode_switch_on_key(event)) {
1843             null;
1844         } else if (event.key in tui.movement_keys) {
1845             explorer.move(tui.movement_keys[event.key]);
1846         } else if (event.key === tui.keys.toggle_tile_draw) {
1847             tui.toggle_tile_draw();
1848         };
1849     } else if (tui.mode.name == 'admin') {
1850         if (tui.mode.mode_switch_on_key(event)) {
1851               null;
1852         } else if (event.key == tui.keys.toggle_map_mode) {
1853             tui.toggle_map_mode();
1854         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1855             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1856         };
1857     } else if (tui.mode.name == 'edit') {
1858         if (tui.mode.mode_switch_on_key(event)) {
1859               null;
1860         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1861             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1862         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1863             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1864           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1865               server.send(["TASK:INSTALL", tui.password]);
1866         } else if (event.key == tui.keys.toggle_map_mode) {
1867             tui.toggle_map_mode();
1868         }
1869     }
1870     tui.full_refresh();
1871 }, false);
1872
1873 rows_selector.addEventListener('input', function() {
1874     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1875         return;
1876     }
1877     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1878     terminal.initialize();
1879     tui.full_refresh();
1880 }, false);
1881 cols_selector.addEventListener('input', function() {
1882     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1883         return;
1884     }
1885     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1886     terminal.initialize();
1887     tui.reset_screen_size();
1888     tui.full_refresh();
1889 }, false);
1890 for (let key_selector of key_selectors) {
1891     key_selector.addEventListener('input', function() {
1892         window.localStorage.setItem(key_selector.id, key_selector.value);
1893         tui.init_keys();
1894     }, false);
1895 }
1896 window.setInterval(function() {
1897     if (server.websocket.readyState == 1) {
1898         server.send(['PING']);
1899     } else if (server.websocket.readyState != 0) {
1900         server.reconnect_to(server.url);
1901         tui.log_msg('@ attempting reconnect …')
1902     }
1903 }, 1000);
1904 window.setInterval(function() {
1905     if (document.activeElement.tagName.toLowerCase() != 'input') {
1906         const scroll_x = window.scrollX;
1907         const scroll_y = window.scrollY;
1908         tui.inputEl.focus();
1909         window.scrollTo(scroll_x, scroll_y);
1910     };
1911 }, 100);
1912 document.getElementById("help").onclick = function() {
1913     tui.show_help = true;
1914     tui.full_refresh();
1915 };
1916 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1917     const mode = switchEl.id.slice("switch_to_".length);
1918     switchEl.onclick = function() {
1919         tui.switch_mode(mode);
1920         tui.full_refresh();
1921     }
1922 };
1923 document.getElementById("toggle_tile_draw").onclick = function() {
1924     tui.toggle_tile_draw();
1925 }
1926 document.getElementById("toggle_map_mode").onclick = function() {
1927     tui.toggle_map_mode();
1928     tui.full_refresh();
1929 };
1930 document.getElementById("flatten").onclick = function() {
1931     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1932 };
1933 document.getElementById("door").onclick = function() {
1934     server.send(['TASK:DOOR']);
1935 };
1936 document.getElementById("consume").onclick = function() {
1937     server.send(['TASK:INTOXICATE']);
1938 };
1939 document.getElementById("install").onclick = function() {
1940     server.send(['TASK:INSTALL', tui.password]);
1941 };
1942 document.getElementById("wear").onclick = function() {
1943     server.send(['TASK:WEAR']);
1944 };
1945 document.getElementById("spin").onclick = function() {
1946     server.send(['TASK:SPIN']);
1947 };
1948 document.getElementById("dance").onclick = function() {
1949     server.send(['TASK:DANCE']);
1950 };
1951 document.getElementById("teleport").onclick = function() {
1952     game.teleport();
1953 };
1954 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1955     if (move_button.id.startsWith('key_')) {  // not a move button
1956         continue;
1957     };
1958     let direction = move_button.id.split('_')[2].toUpperCase();
1959     let move_repeat;
1960     function move() {
1961         if (tui.mode.available_actions.includes("move")) {
1962             server.send(['TASK:MOVE', direction]);
1963         } else if (tui.mode.available_actions.includes("move_explorer")) {
1964             explorer.move(direction);
1965             tui.full_refresh();
1966         };
1967     }
1968     move_button.onmousedown = function() {
1969         move();
1970         move_repeat = window.setInterval(move, 100);
1971     };
1972     move_button.onmouseup = function() {
1973         window.clearInterval(move_repeat);
1974     }
1975 };
1976 </script>
1977 </body></html>