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