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