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