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