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