home · contact · privacy
Show portal char next to terrain char.
[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             let original = map_lines_split[coordinate[0]][coordinate[1]];
818             map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
819         }
820         let used_positions = [];
821         for (const thing_id in game.things) {
822             let t = game.things[thing_id];
823             let symbol = game.thing_types[t.type_];
824             let meta_char = ' ';
825             if (t.player_char) {
826                 meta_char = t.player_char;
827             }
828             if (used_positions.includes(t.position.toString())) {
829                 meta_char = '+';
830             };
831             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
832             used_positions.push(t.position.toString());
833         };
834     }
835     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
836         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
837     }
838     let map_lines = []
839     if (game.map_geometry == 'Square') {
840         for (let line_split of map_lines_split) {
841             map_lines.push(line_split.join(''));
842         };
843     } else if (game.map_geometry == 'Hex') {
844         let indent = 0
845         for (let line_split of map_lines_split) {
846             map_lines.push(' '.repeat(indent) + line_split.join(''));
847             if (indent == 0) {
848                 indent = 1;
849             } else {
850                 indent = 0;
851             };
852         };
853     }
854     let window_center = [terminal.rows / 2, this.window_width / 2];
855     let player = game.things[game.player_id];
856     let center_position = [player.position[0], player.position[1]];
857     if (tui.mode.shows_info) {
858         center_position = [explorer.position[0], explorer.position[1]];
859     }
860     center_position[1] = center_position[1] * 2;
861     let offset = [center_position[0] - window_center[0],
862                   center_position[1] - window_center[1]]
863     if (game.map_geometry == 'Hex' && offset[0] % 2) {
864         offset[1] += 1;
865     };
866     let term_y = Math.max(0, -offset[0]);
867     let term_x = Math.max(0, -offset[1]);
868     let map_y = Math.max(0, offset[0]);
869     let map_x = Math.max(0, offset[1]);
870     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
871         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
872         terminal.write(term_y, term_x, to_draw);
873     }
874   },
875   draw_mode_line: function() {
876       let help = 'hit [' + this.keys.help + '] for help';
877       if (this.mode.has_input_prompt) {
878           help = 'enter /help for help';
879       }
880       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
881   },
882   draw_turn_line: function(n) {
883     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
884   },
885   draw_history: function() {
886       let log_display_lines = [];
887       let log_links = {};
888       let y_offset_in_log = 0;
889       for (let line of this.log) {
890           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
891                                                                     this.window_width)
892           log_display_lines = log_display_lines.concat(new_lines);
893           for (const y in link_data) {
894               const rel_y = y_offset_in_log + parseInt(y);
895               log_links[rel_y] = [];
896               for (let link of link_data[y]) {
897                   log_links[rel_y].push(link);
898               }
899           }
900           y_offset_in_log += new_lines.length;
901       };
902       let i = log_display_lines.length - 1;
903       for (let y = terminal.rows - 1 - this.height_input;
904            y >= this.height_header && i >= 0;
905            y--, i--) {
906           terminal.write(y, this.window_width, log_display_lines[i]);
907       }
908       for (const key of Object.keys(log_links)) {
909           if (parseInt(key) <= i) {
910               delete log_links[key];
911           }
912       }
913       let offset = [terminal.rows - this.height_input - log_display_lines.length,
914                     this.window_width];
915       this.offset_links(offset, log_links);
916   },
917   draw_info: function() {
918       let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
919                                                             this.window_width);
920       let offset = [this.height_header, this.window_width];
921       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
922         terminal.write(y, offset[1], lines[i]);
923       }
924       this.offset_links(offset, link_data);
925   },
926   draw_input: function() {
927     if (this.mode.has_input_prompt) {
928         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
929             terminal.write(y, this.window_width, this.input_lines[i]);
930         }
931     }
932   },
933   draw_help: function() {
934       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
935       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
936       if (this.mode.name == 'play') {
937           content += "Available actions:\n";
938           if (game.tasks.includes('MOVE')) {
939               content += "[" + movement_keys_desc + "] – move player\n";
940           }
941           if (game.tasks.includes('PICK_UP')) {
942               content += "[" + this.keys.take_thing + "] – pick up thing\n";
943           }
944           if (game.tasks.includes('DROP')) {
945               content += "[" + this.keys.drop_thing + "] – drop picked up thing\n";
946           }
947           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
948           content += '\n';
949       } else if (this.mode.name == 'study') {
950           content += "Available actions:\n";
951           content += '[' + movement_keys_desc + '] – move question mark\n';
952           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
953           content += '\n';
954       } else if (this.mode.name == 'edit') {
955           content += "Available actions:\n";
956           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
957               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
958           }
959           content += '\n';
960       } else if (this.mode.name == 'chat') {
961           content += '/nick NAME – re-name yourself to NAME\n';
962           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
963           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
964           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
965           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
966       }
967       content += this.mode.list_available_modes();
968       let start_x = 0;
969       if (!this.mode.has_input_prompt) {
970           start_x = this.window_width
971       }
972       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
973       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
974       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
975           terminal.write(y, start_x, lines[i]);
976       }
977   },
978   full_refresh: function() {
979     this.links = {};
980     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
981     if (this.mode.is_intro) {
982         this.draw_history();
983         this.draw_input();
984     } else {
985         if (game.turn_complete) {
986             this.draw_map();
987             this.draw_turn_line();
988         }
989         this.draw_mode_line();
990         if (this.mode.shows_info) {
991           this.draw_info();
992         } else {
993           this.draw_history();
994         }
995         this.draw_input();
996     }
997     if (this.show_help) {
998         this.draw_help();
999     }
1000     terminal.refresh();
1001   }
1002 }
1003
1004 let game = {
1005     init: function() {
1006         this.things = {};
1007         this.turn = -1;
1008         this.map = "";
1009         this.map_control = "";
1010         this.map_size = [0,0];
1011         this.player_id = -1;
1012         this.portals = {};
1013         this.tasks = {};
1014     },
1015     get_thing: function(id_, create_if_not_found=false) {
1016         if (id_ in game.things) {
1017             return game.things[id_];
1018         } else if (create_if_not_found) {
1019             let t = new Thing([0,0]);
1020             game.things[id_] = t;
1021             return t;
1022         };
1023     },
1024     move: function(start_position, direction) {
1025         let target = [start_position[0], start_position[1]];
1026         if (direction == 'LEFT') {
1027             target[1] -= 1;
1028         } else if (direction == 'RIGHT') {
1029             target[1] += 1;
1030         } else if (game.map_geometry == 'Square') {
1031             if (direction == 'UP') {
1032                 target[0] -= 1;
1033             } else if (direction == 'DOWN') {
1034                 target[0] += 1;
1035             };
1036         } else if (game.map_geometry == 'Hex') {
1037             let start_indented = start_position[0] % 2;
1038             if (direction == 'UPLEFT') {
1039                 target[0] -= 1;
1040                 if (!start_indented) {
1041                     target[1] -= 1;
1042                 }
1043             } else if (direction == 'UPRIGHT') {
1044                 target[0] -= 1;
1045                 if (start_indented) {
1046                     target[1] += 1;
1047                 }
1048             } else if (direction == 'DOWNLEFT') {
1049                 target[0] += 1;
1050                 if (!start_indented) {
1051                     target[1] -= 1;
1052                 }
1053             } else if (direction == 'DOWNRIGHT') {
1054                 target[0] += 1;
1055                 if (start_indented) {
1056                     target[1] += 1;
1057                 }
1058             };
1059         };
1060         if (target[0] < 0 || target[1] < 0 ||
1061             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1062             return null;
1063         };
1064         return target;
1065     },
1066     teleport: function() {
1067         let player = this.get_thing(game.player_id);
1068         if (player.position in this.portals) {
1069             server.reconnect_to(this.portals[player.position]);
1070         } else {
1071             terminal.blink_screen();
1072             tui.log_msg('? not standing on portal')
1073         }
1074     }
1075 }
1076
1077 game.init();
1078 tui.init();
1079 tui.full_refresh();
1080 server.init(websocket_location);
1081
1082 let explorer = {
1083     position: [0,0],
1084     info_db: {},
1085     info_hints: [],
1086     move: function(direction) {
1087         let target = game.move(this.position, direction);
1088         if (target) {
1089             this.position = target
1090             if (tui.mode.shows_info) {
1091                 this.query_info();
1092             } else if (tui.mode.name == 'control_tile_draw') {
1093                 this.send_tile_control_command();
1094             }
1095         } else {
1096             terminal.blink_screen();
1097         };
1098     },
1099     update_info_db: function(yx, str) {
1100         this.info_db[yx] = str;
1101         if (tui.mode.name == 'study') {
1102             tui.full_refresh();
1103         }
1104     },
1105     empty_info_db: function() {
1106         this.info_db = {};
1107         this.info_hints = [];
1108         if (tui.mode.name == 'study') {
1109             tui.full_refresh();
1110         }
1111     },
1112     query_info: function() {
1113         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1114     },
1115     get_info: function() {
1116         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1117         if (game.fov[position_i] != '.') {
1118             return 'outside field of view';
1119         };
1120         let info = "";
1121         let terrain_char = game.map[position_i]
1122         let terrain_desc = '?'
1123         if (game.terrains[terrain_char]) {
1124             terrain_desc = game.terrains[terrain_char];
1125         };
1126         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1127         let protection = game.map_control[position_i];
1128         if (protection == '.') {
1129             protection = 'unprotected';
1130         };
1131         info += 'PROTECTION: ' + protection + '\n';
1132         for (let t_id in game.things) {
1133              let t = game.things[t_id];
1134              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1135                  let symbol = game.thing_types[t.type_];
1136                  info += "THING: " + t.type_ + " / " + symbol;
1137                  if (t.player_char) {
1138                      info += t.player_char;
1139                  };
1140                  if (t.name_) {
1141                      info += " (" + t.name_ + ")";
1142                  }
1143                  info += "\n";
1144              }
1145         }
1146         if (this.position in game.portals) {
1147             info += "PORTAL: " + game.portals[this.position] + "\n";
1148         }
1149         if (this.position in this.info_db) {
1150             info += "ANNOTATIONS: " + this.info_db[this.position];
1151         } else {
1152             info += 'waiting …';
1153         }
1154         return info;
1155     },
1156     annotate: function(msg) {
1157         if (msg.length == 0) {
1158             msg = " ";  // triggers annotation deletion
1159         }
1160         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1161     },
1162     set_portal: function(msg) {
1163         if (msg.length == 0) {
1164             msg = " ";  // triggers portal deletion
1165         }
1166         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1167     },
1168     send_tile_control_command: function() {
1169         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1170     }
1171 }
1172
1173 tui.inputEl.addEventListener('input', (event) => {
1174     if (tui.mode.has_input_prompt) {
1175         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1176         if (tui.inputEl.value.length > max_length) {
1177             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1178         };
1179         tui.recalc_input_lines();
1180     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1181         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1182         tui.switch_mode('edit');
1183     } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
1184         tui.tile_control_char = tui.inputEl.value[0];
1185         tui.switch_mode('control_pw_pw');
1186     } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
1187         tui.tile_control_char = tui.inputEl.value[0];
1188         tui.switch_mode('control_tile_draw');
1189     }
1190     tui.full_refresh();
1191 }, false);
1192 tui.inputEl.addEventListener('keydown', (event) => {
1193     tui.show_help = false;
1194     if (event.key == 'Enter') {
1195         event.preventDefault();
1196     }
1197     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1198         tui.show_help = true;
1199         tui.empty_input();
1200         tui.restore_input_values();
1201     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1202                && !tui.mode.is_single_char_entry) {
1203         tui.show_help = true;
1204     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1205         tui.login_name = tui.inputEl.value;
1206         server.send(['LOGIN', tui.inputEl.value]);
1207         tui.empty_input();
1208     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1209         if (tui.inputEl.value.length == 0) {
1210             tui.log_msg('@ aborted');
1211         } else {
1212             server.send(['SET_MAP_CONTROL_PASSWORD',
1213                         tui.tile_control_char, tui.inputEl.value]);
1214         }
1215         tui.switch_mode('admin');
1216     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1217         explorer.set_portal(tui.inputEl.value);
1218         tui.switch_mode('edit');
1219     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1220         explorer.annotate(tui.inputEl.value);
1221         tui.switch_mode('edit');
1222     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1223         if (tui.inputEl.value.length == 0) {
1224             tui.inputEl.value = " ";
1225         }
1226         tui.password = tui.inputEl.value
1227         tui.switch_mode('edit');
1228     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1229         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1230         tui.switch_mode('play');
1231     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1232         let tokens = parser.tokenize(tui.inputEl.value);
1233         if (tokens.length > 0 && tokens[0].length > 0) {
1234             if (tui.inputEl.value[0][0] == '/') {
1235                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1236                     tui.switch_mode('play');
1237                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1238                     tui.switch_mode('study');
1239                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1240                     tui.switch_mode('edit');
1241                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1242                     tui.switch_mode('admin_enter');
1243                 } else if (tokens[0].slice(1) == 'nick') {
1244                     if (tokens.length > 1) {
1245                         server.send(['NICK', tokens[1]]);
1246                     } else {
1247                         tui.log_msg('? need new name');
1248                     }
1249                 } else {
1250                     tui.log_msg('? unknown command');
1251                 }
1252             } else {
1253                     server.send(['ALL', tui.inputEl.value]);
1254             }
1255         } else if (tui.inputEl.valuelength > 0) {
1256                 server.send(['ALL', tui.inputEl.value]);
1257         }
1258         tui.empty_input();
1259     } else if (tui.mode.name == 'play') {
1260           if (tui.mode.mode_switch_on_key(event)) {
1261               null;
1262           } else if (event.key === tui.keys.take_thing
1263                      && game.tasks.includes('PICK_UP')) {
1264               server.send(["TASK:PICK_UP"]);
1265           } else if (event.key === tui.keys.drop_thing
1266                      && game.tasks.includes('DROP')) {
1267               server.send(["TASK:DROP"]);
1268           } else if (event.key in tui.movement_keys
1269                      && game.tasks.includes('MOVE')) {
1270               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1271           } else if (event.key === tui.keys.teleport) {
1272               game.teleport();
1273           };
1274     } else if (tui.mode.name == 'study') {
1275         if (tui.mode.mode_switch_on_key(event)) {
1276               null;
1277         } else if (event.key in tui.movement_keys) {
1278             explorer.move(tui.movement_keys[event.key]);
1279         } else if (event.key == tui.keys.toggle_map_mode) {
1280             if (tui.map_mode == 'terrain') {
1281                 tui.map_mode = 'annotations';
1282             } else if (tui.map_mode == 'annotations') {
1283                 tui.map_mode = 'control';
1284             } else {
1285                 tui.map_mode = 'terrain';
1286             }
1287         };
1288     } else if (tui.mode.name == 'control_tile_draw') {
1289         if (tui.mode.mode_switch_on_key(event)) {
1290               null;
1291         } else if (event.key in tui.movement_keys) {
1292             explorer.move(tui.movement_keys[event.key]);
1293         };
1294     } else if (tui.mode.name == 'admin') {
1295         if (tui.mode.mode_switch_on_key(event)) {
1296               null;
1297         };
1298     } else if (tui.mode.name == 'edit') {
1299         if (tui.mode.mode_switch_on_key(event)) {
1300               null;
1301         } else if (event.key in tui.movement_keys
1302                    && game.tasks.includes('MOVE')) {
1303             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1304         } else if (event.key === tui.keys.flatten
1305                    && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1306             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1307         }
1308     }
1309     tui.full_refresh();
1310 }, false);
1311
1312 rows_selector.addEventListener('input', function() {
1313     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1314         return;
1315     }
1316     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1317     terminal.initialize();
1318     tui.full_refresh();
1319 }, false);
1320 cols_selector.addEventListener('input', function() {
1321     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1322         return;
1323     }
1324     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1325     terminal.initialize();
1326     tui.window_width = terminal.cols / 2,
1327     tui.full_refresh();
1328 }, false);
1329 for (let key_selector of key_selectors) {
1330     key_selector.addEventListener('input', function() {
1331         window.localStorage.setItem(key_selector.id, key_selector.value);
1332         tui.init_keys();
1333     }, false);
1334 }
1335 window.setInterval(function() {
1336     if (server.connected) {
1337         server.send(['PING']);
1338     } else {
1339         server.reconnect_to(server.url);
1340         tui.log_msg('@ attempting reconnect …')
1341     }
1342 }, 5000);
1343 document.getElementById("terminal").onclick = function() {
1344     tui.inputEl.focus();
1345 };
1346 document.getElementById("help").onclick = function() {
1347     tui.show_help = true;
1348     tui.full_refresh();
1349 };
1350 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1351     const mode = switchEl.id.slice("switch_to_".length);
1352     switchEl.onclick = function() {
1353         tui.switch_mode(mode);
1354         tui.full_refresh();
1355     }
1356 };
1357 document.getElementById("toggle_map_mode").onclick = function() {
1358     if (tui.map_mode == 'terrain') {
1359         tui.map_mode = 'annotations';
1360     } else if (tui.map_mode == 'annotations') {
1361         tui.map_mode = 'control';
1362     } else {
1363         tui.map_mode = 'terrain';
1364     }
1365     tui.full_refresh();
1366 };
1367 document.getElementById("take_thing").onclick = function() {
1368         server.send(['TASK:PICK_UP']);
1369 };
1370 document.getElementById("drop_thing").onclick = function() {
1371         server.send(['TASK:DROP']);
1372 };
1373 document.getElementById("flatten").onclick = function() {
1374     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1375 };
1376 document.getElementById("teleport").onclick = function() {
1377     game.teleport();
1378 };
1379 document.getElementById("move_upleft").onclick = function() {
1380     if (tui.mode.name == 'play') {
1381         server.send(['TASK:MOVE', 'UPLEFT']);
1382     } else {
1383         explorer.move('UPLEFT');
1384     };
1385 };
1386 document.getElementById("move_left").onclick = function() {
1387     if (tui.mode.name == 'play') {
1388         server.send(['TASK:MOVE', 'LEFT']);
1389     } else {
1390         explorer.move('LEFT');
1391     };
1392 };
1393 document.getElementById("move_downleft").onclick = function() {
1394     if (tui.mode.name == 'play') {
1395         server.send(['TASK:MOVE', 'DOWNLEFT']);
1396     } else {
1397         explorer.move('DOWNLEFT');
1398     };
1399 };
1400 document.getElementById("move_down").onclick = function() {
1401     if (tui.mode.name == 'play') {
1402         server.send(['TASK:MOVE', 'DOWN']);
1403     } else {
1404         explorer.move('DOWN');
1405     };
1406 };
1407 document.getElementById("move_up").onclick = function() {
1408     if (tui.mode.name == 'play') {
1409         server.send(['TASK:MOVE', 'UP']);
1410     } else {
1411         explorer.move('UP');
1412     };
1413 };
1414 document.getElementById("move_upright").onclick = function() {
1415     if (tui.mode.name == 'play') {
1416         server.send(['TASK:MOVE', 'UPRIGHT']);
1417     } else {
1418         explorer.move('UPRIGHT');
1419     };
1420 };
1421 document.getElementById("move_right").onclick = function() {
1422     if (tui.mode.name == 'play') {
1423         server.send(['TASK:MOVE', 'RIGHT']);
1424     } else {
1425         explorer.move('RIGHT');
1426     };
1427 };
1428 document.getElementById("move_downright").onclick = function() {
1429     if (tui.mode.name == 'play') {
1430         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1431     } else {
1432         explorer.move('DOWNRIGHT');
1433     };
1434 };
1435 </script>
1436 </body></html>