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