home · contact · privacy
45f04a6ccf70e8e384f2e47d72fa358ce8382c2f
[plomrogue2] / rogue_chat.html
1 <!DOCTYPE html>
2 <html><head>
3 <style>
4   pre {
5       display: inline-block;
6   }
7   pre a {
8       color: white;
9   }
10 </style>
11 </head><body>
12 <div>
13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
16 </div>
17 <div style="position: relative; display: inline-block;">
18 <pre id="terminal"></pre>
19 <textarea id="input" style="position: absolute; left: 0; height: 100%; width: 100%; opacity: 0; z-index: -1;"></textarea>
20 </div>
21 <h3>button controls for hard-to-remember keybindings</h3>
22 <table id="move_table" style="float: left">
23   <tr>
24     <td style="text-align: right"><button id="hex_move_upleft"></button></td>
25     <td style="text-align: center"><button id="square_move_up"></button></td>
26     <td><button id="hex_move_upright"></button></td>
27   </tr>
28   <tr>
29     <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
30     <td stlye="text-align: center;">move</td>
31     <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
32   </tr>
33   <tr>
34     <td><button id="hex_move_downleft"></button></td>
35     <td style="text-align: center"><button id="square_move_down"></button></td>
36     <td><button id="hex_move_downright"></button></td>
37   </tr>
38 </table>
39 <table>
40   <tr>
41     <td><button id="help"></button></td>
42   </tr>
43   <tr>
44     <td><button id="switch_to_chat"></button><br /></td>
45   </tr>
46   <tr>
47     <td><button id="switch_to_study"></button></td>
48     <td><button id="toggle_map_mode"></button>
49   </tr>
50   <tr>
51     <td><button id="switch_to_play"></button></td>
52     <td>
53       <button id="switch_to_take_thing"></button>
54       <button id="switch_to_drop_thing"></button>
55       <button id="door"></button>
56       <button id="consume"></button>
57       <button id="dance"></button>
58       <button id="switch_to_command_thing"></button>
59       <button id="teleport"></button>
60       <button id="wear"></button>
61       <button id="spin"></button>
62     </td>
63   </tr>
64   <tr>
65     <td><button id="switch_to_edit"></button></td>
66     <td>
67       <button id="switch_to_write"></button>
68       <button id="flatten"></button>
69       <button id="install"></button>
70       <button id="switch_to_annotate"></button>
71       <button id="switch_to_portal"></button>
72       <button id="switch_to_name_thing"></button>
73       <button id="switch_to_password"></button>
74       <button id="switch_to_enter_face"></button>
75       <button id="switch_to_enter_hat"></button>
76     </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_hat" type="text" value="H" />
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 (enter nothing to abort):',
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_hat': {
183         'short': 'edit hat',
184         'intro': '@ enter hat line (enter nothing to abort):',
185         'long': 'Draw your hat as ASCII art.  The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
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_CHAR') {
515             let t = game.get_thing_temp(tokens[1]);
516             t.thing_char = tokens[2];
517         } else if (tokens[0] === 'TASKS') {
518             game.tasks = tokens[1].split(',');
519             tui.mode_write.legal = game.tasks.includes('WRITE');
520             tui.mode_command_thing.legal = game.tasks.includes('WRITE');
521             tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
522             tui.mode_drop_thing.legal = game.tasks.includes('DROP');
523         } else if (tokens[0] === 'THING_TYPE') {
524             game.thing_types[tokens[1]] = tokens[2]
525         } else if (tokens[0] === 'THING_CARRYING') {
526             let t = game.get_thing_temp(tokens[1]);
527             t.carrying = game.get_thing_temp(tokens[2], false);
528         } else if (tokens[0] === 'THING_INSTALLED') {
529             let t = game.get_thing_temp(tokens[1]);
530             t.installed = true;
531         } else if (tokens[0] === 'TERRAIN') {
532             game.terrains[tokens[1]] = tokens[2]
533         } else if (tokens[0] === 'MAP') {
534             game.map_geometry_new = tokens[1];
535             game.map_size_new = parser.parse_yx(tokens[2]);
536             game.map_new = tokens[3]
537         } else if (tokens[0] === 'FOV') {
538             game.fov_new = tokens[1]
539         } else if (tokens[0] === 'MAP_CONTROL') {
540             game.map_control_new = tokens[1]
541         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
542             game.portals = game.portals_new;
543             game.map_geometry = game.map_geometry_new;
544             game.map_size = game.map_size_new;
545             game.map = game.map_new;
546             game.fov = game.fov_new;
547             tui.init_keys();
548             game.map_control = game.map_control_new;
549             explorer.annotations = explorer.annotations_new;
550             explorer.info_cached = false;
551             game.things = game.things_new;
552             game.player = game.things[game.player_id];
553             game.players_hat_chars = game.players_hat_chars_new;
554             game.bladder_pressure = game.bladder_pressure_new
555             game.energy = game.energy_new
556             game.turn_complete = true;
557             if (tui.mode.name == 'post_login_wait') {
558                 tui.switch_mode('play');
559             } else {
560                 tui.full_refresh();
561             }
562         } else if (tokens[0] === 'CHAT') {
563              tui.log_msg('# ' + tokens[1], 1);
564         } else if (tokens[0] === 'CHATFACE') {
565             tui.draw_face = tokens[1];
566             tui.full_refresh();
567         } else if (tokens[0] === 'REPLY') {
568              tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
569         } else if (tokens[0] === 'PLAYER_ID') {
570             game.player_id = parseInt(tokens[1]);
571         } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
572             game.players_hat_chars_new = tokens[1];
573         } else if (tokens[0] === 'LOGIN_OK') {
574             this.send(['GET_GAMESTATE']);
575             tui.switch_mode('post_login_wait');
576             tui.log_msg('@ welcome!')
577             tui.log_msg('@ hint: see top of terminal for how to get help.')
578             tui.log_msg('@ hint: enter study mode to understand your environment.')
579         } else if (tokens[0] === 'DEFAULT_COLORS') {
580             terminal.set_default_colors();
581         } else if (tokens[0] === 'RANDOM_COLORS') {
582             terminal.set_random_colors();
583         } else if (tokens[0] === 'ADMIN_OK') {
584             tui.is_admin = true;
585             tui.log_msg('@ you now have admin rights');
586             tui.switch_mode('admin');
587         } else if (tokens[0] === 'PORTAL') {
588             let position = parser.parse_yx(tokens[1]);
589             game.portals_new[position] = tokens[2];
590         } else if (tokens[0] === 'ANNOTATION') {
591             let position = parser.parse_yx(tokens[1]);
592             explorer.annotations_new[position] = tokens[2];
593         } else if (tokens[0] === 'UNHANDLED_INPUT') {
594             tui.log_msg('? unknown command');
595         } else if (tokens[0] === 'PLAY_ERROR') {
596             tui.log_msg('? ' + tokens[1]);
597             terminal.blink_screen();
598         } else if (tokens[0] === 'ARGUMENT_ERROR') {
599             tui.log_msg('? syntax error: ' + tokens[1]);
600         } else if (tokens[0] === 'GAME_ERROR') {
601             tui.log_msg('? game error: ' + tokens[1]);
602         } else if (tokens[0] === 'PONG') {
603             ;
604         } else {
605             tui.log_msg('? unhandled input: ' + event.data);
606         }
607     }
608 }
609
610 let unparser = {
611     quote: function(str) {
612         let quoted = ['"'];
613         for (let i = 0; i < str.length; i++) {
614             let c = str[i];
615             if (['"', '\\'].includes(c)) {
616                 quoted.push('\\');
617             };
618             quoted.push(c);
619         }
620         quoted.push('"');
621         return quoted.join('');
622     },
623     to_yx: function(yx_coordinate) {
624         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
625     },
626     untokenize: function(tokens) {
627         let quoted_tokens = [];
628         for (let token of tokens) {
629             quoted_tokens.push(this.quote(token));
630         }
631         return quoted_tokens.join(" ");
632     }
633 }
634
635 class Mode {
636     constructor(name, has_input_prompt=false, shows_info=false,
637                 is_intro=false, is_single_char_entry=false) {
638         this.name = name;
639         this.short_desc = mode_helps[name].short;
640         this.available_modes = [];
641         this.available_actions = [];
642         this.has_input_prompt = has_input_prompt;
643         this.shows_info= shows_info;
644         this.is_intro = is_intro;
645         this.help_intro = mode_helps[name].long;
646         this.intro_msg = mode_helps[name].intro;
647         this.is_single_char_entry = is_single_char_entry;
648         this.legal = true;
649     }
650     *iter_available_modes() {
651         for (let mode_name of this.available_modes) {
652             let mode = tui['mode_' + mode_name];
653             if (!mode.legal) {
654                 continue;
655             }
656             let key = tui.keys['switch_to_' + mode.name];
657             yield [mode, key]
658         }
659     }
660     list_available_modes() {
661         let msg = ''
662         if (this.available_modes.length > 0) {
663             msg += 'Other modes available from here:\n';
664             for (let [mode, key] of this.iter_available_modes()) {
665                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
666             }
667         }
668         return msg;
669     }
670     mode_switch_on_key(key_event) {
671         for (let [mode, key] of this.iter_available_modes()) {
672             if (key_event.key == key) {
673                 event.preventDefault();
674                 tui.switch_mode(mode.name);
675                 return true;
676             };
677         }
678         return false;
679     }
680 }
681 let tui = {
682   links: {},
683   log: [],
684   input_prompt: '> ',
685   input_lines: [],
686   window_width: terminal.cols / 2,
687   height_turn_line: 1,
688   height_mode_line: 1,
689   height_input: 1,
690   password: 'foo',
691   show_help: false,
692   is_admin: false,
693   tile_draw: false,
694   mode_waiting_for_server: new Mode('waiting_for_server',
695                                      false, false, true),
696   mode_login: new Mode('login', true, false, true),
697   mode_post_login_wait: new Mode('post_login_wait'),
698   mode_chat: new Mode('chat', true),
699   mode_annotate: new Mode('annotate', true, true),
700   mode_play: new Mode('play'),
701   mode_study: new Mode('study', false, true),
702   mode_write: new Mode('write', false, false, false, true),
703   mode_edit: new Mode('edit'),
704   mode_control_pw_type: new Mode('control_pw_type', true),
705   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
706   mode_portal: new Mode('portal', true, true),
707   mode_password: new Mode('password', true),
708   mode_name_thing: new Mode('name_thing', true, true),
709   mode_command_thing: new Mode('command_thing', true),
710   mode_take_thing: new Mode('take_thing', true),
711   mode_drop_thing: new Mode('drop_thing', true),
712   mode_enter_face: new Mode('enter_face', true),
713   mode_enter_hat: new Mode('enter_hat', true),
714   mode_admin_enter: new Mode('admin_enter', true),
715   mode_admin: new Mode('admin'),
716   mode_control_pw_pw: new Mode('control_pw_pw', true),
717   mode_control_tile_type: new Mode('control_tile_type', true),
718   mode_control_tile_draw: new Mode('control_tile_draw'),
719   action_tasks: {
720       'flatten': 'FLATTEN_SURROUNDINGS',
721       'take_thing': 'PICK_UP',
722       'drop_thing': 'DROP',
723       'move': 'MOVE',
724       'door': 'DOOR',
725       'install': 'INSTALL',
726       'wear': 'WEAR',
727       'command': 'COMMAND',
728       'consume': 'INTOXICATE',
729       'spin': 'SPIN',
730       'dance': 'DANCE',
731   },
732   offset: [0,0],
733   map_lines: [],
734   ascii_draw_stage: 0,
735   full_ascii_draw: '',
736   selectables: [],
737   draw_face: false,
738   init: function() {
739       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
740                                         "command_thing", "take_thing", "drop_thing"]
741       this.mode_play.available_actions = ["move", "teleport", "door", "consume",
742                                           "wear", "spin", "dance"];
743       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
744       this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
745       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
746                                          "control_tile_type", "chat",
747                                          "study", "play", "edit"]
748       this.mode_admin.available_actions = ["move", "toggle_map_mode"];
749       this.mode_control_tile_draw.available_modes = ["admin_enter"]
750       this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
751       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
752                                         "password", "chat", "study", "play",
753                                         "admin_enter", "enter_face", "enter_hat"]
754       this.mode_edit.available_actions = ["move", "flatten", "install",
755                                           "toggle_map_mode"]
756       this.inputEl = document.getElementById("input");
757       this.switch_mode('waiting_for_server');
758       this.recalc_input_lines();
759       this.height_header = this.height_turn_line + this.height_mode_line;
760       this.init_keys();
761   },
762   init_keys: function() {
763     document.getElementById("move_table").hidden = true;
764     this.keys = {};
765     for (let key_selector of key_selectors) {
766         this.keys[key_selector.id.slice(4)] = key_selector.value;
767     }
768     this.movement_keys = {};
769     let geometry_prefix = 'undefinedMapGeometry_';
770     if (game.map_geometry) {
771         geometry_prefix = game.map_geometry.toLowerCase() + '_';
772     }
773     for (const key_name of Object.keys(key_descriptions)) {
774         if (key_name.startsWith(geometry_prefix)) {
775             let direction = key_name.split('_')[2].toUpperCase();
776             let key = this.keys[key_name];
777             this.movement_keys[key] = direction;
778         }
779     };
780     for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
781         if (move_button.id.startsWith('key_')) {
782             continue;
783         }
784         move_button.hidden = true;
785     };
786     for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
787         document.getElementById("move_table").hidden = false;
788         move_button.hidden = false;
789     };
790     for (let el of document.getElementsByTagName("button")) {
791       let action_desc = key_descriptions[el.id];
792       let action_key = '[' + this.keys[el.id] + ']';
793       el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
794     }
795   },
796   task_action_on: function(action) {
797       return game.tasks.includes(this.action_tasks[action]);
798   },
799   switch_mode: function(mode_name) {
800
801     function fail(msg, return_mode='play') {
802         tui.log_msg('? ' + msg);
803         terminal.blink_screen();
804         tui.switch_mode(return_mode);
805     }
806
807     if (this.mode && this.mode.name == 'control_tile_draw') {
808         tui.log_msg('@ finished tile protection drawing.')
809     }
810     this.draw_face = false;
811     this.tile_draw = false;
812     if (mode_name == 'command_thing' && (!game.player.carrying
813                                          || !game.player.carrying.commandable)) {
814         return fail('not carrying anything commandable');
815     } else if (mode_name == 'name_thing' && !game.player.carrying) {
816         return fail('not carrying anything to re-name', 'edit');
817     } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
818         return fail('not carrying anything to protect')
819     } else if (mode_name == 'take_thing' && game.player.carrying) {
820         return fail('already carrying something');
821     } else if (mode_name == 'drop_thing' && !game.player.carrying) {
822         return fail('not carrying anything droppable');
823     } else if (mode_name == 'enter_hat' && !game.player.hat) {
824         return fail('not wearing hat to edit', 'edit');
825     }
826     if (mode_name == 'admin_enter' && this.is_admin) {
827         mode_name = 'admin';
828     };
829     this.mode = this['mode_' + mode_name];
830     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
831         this.map_mode = 'protections';
832     } else if (this.mode.name != "edit") {
833         this.map_mode = 'terrain + things';
834     };
835     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
836         explorer.position = game.player.position;
837     }
838     this.inputEl.value = "";
839     this.restore_input_values();
840     for (let el of document.getElementsByTagName("button")) {
841         el.disabled = true;
842     }
843     document.getElementById("help").disabled = false;
844     for (const action of this.mode.available_actions) {
845         if (["move", "move_explorer"].includes(action)) {
846             for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
847                 move_key.disabled = false;
848             }
849         } else if (Object.keys(this.action_tasks).includes(action)) {
850             if (this.task_action_on(action)) {
851                 document.getElementById(action).disabled = false;
852             }
853         } else {
854             document.getElementById(action).disabled = false;
855         };
856     }
857     for (const mode_name of this.mode.available_modes) {
858             document.getElementById('switch_to_' + mode_name).disabled = false;
859     }
860     if (this.mode.intro_msg.length > 0) {
861         this.log_msg(this.mode.intro_msg);
862     }
863     if (this.mode.name == 'login') {
864         if (this.login_name) {
865             server.send(['LOGIN', this.login_name]);
866         } else {
867             this.log_msg("? need login name");
868         }
869     } else if (this.mode.is_single_char_entry) {
870         this.show_help = true;
871     } else if (this.mode.name == 'take_thing') {
872         this.log_msg("Portable things in reach for pick-up:");
873         const y = game.player.position[0]
874         const x = game.player.position[1]
875         let directed_moves = {
876             'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
877         }
878         if (game.map_geometry == 'Square') {
879             directed_moves['UP'] = [-1, 0];
880             directed_moves['DOWN'] = [1, 0];
881         } else if (game.map_geometry == 'Hex') {
882             if (y % 2) {
883                 directed_moves['UPLEFT'] = [-1, 0];
884                 directed_moves['UPRIGHT'] = [-1, 1];
885                 directed_moves['DOWNLEFT'] = [1, 0];
886                 directed_moves['DOWNRIGHT'] = [1, 1];
887             } else {
888                 directed_moves['UPLEFT'] = [-1, -1];
889                 directed_moves['UPRIGHT'] = [-1, 0];
890                 directed_moves['DOWNLEFT'] = [1, -1];
891                 directed_moves['DOWNRIGHT'] = [1, 0];
892             }
893         }
894         console.log(directed_moves);
895         let select_range = {};
896         for (const direction in directed_moves) {
897             const move = directed_moves[direction];
898             select_range[direction] = [y + move[0], x + move[1]];
899         }
900         this.selectables = [];
901         let directions = [];
902         for (const direction in select_range) {
903             for (const t_id in game.things) {
904                 const t = game.things[t_id];
905                 const position = select_range[direction];
906                 if (t.portable
907                     && t.position[0] == position[0]
908                     && t.position[1] == position[1]) {
909                     this.selectables.push(t_id);
910                     directions.push(direction);
911                 }
912             }
913         }
914         if (this.selectables.length == 0) {
915             this.log_msg('none');
916             terminal.blink_screen();
917             this.switch_mode('play');
918             return;
919         } else {
920             for (let [i, t_id] of this.selectables.entries()) {
921                 const t = game.things[t_id];
922                 const direction = directions[i];
923                 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
924             }
925         }
926     } else if (this.mode.name == 'drop_thing') {
927         this.log_msg('Direction to drop thing to:');
928         this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
929         for (let [i, direction] of this.selectables.entries()) {
930             this.log_msg(i + ': ' + direction);
931         };
932     } else if (this.mode.name == 'enter_hat') {
933         this.log_msg('legal characters: ' + game.players_hat_chars);
934     } else if (this.mode.name == 'command_thing') {
935         server.send(['TASK:COMMAND', 'HELP']);
936     } else if (this.mode.name == 'control_pw_pw') {
937         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
938     } else if (this.mode.name == 'control_tile_draw') {
939         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 + '].')
940     }
941     this.full_refresh();
942   },
943   offset_links: function(offset, links) {
944       for (let y in links) {
945           let real_y = offset[0] + parseInt(y);
946           if (!this.links[real_y]) {
947               this.links[real_y] = [];
948           }
949           for (let link of links[y]) {
950               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
951               this.links[real_y].push(offset_link);
952           }
953       }
954   },
955   restore_input_values: function() {
956       if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
957           let info = explorer.annotations[explorer.position];
958           if (info != "(none)") {
959               this.inputEl.value = info;
960           }
961       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
962           let portal = game.portals[explorer.position]
963           this.inputEl.value = portal;
964       } else if (this.mode.name == 'password') {
965           this.inputEl.value = this.password;
966       } else if (this.mode.name == 'name_thing') {
967           if (game.player.carrying && game.player.carrying.name_) {
968               this.inputEl.value = game.player.carrying.name_;
969           }
970       } else if (this.mode.name == 'admin_thing_protect') {
971           if (game.player.carrying && game.player.carrying.protection) {
972               this.inputEl.value = game.player.carrying.protection;
973           }
974       } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
975           const start = this.ascii_draw_stage * 6;
976           const end = (this.ascii_draw_stage + 1) * 6;
977           if (this.mode.name == 'enter_face') {
978               this.inputEl.value = game.player.face.slice(start, end);
979           } else if (this.mode.name == 'enter_hat') {
980               this.inputEl.value = game.player.hat.slice(start, end);
981           }
982       }
983   },
984   recalc_input_lines: function() {
985       if (this.mode.has_input_prompt) {
986           let _ = null;
987           [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
988       } else {
989           this.input_lines = [];
990       }
991       this.height_input = this.input_lines.length;
992   },
993   msg_into_lines_of_width: function(msg, width) {
994       function push_inner_link(y, end_x) {
995           if (!inner_links[y]) {
996               inner_links[y] = [];
997           };
998           inner_links[y].push([url_start_x, end_x, url]);
999       };
1000       let link_data = {};
1001       let url_ends = [];
1002       const regexp = RegExp('https?://[^\\s]+', 'g');
1003       let match;
1004       while ((match = regexp.exec(msg)) !== null) {
1005           const url = match[0];
1006           const url_start = match.index;
1007           const url_end = match.index + match[0].length;
1008           link_data[url_start] = url;
1009           url_ends.push(url_end);
1010       }
1011       let url_start_x = 0;
1012       let url = '';
1013       let inner_links = {};
1014       let in_link = false;
1015       let chunk = "";
1016       let lines = [];
1017       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1018           if (x >= width || msg[i] == "\n") {
1019               if (in_link) {
1020                   push_inner_link(y, chunk.length);
1021                   url_start_x = 0;
1022                   if (url_ends[0] == i) {
1023                       in_link = false;
1024                       url_ends.shift();
1025                   }
1026               };
1027               lines.push(chunk);
1028               chunk = "";
1029               x = 0;
1030               if (msg[i] == "\n") {
1031                   x -= 1;
1032               };
1033               y += 1;
1034           };
1035           if (msg[i] != "\n") {
1036               chunk += msg[i];
1037           };
1038           if (i in link_data) {
1039               url_start_x = x;
1040               url = link_data[i];
1041               in_link = true;
1042           } else if (url_ends[0] == i) {
1043               url_ends.shift();
1044               push_inner_link(y, x);
1045               in_link = false;
1046           }
1047       }
1048       lines.push(chunk);
1049       if (in_link) {
1050           push_inner_link(lines.length - 1, chunk.length);
1051       }
1052       return [lines, inner_links];
1053   },
1054   log_msg: function(msg) {
1055       this.log.push(msg);
1056       while (this.log.length > 100) {
1057         this.log.shift();
1058       };
1059       this.full_refresh();
1060   },
1061   pick_selectable: function(task_name) {
1062       const i = parseInt(this.inputEl.value);
1063       if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1064           tui.log_msg('? invalid index, aborted');
1065       } else {
1066           server.send(['TASK:' + task_name, tui.selectables[i]]);
1067       }
1068       this.inputEl.value = "";
1069       this.switch_mode('play');
1070   },
1071   enter_ascii_art: function(command) {
1072       if (this.inputEl.value.length != 6) {
1073           this.log_msg('? wrong input length, must be 6; try again');
1074           return;
1075       }
1076       this.log_msg('  ' + this.inputEl.value);
1077       this.full_ascii_draw += this.inputEl.value;
1078       this.ascii_draw_stage += 1;
1079       if (this.ascii_draw_stage < 3) {
1080           this.restore_input_values();
1081       } else {
1082           server.send([command, this.full_ascii_draw]);
1083           this.full_ascii_draw = '';
1084           this.ascii_draw_stage = 0;
1085           this.inputEl.value = '';
1086           this.switch_mode('edit');
1087       }
1088   },
1089   draw_map: function() {
1090     if (!game.turn_complete && this.map_lines.length == 0) {
1091         return;
1092     }
1093     if (game.turn_complete) {
1094         let map_lines_split = [];
1095         let line = [];
1096         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1097             if (j == game.map_size[1]) {
1098                 map_lines_split.push(line);
1099                 line = [];
1100                 j = 0;
1101             };
1102             if (this.map_mode == 'protections') {
1103                 line.push(game.map_control[i] + ' ');
1104             } else {
1105                 line.push(game.map[i] + ' ');
1106             }
1107         };
1108         map_lines_split.push(line);
1109         if (this.map_mode == 'terrain + annotations') {
1110             for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1111                 const yx = coordinate.split(',')
1112                 map_lines_split[yx[0]][yx[1]] = 'A ';
1113             }
1114         } else if (this.map_mode == 'terrain + things') {
1115             for (const p in game.portals) {
1116                 let coordinate = p.split(',')
1117                 let original = map_lines_split[coordinate[0]][coordinate[1]];
1118                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1119             }
1120             let used_positions = [];
1121             function draw_thing(t, used_positions) {
1122                 let symbol = game.thing_types[t.type_];
1123                 let meta_char = ' ';
1124                 if (t.thing_char) {
1125                     meta_char = t.thing_char;
1126                 }
1127                 if (used_positions.includes(t.position.toString())) {
1128                     meta_char = '+';
1129                 };
1130                 if (t.carrying) {
1131                     meta_char = '$';
1132                 }
1133                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1134                 used_positions.push(t.position.toString());
1135             }
1136             for (const thing_id in game.things) {
1137                 let t = game.things[thing_id];
1138                 if (t.type_ != 'Player') {
1139                     draw_thing(t, used_positions);
1140                 }
1141             };
1142             for (const thing_id in game.things) {
1143                 let t = game.things[thing_id];
1144                 if (t.type_ == 'Player') {
1145                     draw_thing(t, used_positions);
1146                 }
1147             };
1148         }
1149         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1150             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1151         } else if (tui.map_mode != 'terrain + things') {
1152             map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1153         }
1154         this.map_lines = []
1155         if (game.map_geometry == 'Square') {
1156             for (let line_split of map_lines_split) {
1157                 this.map_lines.push(line_split.join(''));
1158             };
1159         } else if (game.map_geometry == 'Hex') {
1160             let indent = 0
1161             for (let line_split of map_lines_split) {
1162                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1163                 if (indent == 0) {
1164                     indent = 1;
1165                 } else {
1166                     indent = 0;
1167                 };
1168             };
1169         }
1170         let window_center = [terminal.rows / 2, this.window_width / 2];
1171         let center_position = [game.player.position[0], game.player.position[1]];
1172         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1173             center_position = [explorer.position[0], explorer.position[1]];
1174         }
1175         center_position[1] = center_position[1] * 2;
1176         this.offset = [center_position[0] - window_center[0],
1177                        center_position[1] - window_center[1]]
1178         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1179             this.offset[1] += 1;
1180         };
1181     };
1182     let term_y = Math.max(0, -this.offset[0]);
1183     let term_x = Math.max(0, -this.offset[1]);
1184     let map_y = Math.max(0, this.offset[0]);
1185     let map_x = Math.max(0, this.offset[1]);
1186     for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1187         let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1188         terminal.write(term_y, term_x, to_draw);
1189     }
1190   },
1191   draw_face_popup: function() {
1192       const t = game.things[this.draw_face];
1193       if (!t || !t.face) {
1194           this.draw_face = false;
1195           return;
1196       }
1197       const start_x = tui.window_width - 10;
1198       let t_char = ' ';
1199       if (t.thing_char) {
1200           t_char = t.thing_char;
1201       }
1202       function draw_body_part(body_part, end_y) {
1203           terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1204           terminal.write(end_y - 3, start_x, '|        |');
1205           terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1206           terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1207           terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1208       }
1209       if (t.face) {
1210           draw_body_part(t.face, terminal.rows - 2);
1211       }
1212       if (t.hat) {
1213           draw_body_part(t.hat, terminal.rows - 5);
1214       }
1215       terminal.write(terminal.rows - 1, start_x, '|        |');
1216   },
1217   draw_mode_line: function() {
1218       let help = 'hit [' + this.keys.help + '] for help';
1219       if (this.mode.has_input_prompt) {
1220           help = 'enter /help for help';
1221       }
1222       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1223   },
1224   draw_stats_line: function(n) {
1225       terminal.write(1, this.window_width,
1226                      'ENERGY: ' + game.energy +
1227                      ' BLADDER: ' + game.bladder_pressure);
1228   },
1229   draw_history: function() {
1230       let log_display_lines = [];
1231       let log_links = {};
1232       let y_offset_in_log = 0;
1233       for (let line of this.log) {
1234           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1235                                                                     this.window_width)
1236           log_display_lines = log_display_lines.concat(new_lines);
1237           for (const y in link_data) {
1238               const rel_y = y_offset_in_log + parseInt(y);
1239               log_links[rel_y] = [];
1240               for (let link of link_data[y]) {
1241                   log_links[rel_y].push(link);
1242               }
1243           }
1244           y_offset_in_log += new_lines.length;
1245       };
1246       let i = log_display_lines.length - 1;
1247       for (let y = terminal.rows - 1 - this.height_input;
1248            y >= this.height_header && i >= 0;
1249            y--, i--) {
1250           terminal.write(y, this.window_width, log_display_lines[i]);
1251       }
1252       for (const key of Object.keys(log_links)) {
1253           if (parseInt(key) <= i) {
1254               delete log_links[key];
1255           }
1256       }
1257       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1258                     this.window_width];
1259       this.offset_links(offset, log_links);
1260   },
1261   draw_info: function() {
1262       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1263       let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1264       let offset = [this.height_header, this.window_width];
1265       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1266         terminal.write(y, offset[1], lines[i]);
1267       }
1268       this.offset_links(offset, link_data);
1269   },
1270   draw_input: function() {
1271     if (this.mode.has_input_prompt) {
1272         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1273             terminal.write(y, this.window_width, this.input_lines[i]);
1274         }
1275     }
1276   },
1277   draw_help: function() {
1278       let movement_keys_desc = '';
1279       if (!this.mode.is_intro) {
1280           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1281       }
1282       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1283       if (this.mode.available_actions.length > 0) {
1284           content += "Available actions:\n";
1285           for (let action of this.mode.available_actions) {
1286               if (Object.keys(this.action_tasks).includes(action)) {
1287                   if (!this.task_action_on(action)) {
1288                       continue;
1289                   }
1290               }
1291               if (action == 'move_explorer') {
1292                   action = 'move';
1293               }
1294               if (action == 'move') {
1295                   content += "[" + movement_keys_desc + "] – move\n"
1296               } else {
1297                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1298               }
1299           }
1300           content += '\n';
1301       }
1302       content += this.mode.list_available_modes();
1303       let start_x = 0;
1304       if (!this.mode.has_input_prompt) {
1305           start_x = this.window_width;
1306           this.draw_links = false;
1307       }
1308       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1309       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1310       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1311           terminal.write(y, start_x, lines[i]);
1312       }
1313   },
1314   toggle_tile_draw: function() {
1315       if (tui.tile_draw) {
1316           tui.tile_draw = false;
1317       } else {
1318           tui.tile_draw = true;
1319       }
1320   },
1321   toggle_map_mode: function() {
1322       if (tui.map_mode == 'terrain only') {
1323           tui.map_mode = 'terrain + annotations';
1324       } else if (tui.map_mode == 'terrain + annotations') {
1325           tui.map_mode = 'terrain + things';
1326       } else if (tui.map_mode == 'terrain + things') {
1327           tui.map_mode = 'protections';
1328       } else if (tui.map_mode == 'protections') {
1329           tui.map_mode = 'terrain only';
1330       }
1331   },
1332   full_refresh: function() {
1333     this.draw_links = true;
1334     this.links = {};
1335     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1336     this.recalc_input_lines();
1337     if (this.mode.is_intro) {
1338         this.draw_history();
1339         this.draw_input();
1340     } else {
1341         this.draw_map();
1342         this.draw_stats_line();
1343         this.draw_mode_line();
1344         if (this.mode.shows_info) {
1345           this.draw_info();
1346         } else {
1347           this.draw_history();
1348         }
1349         this.draw_input();
1350     }
1351     if (this.show_help) {
1352         this.draw_help();
1353     }
1354     if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1355         this.draw_face_popup();
1356     }
1357     if (!this.draw_links) {
1358         this.links = {};
1359     }
1360     terminal.refresh();
1361   }
1362 }
1363
1364 let game = {
1365     init: function() {
1366         this.turn = -1;
1367         this.player_id = -1;
1368         this.tasks = {};
1369         this.things = {};
1370         this.things_new = {};
1371         this.fov = "";
1372         this.fov_new = "";
1373         this.map = "";
1374         this.map_new = "";
1375         this.map_control = "";
1376         this.map_control_new = "";
1377         this.map_size = [0,0];
1378         this.map_size_new = [0,0];
1379         this.portals = {};
1380         this.portals_new = {};
1381         this.players_hat_chars = "";
1382         this.bladder_pressure = 0;
1383         this.bladder_pressure_new = 0;
1384     },
1385     get_thing_temp: function(id_, create_if_not_found=false) {
1386         if (id_ in game.things_new) {
1387             return game.things_new[id_];
1388         } else if (create_if_not_found) {
1389             let t = new Thing([0,0]);
1390             game.things_new[id_] = t;
1391             return t;
1392         };
1393     },
1394     get_thing: function(id_, create_if_not_found=false) {
1395         if (id_ in game.things) {
1396             return game.things[id_];
1397         };
1398     },
1399     move: function(start_position, direction) {
1400         let target = [start_position[0], start_position[1]];
1401         if (direction == 'LEFT') {
1402             target[1] -= 1;
1403         } else if (direction == 'RIGHT') {
1404             target[1] += 1;
1405         } else if (game.map_geometry == 'Square') {
1406             if (direction == 'UP') {
1407                 target[0] -= 1;
1408             } else if (direction == 'DOWN') {
1409                 target[0] += 1;
1410             };
1411         } else if (game.map_geometry == 'Hex') {
1412             let start_indented = start_position[0] % 2;
1413             if (direction == 'UPLEFT') {
1414                 target[0] -= 1;
1415                 if (!start_indented) {
1416                     target[1] -= 1;
1417                 }
1418             } else if (direction == 'UPRIGHT') {
1419                 target[0] -= 1;
1420                 if (start_indented) {
1421                     target[1] += 1;
1422                 }
1423             } else if (direction == 'DOWNLEFT') {
1424                 target[0] += 1;
1425                 if (!start_indented) {
1426                     target[1] -= 1;
1427                 }
1428             } else if (direction == 'DOWNRIGHT') {
1429                 target[0] += 1;
1430                 if (start_indented) {
1431                     target[1] += 1;
1432                 }
1433             };
1434         };
1435         if (target[0] < 0 || target[1] < 0 ||
1436             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1437             return null;
1438         };
1439         return target;
1440     },
1441     teleport: function() {
1442         if (game.player.position in this.portals) {
1443             server.reconnect_to(this.portals[game.player.position]);
1444         } else {
1445             terminal.blink_screen();
1446             tui.log_msg('? not standing on portal')
1447         }
1448     }
1449 }
1450
1451 game.init();
1452 tui.init();
1453 tui.full_refresh();
1454 server.init(websocket_location);
1455
1456 let explorer = {
1457     position: [0,0],
1458     annotations: {},
1459     annotations_new: {},
1460     info_cached: false,
1461     move: function(direction) {
1462         let target = game.move(this.position, direction);
1463         if (target) {
1464             this.position = target
1465             this.info_cached = false;
1466             if (tui.tile_draw) {
1467                 this.send_tile_control_command();
1468             }
1469         } else {
1470             terminal.blink_screen();
1471         };
1472     },
1473     get_info: function() {
1474         if (this.info_cached) {
1475             return this.info_cached;
1476         }
1477         let info_to_cache = '';
1478         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1479         if (game.fov[position_i] != '.') {
1480             info_to_cache += 'outside field of view';
1481         } else {
1482             for (let t_id in game.things) {
1483                  let t = game.things[t_id];
1484                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1485                      info_to_cache += "THING: " + this.get_thing_info(t);
1486                      let protection = t.protection;
1487                      if (protection == '.') {
1488                          protection = 'none';
1489                      }
1490                      info_to_cache += " / protection: " + protection + "\n";
1491                      if (t.hat) {
1492                          info_to_cache += t.hat.slice(0, 6) + '\n';
1493                          info_to_cache += t.hat.slice(6, 12) + '\n';
1494                          info_to_cache += t.hat.slice(12, 18) + '\n';
1495                      }
1496                      if (t.face) {
1497                          info_to_cache += t.face.slice(0, 6) + '\n';
1498                          info_to_cache += t.face.slice(6, 12) + '\n';
1499                          info_to_cache += t.face.slice(12, 18) + '\n';
1500                      }
1501                  }
1502             }
1503             let terrain_char = game.map[position_i]
1504             let terrain_desc = '?'
1505             if (game.terrains[terrain_char]) {
1506                 terrain_desc = game.terrains[terrain_char];
1507             };
1508             info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1509             let protection = game.map_control[position_i];
1510             if (protection == '.') {
1511                 protection = 'unprotected';
1512             };
1513             info_to_cache += 'PROTECTION: ' + protection + '\n';
1514             if (this.position in game.portals) {
1515                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1516             }
1517             if (this.position in this.annotations) {
1518                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1519             }
1520         }
1521         this.info_cached = info_to_cache;
1522         return this.info_cached;
1523     },
1524     get_thing_info: function(t) {
1525         const symbol = game.thing_types[t.type_];
1526         let info = t.type_ + " / " + symbol;
1527         if (t.thing_char) {
1528             info += t.thing_char;
1529         };
1530         if (t.name_) {
1531             info += " (" + t.name_ + ")";
1532         }
1533         if (t.installed) {
1534             info += " / installed";
1535         }
1536         return info;
1537     },
1538     annotate: function(msg) {
1539         if (msg.length == 0) {
1540             msg = " ";  // triggers annotation deletion
1541         }
1542         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1543     },
1544     set_portal: function(msg) {
1545         if (msg.length == 0) {
1546             msg = " ";  // triggers portal deletion
1547         }
1548         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1549     },
1550     send_tile_control_command: function() {
1551         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1552     }
1553 }
1554
1555 tui.inputEl.addEventListener('input', (event) => {
1556     if (tui.mode.has_input_prompt) {
1557         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1558         if (tui.inputEl.value.length > max_length) {
1559             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1560         };
1561     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1562         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1563         tui.switch_mode('edit');
1564     }
1565     tui.full_refresh();
1566 }, false);
1567 document.onclick = function() {
1568     if (!tui.mode.is_single_char_entry) {
1569         tui.show_help = false;
1570     }
1571 };
1572 tui.inputEl.addEventListener('keydown', (event) => {
1573     tui.show_help = false;
1574     if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1575         event.preventDefault();
1576     }
1577     if ((!tui.mode.is_intro && event.key == 'Escape')
1578         || (tui.mode.has_input_prompt && event.key == 'Enter'
1579             && tui.inputEl.value.length == 0
1580             && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1581                 'admin_enter'].includes(tui.mode.name))) {
1582         if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1583             tui.log_msg('@ aborted');
1584         }
1585         tui.switch_mode('play');
1586     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1587         tui.show_help = true;
1588         tui.inputEl.value = "";
1589         tui.restore_input_values();
1590     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1591                && !tui.mode.is_single_char_entry) {
1592         tui.show_help = true;
1593     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1594         tui.login_name = tui.inputEl.value;
1595         server.send(['LOGIN', tui.inputEl.value]);
1596         tui.inputEl.value = "";
1597     } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1598         tui.enter_ascii_art('PLAYER_FACE');
1599     } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1600         tui.enter_ascii_art('PLAYER_HAT');
1601     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1602         server.send(['TASK:COMMAND', tui.inputEl.value]);
1603         tui.inputEl.value = "";
1604     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1605         tui.pick_selectable('PICK_UP');
1606     } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1607         tui.pick_selectable('DROP');
1608     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1609         if (tui.inputEl.value.length == 0) {
1610             tui.log_msg('@ aborted');
1611         } else {
1612             server.send(['SET_MAP_CONTROL_PASSWORD',
1613                         tui.tile_control_char, tui.inputEl.value]);
1614             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1615         }
1616         tui.switch_mode('admin');
1617     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1618         explorer.set_portal(tui.inputEl.value);
1619         tui.switch_mode('edit');
1620     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1621         if (tui.inputEl.value.length == 0) {
1622             tui.inputEl.value = " ";
1623         }
1624         server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1625         tui.switch_mode('edit');
1626     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1627         explorer.annotate(tui.inputEl.value);
1628         tui.switch_mode('edit');
1629     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1630         if (tui.inputEl.value.length == 0) {
1631             tui.inputEl.value = " ";
1632         }
1633         tui.password = tui.inputEl.value
1634         tui.switch_mode('edit');
1635     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1636         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1637         tui.switch_mode('play');
1638     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1639         if (tui.inputEl.value.length != 1) {
1640             tui.log_msg('@ entered non-single-char, therefore aborted');
1641             tui.switch_mode('admin');
1642         } else {
1643             tui.tile_control_char = tui.inputEl.value[0];
1644             tui.switch_mode('control_pw_pw');
1645         }
1646     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1647         if (tui.inputEl.value.length != 1) {
1648             tui.log_msg('@ entered non-single-char, therefore aborted');
1649             tui.switch_mode('admin');
1650         } else {
1651             tui.tile_control_char = tui.inputEl.value[0];
1652             tui.switch_mode('control_tile_draw');
1653         }
1654     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1655         if (tui.inputEl.value.length != 1) {
1656             tui.log_msg('@ entered non-single-char, therefore aborted');
1657         } else {
1658             server.send(['THING_PROTECTION', tui.inputEl.value])
1659             tui.log_msg('@ sent new protection character for thing');
1660         }
1661         tui.switch_mode('admin');
1662     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1663         let tokens = parser.tokenize(tui.inputEl.value);
1664         if (tokens.length > 0 && tokens[0].length > 0) {
1665             if (tui.inputEl.value[0][0] == '/') {
1666                 if (tokens[0].slice(1) == 'nick') {
1667                     if (tokens.length > 1) {
1668                         server.send(['NICK', tokens[1]]);
1669                     } else {
1670                         tui.log_msg('? need new name');
1671                     }
1672                 } else {
1673                     tui.log_msg('? unknown command');
1674                 }
1675             } else {
1676                     server.send(['ALL', tui.inputEl.value]);
1677             }
1678         } else if (tui.inputEl.valuelength > 0) {
1679                 server.send(['ALL', tui.inputEl.value]);
1680         }
1681         tui.inputEl.value = "";
1682     } else if (tui.mode.name == 'play') {
1683           if (tui.mode.mode_switch_on_key(event)) {
1684               null;
1685           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1686               server.send(["TASK:INTOXICATE"]);
1687           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1688               server.send(["TASK:DOOR"]);
1689           } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1690               server.send(["TASK:WEAR"]);
1691           } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1692               server.send(["TASK:SPIN"]);
1693           } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1694               server.send(["TASK:DANCE"]);
1695           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1696               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1697           } else if (event.key === tui.keys.teleport) {
1698               game.teleport();
1699           };
1700     } else if (tui.mode.name == 'study') {
1701         if (tui.mode.mode_switch_on_key(event)) {
1702               null;
1703         } else if (event.key in tui.movement_keys) {
1704             explorer.move(tui.movement_keys[event.key]);
1705         } else if (event.key == tui.keys.toggle_map_mode) {
1706             tui.toggle_map_mode();
1707         };
1708     } else if (tui.mode.name == 'control_tile_draw') {
1709         if (tui.mode.mode_switch_on_key(event)) {
1710             null;
1711         } else if (event.key in tui.movement_keys) {
1712             explorer.move(tui.movement_keys[event.key]);
1713         } else if (event.key === tui.keys.toggle_tile_draw) {
1714             tui.toggle_tile_draw();
1715         };
1716     } else if (tui.mode.name == 'admin') {
1717         if (tui.mode.mode_switch_on_key(event)) {
1718               null;
1719         } else if (event.key == tui.keys.toggle_map_mode) {
1720             tui.toggle_map_mode();
1721         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1722             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1723         };
1724     } else if (tui.mode.name == 'edit') {
1725         if (tui.mode.mode_switch_on_key(event)) {
1726               null;
1727         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1728             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1729         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1730             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1731           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1732               server.send(["TASK:INSTALL", tui.password]);
1733         } else if (event.key == tui.keys.toggle_map_mode) {
1734             tui.toggle_map_mode();
1735         }
1736     }
1737     tui.full_refresh();
1738 }, false);
1739
1740 rows_selector.addEventListener('input', function() {
1741     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1742         return;
1743     }
1744     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1745     terminal.initialize();
1746     tui.full_refresh();
1747 }, false);
1748 cols_selector.addEventListener('input', function() {
1749     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1750         return;
1751     }
1752     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1753     terminal.initialize();
1754     tui.window_width = terminal.cols / 2,
1755     tui.full_refresh();
1756 }, false);
1757 for (let key_selector of key_selectors) {
1758     key_selector.addEventListener('input', function() {
1759         window.localStorage.setItem(key_selector.id, key_selector.value);
1760         tui.init_keys();
1761     }, false);
1762 }
1763 window.setInterval(function() {
1764     if (server.websocket.readyState == 1) {
1765         server.send(['PING']);
1766     } else if (server.websocket.readyState != 0) {
1767         server.reconnect_to(server.url);
1768         tui.log_msg('@ attempting reconnect …')
1769     }
1770 }, 1000);
1771 window.setInterval(function() {
1772     if (document.activeElement.tagName.toLowerCase() != 'input') {
1773         const scroll_x = window.scrollX;
1774         const scroll_y = window.scrollY;
1775         tui.inputEl.focus();
1776         window.scrollTo(scroll_x, scroll_y);
1777     };
1778 }, 100);
1779 document.getElementById("help").onclick = function() {
1780     tui.show_help = true;
1781     tui.full_refresh();
1782 };
1783 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1784     const mode = switchEl.id.slice("switch_to_".length);
1785     switchEl.onclick = function() {
1786         tui.switch_mode(mode);
1787         tui.full_refresh();
1788     }
1789 };
1790 document.getElementById("toggle_tile_draw").onclick = function() {
1791     tui.toggle_tile_draw();
1792 }
1793 document.getElementById("toggle_map_mode").onclick = function() {
1794     tui.toggle_map_mode();
1795     tui.full_refresh();
1796 };
1797 document.getElementById("flatten").onclick = function() {
1798     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1799 };
1800 document.getElementById("door").onclick = function() {
1801     server.send(['TASK:DOOR']);
1802 };
1803 document.getElementById("consume").onclick = function() {
1804     server.send(['TASK:INTOXICATE']);
1805 };
1806 document.getElementById("install").onclick = function() {
1807     server.send(['TASK:INSTALL', tui.password]);
1808 };
1809 document.getElementById("wear").onclick = function() {
1810     server.send(['TASK:WEAR']);
1811 };
1812 document.getElementById("spin").onclick = function() {
1813     server.send(['TASK:SPIN']);
1814 };
1815 document.getElementById("dance").onclick = function() {
1816     server.send(['TASK:DANCE']);
1817 };
1818 document.getElementById("teleport").onclick = function() {
1819     game.teleport();
1820 };
1821 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1822     if (move_button.id.startsWith('key_')) {  // not a move button
1823         continue;
1824     };
1825     let direction = move_button.id.split('_')[2].toUpperCase();
1826     let move_repeat;
1827     function move() {
1828         if (tui.mode.available_actions.includes("move")) {
1829             server.send(['TASK:MOVE', direction]);
1830         } else if (tui.mode.available_actions.includes("move_explorer")) {
1831             explorer.move(direction);
1832             tui.full_refresh();
1833         };
1834     }
1835     move_button.onmousedown = function() {
1836         move();
1837         move_repeat = window.setInterval(move, 100);
1838     };
1839     move_button.onmouseup = function() {
1840         window.clearInterval(move_repeat);
1841     }
1842 };
1843 </script>
1844 </body></html>