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