home · contact · privacy
f9a6a01c6b20f5405a4648c12f3ccb081199511a
[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       let t_char = ' ';
1217       if (t.thing_char) {
1218           t_char = t.thing_char;
1219       }
1220       function draw_body_part(body_part, end_y) {
1221           terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1222           terminal.write(end_y - 3, start_x, '|        |');
1223           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1224           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1225           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1226       }
1227       if (t.face) {
1228           draw_body_part(t.face, terminal.rows - 2);
1229       }
1230       if (t.hat) {
1231           draw_body_part(t.hat, terminal.rows - 5);
1232       }
1233       terminal.write(terminal.rows - 1, start_x, '|        |');
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);
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) {
1528         const symbol = game.thing_types[t.type_];
1529         let info = '- ' + symbol;
1530         if (t.thing_char) {
1531             info += t.thing_char;
1532         };
1533         if (t.name_) {
1534             info += ": " + t.name_;
1535         }
1536         info += ' (' + t.type_;
1537         if (t.installed) {
1538             info += "/installed";
1539         }
1540         const protection = t.protection;
1541         if (protection != '.') {
1542             info += '/protection:' + protection;
1543         }
1544         info += ')\n';
1545         if (t.hat || t.face) {
1546             info += '----------\n';
1547         }
1548         if (t.hat) {
1549             info += '| ' + t.hat.slice(0, 6) + ' |\n';
1550             info += '| ' + t.hat.slice(6, 12) + ' |\n';
1551             info += '| ' + t.hat.slice(12, 18) + ' |\n';
1552         }
1553         if (t.face) {
1554             info += '| ' + t.face.slice(0, 6) + ' |\n';
1555             info += '| ' + t.face.slice(6, 12) + ' |\n';
1556             info += '| ' + t.face.slice(12, 18) + ' |\n';
1557             info += '----------\n';
1558         }
1559         if (t.design) {
1560             const line_length = t.design[0][1];
1561             info += '-'.repeat(line_length + 4) + '\n';
1562             const regexp = RegExp('.{1,' + line_length + '}', 'g');
1563             const lines = t.design[1].match(regexp);
1564             for (const line of lines) {
1565                 info += '| ' + line + ' |\n';
1566             }
1567             info += '-'.repeat(line_length + 4) + '\n';
1568         }
1569         return info;
1570     },
1571     annotate: function(msg) {
1572         if (msg.length == 0) {
1573             msg = " ";  // triggers annotation deletion
1574         }
1575         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1576     },
1577     set_portal: function(msg) {
1578         if (msg.length == 0) {
1579             msg = " ";  // triggers portal deletion
1580         }
1581         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1582     },
1583     send_tile_control_command: function() {
1584         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1585     }
1586 }
1587
1588 tui.inputEl.addEventListener('input', (event) => {
1589     if (tui.mode.has_input_prompt) {
1590         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1591         if (tui.inputEl.value.length > max_length) {
1592             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1593         };
1594     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1595         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1596         tui.switch_mode('edit');
1597     }
1598     tui.full_refresh();
1599 }, false);
1600 document.onclick = function() {
1601     if (!tui.mode.is_single_char_entry) {
1602         tui.show_help = false;
1603     }
1604 };
1605 tui.inputEl.addEventListener('keydown', (event) => {
1606     tui.show_help = false;
1607     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1608         event.preventDefault();
1609     }
1610     if ((!tui.mode.is_intro && event.key == 'Escape')
1611         || (tui.mode.has_input_prompt && event.key == 'Enter'
1612             && tui.inputEl.value.length == 0
1613             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1614                 'admin_enter'].includes(tui.mode.name))) {
1615         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1616             tui.log_msg('@ aborted');
1617         }
1618         tui.switch_mode('play');
1619     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1620         tui.show_help = true;
1621         tui.inputEl.value = "";
1622         tui.restore_input_values();
1623     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1624                && !tui.mode.is_single_char_entry) {
1625         tui.show_help = true;
1626     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1627         tui.login_name = tui.inputEl.value;
1628         server.send(['LOGIN', tui.inputEl.value]);
1629         tui.inputEl.value = "";
1630     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1631         tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1632     } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1633         tui.enter_ascii_art('THING_DESIGN',
1634                             game.player.carrying.design[0][0],
1635                             game.player.carrying.design[0][1], true);
1636     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1637         server.send(['TASK:COMMAND', tui.inputEl.value]);
1638         tui.inputEl.value = "";
1639     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1640         tui.pick_selectable('PICK_UP');
1641     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1642         tui.pick_selectable('DROP');
1643     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1644         if (tui.inputEl.value.length == 0) {
1645             tui.log_msg('@ aborted');
1646         } else {
1647             server.send(['SET_MAP_CONTROL_PASSWORD',
1648                         tui.tile_control_char, tui.inputEl.value]);
1649             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1650         }
1651         tui.switch_mode('admin');
1652     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1653         explorer.set_portal(tui.inputEl.value);
1654         tui.switch_mode('edit');
1655     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1656         if (tui.inputEl.value.length == 0) {
1657             tui.inputEl.value = " ";
1658         }
1659         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1660         tui.switch_mode('edit');
1661     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1662         explorer.annotate(tui.inputEl.value);
1663         tui.switch_mode('edit');
1664     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1665         if (tui.inputEl.value.length == 0) {
1666             tui.inputEl.value = " ";
1667         }
1668         tui.password = tui.inputEl.value
1669         tui.switch_mode('edit');
1670     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1671         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1672         tui.switch_mode('play');
1673     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1674         if (tui.inputEl.value.length != 1) {
1675             tui.log_msg('@ entered non-single-char, therefore aborted');
1676             tui.switch_mode('admin');
1677         } else {
1678             tui.tile_control_char = tui.inputEl.value[0];
1679             tui.switch_mode('control_pw_pw');
1680         }
1681     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1682         if (tui.inputEl.value.length != 1) {
1683             tui.log_msg('@ entered non-single-char, therefore aborted');
1684             tui.switch_mode('admin');
1685         } else {
1686             tui.tile_control_char = tui.inputEl.value[0];
1687             tui.switch_mode('control_tile_draw');
1688         }
1689     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1690         if (tui.inputEl.value.length != 1) {
1691             tui.log_msg('@ entered non-single-char, therefore aborted');
1692         } else {
1693             server.send(['THING_PROTECTION', tui.inputEl.value])
1694             tui.log_msg('@ sent new protection character for thing');
1695         }
1696         tui.switch_mode('admin');
1697     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1698         let tokens = parser.tokenize(tui.inputEl.value);
1699         if (tokens.length > 0 && tokens[0].length > 0) {
1700             if (tui.inputEl.value[0][0] == '/') {
1701                 if (tokens[0].slice(1) == 'nick') {
1702                     if (tokens.length > 1) {
1703                         server.send(['NICK', tokens[1]]);
1704                     } else {
1705                         tui.log_msg('? need new name');
1706                     }
1707                 } else {
1708                     tui.log_msg('? unknown command');
1709                 }
1710             } else {
1711                     server.send(['ALL', tui.inputEl.value]);
1712             }
1713         } else if (tui.inputEl.valuelength > 0) {
1714                 server.send(['ALL', tui.inputEl.value]);
1715         }
1716         tui.inputEl.value = "";
1717     } else if (tui.mode.name == 'play') {
1718           if (tui.mode.mode_switch_on_key(event)) {
1719               null;
1720           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1721               server.send(["TASK:INTOXICATE"]);
1722           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1723               server.send(["TASK:DOOR"]);
1724           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1725               server.send(["TASK:WEAR"]);
1726           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1727               server.send(["TASK:SPIN"]);
1728           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1729               server.send(["TASK:DANCE"]);
1730           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1731               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1732           } else if (event.key === tui.keys.teleport) {
1733               game.teleport();
1734           };
1735     } else if (tui.mode.name == 'study') {
1736         if (tui.mode.mode_switch_on_key(event)) {
1737               null;
1738         } else if (event.key in tui.movement_keys) {
1739             explorer.move(tui.movement_keys[event.key]);
1740         } else if (event.key == tui.keys.toggle_map_mode) {
1741             tui.toggle_map_mode();
1742         };
1743     } else if (tui.mode.name == 'control_tile_draw') {
1744         if (tui.mode.mode_switch_on_key(event)) {
1745             null;
1746         } else if (event.key in tui.movement_keys) {
1747             explorer.move(tui.movement_keys[event.key]);
1748         } else if (event.key === tui.keys.toggle_tile_draw) {
1749             tui.toggle_tile_draw();
1750         };
1751     } else if (tui.mode.name == 'admin') {
1752         if (tui.mode.mode_switch_on_key(event)) {
1753               null;
1754         } else if (event.key == tui.keys.toggle_map_mode) {
1755             tui.toggle_map_mode();
1756         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1757             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1758         };
1759     } else if (tui.mode.name == 'edit') {
1760         if (tui.mode.mode_switch_on_key(event)) {
1761               null;
1762         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1763             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1764         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1765             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1766           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1767               server.send(["TASK:INSTALL", tui.password]);
1768         } else if (event.key == tui.keys.toggle_map_mode) {
1769             tui.toggle_map_mode();
1770         }
1771     }
1772     tui.full_refresh();
1773 }, false);
1774
1775 rows_selector.addEventListener('input', function() {
1776     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1777         return;
1778     }
1779     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1780     terminal.initialize();
1781     tui.full_refresh();
1782 }, false);
1783 cols_selector.addEventListener('input', function() {
1784     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1785         return;
1786     }
1787     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1788     terminal.initialize();
1789     tui.window_width = terminal.cols / 2,
1790     tui.full_refresh();
1791 }, false);
1792 for (let key_selector of key_selectors) {
1793     key_selector.addEventListener('input', function() {
1794         window.localStorage.setItem(key_selector.id, key_selector.value);
1795         tui.init_keys();
1796     }, false);
1797 }
1798 window.setInterval(function() {
1799     if (server.websocket.readyState == 1) {
1800         server.send(['PING']);
1801     } else if (server.websocket.readyState != 0) {
1802         server.reconnect_to(server.url);
1803         tui.log_msg('@ attempting reconnect …')
1804     }
1805 }, 1000);
1806 window.setInterval(function() {
1807     if (document.activeElement.tagName.toLowerCase() != 'input') {
1808         const scroll_x = window.scrollX;
1809         const scroll_y = window.scrollY;
1810         tui.inputEl.focus();
1811         window.scrollTo(scroll_x, scroll_y);
1812     };
1813 }, 100);
1814 document.getElementById("help").onclick = function() {
1815     tui.show_help = true;
1816     tui.full_refresh();
1817 };
1818 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1819     const mode = switchEl.id.slice("switch_to_".length);
1820     switchEl.onclick = function() {
1821         tui.switch_mode(mode);
1822         tui.full_refresh();
1823     }
1824 };
1825 document.getElementById("toggle_tile_draw").onclick = function() {
1826     tui.toggle_tile_draw();
1827 }
1828 document.getElementById("toggle_map_mode").onclick = function() {
1829     tui.toggle_map_mode();
1830     tui.full_refresh();
1831 };
1832 document.getElementById("flatten").onclick = function() {
1833     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1834 };
1835 document.getElementById("door").onclick = function() {
1836     server.send(['TASK:DOOR']);
1837 };
1838 document.getElementById("consume").onclick = function() {
1839     server.send(['TASK:INTOXICATE']);
1840 };
1841 document.getElementById("install").onclick = function() {
1842     server.send(['TASK:INSTALL', tui.password]);
1843 };
1844 document.getElementById("wear").onclick = function() {
1845     server.send(['TASK:WEAR']);
1846 };
1847 document.getElementById("spin").onclick = function() {
1848     server.send(['TASK:SPIN']);
1849 };
1850 document.getElementById("dance").onclick = function() {
1851     server.send(['TASK:DANCE']);
1852 };
1853 document.getElementById("teleport").onclick = function() {
1854     game.teleport();
1855 };
1856 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1857     if (move_button.id.startsWith('key_')) {  // not a move button
1858         continue;
1859     };
1860     let direction = move_button.id.split('_')[2].toUpperCase();
1861     let move_repeat;
1862     function move() {
1863         if (tui.mode.available_actions.includes("move")) {
1864             server.send(['TASK:MOVE', direction]);
1865         } else if (tui.mode.available_actions.includes("move_explorer")) {
1866             explorer.move(direction);
1867             tui.full_refresh();
1868         };
1869     }
1870     move_button.onmousedown = function() {
1871         move();
1872         move_repeat = window.setInterval(move, 100);
1873     };
1874     move_button.onmouseup = function() {
1875         window.clearInterval(move_repeat);
1876     }
1877 };
1878 </script>
1879 </body></html>