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