home · contact · privacy
e7004a177f913ed3c27e568bdf94b04d3c7f624d
[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             tui.log_msg('@ hint: see top of terminal for how to get help.')
582             tui.log_msg('@ hint: enter study mode to understand your environment.')
583         } else if (tokens[0] === 'DEFAULT_COLORS') {
584             terminal.set_default_colors();
585         } else if (tokens[0] === 'RANDOM_COLORS') {
586             terminal.set_random_colors();
587         } else if (tokens[0] === 'ADMIN_OK') {
588             tui.is_admin = true;
589             tui.log_msg('@ you now have admin rights');
590             tui.switch_mode('admin');
591         } else if (tokens[0] === 'PORTAL') {
592             let position = parser.parse_yx(tokens[1]);
593             game.portals_new[position] = tokens[2];
594         } else if (tokens[0] === 'ANNOTATION') {
595             let position = parser.parse_yx(tokens[1]);
596             explorer.annotations_new[position] = tokens[2];
597         } else if (tokens[0] === 'UNHANDLED_INPUT') {
598             tui.log_msg('? unknown command');
599         } else if (tokens[0] === 'PLAY_ERROR') {
600             tui.log_msg('? ' + tokens[1]);
601             terminal.blink_screen();
602         } else if (tokens[0] === 'ARGUMENT_ERROR') {
603             tui.log_msg('? syntax error: ' + tokens[1]);
604         } else if (tokens[0] === 'GAME_ERROR') {
605             tui.log_msg('? game error: ' + tokens[1]);
606         } else if (tokens[0] === 'PONG') {
607             ;
608         } else {
609             tui.log_msg('? unhandled input: ' + event.data);
610         }
611     }
612 }
613
614 let unparser = {
615     quote: function(str) {
616         let quoted = ['"'];
617         for (let i = 0; i < str.length; i++) {
618             let c = str[i];
619             if (['"', '\\'].includes(c)) {
620                 quoted.push('\\');
621             };
622             quoted.push(c);
623         }
624         quoted.push('"');
625         return quoted.join('');
626     },
627     to_yx: function(yx_coordinate) {
628         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
629     },
630     untokenize: function(tokens) {
631         let quoted_tokens = [];
632         for (let token of tokens) {
633             quoted_tokens.push(this.quote(token));
634         }
635         return quoted_tokens.join(" ");
636     }
637 }
638
639 class Mode {
640     constructor(name, has_input_prompt=false, shows_info=false,
641                 is_intro=false, is_single_char_entry=false) {
642         this.name = name;
643         this.short_desc = mode_helps[name].short;
644         this.available_modes = [];
645         this.available_actions = [];
646         this.has_input_prompt = has_input_prompt;
647         this.shows_info= shows_info;
648         this.is_intro = is_intro;
649         this.help_intro = mode_helps[name].long;
650         this.intro_msg = mode_helps[name].intro;
651         this.is_single_char_entry = is_single_char_entry;
652         this.legal = true;
653     }
654     *iter_available_modes() {
655         for (let mode_name of this.available_modes) {
656             let mode = tui['mode_' + mode_name];
657             if (!mode.legal) {
658                 continue;
659             }
660             let key = tui.keys['switch_to_' + mode.name];
661             yield [mode, key]
662         }
663     }
664     list_available_modes() {
665         let msg = ''
666         if (this.available_modes.length > 0) {
667             msg += 'Other modes available from here:\n';
668             for (let [mode, key] of this.iter_available_modes()) {
669                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
670             }
671         }
672         return msg;
673     }
674     mode_switch_on_key(key_event) {
675         for (let [mode, key] of this.iter_available_modes()) {
676             if (key_event.key == key) {
677                 event.preventDefault();
678                 tui.switch_mode(mode.name);
679                 return true;
680             };
681         }
682         return false;
683     }
684 }
685 let tui = {
686   links: {},
687   log: [],
688   input_prompt: '> ',
689   input_lines: [],
690   height_turn_line: 1,
691   height_mode_line: 1,
692   height_input: 1,
693   password: 'foo',
694   show_help: false,
695   is_admin: false,
696   tile_draw: false,
697   mode_waiting_for_server: new Mode('waiting_for_server',
698                                      false, false, true),
699   mode_login: new Mode('login', true, false, true),
700   mode_post_login_wait: new Mode('post_login_wait'),
701   mode_chat: new Mode('chat', true),
702   mode_annotate: new Mode('annotate', true, true),
703   mode_play: new Mode('play'),
704   mode_study: new Mode('study', false, true),
705   mode_write: new Mode('write', false, false, false, true),
706   mode_edit: new Mode('edit'),
707   mode_control_pw_type: new Mode('control_pw_type', true),
708   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
709   mode_portal: new Mode('portal', true, true),
710   mode_password: new Mode('password', true),
711   mode_name_thing: new Mode('name_thing', true, true),
712   mode_command_thing: new Mode('command_thing', true),
713   mode_take_thing: new Mode('take_thing', true),
714   mode_drop_thing: new Mode('drop_thing', true),
715   mode_enter_face: new Mode('enter_face', true),
716   mode_enter_design: new Mode('enter_design', true),
717   mode_admin_enter: new Mode('admin_enter', true),
718   mode_admin: new Mode('admin'),
719   mode_control_pw_pw: new Mode('control_pw_pw', true),
720   mode_control_tile_type: new Mode('control_tile_type', true),
721   mode_control_tile_draw: new Mode('control_tile_draw'),
722   action_tasks: {
723       'flatten': 'FLATTEN_SURROUNDINGS',
724       'take_thing': 'PICK_UP',
725       'drop_thing': 'DROP',
726       'move': 'MOVE',
727       'door': 'DOOR',
728       'install': 'INSTALL',
729       'wear': 'WEAR',
730       'command': 'COMMAND',
731       'consume': 'INTOXICATE',
732       'spin': 'SPIN',
733       'dance': 'DANCE',
734   },
735   offset: [0,0],
736   map_lines: [],
737   ascii_draw_stage: 0,
738   full_ascii_draw: '',
739   selectables: [],
740   draw_face: false,
741   init: function() {
742       this.reset_screen_size();
743       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
744                                         "command_thing", "take_thing", "drop_thing"]
745       this.mode_play.available_actions = ["move", "teleport", "door", "consume",
746                                           "wear", "spin", "dance"];
747       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
748       this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
749       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
750                                          "control_tile_type", "chat",
751                                          "study", "play", "edit"]
752       this.mode_admin.available_actions = ["move", "toggle_map_mode"];
753       this.mode_control_tile_draw.available_modes = ["admin_enter"]
754       this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
755       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
756                                         "enter_design", "password", "chat", "study",
757                                         "play", "admin_enter", "enter_face"]
758       this.mode_edit.available_actions = ["move", "flatten", "install",
759                                           "toggle_map_mode"]
760       this.inputEl = document.getElementById("input");
761       this.switch_mode('waiting_for_server');
762       this.recalc_input_lines();
763       this.height_header = this.height_turn_line + this.height_mode_line;
764       this.init_keys();
765   },
766   reset_screen_size: function() {
767       this.left_window_width = Math.min(52, terminal.cols / 2);
768       this.right_window_width = terminal.cols - tui.left_window_width;
769   },
770   init_keys: function() {
771     document.getElementById("move_table").hidden = true;
772     this.keys = {};
773     for (let key_selector of key_selectors) {
774         this.keys[key_selector.id.slice(4)] = key_selector.value;
775     }
776     this.movement_keys = {};
777     let geometry_prefix = 'undefinedMapGeometry_';
778     if (game.map_geometry) {
779         geometry_prefix = game.map_geometry.toLowerCase() + '_';
780     }
781     for (const key_name of Object.keys(key_descriptions)) {
782         if (key_name.startsWith(geometry_prefix)) {
783             let direction = key_name.split('_')[2].toUpperCase();
784             let key = this.keys[key_name];
785             this.movement_keys[key] = direction;
786         }
787     };
788     for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
789         if (move_button.id.startsWith('key_')) {
790             continue;
791         }
792         move_button.hidden = true;
793     };
794     for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
795         document.getElementById("move_table").hidden = false;
796         move_button.hidden = false;
797     };
798     for (let el of document.getElementsByTagName("button")) {
799       let action_desc = key_descriptions[el.id];
800       let action_key = '[' + this.keys[el.id] + ']';
801       el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
802     }
803   },
804   task_action_on: function(action) {
805       return game.tasks.includes(this.action_tasks[action]);
806   },
807   switch_mode: function(mode_name) {
808
809     function fail(msg, return_mode='play') {
810         tui.log_msg('? ' + msg);
811         terminal.blink_screen();
812         tui.switch_mode(return_mode);
813     }
814
815     if (this.mode && this.mode.name == 'control_tile_draw') {
816         tui.log_msg('@ finished tile protection drawing.')
817     }
818     this.draw_face = false;
819     this.tile_draw = false;
820     this.ascii_draw_stage = 0;
821     this.full_ascii_draw = '';
822     if (mode_name == 'command_thing' && (!game.player.carrying
823                                          || !game.player.carrying.commandable)) {
824         return fail('not carrying anything commandable');
825     } else if (mode_name == 'name_thing' && !game.player.carrying) {
826         return fail('not carrying anything to re-name', 'edit');
827     } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
828         return fail('not carrying anything to protect')
829     } else if (mode_name == 'take_thing' && game.player.carrying) {
830         return fail('already carrying something');
831     } else if (mode_name == 'drop_thing' && !game.player.carrying) {
832         return fail('not carrying anything droppable');
833     } else if (mode_name == 'enter_design' && (!game.player.carrying
834                                                || !game.player.carrying.design)) {
835         return fail('not carrying designable to edit', 'edit');
836     }
837     if (mode_name == 'admin_enter' && this.is_admin) {
838         mode_name = 'admin';
839     };
840     this.mode = this['mode_' + mode_name];
841     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
842         this.map_mode = 'protections';
843     } else if (this.mode.name != "edit") {
844         this.map_mode = 'terrain + things';
845     };
846     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
847         explorer.position = game.player.position;
848     }
849     this.inputEl.value = "";
850     this.restore_input_values();
851     for (let el of document.getElementsByTagName("button")) {
852         el.disabled = true;
853     }
854     document.getElementById("help").disabled = false;
855     for (const action of this.mode.available_actions) {
856         if (["move", "move_explorer"].includes(action)) {
857             for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
858                 move_key.disabled = false;
859             }
860         } else if (Object.keys(this.action_tasks).includes(action)) {
861             if (this.task_action_on(action)) {
862                 document.getElementById(action).disabled = false;
863             }
864         } else {
865             document.getElementById(action).disabled = false;
866         };
867     }
868     for (const mode_name of this.mode.available_modes) {
869             document.getElementById('switch_to_' + mode_name).disabled = false;
870     }
871     if (this.mode.intro_msg.length > 0) {
872         this.log_msg(this.mode.intro_msg);
873     }
874     if (this.mode.name == 'login') {
875         if (this.login_name) {
876             server.send(['LOGIN', this.login_name]);
877         } else {
878             this.log_msg("? need login name");
879         }
880     } else if (this.mode.is_single_char_entry) {
881         this.show_help = true;
882     } else if (this.mode.name == 'take_thing') {
883         this.log_msg("Portable things in reach for pick-up:");
884         const y = game.player.position[0]
885         const x = game.player.position[1]
886         let directed_moves = {
887             'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
888         }
889         if (game.map_geometry == 'Square') {
890             directed_moves['UP'] = [-1, 0];
891             directed_moves['DOWN'] = [1, 0];
892         } else if (game.map_geometry == 'Hex') {
893             if (y % 2) {
894                 directed_moves['UPLEFT'] = [-1, 0];
895                 directed_moves['UPRIGHT'] = [-1, 1];
896                 directed_moves['DOWNLEFT'] = [1, 0];
897                 directed_moves['DOWNRIGHT'] = [1, 1];
898             } else {
899                 directed_moves['UPLEFT'] = [-1, -1];
900                 directed_moves['UPRIGHT'] = [-1, 0];
901                 directed_moves['DOWNLEFT'] = [1, -1];
902                 directed_moves['DOWNRIGHT'] = [1, 0];
903             }
904         }
905         let select_range = {};
906         for (const direction in directed_moves) {
907             const move = directed_moves[direction];
908             select_range[direction] = [y + move[0], x + move[1]];
909         }
910         this.selectables = [];
911         let directions = [];
912         for (const direction in select_range) {
913             for (const t_id in game.things) {
914                 const t = game.things[t_id];
915                 const position = select_range[direction];
916                 if (t.portable
917                     && t.position[0] == position[0]
918                     && t.position[1] == position[1]) {
919                     this.selectables.push(t_id);
920                     directions.push(direction);
921                 }
922             }
923         }
924         if (this.selectables.length == 0) {
925             this.log_msg('none');
926             terminal.blink_screen();
927             this.switch_mode('play');
928             return;
929         } else {
930             for (let [i, t_id] of this.selectables.entries()) {
931                 const t = game.things[t_id];
932                 const direction = directions[i];
933                 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
934             }
935         }
936     } else if (this.mode.name == 'drop_thing') {
937         this.log_msg('Direction to drop thing to:');
938         this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
939         for (let [i, direction] of this.selectables.entries()) {
940             this.log_msg(i + ': ' + direction);
941         };
942     } else if (this.mode.name == 'enter_design') {
943         if (game.player.carrying.type_ == 'Hat') {
944             this.log_msg('@ The design you enter must be '
945                          + game.player.carrying.design[0][0] + ' lines of max '
946                          + game.player.carrying.design[0][1] + ' characters width each');
947             this.log_msg('@ Legal characters: ' + game.players_hat_chars);
948             this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
949         } else {
950             this.log_msg('@ Width of first line determines maximum width for remaining design')
951             this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
952         }
953     } else if (this.mode.name == 'command_thing') {
954         server.send(['TASK:COMMAND', 'HELP']);
955     } else if (this.mode.name == 'control_pw_pw') {
956         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
957     } else if (this.mode.name == 'control_tile_draw') {
958         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 + '].')
959     }
960     this.full_refresh();
961   },
962   offset_links: function(offset, links) {
963       for (let y in links) {
964           let real_y = offset[0] + parseInt(y);
965           if (!this.links[real_y]) {
966               this.links[real_y] = [];
967           }
968           for (let link of links[y]) {
969               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
970               this.links[real_y].push(offset_link);
971           }
972       }
973   },
974   restore_input_values: function() {
975       if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
976           let info = explorer.annotations[explorer.position];
977           if (info != "(none)") {
978               this.inputEl.value = info;
979           }
980       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
981           let portal = game.portals[explorer.position]
982           this.inputEl.value = portal;
983       } else if (this.mode.name == 'password') {
984           this.inputEl.value = this.password;
985       } else if (this.mode.name == 'name_thing') {
986           if (game.player.carrying && game.player.carrying.name_) {
987               this.inputEl.value = game.player.carrying.name_;
988           }
989       } else if (this.mode.name == 'admin_thing_protect') {
990           if (game.player.carrying && game.player.carrying.protection) {
991               this.inputEl.value = game.player.carrying.protection;
992           }
993       } else if (this.mode.name == 'enter_face') {
994           const start = this.ascii_draw_stage * 6;
995           const end = (this.ascii_draw_stage + 1) * 6;
996           this.inputEl.value = game.player.face.slice(start, end);
997       } else if (this.mode.name == 'enter_design') {
998           const width = game.player.carrying.design[0][1];
999           const start = this.ascii_draw_stage * width;
1000           const end = (this.ascii_draw_stage + 1) * width;
1001           this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1002       }
1003   },
1004   recalc_input_lines: function() {
1005       if (this.mode.has_input_prompt) {
1006           let _ = null;
1007           [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1008       } else {
1009           this.input_lines = [];
1010       }
1011       this.height_input = this.input_lines.length;
1012   },
1013   msg_into_lines_of_width: function(msg, width) {
1014       function push_inner_link(y, end_x) {
1015           if (!inner_links[y]) {
1016               inner_links[y] = [];
1017           };
1018           inner_links[y].push([url_start_x, end_x, url]);
1019       };
1020       let link_data = {};
1021       let url_ends = [];
1022       const regexp = RegExp('https?://[^\\s]+', 'g');
1023       let match;
1024       while ((match = regexp.exec(msg)) !== null) {
1025           const url = match[0];
1026           const url_start = match.index;
1027           const url_end = match.index + match[0].length;
1028           link_data[url_start] = url;
1029           url_ends.push(url_end);
1030       }
1031       let url_start_x = 0;
1032       let url = '';
1033       let inner_links = {};
1034       let in_link = false;
1035       let chunk = "";
1036       let lines = [];
1037       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1038           if (x >= width || msg[i] == "\n") {
1039               if (in_link) {
1040                   push_inner_link(y, chunk.length);
1041                   url_start_x = 0;
1042                   if (url_ends[0] == i) {
1043                       in_link = false;
1044                       url_ends.shift();
1045                   }
1046               };
1047               lines.push(chunk);
1048               chunk = "";
1049               x = 0;
1050               if (msg[i] == "\n") {
1051                   x -= 1;
1052               };
1053               y += 1;
1054           };
1055           if (msg[i] != "\n") {
1056               chunk += msg[i];
1057           };
1058           if (i in link_data) {
1059               url_start_x = x;
1060               url = link_data[i];
1061               in_link = true;
1062           } else if (url_ends[0] == i) {
1063               url_ends.shift();
1064               push_inner_link(y, x);
1065               in_link = false;
1066           }
1067       }
1068       lines.push(chunk);
1069       if (in_link) {
1070           push_inner_link(lines.length - 1, chunk.length);
1071       }
1072       return [lines, inner_links];
1073   },
1074   log_msg: function(msg) {
1075       this.log.push(msg);
1076       while (this.log.length > 100) {
1077         this.log.shift();
1078       };
1079       this.full_refresh();
1080   },
1081   pick_selectable: function(task_name) {
1082       const i = parseInt(this.inputEl.value);
1083       if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1084           tui.log_msg('? invalid index, aborted');
1085       } else {
1086           server.send(['TASK:' + task_name, tui.selectables[i]]);
1087       }
1088       this.inputEl.value = "";
1089       this.switch_mode('play');
1090   },
1091     enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1092         if (with_size && this.ascii_draw_stage == 0) {
1093             width = this.inputEl.value.length;
1094             if (width > 36) {
1095                 this.log_msg('? wrong input length, must be max 36; try again');
1096                 return;
1097             }
1098             if (width != game.player.carrying.design[0][1]) {
1099                 game.player.carrying.design[1] = '';
1100                 game.player.carrying.design[0][1] = width;
1101             }
1102         } else if (this.inputEl.value.length > width) {
1103             this.log_msg('? wrong input length, must be max ' + width + '; try again');
1104             return;
1105         }
1106         this.log_msg('  ' + this.inputEl.value);
1107         if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1108           height = this.ascii_draw_stage;
1109         } else {
1110             if (with_size) {
1111                 height = this.ascii_draw_stage + 2;
1112             }
1113             while (this.inputEl.value.length < width) {
1114                 this.inputEl.value += ' ';
1115             }
1116             this.full_ascii_draw += this.inputEl.value;
1117         }
1118         if (with_size) {
1119             game.player.carrying.design[0][0] = height;
1120         }
1121         this.ascii_draw_stage += 1;
1122         if (this.ascii_draw_stage < height) {
1123             this.restore_input_values();
1124         } else {
1125             if (with_pw && with_size) {
1126                 server.send([command + '_SIZE',
1127                              unparser.to_yx(game.player.carrying.design[0]),
1128                              this.password]);
1129             }
1130             if (with_pw) {
1131                 server.send([command, this.full_ascii_draw, this.password]);
1132             } else {
1133                 server.send([command, this.full_ascii_draw]);
1134             }
1135             this.full_ascii_draw = '';
1136             this.ascii_draw_stage = 0;
1137             this.inputEl.value = '';
1138             this.switch_mode('edit');
1139         }
1140   },
1141   draw_map: function() {
1142     if (!game.turn_complete && this.map_lines.length == 0) {
1143         return;
1144     }
1145     if (game.turn_complete) {
1146         let map_lines_split = [];
1147         let line = [];
1148         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1149             if (j == game.map_size[1]) {
1150                 map_lines_split.push(line);
1151                 line = [];
1152                 j = 0;
1153             };
1154             if (this.map_mode == 'protections') {
1155                 line.push(game.map_control[i] + ' ');
1156             } else {
1157                 line.push(game.map[i] + ' ');
1158             }
1159         };
1160         map_lines_split.push(line);
1161         if (this.map_mode == 'terrain + annotations') {
1162             for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1163                 const yx = coordinate.split(',')
1164                 map_lines_split[yx[0]][yx[1]] = 'A ';
1165             }
1166         } else if (this.map_mode == 'terrain + things') {
1167             for (const p in game.portals) {
1168                 let coordinate = p.split(',')
1169                 let original = map_lines_split[coordinate[0]][coordinate[1]];
1170                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1171             }
1172             let used_positions = [];
1173             function draw_thing(t, used_positions) {
1174                 let symbol = game.thing_types[t.type_];
1175                 let meta_char = ' ';
1176                 if (t.thing_char) {
1177                     meta_char = t.thing_char;
1178                 }
1179                 if (used_positions.includes(t.position.toString())) {
1180                     meta_char = '+';
1181                 };
1182                 if (t.carrying) {
1183                     meta_char = '$';
1184                 }
1185                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1186                 used_positions.push(t.position.toString());
1187             }
1188             for (const thing_id in game.things) {
1189                 let t = game.things[thing_id];
1190                 if (t.type_ != 'Player') {
1191                     draw_thing(t, used_positions);
1192                 }
1193             };
1194             for (const thing_id in game.things) {
1195                 let t = game.things[thing_id];
1196                 if (t.type_ == 'Player') {
1197                     draw_thing(t, used_positions);
1198                 }
1199             };
1200         }
1201         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1202             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1203         } else if (tui.map_mode != 'terrain + things') {
1204             map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1205         }
1206         this.map_lines = []
1207         if (game.map_geometry == 'Square') {
1208             for (let line_split of map_lines_split) {
1209                 this.map_lines.push(line_split.join(''));
1210             };
1211         } else if (game.map_geometry == 'Hex') {
1212             let indent = 0
1213             for (let line_split of map_lines_split) {
1214                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1215                 if (indent == 0) {
1216                     indent = 1;
1217                 } else {
1218                     indent = 0;
1219                 };
1220             };
1221         }
1222         let window_center = [terminal.rows / 2, this.left_window_width / 2];
1223         let center_position = [game.player.position[0], game.player.position[1]];
1224         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1225             center_position = [explorer.position[0], explorer.position[1]];
1226         }
1227         center_position[1] = center_position[1] * 2;
1228         this.offset = [center_position[0] - window_center[0],
1229                        center_position[1] - window_center[1]]
1230         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1231             this.offset[1] += 1;
1232         };
1233     };
1234     let term_y = Math.max(0, -this.offset[0]);
1235     let term_x = Math.max(0, -this.offset[1]);
1236     let map_y = Math.max(0, this.offset[0]);
1237     let map_x = Math.max(0, this.offset[1]);
1238     for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1239         let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1240         terminal.write(term_y, term_x, to_draw);
1241     }
1242   },
1243   draw_face_popup: function() {
1244       const t = game.things[this.draw_face];
1245       if (!t || !t.face) {
1246           this.draw_face = false;
1247           return;
1248       }
1249       const start_x = tui.left_window_width - 10;
1250       function draw_body_part(body_part, end_y) {
1251           terminal.write(end_y - 3, start_x, '----------');
1252           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1253           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1254           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1255       }
1256       if (t.face) {
1257           draw_body_part(t.face, terminal.rows - 3);
1258       }
1259       if (t.hat) {
1260           draw_body_part(t.hat, terminal.rows - 6);
1261       }
1262       terminal.write(terminal.rows - 2, start_x, '----------');
1263       let name = t.name_;
1264       if (name.length > 6) {
1265           name = name.slice(0, 6) + '…';
1266       }
1267       terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1268   },
1269   draw_mode_line: function() {
1270       let help = 'hit [' + this.keys.help + '] for help';
1271       if (this.mode.has_input_prompt) {
1272           help = 'enter /help for help';
1273       }
1274       terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1275   },
1276   draw_stats_line: function(n) {
1277       terminal.write(0, this.left_window_width,
1278                      'ENERGY: ' + game.energy +
1279                      ' BLADDER: ' + game.bladder_pressure);
1280   },
1281   draw_history: function() {
1282       let log_display_lines = [];
1283       let log_links = {};
1284       let y_offset_in_log = 0;
1285       for (let line of this.log) {
1286           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1287                                                                     this.right_window_width)
1288           log_display_lines = log_display_lines.concat(new_lines);
1289           for (const y in link_data) {
1290               const rel_y = y_offset_in_log + parseInt(y);
1291               log_links[rel_y] = [];
1292               for (let link of link_data[y]) {
1293                   log_links[rel_y].push(link);
1294               }
1295           }
1296           y_offset_in_log += new_lines.length;
1297       };
1298       let i = log_display_lines.length - 1;
1299       for (let y = terminal.rows - 1 - this.height_input;
1300            y >= this.height_header && i >= 0;
1301            y--, i--) {
1302           terminal.write(y, this.left_window_width, log_display_lines[i]);
1303       }
1304       for (const key of Object.keys(log_links)) {
1305           if (parseInt(key) <= i) {
1306               delete log_links[key];
1307           }
1308       }
1309       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1310                     this.left_window_width];
1311       this.offset_links(offset, log_links);
1312   },
1313   draw_info: function() {
1314       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1315       let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1316       let offset = [this.height_header, this.left_window_width];
1317       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1318         terminal.write(y, offset[1], lines[i]);
1319       }
1320       this.offset_links(offset, link_data);
1321   },
1322   draw_input: function() {
1323     if (this.mode.has_input_prompt) {
1324         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1325             terminal.write(y, this.left_window_width, this.input_lines[i]);
1326         }
1327     }
1328   },
1329   draw_help: function() {
1330       let movement_keys_desc = '';
1331       if (!this.mode.is_intro) {
1332           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1333       }
1334       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1335       if (this.mode.available_actions.length > 0) {
1336           content += "Available actions:\n";
1337           for (let action of this.mode.available_actions) {
1338               if (Object.keys(this.action_tasks).includes(action)) {
1339                   if (!this.task_action_on(action)) {
1340                       continue;
1341                   }
1342               }
1343               if (action == 'move_explorer') {
1344                   action = 'move';
1345               }
1346               if (action == 'move') {
1347                   content += "[" + movement_keys_desc + "] – move\n"
1348               } else {
1349                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1350               }
1351           }
1352           content += '\n';
1353       }
1354       content += this.mode.list_available_modes();
1355       let start_x = 0;
1356       let lines = [];
1357       let _ = undefined;
1358       if (!this.mode.has_input_prompt) {
1359           start_x = this.left_window_width;
1360           this.draw_links = false;
1361           terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1362           [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1363       } else {
1364           start_x = 0;
1365           terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1366           [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1367       }
1368       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1369           terminal.write(y, start_x, lines[i]);
1370       }
1371   },
1372   toggle_tile_draw: function() {
1373       if (tui.tile_draw) {
1374           tui.tile_draw = false;
1375       } else {
1376           tui.tile_draw = true;
1377       }
1378   },
1379   toggle_map_mode: function() {
1380       if (tui.map_mode == 'terrain only') {
1381           tui.map_mode = 'terrain + annotations';
1382       } else if (tui.map_mode == 'terrain + annotations') {
1383           tui.map_mode = 'terrain + things';
1384       } else if (tui.map_mode == 'terrain + things') {
1385           tui.map_mode = 'protections';
1386       } else if (tui.map_mode == 'protections') {
1387           tui.map_mode = 'terrain only';
1388       }
1389   },
1390   full_refresh: function() {
1391     this.draw_links = true;
1392     this.links = {};
1393     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1394     this.recalc_input_lines();
1395     if (this.mode.is_intro) {
1396         this.draw_history();
1397         this.draw_input();
1398     } else {
1399         this.draw_map();
1400         this.draw_stats_line();
1401         this.draw_mode_line();
1402         if (this.mode.shows_info) {
1403           this.draw_info();
1404         } else {
1405           this.draw_history();
1406         }
1407         this.draw_input();
1408     }
1409     if (this.show_help) {
1410         this.draw_help();
1411     }
1412     if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1413         this.draw_face_popup();
1414     }
1415     if (!this.draw_links) {
1416         this.links = {};
1417     }
1418     terminal.refresh();
1419   }
1420 }
1421
1422 let game = {
1423     init: function() {
1424         this.turn = -1;
1425         this.player_id = -1;
1426         this.tasks = {};
1427         this.things = {};
1428         this.things_new = {};
1429         this.fov = "";
1430         this.fov_new = "";
1431         this.map = "";
1432         this.map_new = "";
1433         this.map_control = "";
1434         this.map_control_new = "";
1435         this.map_size = [0,0];
1436         this.map_size_new = [0,0];
1437         this.portals = {};
1438         this.portals_new = {};
1439         this.players_hat_chars = "";
1440         this.bladder_pressure = 0;
1441         this.bladder_pressure_new = 0;
1442     },
1443     get_thing_temp: function(id_, create_if_not_found=false) {
1444         if (id_ in game.things_new) {
1445             return game.things_new[id_];
1446         } else if (create_if_not_found) {
1447             let t = new Thing([0,0]);
1448             game.things_new[id_] = t;
1449             return t;
1450         };
1451     },
1452     get_thing: function(id_, create_if_not_found=false) {
1453         if (id_ in game.things) {
1454             return game.things[id_];
1455         };
1456     },
1457     move: function(start_position, direction) {
1458         let target = [start_position[0], start_position[1]];
1459         if (direction == 'LEFT') {
1460             target[1] -= 1;
1461         } else if (direction == 'RIGHT') {
1462             target[1] += 1;
1463         } else if (game.map_geometry == 'Square') {
1464             if (direction == 'UP') {
1465                 target[0] -= 1;
1466             } else if (direction == 'DOWN') {
1467                 target[0] += 1;
1468             };
1469         } else if (game.map_geometry == 'Hex') {
1470             let start_indented = start_position[0] % 2;
1471             if (direction == 'UPLEFT') {
1472                 target[0] -= 1;
1473                 if (!start_indented) {
1474                     target[1] -= 1;
1475                 }
1476             } else if (direction == 'UPRIGHT') {
1477                 target[0] -= 1;
1478                 if (start_indented) {
1479                     target[1] += 1;
1480                 }
1481             } else if (direction == 'DOWNLEFT') {
1482                 target[0] += 1;
1483                 if (!start_indented) {
1484                     target[1] -= 1;
1485                 }
1486             } else if (direction == 'DOWNRIGHT') {
1487                 target[0] += 1;
1488                 if (start_indented) {
1489                     target[1] += 1;
1490                 }
1491             };
1492         };
1493         if (target[0] < 0 || target[1] < 0 ||
1494             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1495             return null;
1496         };
1497         return target;
1498     },
1499     teleport: function() {
1500         if (game.player.position in this.portals) {
1501             server.reconnect_to(this.portals[game.player.position]);
1502         } else {
1503             terminal.blink_screen();
1504             tui.log_msg('? not standing on portal')
1505         }
1506     }
1507 }
1508
1509 game.init();
1510 tui.init();
1511 tui.full_refresh();
1512 server.init(websocket_location);
1513
1514 let explorer = {
1515     position: [0,0],
1516     annotations: {},
1517     annotations_new: {},
1518     info_cached: false,
1519     move: function(direction) {
1520         let target = game.move(this.position, direction);
1521         if (target) {
1522             this.position = target
1523             this.info_cached = false;
1524             if (tui.tile_draw) {
1525                 this.send_tile_control_command();
1526             }
1527         } else {
1528             terminal.blink_screen();
1529         };
1530     },
1531     get_info: function() {
1532         if (this.info_cached) {
1533             return this.info_cached;
1534         }
1535         let info_to_cache = '';
1536         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1537         if (game.fov[position_i] != '.') {
1538             info_to_cache += 'outside field of view';
1539         } else {
1540             for (let t_id in game.things) {
1541                  let t = game.things[t_id];
1542                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1543                      info_to_cache += this.get_thing_info(t, true);
1544                  }
1545             }
1546             let terrain_char = game.map[position_i]
1547             let terrain_desc = '?'
1548             if (game.terrains[terrain_char]) {
1549                 terrain_desc = game.terrains[terrain_char];
1550             };
1551             info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1552             let protection = game.map_control[position_i];
1553             if (protection != '.') {
1554                 info_to_cache += '/protection:' + protection;
1555             };
1556             info_to_cache += ')\n';
1557             if (this.position in game.portals) {
1558                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1559             }
1560             if (this.position in this.annotations) {
1561                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1562             }
1563         }
1564         this.info_cached = info_to_cache;
1565         return this.info_cached;
1566     },
1567     get_thing_info: function(t, detailed=false) {
1568         let info = '';
1569         if (detailed) {
1570             info += '- ';
1571         }
1572         info += game.thing_types[t.type_];
1573         if (t.thing_char) {
1574             info += t.thing_char;
1575         };
1576         if (t.name_) {
1577             info += ": " + t.name_;
1578         }
1579         info += ' (' + t.type_;
1580         if (t.installed) {
1581             info += "/installed";
1582         }
1583         if (t.type_ == 'Bottle') {
1584             if (t.thing_char == '_') {
1585                 info += '/empty';
1586             } else if (t.thing_char == '~') {
1587                 info += '/full';
1588             }
1589         }
1590         if (detailed) {
1591             const protection = t.protection;
1592             if (protection != '.') {
1593                 info += '/protection:' + protection;
1594             }
1595             info += ')\n';
1596             if (t.hat || t.face) {
1597                 info += '----------\n';
1598             }
1599             if (t.hat) {
1600                 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1601                 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1602                 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1603             }
1604             if (t.face) {
1605                 info += '| ' + t.face.slice(0, 6) + ' |\n';
1606                 info += '| ' + t.face.slice(6, 12) + ' |\n';
1607                 info += '| ' + t.face.slice(12, 18) + ' |\n';
1608                 info += '----------\n';
1609             }
1610             if (t.design) {
1611                 const line_length = t.design[0][1];
1612                 info += '-'.repeat(line_length + 4) + '\n';
1613                 let lines = ['']
1614                 if (line_length > 0) {
1615                     const regexp = RegExp('.{1,' + line_length + '}', 'g');
1616                     lines = t.design[1].match(regexp);
1617                 }
1618                 for (const line of lines) {
1619                     info += '| ' + line + ' |\n';
1620                 }
1621                 info += '-'.repeat(line_length + 4) + '\n';
1622             }
1623         } else {
1624             info += ')';
1625         }
1626         return info;
1627     },
1628     annotate: function(msg) {
1629         if (msg.length == 0) {
1630             msg = " ";  // triggers annotation deletion
1631         }
1632         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1633     },
1634     set_portal: function(msg) {
1635         if (msg.length == 0) {
1636             msg = " ";  // triggers portal deletion
1637         }
1638         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1639     },
1640     send_tile_control_command: function() {
1641         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1642     }
1643 }
1644
1645 tui.inputEl.addEventListener('input', (event) => {
1646     if (tui.mode.has_input_prompt) {
1647         let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1648         if (tui.inputEl.value.length > max_length) {
1649             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1650         };
1651     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1652         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1653         tui.switch_mode('edit');
1654     }
1655     tui.full_refresh();
1656 }, false);
1657 document.onclick = function() {
1658     if (!tui.mode.is_single_char_entry) {
1659         tui.show_help = false;
1660     }
1661 };
1662 tui.inputEl.addEventListener('keydown', (event) => {
1663     tui.show_help = false;
1664     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1665         event.preventDefault();
1666     }
1667     if ((!tui.mode.is_intro && event.key == 'Escape')
1668         || (tui.mode.has_input_prompt && event.key == 'Enter'
1669             && tui.inputEl.value.length == 0
1670             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1671                 'admin_enter'].includes(tui.mode.name))) {
1672         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1673             tui.log_msg('@ aborted');
1674         }
1675         tui.switch_mode('play');
1676     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1677         tui.show_help = true;
1678         tui.inputEl.value = "";
1679         tui.restore_input_values();
1680     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1681                && !tui.mode.is_single_char_entry) {
1682         tui.show_help = true;
1683     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1684         tui.login_name = tui.inputEl.value;
1685         server.send(['LOGIN', tui.inputEl.value]);
1686         tui.inputEl.value = "";
1687     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1688         tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1689     } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1690         if (game.player.carrying.type_ == 'Hat') {
1691             tui.enter_ascii_art('THING_DESIGN',
1692                                 game.player.carrying.design[0][0],
1693                                 game.player.carrying.design[0][1], true);
1694         } else {
1695             tui.enter_ascii_art('THING_DESIGN',
1696                                 game.player.carrying.design[0][0],
1697                                 game.player.carrying.design[0][1], true, true);
1698         }
1699     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1700         server.send(['TASK:COMMAND', tui.inputEl.value]);
1701         tui.inputEl.value = "";
1702     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1703         tui.pick_selectable('PICK_UP');
1704     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1705         tui.pick_selectable('DROP');
1706     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1707         if (tui.inputEl.value.length == 0) {
1708             tui.log_msg('@ aborted');
1709         } else {
1710             server.send(['SET_MAP_CONTROL_PASSWORD',
1711                         tui.tile_control_char, tui.inputEl.value]);
1712             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1713         }
1714         tui.switch_mode('admin');
1715     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1716         explorer.set_portal(tui.inputEl.value);
1717         tui.switch_mode('edit');
1718     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1719         if (tui.inputEl.value.length == 0) {
1720             tui.inputEl.value = " ";
1721         }
1722         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1723         tui.switch_mode('edit');
1724     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1725         explorer.annotate(tui.inputEl.value);
1726         tui.switch_mode('edit');
1727     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1728         if (tui.inputEl.value.length == 0) {
1729             tui.inputEl.value = " ";
1730         }
1731         tui.password = tui.inputEl.value
1732         tui.switch_mode('edit');
1733     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1734         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1735         tui.switch_mode('play');
1736     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1737         if (tui.inputEl.value.length != 1) {
1738             tui.log_msg('@ entered non-single-char, therefore aborted');
1739             tui.switch_mode('admin');
1740         } else {
1741             tui.tile_control_char = tui.inputEl.value[0];
1742             tui.switch_mode('control_pw_pw');
1743         }
1744     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1745         if (tui.inputEl.value.length != 1) {
1746             tui.log_msg('@ entered non-single-char, therefore aborted');
1747             tui.switch_mode('admin');
1748         } else {
1749             tui.tile_control_char = tui.inputEl.value[0];
1750             tui.switch_mode('control_tile_draw');
1751         }
1752     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1753         if (tui.inputEl.value.length != 1) {
1754             tui.log_msg('@ entered non-single-char, therefore aborted');
1755         } else {
1756             server.send(['THING_PROTECTION', tui.inputEl.value])
1757             tui.log_msg('@ sent new protection character for thing');
1758         }
1759         tui.switch_mode('admin');
1760     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1761         let tokens = parser.tokenize(tui.inputEl.value);
1762         if (tokens.length > 0 && tokens[0].length > 0) {
1763             if (tui.inputEl.value[0][0] == '/') {
1764                 if (tokens[0].slice(1) == 'nick') {
1765                     if (tokens.length > 1) {
1766                         server.send(['NICK', tokens[1]]);
1767                     } else {
1768                         tui.log_msg('? need new name');
1769                     }
1770                 } else {
1771                     tui.log_msg('? unknown command');
1772                 }
1773             } else {
1774                     server.send(['ALL', tui.inputEl.value]);
1775             }
1776         } else if (tui.inputEl.valuelength > 0) {
1777                 server.send(['ALL', tui.inputEl.value]);
1778         }
1779         tui.inputEl.value = "";
1780     } else if (tui.mode.name == 'play') {
1781           if (tui.mode.mode_switch_on_key(event)) {
1782               null;
1783           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1784               server.send(["TASK:INTOXICATE"]);
1785           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1786               server.send(["TASK:DOOR"]);
1787           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1788               server.send(["TASK:WEAR"]);
1789           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1790               server.send(["TASK:SPIN"]);
1791           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1792               server.send(["TASK:DANCE"]);
1793           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1794               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1795           } else if (event.key === tui.keys.teleport) {
1796               game.teleport();
1797           };
1798     } else if (tui.mode.name == 'study') {
1799         if (tui.mode.mode_switch_on_key(event)) {
1800               null;
1801         } else if (event.key in tui.movement_keys) {
1802             explorer.move(tui.movement_keys[event.key]);
1803         } else if (event.key == tui.keys.toggle_map_mode) {
1804             tui.toggle_map_mode();
1805         };
1806     } else if (tui.mode.name == 'control_tile_draw') {
1807         if (tui.mode.mode_switch_on_key(event)) {
1808             null;
1809         } else if (event.key in tui.movement_keys) {
1810             explorer.move(tui.movement_keys[event.key]);
1811         } else if (event.key === tui.keys.toggle_tile_draw) {
1812             tui.toggle_tile_draw();
1813         };
1814     } else if (tui.mode.name == 'admin') {
1815         if (tui.mode.mode_switch_on_key(event)) {
1816               null;
1817         } else if (event.key == tui.keys.toggle_map_mode) {
1818             tui.toggle_map_mode();
1819         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1820             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1821         };
1822     } else if (tui.mode.name == 'edit') {
1823         if (tui.mode.mode_switch_on_key(event)) {
1824               null;
1825         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1826             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1827         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1828             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1829           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1830               server.send(["TASK:INSTALL", tui.password]);
1831         } else if (event.key == tui.keys.toggle_map_mode) {
1832             tui.toggle_map_mode();
1833         }
1834     }
1835     tui.full_refresh();
1836 }, false);
1837
1838 rows_selector.addEventListener('input', function() {
1839     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1840         return;
1841     }
1842     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1843     terminal.initialize();
1844     tui.full_refresh();
1845 }, false);
1846 cols_selector.addEventListener('input', function() {
1847     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1848         return;
1849     }
1850     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1851     terminal.initialize();
1852     tui.reset_screen_size();
1853     tui.full_refresh();
1854 }, false);
1855 for (let key_selector of key_selectors) {
1856     key_selector.addEventListener('input', function() {
1857         window.localStorage.setItem(key_selector.id, key_selector.value);
1858         tui.init_keys();
1859     }, false);
1860 }
1861 window.setInterval(function() {
1862     if (server.websocket.readyState == 1) {
1863         server.send(['PING']);
1864     } else if (server.websocket.readyState != 0) {
1865         server.reconnect_to(server.url);
1866         tui.log_msg('@ attempting reconnect …')
1867     }
1868 }, 1000);
1869 window.setInterval(function() {
1870     if (document.activeElement.tagName.toLowerCase() != 'input') {
1871         const scroll_x = window.scrollX;
1872         const scroll_y = window.scrollY;
1873         tui.inputEl.focus();
1874         window.scrollTo(scroll_x, scroll_y);
1875     };
1876 }, 100);
1877 document.getElementById("help").onclick = function() {
1878     tui.show_help = true;
1879     tui.full_refresh();
1880 };
1881 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1882     const mode = switchEl.id.slice("switch_to_".length);
1883     switchEl.onclick = function() {
1884         tui.switch_mode(mode);
1885         tui.full_refresh();
1886     }
1887 };
1888 document.getElementById("toggle_tile_draw").onclick = function() {
1889     tui.toggle_tile_draw();
1890 }
1891 document.getElementById("toggle_map_mode").onclick = function() {
1892     tui.toggle_map_mode();
1893     tui.full_refresh();
1894 };
1895 document.getElementById("flatten").onclick = function() {
1896     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1897 };
1898 document.getElementById("door").onclick = function() {
1899     server.send(['TASK:DOOR']);
1900 };
1901 document.getElementById("consume").onclick = function() {
1902     server.send(['TASK:INTOXICATE']);
1903 };
1904 document.getElementById("install").onclick = function() {
1905     server.send(['TASK:INSTALL', tui.password]);
1906 };
1907 document.getElementById("wear").onclick = function() {
1908     server.send(['TASK:WEAR']);
1909 };
1910 document.getElementById("spin").onclick = function() {
1911     server.send(['TASK:SPIN']);
1912 };
1913 document.getElementById("dance").onclick = function() {
1914     server.send(['TASK:DANCE']);
1915 };
1916 document.getElementById("teleport").onclick = function() {
1917     game.teleport();
1918 };
1919 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1920     if (move_button.id.startsWith('key_')) {  // not a move button
1921         continue;
1922     };
1923     let direction = move_button.id.split('_')[2].toUpperCase();
1924     let move_repeat;
1925     function move() {
1926         if (tui.mode.available_actions.includes("move")) {
1927             server.send(['TASK:MOVE', direction]);
1928         } else if (tui.mode.available_actions.includes("move_explorer")) {
1929             explorer.move(direction);
1930             tui.full_refresh();
1931         };
1932     }
1933     move_button.onmousedown = function() {
1934         move();
1935         move_repeat = window.setInterval(move, 100);
1936     };
1937     move_button.onmouseup = function() {
1938         window.clearInterval(move_repeat);
1939     }
1940 };
1941 </script>
1942 </body></html>