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