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