home · contact · privacy
Change some thing type symbols.
[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_face_popup: function() {
1242       const t = game.things[this.draw_face];
1243       if (!t || !t.face) {
1244           this.draw_face = false;
1245           return;
1246       }
1247       const start_x = tui.left_window_width - 10;
1248       function draw_body_part(body_part, end_y) {
1249           terminal.write(end_y - 3, start_x, '----------');
1250           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1251           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1252           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1253       }
1254       if (t.face) {
1255           draw_body_part(t.face, terminal.rows - 3);
1256       }
1257       if (t.hat) {
1258           draw_body_part(t.hat, terminal.rows - 6);
1259       }
1260       terminal.write(terminal.rows - 2, start_x, '----------');
1261       let name = t.name_;
1262       if (name.length > 6) {
1263           name = name.slice(0, 6) + '…';
1264       }
1265       terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1266   },
1267   draw_mode_line: function() {
1268       let help = 'hit [' + this.keys.help + '] for help';
1269       if (this.mode.has_input_prompt) {
1270           help = 'enter /help for help';
1271       }
1272       terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1273   },
1274   draw_stats_line: function(n) {
1275       terminal.write(0, this.left_window_width,
1276                      'ENERGY: ' + game.energy +
1277                      ' BLADDER: ' + game.bladder_pressure);
1278   },
1279   draw_history: function() {
1280       let log_display_lines = [];
1281       let log_links = {};
1282       let y_offset_in_log = 0;
1283       for (let line of this.log) {
1284           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1285                                                                     this.right_window_width)
1286           log_display_lines = log_display_lines.concat(new_lines);
1287           for (const y in link_data) {
1288               const rel_y = y_offset_in_log + parseInt(y);
1289               log_links[rel_y] = [];
1290               for (let link of link_data[y]) {
1291                   log_links[rel_y].push(link);
1292               }
1293           }
1294           y_offset_in_log += new_lines.length;
1295       };
1296       let i = log_display_lines.length - 1;
1297       for (let y = terminal.rows - 1 - this.height_input;
1298            y >= this.height_header && i >= 0;
1299            y--, i--) {
1300           terminal.write(y, this.left_window_width, log_display_lines[i]);
1301       }
1302       for (const key of Object.keys(log_links)) {
1303           if (parseInt(key) <= i) {
1304               delete log_links[key];
1305           }
1306       }
1307       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1308                     this.left_window_width];
1309       this.offset_links(offset, log_links);
1310   },
1311   draw_info: function() {
1312       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1313       let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1314       let offset = [this.height_header, this.left_window_width];
1315       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1316         terminal.write(y, offset[1], lines[i]);
1317       }
1318       this.offset_links(offset, link_data);
1319   },
1320   draw_input: function() {
1321     if (this.mode.has_input_prompt) {
1322         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1323             terminal.write(y, this.left_window_width, this.input_lines[i]);
1324         }
1325     }
1326   },
1327   draw_help: function() {
1328       let movement_keys_desc = '';
1329       if (!this.mode.is_intro) {
1330           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1331       }
1332       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1333       if (this.mode.available_actions.length > 0) {
1334           content += "Available actions:\n";
1335           for (let action of this.mode.available_actions) {
1336               if (Object.keys(this.action_tasks).includes(action)) {
1337                   if (!this.task_action_on(action)) {
1338                       continue;
1339                   }
1340               }
1341               if (action == 'move_explorer') {
1342                   action = 'move';
1343               }
1344               if (action == 'move') {
1345                   content += "[" + movement_keys_desc + "] – move\n"
1346               } else {
1347                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1348               }
1349           }
1350           content += '\n';
1351       }
1352       content += this.mode.list_available_modes();
1353       let start_x = 0;
1354       let lines = [];
1355       let _ = undefined;
1356       if (!this.mode.has_input_prompt) {
1357           start_x = this.left_window_width;
1358           this.draw_links = false;
1359           terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1360           [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1361       } else {
1362           start_x = 0;
1363           terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1364           [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1365       }
1366       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1367           terminal.write(y, start_x, lines[i]);
1368       }
1369   },
1370   toggle_tile_draw: function() {
1371       if (tui.tile_draw) {
1372           tui.tile_draw = false;
1373       } else {
1374           tui.tile_draw = true;
1375       }
1376   },
1377   toggle_map_mode: function() {
1378       if (tui.map_mode == 'terrain only') {
1379           tui.map_mode = 'terrain + annotations';
1380       } else if (tui.map_mode == 'terrain + annotations') {
1381           tui.map_mode = 'terrain + things';
1382       } else if (tui.map_mode == 'terrain + things') {
1383           tui.map_mode = 'protections';
1384       } else if (tui.map_mode == 'protections') {
1385           tui.map_mode = 'terrain only';
1386       }
1387   },
1388   full_refresh: function() {
1389     this.draw_links = true;
1390     this.links = {};
1391     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1392     this.recalc_input_lines();
1393     if (this.mode.is_intro) {
1394         this.draw_history();
1395         this.draw_input();
1396     } else {
1397         this.draw_map();
1398         this.draw_stats_line();
1399         this.draw_mode_line();
1400         if (this.mode.shows_info) {
1401           this.draw_info();
1402         } else {
1403           this.draw_history();
1404         }
1405         this.draw_input();
1406     }
1407     if (this.show_help) {
1408         this.draw_help();
1409     }
1410     if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1411         this.draw_face_popup();
1412     }
1413     if (!this.draw_links) {
1414         this.links = {};
1415     }
1416     terminal.refresh();
1417   }
1418 }
1419
1420 let game = {
1421     init: function() {
1422         this.turn = -1;
1423         this.player_id = -1;
1424         this.tasks = {};
1425         this.things = {};
1426         this.things_new = {};
1427         this.fov = "";
1428         this.fov_new = "";
1429         this.map = "";
1430         this.map_new = "";
1431         this.map_control = "";
1432         this.map_control_new = "";
1433         this.map_size = [0,0];
1434         this.map_size_new = [0,0];
1435         this.portals = {};
1436         this.portals_new = {};
1437         this.players_hat_chars = "";
1438         this.bladder_pressure = 0;
1439         this.bladder_pressure_new = 0;
1440     },
1441     get_thing_temp: function(id_, create_if_not_found=false) {
1442         if (id_ in game.things_new) {
1443             return game.things_new[id_];
1444         } else if (create_if_not_found) {
1445             let t = new Thing([0,0]);
1446             game.things_new[id_] = t;
1447             return t;
1448         };
1449     },
1450     get_thing: function(id_, create_if_not_found=false) {
1451         if (id_ in game.things) {
1452             return game.things[id_];
1453         };
1454     },
1455     move: function(start_position, direction) {
1456         let target = [start_position[0], start_position[1]];
1457         if (direction == 'LEFT') {
1458             target[1] -= 1;
1459         } else if (direction == 'RIGHT') {
1460             target[1] += 1;
1461         } else if (game.map_geometry == 'Square') {
1462             if (direction == 'UP') {
1463                 target[0] -= 1;
1464             } else if (direction == 'DOWN') {
1465                 target[0] += 1;
1466             };
1467         } else if (game.map_geometry == 'Hex') {
1468             let start_indented = start_position[0] % 2;
1469             if (direction == 'UPLEFT') {
1470                 target[0] -= 1;
1471                 if (!start_indented) {
1472                     target[1] -= 1;
1473                 }
1474             } else if (direction == 'UPRIGHT') {
1475                 target[0] -= 1;
1476                 if (start_indented) {
1477                     target[1] += 1;
1478                 }
1479             } else if (direction == 'DOWNLEFT') {
1480                 target[0] += 1;
1481                 if (!start_indented) {
1482                     target[1] -= 1;
1483                 }
1484             } else if (direction == 'DOWNRIGHT') {
1485                 target[0] += 1;
1486                 if (start_indented) {
1487                     target[1] += 1;
1488                 }
1489             };
1490         };
1491         if (target[0] < 0 || target[1] < 0 ||
1492             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1493             return null;
1494         };
1495         return target;
1496     },
1497     teleport: function() {
1498         if (game.player.position in this.portals) {
1499             server.reconnect_to(this.portals[game.player.position]);
1500         } else {
1501             terminal.blink_screen();
1502             tui.log_msg('? not standing on portal')
1503         }
1504     }
1505 }
1506
1507 game.init();
1508 tui.init();
1509 tui.full_refresh();
1510 server.init(websocket_location);
1511
1512 let explorer = {
1513     position: [0,0],
1514     annotations: {},
1515     annotations_new: {},
1516     info_cached: false,
1517     move: function(direction) {
1518         let target = game.move(this.position, direction);
1519         if (target) {
1520             this.position = target
1521             this.info_cached = false;
1522             if (tui.tile_draw) {
1523                 this.send_tile_control_command();
1524             }
1525         } else {
1526             terminal.blink_screen();
1527         };
1528     },
1529     get_info: function() {
1530         if (this.info_cached) {
1531             return this.info_cached;
1532         }
1533         let info_to_cache = '';
1534         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1535         if (game.fov[position_i] != '.') {
1536             info_to_cache += 'outside field of view';
1537         } else {
1538             for (let t_id in game.things) {
1539                  let t = game.things[t_id];
1540                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1541                      info_to_cache += this.get_thing_info(t, true);
1542                  }
1543             }
1544             let terrain_char = game.map[position_i]
1545             let terrain_desc = '?'
1546             if (game.terrains[terrain_char]) {
1547                 terrain_desc = game.terrains[terrain_char];
1548             };
1549             info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1550             let protection = game.map_control[position_i];
1551             if (protection != '.') {
1552                 info_to_cache += '/protection:' + protection;
1553             };
1554             info_to_cache += ')\n';
1555             if (this.position in game.portals) {
1556                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1557             }
1558             if (this.position in this.annotations) {
1559                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1560             }
1561         }
1562         this.info_cached = info_to_cache;
1563         return this.info_cached;
1564     },
1565     get_thing_info: function(t, detailed=false) {
1566         let info = '';
1567         if (detailed) {
1568             info += '- ';
1569         }
1570         info += game.thing_types[t.type_];
1571         if (t.thing_char) {
1572             info += t.thing_char;
1573         };
1574         if (t.name_) {
1575             info += ": " + t.name_;
1576         }
1577         info += ' (' + t.type_;
1578         if (t.installed) {
1579             info += "/installed";
1580         }
1581         if (t.type_ == 'Bottle') {
1582             if (t.thing_char == '_') {
1583                 info += '/empty';
1584             } else if (t.thing_char == '~') {
1585                 info += '/full';
1586             }
1587         }
1588         if (detailed) {
1589             const protection = t.protection;
1590             if (protection != '.') {
1591                 info += '/protection:' + protection;
1592             }
1593             info += ')\n';
1594             if (t.hat || t.face) {
1595                 info += '----------\n';
1596             }
1597             if (t.hat) {
1598                 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1599                 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1600                 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1601             }
1602             if (t.face) {
1603                 info += '| ' + t.face.slice(0, 6) + ' |\n';
1604                 info += '| ' + t.face.slice(6, 12) + ' |\n';
1605                 info += '| ' + t.face.slice(12, 18) + ' |\n';
1606                 info += '----------\n';
1607             }
1608             if (t.design) {
1609                 const line_length = t.design[0][1];
1610                 info += '-'.repeat(line_length + 4) + '\n';
1611                 let lines = ['']
1612                 if (line_length > 0) {
1613                     const regexp = RegExp('.{1,' + line_length + '}', 'g');
1614                     lines = t.design[1].match(regexp);
1615                 }
1616                 for (const line of lines) {
1617                     info += '| ' + line + ' |\n';
1618                 }
1619                 info += '-'.repeat(line_length + 4) + '\n';
1620             }
1621         } else {
1622             info += ')';
1623         }
1624         return info;
1625     },
1626     annotate: function(msg) {
1627         if (msg.length == 0) {
1628             msg = " ";  // triggers annotation deletion
1629         }
1630         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1631     },
1632     set_portal: function(msg) {
1633         if (msg.length == 0) {
1634             msg = " ";  // triggers portal deletion
1635         }
1636         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1637     },
1638     send_tile_control_command: function() {
1639         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1640     }
1641 }
1642
1643 tui.inputEl.addEventListener('input', (event) => {
1644     if (tui.mode.has_input_prompt) {
1645         let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1646         if (tui.inputEl.value.length > max_length) {
1647             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1648         };
1649     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1650         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1651         tui.switch_mode('edit');
1652     }
1653     tui.full_refresh();
1654 }, false);
1655 document.onclick = function() {
1656     if (!tui.mode.is_single_char_entry) {
1657         tui.show_help = false;
1658     }
1659 };
1660 tui.inputEl.addEventListener('keydown', (event) => {
1661     tui.show_help = false;
1662     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1663         event.preventDefault();
1664     }
1665     if ((!tui.mode.is_intro && event.key == 'Escape')
1666         || (tui.mode.has_input_prompt && event.key == 'Enter'
1667             && tui.inputEl.value.length == 0
1668             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1669                 'admin_enter'].includes(tui.mode.name))) {
1670         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1671             tui.log_msg('@ aborted');
1672         }
1673         tui.switch_mode('play');
1674     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1675         tui.show_help = true;
1676         tui.inputEl.value = "";
1677         tui.restore_input_values();
1678     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1679                && !tui.mode.is_single_char_entry) {
1680         tui.show_help = true;
1681     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1682         tui.login_name = tui.inputEl.value;
1683         server.send(['LOGIN', tui.inputEl.value]);
1684         tui.inputEl.value = "";
1685     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1686         tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1687     } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1688         if (game.player.carrying.type_ == 'Hat') {
1689             tui.enter_ascii_art('THING_DESIGN',
1690                                 game.player.carrying.design[0][0],
1691                                 game.player.carrying.design[0][1], true);
1692         } else {
1693             tui.enter_ascii_art('THING_DESIGN',
1694                                 game.player.carrying.design[0][0],
1695                                 game.player.carrying.design[0][1], true, true);
1696         }
1697     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1698         server.send(['TASK:COMMAND', tui.inputEl.value]);
1699         tui.inputEl.value = "";
1700     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1701         tui.pick_selectable('PICK_UP');
1702     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1703         tui.pick_selectable('DROP');
1704     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1705         if (tui.inputEl.value.length == 0) {
1706             tui.log_msg('@ aborted');
1707         } else {
1708             server.send(['SET_MAP_CONTROL_PASSWORD',
1709                         tui.tile_control_char, tui.inputEl.value]);
1710             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1711         }
1712         tui.switch_mode('admin');
1713     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1714         explorer.set_portal(tui.inputEl.value);
1715         tui.switch_mode('edit');
1716     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1717         if (tui.inputEl.value.length == 0) {
1718             tui.inputEl.value = " ";
1719         }
1720         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1721         tui.switch_mode('edit');
1722     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1723         explorer.annotate(tui.inputEl.value);
1724         tui.switch_mode('edit');
1725     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1726         if (tui.inputEl.value.length == 0) {
1727             tui.inputEl.value = " ";
1728         }
1729         tui.password = tui.inputEl.value
1730         tui.switch_mode('edit');
1731     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1732         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1733         tui.switch_mode('play');
1734     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1735         if (tui.inputEl.value.length != 1) {
1736             tui.log_msg('@ entered non-single-char, therefore aborted');
1737             tui.switch_mode('admin');
1738         } else {
1739             tui.tile_control_char = tui.inputEl.value[0];
1740             tui.switch_mode('control_pw_pw');
1741         }
1742     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1743         if (tui.inputEl.value.length != 1) {
1744             tui.log_msg('@ entered non-single-char, therefore aborted');
1745             tui.switch_mode('admin');
1746         } else {
1747             tui.tile_control_char = tui.inputEl.value[0];
1748             tui.switch_mode('control_tile_draw');
1749         }
1750     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1751         if (tui.inputEl.value.length != 1) {
1752             tui.log_msg('@ entered non-single-char, therefore aborted');
1753         } else {
1754             server.send(['THING_PROTECTION', tui.inputEl.value])
1755             tui.log_msg('@ sent new protection character for thing');
1756         }
1757         tui.switch_mode('admin');
1758     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1759         let tokens = parser.tokenize(tui.inputEl.value);
1760         if (tokens.length > 0 && tokens[0].length > 0) {
1761             if (tui.inputEl.value[0][0] == '/') {
1762                 if (tokens[0].slice(1) == 'nick') {
1763                     if (tokens.length > 1) {
1764                         server.send(['NICK', tokens[1]]);
1765                     } else {
1766                         tui.log_msg('? need new name');
1767                     }
1768                 } else {
1769                     tui.log_msg('? unknown command');
1770                 }
1771             } else {
1772                     server.send(['ALL', tui.inputEl.value]);
1773             }
1774         } else if (tui.inputEl.valuelength > 0) {
1775                 server.send(['ALL', tui.inputEl.value]);
1776         }
1777         tui.inputEl.value = "";
1778     } else if (tui.mode.name == 'play') {
1779           if (tui.mode.mode_switch_on_key(event)) {
1780               null;
1781           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1782               server.send(["TASK:INTOXICATE"]);
1783           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1784               server.send(["TASK:DOOR"]);
1785           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1786               server.send(["TASK:WEAR"]);
1787           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1788               server.send(["TASK:SPIN"]);
1789           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1790               server.send(["TASK:DANCE"]);
1791           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1792               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1793           } else if (event.key === tui.keys.teleport) {
1794               game.teleport();
1795           };
1796     } else if (tui.mode.name == 'study') {
1797         if (tui.mode.mode_switch_on_key(event)) {
1798               null;
1799         } else if (event.key in tui.movement_keys) {
1800             explorer.move(tui.movement_keys[event.key]);
1801         } else if (event.key == tui.keys.toggle_map_mode) {
1802             tui.toggle_map_mode();
1803         };
1804     } else if (tui.mode.name == 'control_tile_draw') {
1805         if (tui.mode.mode_switch_on_key(event)) {
1806             null;
1807         } else if (event.key in tui.movement_keys) {
1808             explorer.move(tui.movement_keys[event.key]);
1809         } else if (event.key === tui.keys.toggle_tile_draw) {
1810             tui.toggle_tile_draw();
1811         };
1812     } else if (tui.mode.name == 'admin') {
1813         if (tui.mode.mode_switch_on_key(event)) {
1814               null;
1815         } else if (event.key == tui.keys.toggle_map_mode) {
1816             tui.toggle_map_mode();
1817         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1818             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1819         };
1820     } else if (tui.mode.name == 'edit') {
1821         if (tui.mode.mode_switch_on_key(event)) {
1822               null;
1823         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1824             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1825         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1826             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1827           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1828               server.send(["TASK:INSTALL", tui.password]);
1829         } else if (event.key == tui.keys.toggle_map_mode) {
1830             tui.toggle_map_mode();
1831         }
1832     }
1833     tui.full_refresh();
1834 }, false);
1835
1836 rows_selector.addEventListener('input', function() {
1837     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1838         return;
1839     }
1840     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1841     terminal.initialize();
1842     tui.full_refresh();
1843 }, false);
1844 cols_selector.addEventListener('input', function() {
1845     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1846         return;
1847     }
1848     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1849     terminal.initialize();
1850     tui.reset_screen_size();
1851     tui.full_refresh();
1852 }, false);
1853 for (let key_selector of key_selectors) {
1854     key_selector.addEventListener('input', function() {
1855         window.localStorage.setItem(key_selector.id, key_selector.value);
1856         tui.init_keys();
1857     }, false);
1858 }
1859 window.setInterval(function() {
1860     if (server.websocket.readyState == 1) {
1861         server.send(['PING']);
1862     } else if (server.websocket.readyState != 0) {
1863         server.reconnect_to(server.url);
1864         tui.log_msg('@ attempting reconnect …')
1865     }
1866 }, 1000);
1867 window.setInterval(function() {
1868     if (document.activeElement.tagName.toLowerCase() != 'input') {
1869         const scroll_x = window.scrollX;
1870         const scroll_y = window.scrollY;
1871         tui.inputEl.focus();
1872         window.scrollTo(scroll_x, scroll_y);
1873     };
1874 }, 100);
1875 document.getElementById("help").onclick = function() {
1876     tui.show_help = true;
1877     tui.full_refresh();
1878 };
1879 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1880     const mode = switchEl.id.slice("switch_to_".length);
1881     switchEl.onclick = function() {
1882         tui.switch_mode(mode);
1883         tui.full_refresh();
1884     }
1885 };
1886 document.getElementById("toggle_tile_draw").onclick = function() {
1887     tui.toggle_tile_draw();
1888 }
1889 document.getElementById("toggle_map_mode").onclick = function() {
1890     tui.toggle_map_mode();
1891     tui.full_refresh();
1892 };
1893 document.getElementById("flatten").onclick = function() {
1894     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1895 };
1896 document.getElementById("door").onclick = function() {
1897     server.send(['TASK:DOOR']);
1898 };
1899 document.getElementById("consume").onclick = function() {
1900     server.send(['TASK:INTOXICATE']);
1901 };
1902 document.getElementById("install").onclick = function() {
1903     server.send(['TASK:INSTALL', tui.password]);
1904 };
1905 document.getElementById("wear").onclick = function() {
1906     server.send(['TASK:WEAR']);
1907 };
1908 document.getElementById("spin").onclick = function() {
1909     server.send(['TASK:SPIN']);
1910 };
1911 document.getElementById("dance").onclick = function() {
1912     server.send(['TASK:DANCE']);
1913 };
1914 document.getElementById("teleport").onclick = function() {
1915     game.teleport();
1916 };
1917 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1918     if (move_button.id.startsWith('key_')) {  // not a move button
1919         continue;
1920     };
1921     let direction = move_button.id.split('_')[2].toUpperCase();
1922     let move_repeat;
1923     function move() {
1924         if (tui.mode.available_actions.includes("move")) {
1925             server.send(['TASK:MOVE', direction]);
1926         } else if (tui.mode.available_actions.includes("move_explorer")) {
1927             explorer.move(direction);
1928             tui.full_refresh();
1929         };
1930     }
1931     move_button.onmousedown = function() {
1932         move();
1933         move_repeat = window.setInterval(move, 100);
1934     };
1935     move_button.onmouseup = function() {
1936         window.clearInterval(move_repeat);
1937     }
1938 };
1939 </script>
1940 </body></html>