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