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