home · contact · privacy
f7d02348195b169a3fe277bec9140ebc6b037202
[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_hat"></button>
76       <button id="switch_to_enter_design"></button>
77     </td>
78   </tr>
79   <tr>
80     <td><button id="switch_to_admin_enter"></button></td>
81     <td>
82       <button id="switch_to_control_pw_type"></button>
83       <button id="switch_to_control_tile_type"></button>
84       <button id="switch_to_admin_thing_protect"></button>
85       <button id="toggle_tile_draw"></button>
86     </td>
87   <tr>
88   </tr>
89 </table>
90 <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 />
91 <ul>
92 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
93 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
94 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
95 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
96 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
97 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
98 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
99 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
100 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
101 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
102 <li>help: <input id="key_help" type="text" value="h" />
103 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
104 <li>teleport: <input id="key_teleport" type="text" value="p" />
105 <li>spin: <input id="key_spin" type="text" value="S" />
106 <li>dance: <input id="key_dance" type="text" value="T" />
107 <li>open/close: <input id="key_door" type="text" value="D" />
108 <li>consume: <input id="key_consume" type="text" value="C" />
109 <li>install: <input id="key_install" type="text" value="I" />
110 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
111 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
112 <li><input id="key_switch_to_enter_face" type="text" value="f" />
113 <li><input id="key_switch_to_enter_hat" type="text" value="H" />
114 <li><input id="key_switch_to_enter_design" type="text" value="D" />
115 <li><input id="key_switch_to_take_thing" type="text" value="z" />
116 <li><input id="key_switch_to_chat" type="text" value="t" />
117 <li><input id="key_switch_to_play" type="text" value="p" />
118 <li><input id="key_switch_to_study" type="text" value="?" />
119 <li><input id="key_switch_to_edit" type="text" value="E" />
120 <li><input id="key_switch_to_write" type="text" value="m" />
121 <li><input id="key_switch_to_name_thing" type="text" value="N" />
122 <li><input id="key_switch_to_command_thing" type="text" value="O" />
123 <li><input id="key_switch_to_password" type="text" value="P" />
124 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
125 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
126 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
127 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
128 <li><input id="key_switch_to_annotate" type="text" value="M" />
129 <li><input id="key_switch_to_portal" type="text" value="T" />
130 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
131 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
132 </ul>
133 </div>
134 <script>
135 "use strict";
136 let websocket_location = "wss://plomlompom.com/rogue_chat/";
137 //let websocket_location = "ws://localhost:8000/";
138
139 let mode_helps = {
140     'play': {
141         'short': 'play',
142         'intro': '',
143         'long': 'This mode allows you to interact with the map in various ways.'
144     },
145     'study': {
146         'short': 'study',
147         'intro': '',
148         '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.'},
149     'edit': {
150         'short': 'world edit',
151         'intro': '',
152         '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.'
153     },
154     'name_thing': {
155         'short': 'name thing',
156         'intro': '',
157         'long': 'Give name to/change name of carried thing.'
158     },
159     'command_thing': {
160         'short': 'command',
161         'intro': '',
162         'long': 'Enter a command to the thing you carry.  Enter nothing to return to play mode.'
163     },
164     'take_thing': {
165         'short': 'take',
166         'intro': 'Pick up a thing in reach by entering its index number.  Enter nothing to abort.',
167         'long': 'You see a list of things which you could pick up.  Enter the target thing\'s index, or, to leave, nothing.'
168     },
169     'drop_thing': {
170         'short': 'drop',
171         'intro': 'Enter number of direction to which you want to drop thing.',
172         'long': 'Drop currently carried thing by entering the target direction index.  Enter nothing to return to play mode..'
173     },
174     'admin_thing_protect': {
175         'short': 'change thing protection',
176         'intro': '@ enter thing protection character:',
177         'long': 'Change protection character for carried thing.'
178     },
179     'enter_face': {
180         'short': 'edit face',
181         'intro': '@ enter face line:',
182         '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.'
183     },
184     'enter_design': {
185         'short': 'edit design',
186         'intro': '@ enter design:',
187         'long': 'Enter design for carried thing as ASCII art.'
188     },
189     'enter_hat': {
190         'short': 'edit hat',
191         'intro': '@ enter hat line:',
192         'long': 'Draw your hat 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.'
193     },
194     'write': {
195         'short': 'edit tile',
196         'intro': '',
197         '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.'
198     },
199     'control_pw_type': {
200         'short': 'change protection character password',
201         'intro': '@ enter protection character for which you want to change the password:',
202         '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.'
203     },
204     'control_pw_pw': {
205         'short': 'change protection character password',
206         'intro': '',
207         '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.'
208     },
209     'control_tile_type': {
210         'short': 'change tiles protection',
211         'intro': '@ enter protection character which you want to draw:',
212         '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.'
213     },
214     'control_tile_draw': {
215         'short': 'change tiles protection',
216         'intro': '',
217         '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.'
218     },
219     'annotate': {
220         'short': 'annotate tile',
221         'intro': '',
222         '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.'
223     },
224     'portal': {
225         'short': 'edit portal',
226         'intro': '',
227         '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.'
228     },
229     'chat': {
230         'short': 'chat',
231         'intro': '',
232         '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'
233     },
234     'login': {
235         'short': 'login',
236         'intro': '',
237         'long': 'Enter your player name.'
238     },
239     'waiting_for_server': {
240         'short': 'waiting for server response',
241         'intro': '@ waiting for server …',
242         'long': 'Waiting for a server response.'
243     },
244     'post_login_wait': {
245         'short': 'waiting for server response',
246         'intro': '',
247         'long': 'Waiting for a server response.'
248     },
249     'password': {
250         'short': 'set world edit password',
251         'intro': '',
252         '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.'
253     },
254     'admin_enter': {
255         'short': 'become admin',
256         'intro': '@ enter admin password:',
257         'long': 'This mode allows you to become admin if you know an admin password.'
258     },
259     'admin': {
260         'short': 'admin',
261         'intro': '',
262         'long': 'This mode allows you access to actions limited to administrators.'
263     }
264 }
265 let key_descriptions = {
266     'help': 'help',
267     'flatten': 'flatten surroundings',
268     'teleport': 'teleport',
269     'door': 'open/close',
270     'consume': 'consume',
271     'install': '(un-)install',
272     'wear': '(un-)wear',
273     'spin': 'spin',
274     'dance': 'dance',
275     'toggle_map_mode': 'toggle map view',
276     'toggle_tile_draw': 'toggle protection character drawing',
277     'hex_move_upleft': 'up-left',
278     'hex_move_upright': 'up-right',
279     'hex_move_right': 'right',
280     'hex_move_left': 'left',
281     'hex_move_downleft': 'down-left',
282     'hex_move_downright': 'down-right',
283     'square_move_up': 'up',
284     'square_move_left': 'left',
285     'square_move_down': 'down',
286     'square_move_right': 'right',
287 };
288 for (const mode_name of Object.keys(mode_helps)) {
289     key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
290 };
291
292 let rows_selector = document.getElementById("n_rows");
293 let cols_selector = document.getElementById("n_cols");
294 let key_selectors = document.querySelectorAll('[id^="key_"]');
295
296 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
297     const action = key_switch_selector.id.slice("key_switch_to_".length);
298     key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
299 }
300
301 function restore_selector_value(selector) {
302     let stored_selection = window.localStorage.getItem(selector.id);
303     if (stored_selection) {
304         selector.value = stored_selection;
305     }
306 }
307 restore_selector_value(rows_selector);
308 restore_selector_value(cols_selector);
309 for (let key_selector of key_selectors) {
310     restore_selector_value(key_selector);
311 }
312
313 function escapeHTML(str) {
314     return str.
315         replace(/&/g, '&amp;').
316         replace(/</g, '&lt;').
317         replace(/>/g, '&gt;').
318         replace(/'/g, '&apos;').
319         replace(/"/g, '&quot;');
320 };
321
322 let terminal = {
323   initialize: function() {
324     this.rows = rows_selector.value;
325     this.cols = cols_selector.value;
326     this.pre_el = document.getElementById("terminal");
327     this.set_default_colors();
328     this.apply_colors();
329     this.content = [];
330       let line = []
331     for (let y = 0, x = 0; y <= this.rows; x++) {
332         if (x == this.cols) {
333             x = 0;
334             y += 1;
335             this.content.push(line);
336             line = [];
337             if (y == this.rows) {
338                 break;
339             }
340         }
341         line.push(' ');
342     }
343   },
344   apply_colors: function() {
345     this.pre_el.style.color = this.foreground;
346     this.pre_el.style.backgroundColor = this.background;
347   },
348   set_default_colors: function() {
349       this.foreground = 'white';
350       this.background = 'black';
351       this.apply_colors();
352   },
353   set_random_colors: function() {
354       function rand(offset) {
355           return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
356       }
357       this.foreground = '#' + rand(159) + rand(159) + rand(159);
358       this.background = '#' + rand(0) + rand(0) + rand(0);
359       this.apply_colors();
360   },
361   blink_screen: function() {
362       this.pre_el.style.color = this.background;
363       this.pre_el.style.backgroundColor = this.foreground;
364       setTimeout(() => {
365           this.pre_el.style.color = this.foreground;
366           this.pre_el.style.backgroundColor = this.background;
367       }, 100);
368   },
369   refresh: function() {
370       let pre_content = '';
371       for (let y = 0; y < this.rows; y++) {
372           let line = this.content[y].join('');
373           let chunks = [];
374           if (y in tui.links) {
375               let start_x = 0;
376               for (let span of tui.links[y]) {
377                   chunks.push(escapeHTML(line.slice(start_x, span[0])));
378                   chunks.push('<a target="_blank" href="');
379                   chunks.push(escapeHTML(span[2]));
380                   chunks.push('">');
381                   chunks.push(escapeHTML(line.slice(span[0], span[1])));
382                   chunks.push('</a>');
383                   start_x = span[1];
384               }
385               chunks.push(escapeHTML(line.slice(start_x)));
386           } else {
387               chunks = [escapeHTML(line)];
388           }
389           for (const chunk of chunks) {
390               pre_content += chunk;
391           }
392           pre_content += '\n';
393       }
394       this.pre_el.innerHTML = pre_content;
395   },
396   write: function(start_y, start_x, msg) {
397       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
398           this.content[start_y][x] = msg[i];
399       }
400   },
401   drawBox: function(start_y, start_x, height, width) {
402     let end_y = start_y + height;
403     let end_x = start_x + width;
404     for (let y = start_y, x = start_x; y < this.rows; x++) {
405       if (x == end_x) {
406         x = start_x;
407         y += 1;
408         if (y == end_y) {
409             break;
410         }
411       };
412       this.content[y][x] = ' ';
413     }
414   },
415 }
416 terminal.initialize();
417
418 let parser = {
419   tokenize: function(str) {
420     let tokens = [];
421     let token = ''
422     let quoted = false;
423     let escaped = false;
424     for (let i = 0; i < str.length; i++) {
425       let c = str[i];
426       if (quoted) {
427         if (escaped) {
428           token += c;
429           escaped = false;
430         } else if (c == '\\') {
431           escaped = true;
432         } else if (c == '"') {
433           quoted = false
434         } else {
435           token += c;
436         }
437       } else if (c == '"') {
438         quoted = true
439       } else if (c === ' ') {
440         if (token.length > 0) {
441           tokens.push(token);
442           token = '';
443         }
444       } else {
445         token += c;
446       }
447     }
448     if (token.length > 0) {
449       tokens.push(token);
450     }
451     return tokens;
452   },
453   parse_yx: function(position_string) {
454     let coordinate_strings = position_string.split(',')
455     let position = [0, 0];
456     position[0] = parseInt(coordinate_strings[0].slice(2));
457     position[1] = parseInt(coordinate_strings[1].slice(2));
458     return position;
459   },
460 }
461
462 class Thing {
463     constructor(yx) {
464         this.position = yx;
465     }
466 }
467
468 let server = {
469     init: function(url) {
470         this.url = url;
471         this.websocket = new WebSocket(this.url);
472         this.websocket.onopen = function(event) {
473             game.thing_types = {};
474             game.terrains = {};
475             server.send(['TASKS']);
476             server.send(['TERRAINS']);
477             server.send(['THING_TYPES']);
478             tui.log_msg("@ server connected! :)");
479             tui.switch_mode('login');
480         };
481         this.websocket.onclose = function(event) {
482             tui.switch_mode('waiting_for_server');
483             tui.log_msg("@ server disconnected :(");
484         };
485             this.websocket.onmessage = this.handle_event;
486         },
487     reconnect_to: function(url) {
488         this.websocket.close();
489         this.init(url);
490     },
491     send: function(tokens) {
492         this.websocket.send(unparser.untokenize(tokens));
493     },
494     handle_event: function(event) {
495         let tokens = parser.tokenize(event.data);
496         if (tokens[0] === 'TURN') {
497             game.turn_complete = false;
498         } else if (tokens[0] === 'OTHER_WIPE') {
499             game.portals_new = {};
500             explorer.annotations_new = {};
501             game.things_new = [];
502         } else if (tokens[0] === 'STATS') {
503             game.bladder_pressure_new = parseInt(tokens[1])
504             game.energy_new = parseInt(tokens[2])
505         } else if (tokens[0] === 'THING') {
506             let t = game.get_thing_temp(tokens[4], true);
507             t.position = parser.parse_yx(tokens[1]);
508             t.type_ = tokens[2];
509             t.protection = tokens[3];
510             t.portable = parseInt(tokens[5]);
511             t.commandable = parseInt(tokens[6]);
512         } else if (tokens[0] === 'THING_NAME') {
513             let t = game.get_thing_temp(tokens[1]);
514             t.name_ = tokens[2];
515         } else if (tokens[0] === 'THING_FACE') {
516             let t = game.get_thing_temp(tokens[1]);
517             t.face = tokens[2];
518         } else if (tokens[0] === 'THING_HAT') {
519             let t = game.get_thing_temp(tokens[1]);
520             t.hat = tokens[2];
521         } else if (tokens[0] === 'THING_DESIGN') {
522             let t = game.get_thing_temp(tokens[1]);
523             t.design = [parser.parse_yx(tokens[2]), tokens[3]];
524         } else if (tokens[0] === 'THING_CHAR') {
525             let t = game.get_thing_temp(tokens[1]);
526             t.thing_char = tokens[2];
527         } else if (tokens[0] === 'TASKS') {
528             game.tasks = tokens[1].split(',');
529             tui.mode_write.legal = game.tasks.includes('WRITE');
530             tui.mode_command_thing.legal = game.tasks.includes('WRITE');
531             tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
532             tui.mode_drop_thing.legal = game.tasks.includes('DROP');
533         } else if (tokens[0] === 'THING_TYPE') {
534             game.thing_types[tokens[1]] = tokens[2]
535         } else if (tokens[0] === 'THING_CARRYING') {
536             let t = game.get_thing_temp(tokens[1]);
537             t.carrying = game.get_thing_temp(tokens[2], false);
538         } else if (tokens[0] === 'THING_INSTALLED') {
539             let t = game.get_thing_temp(tokens[1]);
540             t.installed = true;
541         } else if (tokens[0] === 'TERRAIN') {
542             game.terrains[tokens[1]] = tokens[2]
543         } else if (tokens[0] === 'MAP') {
544             game.map_geometry_new = tokens[1];
545             game.map_size_new = parser.parse_yx(tokens[2]);
546             game.map_new = tokens[3]
547         } else if (tokens[0] === 'FOV') {
548             game.fov_new = tokens[1]
549         } else if (tokens[0] === 'MAP_CONTROL') {
550             game.map_control_new = tokens[1]
551         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
552             game.portals = game.portals_new;
553             game.map_geometry = game.map_geometry_new;
554             game.map_size = game.map_size_new;
555             game.map = game.map_new;
556             game.fov = game.fov_new;
557             tui.init_keys();
558             game.map_control = game.map_control_new;
559             explorer.annotations = explorer.annotations_new;
560             explorer.info_cached = false;
561             game.things = game.things_new;
562             game.player = game.things[game.player_id];
563             game.players_hat_chars = game.players_hat_chars_new;
564             game.bladder_pressure = game.bladder_pressure_new
565             game.energy = game.energy_new
566             game.turn_complete = true;
567             if (tui.mode.name == 'post_login_wait') {
568                 tui.switch_mode('play');
569             } else {
570                 tui.full_refresh();
571             }
572         } else if (tokens[0] === 'CHAT') {
573              tui.log_msg('# ' + tokens[1], 1);
574         } else if (tokens[0] === 'CHATFACE') {
575             tui.draw_face = tokens[1];
576             tui.full_refresh();
577         } else if (tokens[0] === 'REPLY') {
578              tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
579         } else if (tokens[0] === 'PLAYER_ID') {
580             game.player_id = parseInt(tokens[1]);
581         } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
582             game.players_hat_chars_new = tokens[1];
583         } else if (tokens[0] === 'LOGIN_OK') {
584             this.send(['GET_GAMESTATE']);
585             tui.switch_mode('post_login_wait');
586             tui.log_msg('@ welcome!')
587             tui.log_msg('@ hint: see top of terminal for how to get help.')
588             tui.log_msg('@ hint: enter study mode to understand your environment.')
589         } else if (tokens[0] === 'DEFAULT_COLORS') {
590             terminal.set_default_colors();
591         } else if (tokens[0] === 'RANDOM_COLORS') {
592             terminal.set_random_colors();
593         } else if (tokens[0] === 'ADMIN_OK') {
594             tui.is_admin = true;
595             tui.log_msg('@ you now have admin rights');
596             tui.switch_mode('admin');
597         } else if (tokens[0] === 'PORTAL') {
598             let position = parser.parse_yx(tokens[1]);
599             game.portals_new[position] = tokens[2];
600         } else if (tokens[0] === 'ANNOTATION') {
601             let position = parser.parse_yx(tokens[1]);
602             explorer.annotations_new[position] = tokens[2];
603         } else if (tokens[0] === 'UNHANDLED_INPUT') {
604             tui.log_msg('? unknown command');
605         } else if (tokens[0] === 'PLAY_ERROR') {
606             tui.log_msg('? ' + tokens[1]);
607             terminal.blink_screen();
608         } else if (tokens[0] === 'ARGUMENT_ERROR') {
609             tui.log_msg('? syntax error: ' + tokens[1]);
610         } else if (tokens[0] === 'GAME_ERROR') {
611             tui.log_msg('? game error: ' + tokens[1]);
612         } else if (tokens[0] === 'PONG') {
613             ;
614         } else {
615             tui.log_msg('? unhandled input: ' + event.data);
616         }
617     }
618 }
619
620 let unparser = {
621     quote: function(str) {
622         let quoted = ['"'];
623         for (let i = 0; i < str.length; i++) {
624             let c = str[i];
625             if (['"', '\\'].includes(c)) {
626                 quoted.push('\\');
627             };
628             quoted.push(c);
629         }
630         quoted.push('"');
631         return quoted.join('');
632     },
633     to_yx: function(yx_coordinate) {
634         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
635     },
636     untokenize: function(tokens) {
637         let quoted_tokens = [];
638         for (let token of tokens) {
639             quoted_tokens.push(this.quote(token));
640         }
641         return quoted_tokens.join(" ");
642     }
643 }
644
645 class Mode {
646     constructor(name, has_input_prompt=false, shows_info=false,
647                 is_intro=false, is_single_char_entry=false) {
648         this.name = name;
649         this.short_desc = mode_helps[name].short;
650         this.available_modes = [];
651         this.available_actions = [];
652         this.has_input_prompt = has_input_prompt;
653         this.shows_info= shows_info;
654         this.is_intro = is_intro;
655         this.help_intro = mode_helps[name].long;
656         this.intro_msg = mode_helps[name].intro;
657         this.is_single_char_entry = is_single_char_entry;
658         this.legal = true;
659     }
660     *iter_available_modes() {
661         for (let mode_name of this.available_modes) {
662             let mode = tui['mode_' + mode_name];
663             if (!mode.legal) {
664                 continue;
665             }
666             let key = tui.keys['switch_to_' + mode.name];
667             yield [mode, key]
668         }
669     }
670     list_available_modes() {
671         let msg = ''
672         if (this.available_modes.length > 0) {
673             msg += 'Other modes available from here:\n';
674             for (let [mode, key] of this.iter_available_modes()) {
675                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
676             }
677         }
678         return msg;
679     }
680     mode_switch_on_key(key_event) {
681         for (let [mode, key] of this.iter_available_modes()) {
682             if (key_event.key == key) {
683                 event.preventDefault();
684                 tui.switch_mode(mode.name);
685                 return true;
686             };
687         }
688         return false;
689     }
690 }
691 let tui = {
692   links: {},
693   log: [],
694   input_prompt: '> ',
695   input_lines: [],
696   window_width: terminal.cols / 2,
697   height_turn_line: 1,
698   height_mode_line: 1,
699   height_input: 1,
700   password: 'foo',
701   show_help: false,
702   is_admin: false,
703   tile_draw: false,
704   mode_waiting_for_server: new Mode('waiting_for_server',
705                                      false, false, true),
706   mode_login: new Mode('login', true, false, true),
707   mode_post_login_wait: new Mode('post_login_wait'),
708   mode_chat: new Mode('chat', true),
709   mode_annotate: new Mode('annotate', true, true),
710   mode_play: new Mode('play'),
711   mode_study: new Mode('study', false, true),
712   mode_write: new Mode('write', false, false, false, true),
713   mode_edit: new Mode('edit'),
714   mode_control_pw_type: new Mode('control_pw_type', true),
715   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
716   mode_portal: new Mode('portal', true, true),
717   mode_password: new Mode('password', true),
718   mode_name_thing: new Mode('name_thing', true, true),
719   mode_command_thing: new Mode('command_thing', true),
720   mode_take_thing: new Mode('take_thing', true),
721   mode_drop_thing: new Mode('drop_thing', true),
722   mode_enter_face: new Mode('enter_face', true),
723   mode_enter_hat: new Mode('enter_hat', true),
724   mode_enter_design: new Mode('enter_design', true),
725   mode_admin_enter: new Mode('admin_enter', true),
726   mode_admin: new Mode('admin'),
727   mode_control_pw_pw: new Mode('control_pw_pw', true),
728   mode_control_tile_type: new Mode('control_tile_type', true),
729   mode_control_tile_draw: new Mode('control_tile_draw'),
730   action_tasks: {
731       'flatten': 'FLATTEN_SURROUNDINGS',
732       'take_thing': 'PICK_UP',
733       'drop_thing': 'DROP',
734       'move': 'MOVE',
735       'door': 'DOOR',
736       'install': 'INSTALL',
737       'wear': 'WEAR',
738       'command': 'COMMAND',
739       'consume': 'INTOXICATE',
740       'spin': 'SPIN',
741       'dance': 'DANCE',
742   },
743   offset: [0,0],
744   map_lines: [],
745   ascii_draw_stage: 0,
746   full_ascii_draw: '',
747   selectables: [],
748   draw_face: false,
749   init: function() {
750       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
751                                         "command_thing", "take_thing", "drop_thing"]
752       this.mode_play.available_actions = ["move", "teleport", "door", "consume",
753                                           "wear", "spin", "dance"];
754       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
755       this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
756       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
757                                          "control_tile_type", "chat",
758                                          "study", "play", "edit"]
759       this.mode_admin.available_actions = ["move", "toggle_map_mode"];
760       this.mode_control_tile_draw.available_modes = ["admin_enter"]
761       this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
762       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
763                                         "enter_design", "password", "chat", "study",
764                                         "play", "admin_enter", "enter_face",
765                                         "enter_hat"]
766       this.mode_edit.available_actions = ["move", "flatten", "install",
767                                           "toggle_map_mode"]
768       this.inputEl = document.getElementById("input");
769       this.switch_mode('waiting_for_server');
770       this.recalc_input_lines();
771       this.height_header = this.height_turn_line + this.height_mode_line;
772       this.init_keys();
773   },
774   init_keys: function() {
775     document.getElementById("move_table").hidden = true;
776     this.keys = {};
777     for (let key_selector of key_selectors) {
778         this.keys[key_selector.id.slice(4)] = key_selector.value;
779     }
780     this.movement_keys = {};
781     let geometry_prefix = 'undefinedMapGeometry_';
782     if (game.map_geometry) {
783         geometry_prefix = game.map_geometry.toLowerCase() + '_';
784     }
785     for (const key_name of Object.keys(key_descriptions)) {
786         if (key_name.startsWith(geometry_prefix)) {
787             let direction = key_name.split('_')[2].toUpperCase();
788             let key = this.keys[key_name];
789             this.movement_keys[key] = direction;
790         }
791     };
792     for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
793         if (move_button.id.startsWith('key_')) {
794             continue;
795         }
796         move_button.hidden = true;
797     };
798     for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
799         document.getElementById("move_table").hidden = false;
800         move_button.hidden = false;
801     };
802     for (let el of document.getElementsByTagName("button")) {
803       let action_desc = key_descriptions[el.id];
804       let action_key = '[' + this.keys[el.id] + ']';
805       el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
806     }
807   },
808   task_action_on: function(action) {
809       return game.tasks.includes(this.action_tasks[action]);
810   },
811   switch_mode: function(mode_name) {
812
813     function fail(msg, return_mode='play') {
814         tui.log_msg('? ' + msg);
815         terminal.blink_screen();
816         tui.switch_mode(return_mode);
817     }
818
819     if (this.mode && this.mode.name == 'control_tile_draw') {
820         tui.log_msg('@ finished tile protection drawing.')
821     }
822     this.draw_face = false;
823     this.tile_draw = false;
824     if (mode_name == 'command_thing' && (!game.player.carrying
825                                          || !game.player.carrying.commandable)) {
826         return fail('not carrying anything commandable');
827     } else if (mode_name == 'name_thing' && !game.player.carrying) {
828         return fail('not carrying anything to re-name', 'edit');
829     } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
830         return fail('not carrying anything to protect')
831     } else if (mode_name == 'take_thing' && game.player.carrying) {
832         return fail('already carrying something');
833     } else if (mode_name == 'drop_thing' && !game.player.carrying) {
834         return fail('not carrying anything droppable');
835     } else if (mode_name == 'enter_hat' && !game.player.hat) {
836         return fail('not wearing hat to edit', 'edit');
837     } else if (mode_name == 'enter_design' && (!game.player.carrying
838                                                || !game.player.carrying.design)) {
839         return fail('not carrying designable to edit', 'edit');
840     }
841     if (mode_name == 'admin_enter' && this.is_admin) {
842         mode_name = 'admin';
843     };
844     this.mode = this['mode_' + mode_name];
845     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
846         this.map_mode = 'protections';
847     } else if (this.mode.name != "edit") {
848         this.map_mode = 'terrain + things';
849     };
850     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
851         explorer.position = game.player.position;
852     }
853     this.inputEl.value = "";
854     this.restore_input_values();
855     for (let el of document.getElementsByTagName("button")) {
856         el.disabled = true;
857     }
858     document.getElementById("help").disabled = false;
859     for (const action of this.mode.available_actions) {
860         if (["move", "move_explorer"].includes(action)) {
861             for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
862                 move_key.disabled = false;
863             }
864         } else if (Object.keys(this.action_tasks).includes(action)) {
865             if (this.task_action_on(action)) {
866                 document.getElementById(action).disabled = false;
867             }
868         } else {
869             document.getElementById(action).disabled = false;
870         };
871     }
872     for (const mode_name of this.mode.available_modes) {
873             document.getElementById('switch_to_' + mode_name).disabled = false;
874     }
875     if (this.mode.intro_msg.length > 0) {
876         this.log_msg(this.mode.intro_msg);
877     }
878     if (this.mode.name == 'login') {
879         if (this.login_name) {
880             server.send(['LOGIN', this.login_name]);
881         } else {
882             this.log_msg("? need login name");
883         }
884     } else if (this.mode.is_single_char_entry) {
885         this.show_help = true;
886     } else if (this.mode.name == 'take_thing') {
887         this.log_msg("Portable things in reach for pick-up:");
888         const y = game.player.position[0]
889         const x = game.player.position[1]
890         let directed_moves = {
891             'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
892         }
893         if (game.map_geometry == 'Square') {
894             directed_moves['UP'] = [-1, 0];
895             directed_moves['DOWN'] = [1, 0];
896         } else if (game.map_geometry == 'Hex') {
897             if (y % 2) {
898                 directed_moves['UPLEFT'] = [-1, 0];
899                 directed_moves['UPRIGHT'] = [-1, 1];
900                 directed_moves['DOWNLEFT'] = [1, 0];
901                 directed_moves['DOWNRIGHT'] = [1, 1];
902             } else {
903                 directed_moves['UPLEFT'] = [-1, -1];
904                 directed_moves['UPRIGHT'] = [-1, 0];
905                 directed_moves['DOWNLEFT'] = [1, -1];
906                 directed_moves['DOWNRIGHT'] = [1, 0];
907             }
908         }
909         let select_range = {};
910         for (const direction in directed_moves) {
911             const move = directed_moves[direction];
912             select_range[direction] = [y + move[0], x + move[1]];
913         }
914         this.selectables = [];
915         let directions = [];
916         for (const direction in select_range) {
917             for (const t_id in game.things) {
918                 const t = game.things[t_id];
919                 const position = select_range[direction];
920                 if (t.portable
921                     && t.position[0] == position[0]
922                     && t.position[1] == position[1]) {
923                     this.selectables.push(t_id);
924                     directions.push(direction);
925                 }
926             }
927         }
928         if (this.selectables.length == 0) {
929             this.log_msg('none');
930             terminal.blink_screen();
931             this.switch_mode('play');
932             return;
933         } else {
934             for (let [i, t_id] of this.selectables.entries()) {
935                 const t = game.things[t_id];
936                 const direction = directions[i];
937                 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
938             }
939         }
940     } else if (this.mode.name == 'drop_thing') {
941         this.log_msg('Direction to drop thing to:');
942         this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
943         for (let [i, direction] of this.selectables.entries()) {
944             this.log_msg(i + ': ' + direction);
945         };
946     } else if (this.mode.name == 'enter_hat') {
947         this.log_msg('legal characters: ' + game.players_hat_chars);
948     } else if (this.mode.name == 'command_thing') {
949         server.send(['TASK:COMMAND', 'HELP']);
950     } else if (this.mode.name == 'control_pw_pw') {
951         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
952     } else if (this.mode.name == 'control_tile_draw') {
953         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 + '].')
954     }
955     this.full_refresh();
956   },
957   offset_links: function(offset, links) {
958       for (let y in links) {
959           let real_y = offset[0] + parseInt(y);
960           if (!this.links[real_y]) {
961               this.links[real_y] = [];
962           }
963           for (let link of links[y]) {
964               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
965               this.links[real_y].push(offset_link);
966           }
967       }
968   },
969   restore_input_values: function() {
970       if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
971           let info = explorer.annotations[explorer.position];
972           if (info != "(none)") {
973               this.inputEl.value = info;
974           }
975       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
976           let portal = game.portals[explorer.position]
977           this.inputEl.value = portal;
978       } else if (this.mode.name == 'password') {
979           this.inputEl.value = this.password;
980       } else if (this.mode.name == 'name_thing') {
981           if (game.player.carrying && game.player.carrying.name_) {
982               this.inputEl.value = game.player.carrying.name_;
983           }
984       } else if (this.mode.name == 'admin_thing_protect') {
985           if (game.player.carrying && game.player.carrying.protection) {
986               this.inputEl.value = game.player.carrying.protection;
987           }
988       } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
989           const start = this.ascii_draw_stage * 6;
990           const end = (this.ascii_draw_stage + 1) * 6;
991           if (this.mode.name == 'enter_face') {
992               this.inputEl.value = game.player.face.slice(start, end);
993           } else if (this.mode.name == 'enter_hat') {
994               this.inputEl.value = game.player.hat.slice(start, end);
995           }
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.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) {
1091       if (this.inputEl.value.length > width) {
1092           this.log_msg('? wrong input length, must be max ' + width + '; try again');
1093           return;
1094       } else if (this.inputEl.value.length < width) {
1095           while (this.inputEl.value.length < width) {
1096               this.inputEl.value += ' ';
1097           }
1098       }
1099       this.log_msg('  ' + this.inputEl.value);
1100       this.full_ascii_draw += this.inputEl.value;
1101       this.ascii_draw_stage += 1;
1102       if (this.ascii_draw_stage < height) {
1103           this.restore_input_values();
1104       } else {
1105           if (with_pw) {
1106               server.send([command, this.full_ascii_draw, this.password]);
1107           } else {
1108               server.send([command, this.full_ascii_draw]);
1109           }
1110           this.full_ascii_draw = '';
1111           this.ascii_draw_stage = 0;
1112           this.inputEl.value = '';
1113           this.switch_mode('edit');
1114       }
1115   },
1116   draw_map: function() {
1117     if (!game.turn_complete && this.map_lines.length == 0) {
1118         return;
1119     }
1120     if (game.turn_complete) {
1121         let map_lines_split = [];
1122         let line = [];
1123         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1124             if (j == game.map_size[1]) {
1125                 map_lines_split.push(line);
1126                 line = [];
1127                 j = 0;
1128             };
1129             if (this.map_mode == 'protections') {
1130                 line.push(game.map_control[i] + ' ');
1131             } else {
1132                 line.push(game.map[i] + ' ');
1133             }
1134         };
1135         map_lines_split.push(line);
1136         if (this.map_mode == 'terrain + annotations') {
1137             for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1138                 const yx = coordinate.split(',')
1139                 map_lines_split[yx[0]][yx[1]] = 'A ';
1140             }
1141         } else if (this.map_mode == 'terrain + things') {
1142             for (const p in game.portals) {
1143                 let coordinate = p.split(',')
1144                 let original = map_lines_split[coordinate[0]][coordinate[1]];
1145                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1146             }
1147             let used_positions = [];
1148             function draw_thing(t, used_positions) {
1149                 let symbol = game.thing_types[t.type_];
1150                 let meta_char = ' ';
1151                 if (t.thing_char) {
1152                     meta_char = t.thing_char;
1153                 }
1154                 if (used_positions.includes(t.position.toString())) {
1155                     meta_char = '+';
1156                 };
1157                 if (t.carrying) {
1158                     meta_char = '$';
1159                 }
1160                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1161                 used_positions.push(t.position.toString());
1162             }
1163             for (const thing_id in game.things) {
1164                 let t = game.things[thing_id];
1165                 if (t.type_ != 'Player') {
1166                     draw_thing(t, used_positions);
1167                 }
1168             };
1169             for (const thing_id in game.things) {
1170                 let t = game.things[thing_id];
1171                 if (t.type_ == 'Player') {
1172                     draw_thing(t, used_positions);
1173                 }
1174             };
1175         }
1176         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1177             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1178         } else if (tui.map_mode != 'terrain + things') {
1179             map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1180         }
1181         this.map_lines = []
1182         if (game.map_geometry == 'Square') {
1183             for (let line_split of map_lines_split) {
1184                 this.map_lines.push(line_split.join(''));
1185             };
1186         } else if (game.map_geometry == 'Hex') {
1187             let indent = 0
1188             for (let line_split of map_lines_split) {
1189                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1190                 if (indent == 0) {
1191                     indent = 1;
1192                 } else {
1193                     indent = 0;
1194                 };
1195             };
1196         }
1197         let window_center = [terminal.rows / 2, this.window_width / 2];
1198         let center_position = [game.player.position[0], game.player.position[1]];
1199         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1200             center_position = [explorer.position[0], explorer.position[1]];
1201         }
1202         center_position[1] = center_position[1] * 2;
1203         this.offset = [center_position[0] - window_center[0],
1204                        center_position[1] - window_center[1]]
1205         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1206             this.offset[1] += 1;
1207         };
1208     };
1209     let term_y = Math.max(0, -this.offset[0]);
1210     let term_x = Math.max(0, -this.offset[1]);
1211     let map_y = Math.max(0, this.offset[0]);
1212     let map_x = Math.max(0, this.offset[1]);
1213     for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1214         let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1215         terminal.write(term_y, term_x, to_draw);
1216     }
1217   },
1218   draw_face_popup: function() {
1219       const t = game.things[this.draw_face];
1220       if (!t || !t.face) {
1221           this.draw_face = false;
1222           return;
1223       }
1224       const start_x = tui.window_width - 10;
1225       let t_char = ' ';
1226       if (t.thing_char) {
1227           t_char = t.thing_char;
1228       }
1229       function draw_body_part(body_part, end_y) {
1230           terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1231           terminal.write(end_y - 3, start_x, '|        |');
1232           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1233           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1234           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1235       }
1236       if (t.face) {
1237           draw_body_part(t.face, terminal.rows - 2);
1238       }
1239       if (t.hat) {
1240           draw_body_part(t.hat, terminal.rows - 5);
1241       }
1242       terminal.write(terminal.rows - 1, start_x, '|        |');
1243   },
1244   draw_mode_line: function() {
1245       let help = 'hit [' + this.keys.help + '] for help';
1246       if (this.mode.has_input_prompt) {
1247           help = 'enter /help for help';
1248       }
1249       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1250   },
1251   draw_stats_line: function(n) {
1252       terminal.write(1, this.window_width,
1253                      'ENERGY: ' + game.energy +
1254                      ' BLADDER: ' + game.bladder_pressure);
1255   },
1256   draw_history: function() {
1257       let log_display_lines = [];
1258       let log_links = {};
1259       let y_offset_in_log = 0;
1260       for (let line of this.log) {
1261           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1262                                                                     this.window_width)
1263           log_display_lines = log_display_lines.concat(new_lines);
1264           for (const y in link_data) {
1265               const rel_y = y_offset_in_log + parseInt(y);
1266               log_links[rel_y] = [];
1267               for (let link of link_data[y]) {
1268                   log_links[rel_y].push(link);
1269               }
1270           }
1271           y_offset_in_log += new_lines.length;
1272       };
1273       let i = log_display_lines.length - 1;
1274       for (let y = terminal.rows - 1 - this.height_input;
1275            y >= this.height_header && i >= 0;
1276            y--, i--) {
1277           terminal.write(y, this.window_width, log_display_lines[i]);
1278       }
1279       for (const key of Object.keys(log_links)) {
1280           if (parseInt(key) <= i) {
1281               delete log_links[key];
1282           }
1283       }
1284       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1285                     this.window_width];
1286       this.offset_links(offset, log_links);
1287   },
1288   draw_info: function() {
1289       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1290       let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1291       let offset = [this.height_header, this.window_width];
1292       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1293         terminal.write(y, offset[1], lines[i]);
1294       }
1295       this.offset_links(offset, link_data);
1296   },
1297   draw_input: function() {
1298     if (this.mode.has_input_prompt) {
1299         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1300             terminal.write(y, this.window_width, this.input_lines[i]);
1301         }
1302     }
1303   },
1304   draw_help: function() {
1305       let movement_keys_desc = '';
1306       if (!this.mode.is_intro) {
1307           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1308       }
1309       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1310       if (this.mode.available_actions.length > 0) {
1311           content += "Available actions:\n";
1312           for (let action of this.mode.available_actions) {
1313               if (Object.keys(this.action_tasks).includes(action)) {
1314                   if (!this.task_action_on(action)) {
1315                       continue;
1316                   }
1317               }
1318               if (action == 'move_explorer') {
1319                   action = 'move';
1320               }
1321               if (action == 'move') {
1322                   content += "[" + movement_keys_desc + "] – move\n"
1323               } else {
1324                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1325               }
1326           }
1327           content += '\n';
1328       }
1329       content += this.mode.list_available_modes();
1330       let start_x = 0;
1331       if (!this.mode.has_input_prompt) {
1332           start_x = this.window_width;
1333           this.draw_links = false;
1334       }
1335       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1336       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1337       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1338           terminal.write(y, start_x, lines[i]);
1339       }
1340   },
1341   toggle_tile_draw: function() {
1342       if (tui.tile_draw) {
1343           tui.tile_draw = false;
1344       } else {
1345           tui.tile_draw = true;
1346       }
1347   },
1348   toggle_map_mode: function() {
1349       if (tui.map_mode == 'terrain only') {
1350           tui.map_mode = 'terrain + annotations';
1351       } else if (tui.map_mode == 'terrain + annotations') {
1352           tui.map_mode = 'terrain + things';
1353       } else if (tui.map_mode == 'terrain + things') {
1354           tui.map_mode = 'protections';
1355       } else if (tui.map_mode == 'protections') {
1356           tui.map_mode = 'terrain only';
1357       }
1358   },
1359   full_refresh: function() {
1360     this.draw_links = true;
1361     this.links = {};
1362     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1363     this.recalc_input_lines();
1364     if (this.mode.is_intro) {
1365         this.draw_history();
1366         this.draw_input();
1367     } else {
1368         this.draw_map();
1369         this.draw_stats_line();
1370         this.draw_mode_line();
1371         if (this.mode.shows_info) {
1372           this.draw_info();
1373         } else {
1374           this.draw_history();
1375         }
1376         this.draw_input();
1377     }
1378     if (this.show_help) {
1379         this.draw_help();
1380     }
1381     if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1382         this.draw_face_popup();
1383     }
1384     if (!this.draw_links) {
1385         this.links = {};
1386     }
1387     terminal.refresh();
1388   }
1389 }
1390
1391 let game = {
1392     init: function() {
1393         this.turn = -1;
1394         this.player_id = -1;
1395         this.tasks = {};
1396         this.things = {};
1397         this.things_new = {};
1398         this.fov = "";
1399         this.fov_new = "";
1400         this.map = "";
1401         this.map_new = "";
1402         this.map_control = "";
1403         this.map_control_new = "";
1404         this.map_size = [0,0];
1405         this.map_size_new = [0,0];
1406         this.portals = {};
1407         this.portals_new = {};
1408         this.players_hat_chars = "";
1409         this.bladder_pressure = 0;
1410         this.bladder_pressure_new = 0;
1411     },
1412     get_thing_temp: function(id_, create_if_not_found=false) {
1413         if (id_ in game.things_new) {
1414             return game.things_new[id_];
1415         } else if (create_if_not_found) {
1416             let t = new Thing([0,0]);
1417             game.things_new[id_] = t;
1418             return t;
1419         };
1420     },
1421     get_thing: function(id_, create_if_not_found=false) {
1422         if (id_ in game.things) {
1423             return game.things[id_];
1424         };
1425     },
1426     move: function(start_position, direction) {
1427         let target = [start_position[0], start_position[1]];
1428         if (direction == 'LEFT') {
1429             target[1] -= 1;
1430         } else if (direction == 'RIGHT') {
1431             target[1] += 1;
1432         } else if (game.map_geometry == 'Square') {
1433             if (direction == 'UP') {
1434                 target[0] -= 1;
1435             } else if (direction == 'DOWN') {
1436                 target[0] += 1;
1437             };
1438         } else if (game.map_geometry == 'Hex') {
1439             let start_indented = start_position[0] % 2;
1440             if (direction == 'UPLEFT') {
1441                 target[0] -= 1;
1442                 if (!start_indented) {
1443                     target[1] -= 1;
1444                 }
1445             } else if (direction == 'UPRIGHT') {
1446                 target[0] -= 1;
1447                 if (start_indented) {
1448                     target[1] += 1;
1449                 }
1450             } else if (direction == 'DOWNLEFT') {
1451                 target[0] += 1;
1452                 if (!start_indented) {
1453                     target[1] -= 1;
1454                 }
1455             } else if (direction == 'DOWNRIGHT') {
1456                 target[0] += 1;
1457                 if (start_indented) {
1458                     target[1] += 1;
1459                 }
1460             };
1461         };
1462         if (target[0] < 0 || target[1] < 0 ||
1463             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1464             return null;
1465         };
1466         return target;
1467     },
1468     teleport: function() {
1469         if (game.player.position in this.portals) {
1470             server.reconnect_to(this.portals[game.player.position]);
1471         } else {
1472             terminal.blink_screen();
1473             tui.log_msg('? not standing on portal')
1474         }
1475     }
1476 }
1477
1478 game.init();
1479 tui.init();
1480 tui.full_refresh();
1481 server.init(websocket_location);
1482
1483 let explorer = {
1484     position: [0,0],
1485     annotations: {},
1486     annotations_new: {},
1487     info_cached: false,
1488     move: function(direction) {
1489         let target = game.move(this.position, direction);
1490         if (target) {
1491             this.position = target
1492             this.info_cached = false;
1493             if (tui.tile_draw) {
1494                 this.send_tile_control_command();
1495             }
1496         } else {
1497             terminal.blink_screen();
1498         };
1499     },
1500     get_info: function() {
1501         if (this.info_cached) {
1502             return this.info_cached;
1503         }
1504         let info_to_cache = '';
1505         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1506         if (game.fov[position_i] != '.') {
1507             info_to_cache += 'outside field of view';
1508         } else {
1509             for (let t_id in game.things) {
1510                  let t = game.things[t_id];
1511                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1512                      info_to_cache += "THING: " + this.get_thing_info(t);
1513                      let protection = t.protection;
1514                      if (protection == '.') {
1515                          protection = 'none';
1516                      }
1517                      info_to_cache += " / protection: " + protection + "\n";
1518                      if (t.hat) {
1519                          info_to_cache += t.hat.slice(0, 6) + '\n';
1520                          info_to_cache += t.hat.slice(6, 12) + '\n';
1521                          info_to_cache += t.hat.slice(12, 18) + '\n';
1522                      }
1523                      if (t.face) {
1524                          info_to_cache += t.face.slice(0, 6) + '\n';
1525                          info_to_cache += t.face.slice(6, 12) + '\n';
1526                          info_to_cache += t.face.slice(12, 18) + '\n';
1527                      }
1528                      if (t.design) {
1529                          const line_length = t.design[0][1];
1530                          if (t.type_ == 'Sign') {
1531                              info_to_cache += '-'.repeat(line_length + 4) + '\n';
1532                          }
1533                          const regexp = RegExp('.{1,' + line_length + '}', 'g');
1534                          const lines = t.design[1].match(regexp);
1535                          console.log(lines);
1536                          for (const line of lines) {
1537                              info_to_cache += '| ' + line + ' |\n';
1538                          }
1539                          if (t.type_ == 'Sign') {
1540                              info_to_cache += '-'.repeat(line_length + 4) + '\n';
1541                          }
1542                      }
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 + "\n";
1551             let protection = game.map_control[position_i];
1552             if (protection == '.') {
1553                 protection = 'unprotected';
1554             };
1555             info_to_cache += 'PROTECTION: ' + protection + '\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) {
1567         const symbol = game.thing_types[t.type_];
1568         let info = t.type_ + " / " + symbol;
1569         if (t.thing_char) {
1570             info += t.thing_char;
1571         };
1572         if (t.name_) {
1573             info += " (" + t.name_ + ")";
1574         }
1575         if (t.installed) {
1576             info += " / installed";
1577         }
1578         return info;
1579     },
1580     annotate: function(msg) {
1581         if (msg.length == 0) {
1582             msg = " ";  // triggers annotation deletion
1583         }
1584         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1585     },
1586     set_portal: function(msg) {
1587         if (msg.length == 0) {
1588             msg = " ";  // triggers portal deletion
1589         }
1590         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1591     },
1592     send_tile_control_command: function() {
1593         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1594     }
1595 }
1596
1597 tui.inputEl.addEventListener('input', (event) => {
1598     if (tui.mode.has_input_prompt) {
1599         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1600         if (tui.inputEl.value.length > max_length) {
1601             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1602         };
1603     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1604         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1605         tui.switch_mode('edit');
1606     }
1607     tui.full_refresh();
1608 }, false);
1609 document.onclick = function() {
1610     if (!tui.mode.is_single_char_entry) {
1611         tui.show_help = false;
1612     }
1613 };
1614 tui.inputEl.addEventListener('keydown', (event) => {
1615     tui.show_help = false;
1616     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1617         event.preventDefault();
1618     }
1619     if ((!tui.mode.is_intro && event.key == 'Escape')
1620         || (tui.mode.has_input_prompt && event.key == 'Enter'
1621             && tui.inputEl.value.length == 0
1622             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1623                 'admin_enter'].includes(tui.mode.name))) {
1624         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1625             tui.log_msg('@ aborted');
1626         }
1627         tui.switch_mode('play');
1628     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1629         tui.show_help = true;
1630         tui.inputEl.value = "";
1631         tui.restore_input_values();
1632     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1633                && !tui.mode.is_single_char_entry) {
1634         tui.show_help = true;
1635     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1636         tui.login_name = tui.inputEl.value;
1637         server.send(['LOGIN', tui.inputEl.value]);
1638         tui.inputEl.value = "";
1639     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1640         tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1641     } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1642         tui.enter_ascii_art('PLAYER_HAT', 3, 6);
1643     } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1644         tui.enter_ascii_art('THING_DESIGN',
1645                             game.player.carrying.design[0][0],
1646                             game.player.carrying.design[0][1], true);
1647     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1648         server.send(['TASK:COMMAND', tui.inputEl.value]);
1649         tui.inputEl.value = "";
1650     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1651         tui.pick_selectable('PICK_UP');
1652     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1653         tui.pick_selectable('DROP');
1654     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1655         if (tui.inputEl.value.length == 0) {
1656             tui.log_msg('@ aborted');
1657         } else {
1658             server.send(['SET_MAP_CONTROL_PASSWORD',
1659                         tui.tile_control_char, tui.inputEl.value]);
1660             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1661         }
1662         tui.switch_mode('admin');
1663     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1664         explorer.set_portal(tui.inputEl.value);
1665         tui.switch_mode('edit');
1666     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1667         if (tui.inputEl.value.length == 0) {
1668             tui.inputEl.value = " ";
1669         }
1670         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1671         tui.switch_mode('edit');
1672     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1673         explorer.annotate(tui.inputEl.value);
1674         tui.switch_mode('edit');
1675     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1676         if (tui.inputEl.value.length == 0) {
1677             tui.inputEl.value = " ";
1678         }
1679         tui.password = tui.inputEl.value
1680         tui.switch_mode('edit');
1681     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1682         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1683         tui.switch_mode('play');
1684     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1685         if (tui.inputEl.value.length != 1) {
1686             tui.log_msg('@ entered non-single-char, therefore aborted');
1687             tui.switch_mode('admin');
1688         } else {
1689             tui.tile_control_char = tui.inputEl.value[0];
1690             tui.switch_mode('control_pw_pw');
1691         }
1692     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1693         if (tui.inputEl.value.length != 1) {
1694             tui.log_msg('@ entered non-single-char, therefore aborted');
1695             tui.switch_mode('admin');
1696         } else {
1697             tui.tile_control_char = tui.inputEl.value[0];
1698             tui.switch_mode('control_tile_draw');
1699         }
1700     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1701         if (tui.inputEl.value.length != 1) {
1702             tui.log_msg('@ entered non-single-char, therefore aborted');
1703         } else {
1704             server.send(['THING_PROTECTION', tui.inputEl.value])
1705             tui.log_msg('@ sent new protection character for thing');
1706         }
1707         tui.switch_mode('admin');
1708     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1709         let tokens = parser.tokenize(tui.inputEl.value);
1710         if (tokens.length > 0 && tokens[0].length > 0) {
1711             if (tui.inputEl.value[0][0] == '/') {
1712                 if (tokens[0].slice(1) == 'nick') {
1713                     if (tokens.length > 1) {
1714                         server.send(['NICK', tokens[1]]);
1715                     } else {
1716                         tui.log_msg('? need new name');
1717                     }
1718                 } else {
1719                     tui.log_msg('? unknown command');
1720                 }
1721             } else {
1722                     server.send(['ALL', tui.inputEl.value]);
1723             }
1724         } else if (tui.inputEl.valuelength > 0) {
1725                 server.send(['ALL', tui.inputEl.value]);
1726         }
1727         tui.inputEl.value = "";
1728     } else if (tui.mode.name == 'play') {
1729           if (tui.mode.mode_switch_on_key(event)) {
1730               null;
1731           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1732               server.send(["TASK:INTOXICATE"]);
1733           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1734               server.send(["TASK:DOOR"]);
1735           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1736               server.send(["TASK:WEAR"]);
1737           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1738               server.send(["TASK:SPIN"]);
1739           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1740               server.send(["TASK:DANCE"]);
1741           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1742               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1743           } else if (event.key === tui.keys.teleport) {
1744               game.teleport();
1745           };
1746     } else if (tui.mode.name == 'study') {
1747         if (tui.mode.mode_switch_on_key(event)) {
1748               null;
1749         } else if (event.key in tui.movement_keys) {
1750             explorer.move(tui.movement_keys[event.key]);
1751         } else if (event.key == tui.keys.toggle_map_mode) {
1752             tui.toggle_map_mode();
1753         };
1754     } else if (tui.mode.name == 'control_tile_draw') {
1755         if (tui.mode.mode_switch_on_key(event)) {
1756             null;
1757         } else if (event.key in tui.movement_keys) {
1758             explorer.move(tui.movement_keys[event.key]);
1759         } else if (event.key === tui.keys.toggle_tile_draw) {
1760             tui.toggle_tile_draw();
1761         };
1762     } else if (tui.mode.name == 'admin') {
1763         if (tui.mode.mode_switch_on_key(event)) {
1764               null;
1765         } else if (event.key == tui.keys.toggle_map_mode) {
1766             tui.toggle_map_mode();
1767         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1768             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1769         };
1770     } else if (tui.mode.name == 'edit') {
1771         if (tui.mode.mode_switch_on_key(event)) {
1772               null;
1773         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1774             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1775         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1776             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1777           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1778               server.send(["TASK:INSTALL", tui.password]);
1779         } else if (event.key == tui.keys.toggle_map_mode) {
1780             tui.toggle_map_mode();
1781         }
1782     }
1783     tui.full_refresh();
1784 }, false);
1785
1786 rows_selector.addEventListener('input', function() {
1787     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1788         return;
1789     }
1790     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1791     terminal.initialize();
1792     tui.full_refresh();
1793 }, false);
1794 cols_selector.addEventListener('input', function() {
1795     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1796         return;
1797     }
1798     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1799     terminal.initialize();
1800     tui.window_width = terminal.cols / 2,
1801     tui.full_refresh();
1802 }, false);
1803 for (let key_selector of key_selectors) {
1804     key_selector.addEventListener('input', function() {
1805         window.localStorage.setItem(key_selector.id, key_selector.value);
1806         tui.init_keys();
1807     }, false);
1808 }
1809 window.setInterval(function() {
1810     if (server.websocket.readyState == 1) {
1811         server.send(['PING']);
1812     } else if (server.websocket.readyState != 0) {
1813         server.reconnect_to(server.url);
1814         tui.log_msg('@ attempting reconnect …')
1815     }
1816 }, 1000);
1817 window.setInterval(function() {
1818     if (document.activeElement.tagName.toLowerCase() != 'input') {
1819         const scroll_x = window.scrollX;
1820         const scroll_y = window.scrollY;
1821         tui.inputEl.focus();
1822         window.scrollTo(scroll_x, scroll_y);
1823     };
1824 }, 100);
1825 document.getElementById("help").onclick = function() {
1826     tui.show_help = true;
1827     tui.full_refresh();
1828 };
1829 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1830     const mode = switchEl.id.slice("switch_to_".length);
1831     switchEl.onclick = function() {
1832         tui.switch_mode(mode);
1833         tui.full_refresh();
1834     }
1835 };
1836 document.getElementById("toggle_tile_draw").onclick = function() {
1837     tui.toggle_tile_draw();
1838 }
1839 document.getElementById("toggle_map_mode").onclick = function() {
1840     tui.toggle_map_mode();
1841     tui.full_refresh();
1842 };
1843 document.getElementById("flatten").onclick = function() {
1844     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1845 };
1846 document.getElementById("door").onclick = function() {
1847     server.send(['TASK:DOOR']);
1848 };
1849 document.getElementById("consume").onclick = function() {
1850     server.send(['TASK:INTOXICATE']);
1851 };
1852 document.getElementById("install").onclick = function() {
1853     server.send(['TASK:INSTALL', tui.password]);
1854 };
1855 document.getElementById("wear").onclick = function() {
1856     server.send(['TASK:WEAR']);
1857 };
1858 document.getElementById("spin").onclick = function() {
1859     server.send(['TASK:SPIN']);
1860 };
1861 document.getElementById("dance").onclick = function() {
1862     server.send(['TASK:DANCE']);
1863 };
1864 document.getElementById("teleport").onclick = function() {
1865     game.teleport();
1866 };
1867 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1868     if (move_button.id.startsWith('key_')) {  // not a move button
1869         continue;
1870     };
1871     let direction = move_button.id.split('_')[2].toUpperCase();
1872     let move_repeat;
1873     function move() {
1874         if (tui.mode.available_actions.includes("move")) {
1875             server.send(['TASK:MOVE', direction]);
1876         } else if (tui.mode.available_actions.includes("move_explorer")) {
1877             explorer.move(direction);
1878             tui.full_refresh();
1879         };
1880     }
1881     move_button.onmousedown = function() {
1882         move();
1883         move_repeat = window.setInterval(move, 100);
1884     };
1885     move_button.onmouseup = function() {
1886         window.clearInterval(move_repeat);
1887     }
1888 };
1889 </script>
1890 </body></html>