home · contact · privacy
Minor refactor.
[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 </div>
16 <pre id="terminal"></pre>
17 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
18 <div>
19 <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal / curses client)
20 <h3>button controls for mouse players</h3>
21 <table style="float: left">
22   <tr>
23     <td style="text-align: right"><button id="move_upleft">up-left</button></td>
24     <td style="text-align: center"><button id="move_up">up</button></td>
25     <td><button id="move_upright">up-right</button></td>
26   </tr>
27   <tr>
28     <td style="text-align: right;"><button id="move_left">left</button></td>
29     <td stlye="text-align: center;">move</td>
30     <td><button id="move_right">right</button></td>
31   </tr>
32   <tr>
33     <td><button id="move_downleft">down-left</button></td>
34     <td style="text-align: center"><button id="move_down">down</button></td>
35     <td><button id="move_downright">down-right</button></td>
36   </tr>
37 </table>
38 <table>
39   <tr>
40     <td><button id="help">help</button></td>
41   </tr>
42   <tr>
43     <td><button id="switch_to_chat">chat mode</button><br /></td>
44   </tr>
45   <tr>
46     <td><button id="switch_to_study">study mode</button></td>
47     <td><button id="toggle_map_mode">toggle terrain/annotations/control view</button>
48   </tr>
49   <tr>
50     <td><button id="switch_to_play">play mode</button></td>
51     <td>
52       <button id="take_thing">take thing</button>
53       <button id="teleport">teleport</button>
54       <button id="drop_thing">drop thing</button>
55     </td>
56   </tr>
57   <tr>
58     <td><button id="switch_to_edit">map edit mode</button></td>
59     <td>
60       <button id="switch_to_write">change tile</button>
61       <button id="flatten">flatten surroundings</button>
62       <button id="switch_to_password">change tile editing password</button>
63       <button id="switch_to_annotate">annotate tile</button>
64       <button id="switch_to_portal">edit portal link</button>
65     </td>
66   </tr>
67   <tr>
68     <td><button id="switch_to_admin_enter">admin mode</button></td>
69     <td>
70       <button id="switch_to_control_pw_type">change tile control password</button>
71       <button id="switch_to_control_tile_type">change tiles control</button>
72     </td>
73   <tr>
74   </tr>
75 </table>
76 <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 />
77 <ul>
78 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
79 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
80 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
81 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
82 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
83 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
84 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
85 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
86 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
87 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
88 <li>help: <input id="key_help" type="text" value="h" />
89 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
90 <li>teleport: <input id="key_teleport" type="text" value="p" />
91 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
92 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
93 <li><input id="key_switch_to_chat" type="text" value="t" />
94 <li><input id="key_switch_to_play" type="text" value="p" />
95 <li><input id="key_switch_to_study" type="text" value="?" />
96 <li><input id="key_switch_to_edit" type="text" value="E" />
97 <li><input id="key_switch_to_write" type="text" value="m" />
98 <li><input id="key_switch_to_password" type="text" value="P" />
99 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
100 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
101 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
102 <li><input id="key_switch_to_annotate" type="text" value="M" />
103 <li><input id="key_switch_to_portal" type="text" value="T" />
104 <li>toggle terrain/annotations/control view: <input id="key_toggle_map_mode" type="text" value="M" />
105 </ul>
106 </div>
107 <script>
108 "use strict";
109 let websocket_location = "wss://plomlompom.com/rogue_chat/";
110 //let websocket_location = "ws://localhost:8000/";
111
112 let mode_helps = {
113     'play': {
114         'short': 'play',
115         'long': 'This mode allows you to interact with the map.'
116     },
117     'study': {
118         'short': 'study',
119         '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.'},
120     'edit': {
121         'short': 'map edit',
122         'long': 'This mode allows you to change the map in various ways.'
123     },
124     'write': {
125         'short': 'terrain write',
126         'long': 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.'
127     },
128     'control_pw_type': {
129         'short': 'change tiles control password',
130         'long': 'This mode is the first of two steps to change the password for a tile control character.  First enter the tile control character for which you want to change the password!'
131     },
132     'control_pw_pw': {
133         'short': 'change tiles control password',
134         'long': 'This mode is the second of two steps to change the password for a tile control character.  Enter the new password for the tile control character you chose.'
135     },
136     'control_tile_type': {
137         'short': 'change tiles control',
138         'long': 'This mode is the first of two steps to change tile control areas on the map.  First enter the tile control character you want to write.'
139     },
140     'control_tile_draw': {
141         'short': 'change tiles control',
142         'long': 'This mode is the second of two steps to change tile control areas on the map.  Move cursor around the map to draw selected tile control character'
143     },
144     'annotate': {
145         'short': 'annotate tile',
146         'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so).  Hit Return to leave.'
147     },
148     'portal': {
149         'short': 'edit portal',
150         'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map 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.'
151     },
152     'chat': {
153         'short': 'chat',
154         '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:'
155     },
156     'login': {
157         'short': 'login',
158         'long': 'Pick your player name.'
159     },
160     'waiting_for_server': {
161         'short': 'waiting for server response',
162         'long': 'Waiting for a server response.'
163     },
164     'post_login_wait': {
165         'short': 'waiting for server response',
166         'long': 'Waiting for a server response.'
167     },
168     'password': {
169         'short': 'map edit password',
170         'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles.  Hit return to confirm and leave.'
171     },
172     'admin_enter': {
173         'short': 'become admin',
174         'long': 'This mode allows you to become admin if you know an admin password.'
175     },
176     'admin': {
177         'short': 'admin',
178         'long': 'This mode allows you access to actions limited to administrators.'
179     }
180 }
181
182 let rows_selector = document.getElementById("n_rows");
183 let cols_selector = document.getElementById("n_cols");
184 let key_selectors = document.querySelectorAll('[id^="key_"]');
185
186 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
187     const action = key_switch_selector.id.slice("key_switch_to_".length);
188     key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
189 }
190
191 function restore_selector_value(selector) {
192     let stored_selection = window.localStorage.getItem(selector.id);
193     if (stored_selection) {
194         selector.value = stored_selection;
195     }
196 }
197 restore_selector_value(rows_selector);
198 restore_selector_value(cols_selector);
199 for (let key_selector of key_selectors) {
200     restore_selector_value(key_selector);
201 }
202
203 let terminal = {
204   foreground: 'white',
205   background: 'black',
206   initialize: function() {
207     this.rows = rows_selector.value;
208     this.cols = cols_selector.value;
209     this.pre_el = document.getElementById("terminal");
210     this.pre_el.style.color = this.foreground;
211     this.pre_el.style.backgroundColor = this.background;
212     this.content = [];
213       let line = []
214     for (let y = 0, x = 0; y <= this.rows; x++) {
215         if (x == this.cols) {
216             x = 0;
217             y += 1;
218             this.content.push(line);
219             line = [];
220             if (y == this.rows) {
221                 break;
222             }
223         }
224         line.push(' ');
225     }
226   },
227   blink_screen: function() {
228       this.pre_el.style.color = this.background;
229       this.pre_el.style.backgroundColor = this.foreground;
230       setTimeout(() => {
231           this.pre_el.style.color = this.foreground;
232           this.pre_el.style.backgroundColor = this.background;
233       }, 100);
234   },
235   refresh: function() {
236       function escapeHTML(str) {
237           return str.
238               replace(/&/g, '&amp;').
239               replace(/</g, '&lt;').
240               replace(/>/g, '&gt;').
241               replace(/'/g, '&apos;').
242               replace(/"/g, '&quot;');
243       };
244       let pre_content = '';
245       for (let y = 0; y < this.rows; y++) {
246           let line = this.content[y].join('');
247           let chunks = [];
248           if (y in tui.links) {
249               let start_x = 0;
250               for (let span of tui.links[y]) {
251                   chunks.push(escapeHTML(line.slice(start_x, span[0])));
252                   chunks.push('<a href="');
253                   chunks.push(escapeHTML(span[2]));
254                   chunks.push('">');
255                   chunks.push(escapeHTML(line.slice(span[0], span[1])));
256                   chunks.push('</a>');
257                   start_x = span[1];
258               }
259               chunks.push(escapeHTML(line.slice(start_x)));
260           } else {
261               chunks = [escapeHTML(line)];
262           }
263           for (const chunk of chunks) {
264               pre_content += chunk;
265           }
266           pre_content += '\n';
267       }
268       this.pre_el.innerHTML = pre_content;
269   },
270   write: function(start_y, start_x, msg) {
271       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
272           this.content[start_y][x] = msg[i];
273       }
274   },
275   drawBox: function(start_y, start_x, height, width) {
276     let end_y = start_y + height;
277     let end_x = start_x + width;
278     for (let y = start_y, x = start_x; y < this.rows; x++) {
279       if (x == end_x) {
280         x = start_x;
281         y += 1;
282         if (y == end_y) {
283             break;
284         }
285       };
286       this.content[y][x] = ' ';
287     }
288   },
289 }
290 terminal.initialize();
291
292 let parser = {
293   tokenize: function(str) {
294     let tokens = [];
295     let token = ''
296     let quoted = false;
297     let escaped = false;
298     for (let i = 0; i < str.length; i++) {
299       let c = str[i];
300       if (quoted) {
301         if (escaped) {
302           token += c;
303           escaped = false;
304         } else if (c == '\\') {
305           escaped = true;
306         } else if (c == '"') {
307           quoted = false
308         } else {
309           token += c;
310         }
311       } else if (c == '"') {
312         quoted = true
313       } else if (c === ' ') {
314         if (token.length > 0) {
315           tokens.push(token);
316           token = '';
317         }
318       } else {
319         token += c;
320       }
321     }
322     if (token.length > 0) {
323       tokens.push(token);
324     }
325     return tokens;
326   },
327   parse_yx: function(position_string) {
328     let coordinate_strings = position_string.split(',')
329     let position = [0, 0];
330     position[0] = parseInt(coordinate_strings[0].slice(2));
331     position[1] = parseInt(coordinate_strings[1].slice(2));
332     return position;
333   },
334 }
335
336 class Thing {
337     constructor(yx) {
338         this.position = yx;
339     }
340 }
341
342 let server = {
343     init: function(url) {
344         this.url = url;
345         this.websocket = new WebSocket(this.url);
346         this.websocket.onopen = function(event) {
347             server.connected = true;
348             game.thing_types = {};
349             game.terrains = {};
350             server.send(['TASKS']);
351             server.send(['TERRAINS']);
352             server.send(['THING_TYPES']);
353             tui.log_msg("@ server connected! :)");
354             tui.switch_mode('login');
355         };
356         this.websocket.onclose = function(event) {
357             server.connected = false;
358             tui.switch_mode('waiting_for_server');
359             tui.log_msg("@ server disconnected :(");
360         };
361             this.websocket.onmessage = this.handle_event;
362         },
363     reconnect_to: function(url) {
364         this.websocket.close();
365         this.init(url);
366     },
367     send: function(tokens) {
368         this.websocket.send(unparser.untokenize(tokens));
369     },
370     handle_event: function(event) {
371         let tokens = parser.tokenize(event.data);
372         if (tokens[0] === 'TURN') {
373             game.turn_complete = false;
374             explorer.empty_info_db();
375             game.things = {};
376             game.portals = {};
377             game.turn = parseInt(tokens[1]);
378         } else if (tokens[0] === 'THING') {
379             let t = game.get_thing(tokens[3], true);
380             t.position = parser.parse_yx(tokens[1]);
381             t.type_ = tokens[2];
382         } else if (tokens[0] === 'THING_NAME') {
383             let t = game.get_thing(tokens[1], false);
384             if (t) {
385                 t.name_ = tokens[2];
386             };
387         } else if (tokens[0] === 'THING_CHAR') {
388             let t = game.get_thing(tokens[1], false);
389             if (t) {
390                 t.player_char = tokens[2];
391             };
392         } else if (tokens[0] === 'TASKS') {
393             game.tasks = tokens[1].split(',');
394             tui.mode_write.legal = game.tasks.includes('WRITE');
395         } else if (tokens[0] === 'THING_TYPE') {
396             game.thing_types[tokens[1]] = tokens[2]
397         } else if (tokens[0] === 'TERRAIN') {
398             game.terrains[tokens[1]] = tokens[2]
399         } else if (tokens[0] === 'MAP') {
400             game.map_geometry = tokens[1];
401             tui.init_keys();
402             game.map_size = parser.parse_yx(tokens[2]);
403             game.map = tokens[3]
404         } else if (tokens[0] === 'FOV') {
405             game.fov = tokens[1]
406         } else if (tokens[0] === 'MAP_CONTROL') {
407             game.map_control = tokens[1]
408         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
409             game.turn_complete = true;
410             if (tui.mode.name == 'post_login_wait') {
411                 tui.switch_mode('play');
412             } else if (tui.mode.name == 'study') {
413                 explorer.query_info();
414             }
415             tui.full_refresh();
416         } else if (tokens[0] === 'CHAT') {
417              tui.log_msg('# ' + tokens[1], 1);
418         } else if (tokens[0] === 'PLAYER_ID') {
419             game.player_id = parseInt(tokens[1]);
420         } else if (tokens[0] === 'LOGIN_OK') {
421             this.send(['GET_GAMESTATE']);
422             tui.switch_mode('post_login_wait');
423         } else if (tokens[0] === 'ADMIN_OK') {
424             tui.is_admin = true;
425             tui.log_msg('@ you now have admin rights');
426             tui.switch_mode('admin');
427         } else if (tokens[0] === 'PORTAL') {
428             let position = parser.parse_yx(tokens[1]);
429             game.portals[position] = tokens[2];
430         } else if (tokens[0] === 'ANNOTATION_HINT') {
431             let position = parser.parse_yx(tokens[1]);
432             explorer.info_hints = explorer.info_hints.concat([position]);
433         } else if (tokens[0] === 'ANNOTATION') {
434             let position = parser.parse_yx(tokens[1]);
435             explorer.update_info_db(position, tokens[2]);
436             tui.restore_input_values();
437             tui.full_refresh();
438         } else if (tokens[0] === 'UNHANDLED_INPUT') {
439             tui.log_msg('? unknown command');
440         } else if (tokens[0] === 'PLAY_ERROR') {
441             tui.log_msg('? ' + tokens[1]);
442             terminal.blink_screen();
443         } else if (tokens[0] === 'ARGUMENT_ERROR') {
444             tui.log_msg('? syntax error: ' + tokens[1]);
445         } else if (tokens[0] === 'GAME_ERROR') {
446             tui.log_msg('? game error: ' + tokens[1]);
447         } else if (tokens[0] === 'PONG') {
448             ;
449         } else {
450             tui.log_msg('? unhandled input: ' + event.data);
451         }
452     }
453 }
454
455 let unparser = {
456     quote: function(str) {
457         let quoted = ['"'];
458         for (let i = 0; i < str.length; i++) {
459             let c = str[i];
460             if (['"', '\\'].includes(c)) {
461                 quoted.push('\\');
462             };
463             quoted.push(c);
464         }
465         quoted.push('"');
466         return quoted.join('');
467     },
468     to_yx: function(yx_coordinate) {
469         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
470     },
471     untokenize: function(tokens) {
472         let quoted_tokens = [];
473         for (let token of tokens) {
474             quoted_tokens.push(this.quote(token));
475         }
476         return quoted_tokens.join(" ");
477     }
478 }
479
480 class Mode {
481     constructor(name, has_input_prompt=false, shows_info=false,
482                 is_intro=false, is_single_char_entry=false) {
483         this.name = name;
484         this.short_desc = mode_helps[name].short;
485         this.available_modes = [];
486         this.has_input_prompt = has_input_prompt;
487         this.shows_info= shows_info;
488         this.is_intro = is_intro;
489         this.help_intro = mode_helps[name].long;
490         this.is_single_char_entry = is_single_char_entry;
491         this.legal = true;
492     }
493     *iter_available_modes() {
494         for (let mode_name of this.available_modes) {
495             let mode = tui['mode_' + mode_name];
496             if (!mode.legal) {
497                 continue;
498             }
499             let key = tui.keys['switch_to_' + mode.name];
500             yield [mode, key]
501         }
502     }
503     list_available_modes() {
504         let msg = ''
505         if (this.available_modes.length > 0) {
506             msg += 'Other modes available from here:\n';
507             for (let [mode, key] of this.iter_available_modes()) {
508                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
509             }
510         }
511         return msg;
512     }
513     mode_switch_on_key(key_event) {
514         for (let [mode, key] of this.iter_available_modes()) {
515             if (key_event.key == key) {
516                 event.preventDefault();
517                 tui.switch_mode(mode.name);
518                 return true;
519             };
520         }
521         return false;
522     }
523 }
524 let tui = {
525   links: {},
526   log: [],
527   input_prompt: '> ',
528   input_lines: [],
529   window_width: terminal.cols / 2,
530   height_turn_line: 1,
531   height_mode_line: 1,
532   height_input: 1,
533   password: 'foo',
534   show_help: false,
535   is_admin: false,
536   mode_waiting_for_server: new Mode('waiting_for_server',
537                                      false, false, true),
538   mode_login: new Mode('login', true, false, true),
539   mode_post_login_wait: new Mode('post_login_wait'),
540   mode_chat: new Mode('chat', true),
541   mode_annotate: new Mode('annotate', true, true),
542   mode_play: new Mode('play'),
543   mode_study: new Mode('study', false, true),
544   mode_write: new Mode('write', false, false, false, true),
545   mode_edit: new Mode('edit'),
546   mode_control_pw_type: new Mode('control_pw_type',
547                                   false, false, false, true),
548   mode_portal: new Mode('portal', true, true),
549   mode_password: new Mode('password', true),
550   mode_admin_enter: new Mode('admin_enter', true),
551   mode_admin: new Mode('admin'),
552   mode_control_pw_pw: new Mode('control_pw_pw', true),
553   mode_control_tile_type: new Mode('control_tile_type',
554                                    false, false, false, true),
555   mode_control_tile_draw: new Mode('control_tile_draw'),
556   init: function() {
557       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
558       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
559       this.mode_admin.available_modes = ["control_pw_type",
560                                          "control_tile_type", "chat",
561                                          "study", "play", "edit"]
562       this.mode_control_tile_draw.available_modes = ["admin"]
563       this.mode_edit.available_modes = ["write", "annotate", "portal",
564                                         "password", "chat", "study", "play",
565                                         "admin_enter"]
566       this.mode = this.mode_waiting_for_server;
567       this.inputEl = document.getElementById("input");
568       this.inputEl.focus();
569       this.recalc_input_lines();
570       this.height_header = this.height_turn_line + this.height_mode_line;
571       this.log_msg("@ waiting for server connection ...");
572       this.init_keys();
573   },
574   init_keys: function() {
575     this.keys = {};
576     for (let key_selector of key_selectors) {
577         this.keys[key_selector.id.slice(4)] = key_selector.value;
578     }
579     if (game.map_geometry == 'Square') {
580         this.movement_keys = {
581             [this.keys.square_move_up]: 'UP',
582             [this.keys.square_move_left]: 'LEFT',
583             [this.keys.square_move_down]: 'DOWN',
584             [this.keys.square_move_right]: 'RIGHT'
585         };
586         document.getElementById("move_upright").hidden = true;
587         document.getElementById("move_upleft").hidden = true;
588         document.getElementById("move_downright").hidden = true;
589         document.getElementById("move_downleft").hidden = true;
590         document.getElementById("move_up").hidden = false;
591         document.getElementById("move_down").hidden = false;
592     } else if (game.map_geometry == 'Hex') {
593         document.getElementById("move_upright").hidden = false;
594         document.getElementById("move_upleft").hidden = false;
595         document.getElementById("move_downright").hidden = false;
596         document.getElementById("move_downleft").hidden = false;
597         document.getElementById("move_up").hidden = true;
598         document.getElementById("move_down").hidden = true;
599         this.movement_keys = {
600             [this.keys.hex_move_upleft]: 'UPLEFT',
601             [this.keys.hex_move_upright]: 'UPRIGHT',
602             [this.keys.hex_move_right]: 'RIGHT',
603             [this.keys.hex_move_downright]: 'DOWNRIGHT',
604             [this.keys.hex_move_downleft]: 'DOWNLEFT',
605             [this.keys.hex_move_left]: 'LEFT'
606         };
607     };
608   },
609   switch_mode: function(mode_name) {
610     this.inputEl.focus();
611     this.map_mode = 'terrain';
612     if (mode_name == 'admin_enter' && this.is_admin) {
613         mode_name = 'admin';
614     };
615     this.mode = this['mode_' + mode_name];
616     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
617         explorer.position = game.things[game.player_id].position;
618         if (this.mode.shows_info) {
619         explorer.query_info();
620         } else if (this.mode.name == 'control_tile_draw') {
621             explorer.send_tile_control_command();
622             this.map_mode = 'control';
623         }
624     }
625     this.empty_input();
626     this.restore_input_values();
627     for (let el of document.getElementsByTagName("button")) {
628         el.disabled = true;
629     }
630     document.getElementById("help").disabled = false;
631     if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
632         for (const move_key of document.querySelectorAll('[id^="move_"]')) {
633             move_key.disabled = false;
634         }
635     }
636     if (!this.mode.is_intro && this.mode.name != 'play') {
637         document.getElementById("switch_to_play").disabled = false;
638     }
639     if (!this.mode.is_intro && this.mode.name != 'study') {
640         document.getElementById("switch_to_study").disabled = false;
641     }
642     if (!this.mode.is_intro && this.mode.name != 'chat') {
643         document.getElementById("switch_to_chat").disabled = false;
644     }
645     if (!this.mode.is_intro && this.mode.name != 'edit') {
646         document.getElementById("switch_to_edit").disabled = false;
647     }
648     if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
649         document.getElementById("switch_to_admin_enter").disabled = false;
650     }
651     if (this.mode.name == 'login') {
652         if (this.login_name) {
653             server.send(['LOGIN', this.login_name]);
654         } else {
655             this.log_msg("? need login name");
656         }
657     } else if (this.mode.name == 'play') {
658         if (game.tasks.includes('PICK_UP')) {
659             document.getElementById("take_thing").disabled = false;
660         }
661         if (game.tasks.includes('DROP')) {
662             document.getElementById("drop_thing").disabled = false;
663         }
664         if (game.tasks.includes('MOVE')) {
665         }
666         document.getElementById("teleport").disabled = false;
667     } else if (this.mode.name == 'edit') {
668         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
669             document.getElementById("flatten").disabled = false;
670         }
671         document.getElementById("switch_to_annotate").disabled = false;
672         document.getElementById("switch_to_write").disabled = false;
673         document.getElementById("switch_to_portal").disabled = false;
674         document.getElementById("switch_to_password").disabled = false;
675     } else if (this.mode.name == 'admin') {
676         document.getElementById("switch_to_control_pw_type").disabled = false;
677         document.getElementById("switch_to_control_tile_type").disabled = false;
678     } else if (this.mode.name == 'study') {
679         document.getElementById("toggle_map_mode").disabled = false;
680     } else if (this.mode.is_single_char_entry) {
681         this.show_help = true;
682     } else if (this.mode.name == 'admin_enter') {
683         this.log_msg('@ enter admin password:')
684     } else if (this.mode.name == 'control_pw_pw') {
685         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
686     } else if (this.mode.name == 'control_pw_pw') {
687         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
688     }
689     this.full_refresh();
690   },
691   offset_links: function(offset, links) {
692       for (let y in links) {
693           let real_y = offset[0] + parseInt(y);
694           if (!this.links[real_y]) {
695               this.links[real_y] = [];
696           }
697           for (let link of links[y]) {
698               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
699               this.links[real_y].push(offset_link);
700           }
701       }
702   },
703   restore_input_values: function() {
704       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
705           let info = explorer.info_db[explorer.position];
706           if (info != "(none)") {
707               this.inputEl.value = info;
708               this.recalc_input_lines();
709           }
710       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
711           let portal = game.portals[explorer.position]
712           this.inputEl.value = portal;
713           this.recalc_input_lines();
714       } else if (this.mode.name == 'password') {
715           this.inputEl.value = this.password;
716           this.recalc_input_lines();
717       }
718   },
719   empty_input: function(str) {
720       this.inputEl.value = "";
721       if (this.mode.has_input_prompt) {
722           this.recalc_input_lines();
723       } else {
724           this.height_input = 0;
725       }
726   },
727   recalc_input_lines: function() {
728       let _ = null;
729       [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
730       this.height_input = this.input_lines.length;
731   },
732   msg_into_lines_of_width: function(msg, width) {
733       function push_inner_link(y, end_x) {
734           if (!inner_links[y]) {
735               inner_links[y] = [];
736           };
737           inner_links[y].push([url_start_x, end_x, url]);
738       };
739       const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
740       let link_data = {};
741       let url_ends = [];
742       for (const match of matches) {
743           const url = match[0];
744           const url_start = match.index;
745           const url_end = match.index + match[0].length;
746           link_data[url_start] = url;
747           url_ends.push(url_end);
748       }
749       let url_start_x = 0;
750       let url = '';
751       let inner_links = {};
752       let in_link = false;
753       let chunk = "";
754       let lines = [];
755       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
756           if (x >= width || msg[i] == "\n") {
757               if (in_link) {
758                   push_inner_link(y, chunk.length);
759                   url_start_x = 0;
760               };
761               lines.push(chunk);
762               chunk = "";
763               x = 0;
764               if (msg[i] == "\n") {
765                   x -= 1;
766               };
767               y += 1;
768           };
769           if (msg[i] != "\n") {
770               chunk += msg[i];
771           };
772           if (i in link_data) {
773               url_start_x = x;
774               url = link_data[i];
775               in_link = true;
776           } else if (url_ends.includes(i)) {
777               push_inner_link(y, x);
778               in_link = false;
779           }
780       }
781       lines.push(chunk);
782       if (in_link) {
783           push_inner_link(lines.length - 1, chunk.length);
784       }
785       return [lines, inner_links];
786   },
787   log_msg: function(msg) {
788       this.log.push(msg);
789       while (this.log.length > 100) {
790         this.log.shift();
791       };
792       this.full_refresh();
793   },
794   draw_map: function() {
795     let map_lines_split = [];
796     let line = [];
797     let map_content = game.map;
798     if (this.map_mode == 'control') {
799         map_content = game.map_control;
800     }
801     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
802         if (j == game.map_size[1]) {
803             map_lines_split.push(line);
804             line = [];
805             j = 0;
806         };
807         line.push(map_content[i] + ' ');
808     };
809     map_lines_split.push(line);
810     if (this.map_mode == 'annotations') {
811         for (const coordinate of explorer.info_hints) {
812             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
813         }
814     } else if (this.map_mode == 'terrain') {
815         for (const p in game.portals) {
816             let coordinate = p.split(',')
817             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
818         }
819         let used_positions = [];
820         for (const thing_id in game.things) {
821             let t = game.things[thing_id];
822             let symbol = game.thing_types[t.type_];
823             let meta_char = ' ';
824             if (t.player_char) {
825                 meta_char = t.player_char;
826             }
827             if (used_positions.includes(t.position.toString())) {
828                 meta_char = '+';
829             };
830             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
831             used_positions.push(t.position.toString());
832         };
833     }
834     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
835         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
836     }
837     let map_lines = []
838     if (game.map_geometry == 'Square') {
839         for (let line_split of map_lines_split) {
840             map_lines.push(line_split.join(''));
841         };
842     } else if (game.map_geometry == 'Hex') {
843         let indent = 0
844         for (let line_split of map_lines_split) {
845             map_lines.push(' '.repeat(indent) + line_split.join(''));
846             if (indent == 0) {
847                 indent = 1;
848             } else {
849                 indent = 0;
850             };
851         };
852     }
853     let window_center = [terminal.rows / 2, this.window_width / 2];
854     let player = game.things[game.player_id];
855     let center_position = [player.position[0], player.position[1]];
856     if (tui.mode.shows_info) {
857         center_position = [explorer.position[0], explorer.position[1]];
858     }
859     center_position[1] = center_position[1] * 2;
860     let offset = [center_position[0] - window_center[0],
861                   center_position[1] - window_center[1]]
862     if (game.map_geometry == 'Hex' && offset[0] % 2) {
863         offset[1] += 1;
864     };
865     let term_y = Math.max(0, -offset[0]);
866     let term_x = Math.max(0, -offset[1]);
867     let map_y = Math.max(0, offset[0]);
868     let map_x = Math.max(0, offset[1]);
869     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
870         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
871         terminal.write(term_y, term_x, to_draw);
872     }
873   },
874   draw_mode_line: function() {
875       let help = 'hit [' + this.keys.help + '] for help';
876       if (this.mode.has_input_prompt) {
877           help = 'enter /help for help';
878       }
879       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
880   },
881   draw_turn_line: function(n) {
882     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
883   },
884   draw_history: function() {
885       let log_display_lines = [];
886       let log_links = {};
887       let y_offset_in_log = 0;
888       for (let line of this.log) {
889           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
890                                                                     this.window_width)
891           log_display_lines = log_display_lines.concat(new_lines);
892           for (const y in link_data) {
893               const rel_y = y_offset_in_log + parseInt(y);
894               log_links[rel_y] = [];
895               for (let link of link_data[y]) {
896                   log_links[rel_y].push(link);
897               }
898           }
899           y_offset_in_log += new_lines.length;
900       };
901       let i = log_display_lines.length - 1;
902       for (let y = terminal.rows - 1 - this.height_input;
903            y >= this.height_header && i >= 0;
904            y--, i--) {
905           terminal.write(y, this.window_width, log_display_lines[i]);
906       }
907       for (const key of Object.keys(log_links)) {
908           if (parseInt(key) <= i) {
909               delete log_links[key];
910           }
911       }
912       let offset = [terminal.rows - this.height_input - log_display_lines.length,
913                     this.window_width];
914       this.offset_links(offset, log_links);
915   },
916   draw_info: function() {
917       let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
918                                                             this.window_width);
919       let offset = [this.height_header, this.window_width];
920       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
921         terminal.write(y, offset[1], lines[i]);
922       }
923       this.offset_links(offset, link_data);
924   },
925   draw_input: function() {
926     if (this.mode.has_input_prompt) {
927         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
928             terminal.write(y, this.window_width, this.input_lines[i]);
929         }
930     }
931   },
932   draw_help: function() {
933       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
934       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
935       if (this.mode.name == 'play') {
936           content += "Available actions:\n";
937           if (game.tasks.includes('MOVE')) {
938               content += "[" + movement_keys_desc + "] – move player\n";
939           }
940           if (game.tasks.includes('PICK_UP')) {
941               content += "[" + this.keys.take_thing + "] – pick up thing\n";
942           }
943           if (game.tasks.includes('DROP')) {
944               content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
945           }
946           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
947           content += '\n';
948       } else if (this.mode.name == 'study') {
949           content += "Available actions:\n";
950           content += '[' + movement_keys_desc + '] – move question mark\n';
951           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
952           content += '\n';
953       } else if (this.mode.name == 'edit') {
954           content += "Available actions:\n";
955           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
956               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
957           }
958           content += '\n';
959       } else if (this.mode.name == 'chat') {
960           content += '/nick NAME – re-name yourself to NAME\n';
961           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
962           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
963           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
964           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
965       }
966       content += this.mode.list_available_modes();
967       let start_x = 0;
968       if (!this.mode.has_input_prompt) {
969           start_x = this.window_width
970       }
971       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
972       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
973       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
974           terminal.write(y, start_x, lines[i]);
975       }
976   },
977   full_refresh: function() {
978     this.links = {};
979     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
980     if (this.mode.is_intro) {
981         this.draw_history();
982         this.draw_input();
983     } else {
984         if (game.turn_complete) {
985             this.draw_map();
986             this.draw_turn_line();
987         }
988         this.draw_mode_line();
989         if (this.mode.shows_info) {
990           this.draw_info();
991         } else {
992           this.draw_history();
993         }
994         this.draw_input();
995     }
996     if (this.show_help) {
997         this.draw_help();
998     }
999     terminal.refresh();
1000   }
1001 }
1002
1003 let game = {
1004     init: function() {
1005         this.things = {};
1006         this.turn = -1;
1007         this.map = "";
1008         this.map_control = "";
1009         this.map_size = [0,0];
1010         this.player_id = -1;
1011         this.portals = {};
1012         this.tasks = {};
1013     },
1014     get_thing: function(id_, create_if_not_found=false) {
1015         if (id_ in game.things) {
1016             return game.things[id_];
1017         } else if (create_if_not_found) {
1018             let t = new Thing([0,0]);
1019             game.things[id_] = t;
1020             return t;
1021         };
1022     },
1023     move: function(start_position, direction) {
1024         let target = [start_position[0], start_position[1]];
1025         if (direction == 'LEFT') {
1026             target[1] -= 1;
1027         } else if (direction == 'RIGHT') {
1028             target[1] += 1;
1029         } else if (game.map_geometry == 'Square') {
1030             if (direction == 'UP') {
1031                 target[0] -= 1;
1032             } else if (direction == 'DOWN') {
1033                 target[0] += 1;
1034             };
1035         } else if (game.map_geometry == 'Hex') {
1036             let start_indented = start_position[0] % 2;
1037             if (direction == 'UPLEFT') {
1038                 target[0] -= 1;
1039                 if (!start_indented) {
1040                     target[1] -= 1;
1041                 }
1042             } else if (direction == 'UPRIGHT') {
1043                 target[0] -= 1;
1044                 if (start_indented) {
1045                     target[1] += 1;
1046                 }
1047             } else if (direction == 'DOWNLEFT') {
1048                 target[0] += 1;
1049                 if (!start_indented) {
1050                     target[1] -= 1;
1051                 }
1052             } else if (direction == 'DOWNRIGHT') {
1053                 target[0] += 1;
1054                 if (start_indented) {
1055                     target[1] += 1;
1056                 }
1057             };
1058         };
1059         if (target[0] < 0 || target[1] < 0 ||
1060             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1061             return null;
1062         };
1063         return target;
1064     },
1065     teleport: function() {
1066         let player = this.get_thing(game.player_id);
1067         if (player.position in this.portals) {
1068             server.reconnect_to(this.portals[player.position]);
1069         } else {
1070             terminal.blink_screen();
1071             tui.log_msg('? not standing on portal')
1072         }
1073     }
1074 }
1075
1076 game.init();
1077 tui.init();
1078 tui.full_refresh();
1079 server.init(websocket_location);
1080
1081 let explorer = {
1082     position: [0,0],
1083     info_db: {},
1084     info_hints: [],
1085     move: function(direction) {
1086         let target = game.move(this.position, direction);
1087         if (target) {
1088             this.position = target
1089             if (tui.mode.shows_info) {
1090                 this.query_info();
1091             } else if (tui.mode.name == 'control_tile_draw') {
1092                 this.send_tile_control_command();
1093             }
1094         } else {
1095             terminal.blink_screen();
1096         };
1097     },
1098     update_info_db: function(yx, str) {
1099         this.info_db[yx] = str;
1100         if (tui.mode.name == 'study') {
1101             tui.full_refresh();
1102         }
1103     },
1104     empty_info_db: function() {
1105         this.info_db = {};
1106         this.info_hints = [];
1107         if (tui.mode.name == 'study') {
1108             tui.full_refresh();
1109         }
1110     },
1111     query_info: function() {
1112         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1113     },
1114     get_info: function() {
1115         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1116         if (game.fov[position_i] != '.') {
1117             return 'outside field of view';
1118         };
1119         let info = "";
1120         let terrain_char = game.map[position_i]
1121         let terrain_desc = '?'
1122         if (game.terrains[terrain_char]) {
1123             terrain_desc = game.terrains[terrain_char];
1124         };
1125         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1126         let protection = game.map_control[position_i];
1127         if (protection == '.') {
1128             protection = 'unprotected';
1129         };
1130         info += 'PROTECTION: ' + protection + '\n';
1131         for (let t_id in game.things) {
1132              let t = game.things[t_id];
1133              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1134                  let symbol = game.thing_types[t.type_];
1135                  info += "THING: " + t.type_ + " / " + symbol;
1136                  if (t.player_char) {
1137                      info += t.player_char;
1138                  };
1139                  if (t.name_) {
1140                      info += " (" + t.name_ + ")";
1141                  }
1142                  info += "\n";
1143              }
1144         }
1145         if (this.position in game.portals) {
1146             info += "PORTAL: " + game.portals[this.position] + "\n";
1147         }
1148         if (this.position in this.info_db) {
1149             info += "ANNOTATIONS: " + this.info_db[this.position];
1150         } else {
1151             info += 'waiting …';
1152         }
1153         return info;
1154     },
1155     annotate: function(msg) {
1156         if (msg.length == 0) {
1157             msg = " ";  // triggers annotation deletion
1158         }
1159         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1160     },
1161     set_portal: function(msg) {
1162         if (msg.length == 0) {
1163             msg = " ";  // triggers portal deletion
1164         }
1165         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1166     },
1167     send_tile_control_command: function() {
1168         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1169     }
1170 }
1171
1172 tui.inputEl.addEventListener('input', (event) => {
1173     if (tui.mode.has_input_prompt) {
1174         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1175         if (tui.inputEl.value.length > max_length) {
1176             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1177         };
1178         tui.recalc_input_lines();
1179     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1180         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1181         tui.switch_mode('edit');
1182     } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1183         tui.tile_control_char = tui.inputEl.value[0];
1184         tui.switch_mode('control_pw_pw');
1185     } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1186         tui.tile_control_char = tui.inputEl.value[0];
1187         tui.switch_mode('control_tile_draw');
1188     }
1189     tui.full_refresh();
1190 }, false);
1191 tui.inputEl.addEventListener('keydown', (event) => {
1192     tui.show_help = false;
1193     if (event.key == 'Enter') {
1194         event.preventDefault();
1195     }
1196     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1197         tui.show_help = true;
1198         tui.empty_input();
1199         tui.restore_input_values();
1200     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1201                && !tui.mode.is_single_char_entry) {
1202         tui.show_help = true;
1203     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1204         tui.login_name = tui.inputEl.value;
1205         server.send(['LOGIN', tui.inputEl.value]);
1206         tui.empty_input();
1207     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1208         if (tui.inputEl.value.length == 0) {
1209             tui.log_msg('@ aborted');
1210         } else {
1211             server.send(['SET_MAP_CONTROL_PASSWORD',
1212                         tui.tile_control_char, tui.inputEl.value]);
1213         }
1214         tui.switch_mode('admin');
1215     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1216         explorer.set_portal(tui.inputEl.value);
1217         tui.switch_mode('edit');
1218     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1219         explorer.annotate(tui.inputEl.value);
1220         tui.switch_mode('edit');
1221     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1222         if (tui.inputEl.value.length == 0) {
1223             tui.inputEl.value = " ";
1224         }
1225         tui.password = tui.inputEl.value
1226         tui.switch_mode('edit');
1227     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1228         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1229         tui.switch_mode('play');
1230     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1231         let tokens = parser.tokenize(tui.inputEl.value);
1232         if (tokens.length > 0 && tokens[0].length > 0) {
1233             if (tui.inputEl.value[0][0] == '/') {
1234                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1235                     tui.switch_mode('play');
1236                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1237                     tui.switch_mode('study');
1238                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1239                     tui.switch_mode('edit');
1240                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1241                     tui.switch_mode('admin_enter');
1242                 } else if (tokens[0].slice(1) == 'nick') {
1243                     if (tokens.length > 1) {
1244                         server.send(['NICK', tokens[1]]);
1245                     } else {
1246                         tui.log_msg('? need new name');
1247                     }
1248                 } else {
1249                     tui.log_msg('? unknown command');
1250                 }
1251             } else {
1252                     server.send(['ALL', tui.inputEl.value]);
1253             }
1254         } else if (tui.inputEl.valuelength > 0) {
1255                 server.send(['ALL', tui.inputEl.value]);
1256         }
1257         tui.empty_input();
1258     } else if (tui.mode.name == 'play') {
1259           if (tui.mode.mode_switch_on_key(event)) {
1260               null;
1261           } else if (event.key === tui.keys.take_thing
1262                      && game.tasks.includes('PICK_UP')) {
1263               server.send(["TASK:PICK_UP"]);
1264           } else if (event.key === tui.keys.drop_thing
1265                      && game.tasks.includes('DROP')) {
1266               server.send(["TASK:DROP"]);
1267           } else if (event.key in tui.movement_keys
1268                      && game.tasks.includes('MOVE')) {
1269               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1270           } else if (event.key === tui.keys.teleport) {
1271               game.teleport();
1272           };
1273     } else if (tui.mode.name == 'study') {
1274         if (tui.mode.mode_switch_on_key(event)) {
1275               null;
1276         } else if (event.key in tui.movement_keys) {
1277             explorer.move(tui.movement_keys[event.key]);
1278         } else if (event.key == tui.keys.toggle_map_mode) {
1279             if (tui.map_mode == 'terrain') {
1280                 tui.map_mode = 'annotations';
1281             } else if (tui.map_mode == 'annotations') {
1282                 tui.map_mode = 'control';
1283             } else {
1284                 tui.map_mode = 'terrain';
1285             }
1286         };
1287     } else if (tui.mode.name == 'control_tile_draw') {
1288         if (tui.mode.mode_switch_on_key(event)) {
1289               null;
1290         } else if (event.key in tui.movement_keys) {
1291             explorer.move(tui.movement_keys[event.key]);
1292         };
1293     } else if (tui.mode.name == 'admin') {
1294         if (tui.mode.mode_switch_on_key(event)) {
1295               null;
1296         };
1297     } else if (tui.mode.name == 'edit') {
1298         if (tui.mode.mode_switch_on_key(event)) {
1299               null;
1300         } else if (event.key in tui.movement_keys
1301                    && game.tasks.includes('MOVE')) {
1302             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1303         } else if (event.key === tui.keys.flatten
1304                    && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1305             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1306         }
1307     }
1308     tui.full_refresh();
1309 }, false);
1310
1311 rows_selector.addEventListener('input', function() {
1312     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1313         return;
1314     }
1315     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1316     terminal.initialize();
1317     tui.full_refresh();
1318 }, false);
1319 cols_selector.addEventListener('input', function() {
1320     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1321         return;
1322     }
1323     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1324     terminal.initialize();
1325     tui.window_width = terminal.cols / 2,
1326     tui.full_refresh();
1327 }, false);
1328 for (let key_selector of key_selectors) {
1329     key_selector.addEventListener('input', function() {
1330         window.localStorage.setItem(key_selector.id, key_selector.value);
1331         tui.init_keys();
1332     }, false);
1333 }
1334 window.setInterval(function() {
1335     if (server.connected) {
1336         server.send(['PING']);
1337     } else {
1338         server.reconnect_to(server.url);
1339         tui.log_msg('@ attempting reconnect …')
1340     }
1341 }, 5000);
1342 document.getElementById("terminal").onclick = function() {
1343     tui.inputEl.focus();
1344 };
1345 document.getElementById("help").onclick = function() {
1346     tui.show_help = true;
1347     tui.full_refresh();
1348 };
1349 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1350     const mode = switchEl.id.slice("switch_to_".length);
1351     switchEl.onclick = function() {
1352         tui.switch_mode(mode);
1353         tui.full_refresh();
1354     }
1355 };
1356 document.getElementById("toggle_map_mode").onclick = function() {
1357     if (tui.map_mode == 'terrain') {
1358         tui.map_mode = 'annotations';
1359     } else if (tui.map_mode == 'annotations') {
1360         tui.map_mode = 'control';
1361     } else {
1362         tui.map_mode = 'terrain';
1363     }
1364     tui.full_refresh();
1365 };
1366 document.getElementById("take_thing").onclick = function() {
1367         server.send(['TASK:PICK_UP']);
1368 };
1369 document.getElementById("drop_thing").onclick = function() {
1370         server.send(['TASK:DROP']);
1371 };
1372 document.getElementById("flatten").onclick = function() {
1373     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1374 };
1375 document.getElementById("teleport").onclick = function() {
1376     game.teleport();
1377 };
1378 document.getElementById("move_upleft").onclick = function() {
1379     if (tui.mode.name == 'play') {
1380         server.send(['TASK:MOVE', 'UPLEFT']);
1381     } else {
1382         explorer.move('UPLEFT');
1383     };
1384 };
1385 document.getElementById("move_left").onclick = function() {
1386     if (tui.mode.name == 'play') {
1387         server.send(['TASK:MOVE', 'LEFT']);
1388     } else {
1389         explorer.move('LEFT');
1390     };
1391 };
1392 document.getElementById("move_downleft").onclick = function() {
1393     if (tui.mode.name == 'play') {
1394         server.send(['TASK:MOVE', 'DOWNLEFT']);
1395     } else {
1396         explorer.move('DOWNLEFT');
1397     };
1398 };
1399 document.getElementById("move_down").onclick = function() {
1400     if (tui.mode.name == 'play') {
1401         server.send(['TASK:MOVE', 'DOWN']);
1402     } else {
1403         explorer.move('DOWN');
1404     };
1405 };
1406 document.getElementById("move_up").onclick = function() {
1407     if (tui.mode.name == 'play') {
1408         server.send(['TASK:MOVE', 'UP']);
1409     } else {
1410         explorer.move('UP');
1411     };
1412 };
1413 document.getElementById("move_upright").onclick = function() {
1414     if (tui.mode.name == 'play') {
1415         server.send(['TASK:MOVE', 'UPRIGHT']);
1416     } else {
1417         explorer.move('UPRIGHT');
1418     };
1419 };
1420 document.getElementById("move_right").onclick = function() {
1421     if (tui.mode.name == 'play') {
1422         server.send(['TASK:MOVE', 'RIGHT']);
1423     } else {
1424         explorer.move('RIGHT');
1425     };
1426 };
1427 document.getElementById("move_downright").onclick = function() {
1428     if (tui.mode.name == 'play') {
1429         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1430     } else {
1431         explorer.move('DOWNRIGHT');
1432     };
1433 };
1434 </script>
1435 </body></html>