home · contact · privacy
Enable edit mode movement in web client.
[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">pick up thing</button>
53       <button id="drop_thing">drop thing</button>
54       <button id="teleport">teleport</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_annotate">annotate tile</button>
63       <button id="switch_to_portal">edit portal link</button>
64       <button id="switch_to_password">change tile editing password</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', true),
550   mode_portal: new Mode('portal', true, true),
551   mode_password: new Mode('password', true),
552   mode_admin_enter: new Mode('admin_enter', true),
553   mode_admin: new Mode('admin'),
554   mode_control_pw_pw: new Mode('control_pw_pw', true),
555   mode_control_tile_type: new Mode('control_tile_type', true),
556   mode_control_tile_draw: new Mode('control_tile_draw'),
557   init: function() {
558       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
559       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
560       this.mode_admin.available_modes = ["control_pw_type",
561                                          "control_tile_type", "chat",
562                                          "study", "play", "edit"]
563       this.mode_control_tile_draw.available_modes = ["admin_enter"]
564       this.mode_edit.available_modes = ["write", "annotate", "portal",
565                                         "password", "chat", "study", "play",
566                                         "admin_enter"]
567       this.mode = this.mode_waiting_for_server;
568       this.inputEl = document.getElementById("input");
569       this.inputEl.focus();
570       this.recalc_input_lines();
571       this.height_header = this.height_turn_line + this.height_mode_line;
572       this.log_msg("@ waiting for server connection ...");
573       this.init_keys();
574   },
575   init_keys: function() {
576     this.keys = {};
577     for (let key_selector of key_selectors) {
578         this.keys[key_selector.id.slice(4)] = key_selector.value;
579     }
580     if (game.map_geometry == 'Square') {
581         this.movement_keys = {
582             [this.keys.square_move_up]: 'UP',
583             [this.keys.square_move_left]: 'LEFT',
584             [this.keys.square_move_down]: 'DOWN',
585             [this.keys.square_move_right]: 'RIGHT'
586         };
587         document.getElementById("move_upright").hidden = true;
588         document.getElementById("move_upleft").hidden = true;
589         document.getElementById("move_downright").hidden = true;
590         document.getElementById("move_downleft").hidden = true;
591         document.getElementById("move_up").hidden = false;
592         document.getElementById("move_down").hidden = false;
593     } else if (game.map_geometry == 'Hex') {
594         document.getElementById("move_upright").hidden = false;
595         document.getElementById("move_upleft").hidden = false;
596         document.getElementById("move_downright").hidden = false;
597         document.getElementById("move_downleft").hidden = false;
598         document.getElementById("move_up").hidden = true;
599         document.getElementById("move_down").hidden = true;
600         this.movement_keys = {
601             [this.keys.hex_move_upleft]: 'UPLEFT',
602             [this.keys.hex_move_upright]: 'UPRIGHT',
603             [this.keys.hex_move_right]: 'RIGHT',
604             [this.keys.hex_move_downright]: 'DOWNRIGHT',
605             [this.keys.hex_move_downleft]: 'DOWNLEFT',
606             [this.keys.hex_move_left]: 'LEFT'
607         };
608     };
609   },
610   switch_mode: function(mode_name) {
611     this.inputEl.focus();
612     this.map_mode = 'terrain + things';
613     this.tile_draw = false;
614     if (mode_name == 'admin_enter' && this.is_admin) {
615         mode_name = 'admin';
616     };
617     this.mode = this['mode_' + mode_name];
618     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
619         explorer.position = game.things[game.player_id].position;
620         if (this.mode.shows_info) {
621             explorer.query_info();
622         }
623     }
624     this.empty_input();
625     this.restore_input_values();
626     for (let el of document.getElementsByTagName("button")) {
627         el.disabled = true;
628     }
629     document.getElementById("help").disabled = false;
630     if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
631         for (const move_key of document.querySelectorAll('[id^="move_"]')) {
632             move_key.disabled = false;
633         }
634     }
635     if (!this.mode.is_intro && this.mode.name != 'play') {
636         document.getElementById("switch_to_play").disabled = false;
637     }
638     if (!this.mode.is_intro && this.mode.name != 'study') {
639         document.getElementById("switch_to_study").disabled = false;
640     }
641     if (!this.mode.is_intro && this.mode.name != 'chat') {
642         document.getElementById("switch_to_chat").disabled = false;
643     }
644     if (!this.mode.is_intro && this.mode.name != 'edit') {
645         document.getElementById("switch_to_edit").disabled = false;
646     }
647     if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
648         document.getElementById("switch_to_admin_enter").disabled = false;
649     }
650     if (this.mode.name == 'login') {
651         if (this.login_name) {
652             server.send(['LOGIN', this.login_name]);
653         } else {
654             this.log_msg("? need login name");
655         }
656     } else if (this.mode.name == 'play') {
657         if (game.tasks.includes('PICK_UP')) {
658             document.getElementById("take_thing").disabled = false;
659         }
660         if (game.tasks.includes('DROP')) {
661             document.getElementById("drop_thing").disabled = false;
662         }
663         if (game.tasks.includes('MOVE')) {
664         }
665         document.getElementById("teleport").disabled = false;
666     } else if (this.mode.name == 'edit') {
667         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
668             document.getElementById("flatten").disabled = false;
669         }
670         document.getElementById("switch_to_annotate").disabled = false;
671         document.getElementById("switch_to_write").disabled = false;
672         document.getElementById("switch_to_portal").disabled = false;
673         document.getElementById("switch_to_password").disabled = false;
674     } else if (this.mode.name == 'admin') {
675         document.getElementById("switch_to_control_pw_type").disabled = false;
676         document.getElementById("switch_to_control_tile_type").disabled = false;
677     } else if (this.mode.name == 'study') {
678         document.getElementById("toggle_map_mode").disabled = false;
679     } else if (this.mode.is_single_char_entry) {
680         this.show_help = true;
681     } else if (this.mode.name == 'admin_enter') {
682         this.log_msg('@ enter admin password:')
683     } else if (this.mode.name == 'control_pw_type') {
684         this.log_msg('@ enter tile control character for which you want to change the password:')
685     } else if (this.mode.name == 'control_tile_type') {
686         this.log_msg('@ enter tile control character which you want to draw:')
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_tile_draw') {
690         document.getElementById("toggle_tile_draw").disabled = false;
691         this.log_msg('@ can draw tile control character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' +  this.keys.switch_to_admin_enter + '].')
692     }
693     this.full_refresh();
694   },
695   offset_links: function(offset, links) {
696       for (let y in links) {
697           let real_y = offset[0] + parseInt(y);
698           if (!this.links[real_y]) {
699               this.links[real_y] = [];
700           }
701           for (let link of links[y]) {
702               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
703               this.links[real_y].push(offset_link);
704           }
705       }
706   },
707   restore_input_values: function() {
708       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
709           let info = explorer.info_db[explorer.position];
710           if (info != "(none)") {
711               this.inputEl.value = info;
712               this.recalc_input_lines();
713           }
714       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
715           let portal = game.portals[explorer.position]
716           this.inputEl.value = portal;
717           this.recalc_input_lines();
718       } else if (this.mode.name == 'password') {
719           this.inputEl.value = this.password;
720           this.recalc_input_lines();
721       }
722   },
723   empty_input: function(str) {
724       this.inputEl.value = "";
725       if (this.mode.has_input_prompt) {
726           this.recalc_input_lines();
727       } else {
728           this.height_input = 0;
729       }
730   },
731   recalc_input_lines: function() {
732       let _ = null;
733       [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
734       this.height_input = this.input_lines.length;
735   },
736   msg_into_lines_of_width: function(msg, width) {
737       function push_inner_link(y, end_x) {
738           if (!inner_links[y]) {
739               inner_links[y] = [];
740           };
741           inner_links[y].push([url_start_x, end_x, url]);
742       };
743       const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
744       let link_data = {};
745       let url_ends = [];
746       for (const match of matches) {
747           const url = match[0];
748           const url_start = match.index;
749           const url_end = match.index + match[0].length;
750           link_data[url_start] = url;
751           url_ends.push(url_end);
752       }
753       let url_start_x = 0;
754       let url = '';
755       let inner_links = {};
756       let in_link = false;
757       let chunk = "";
758       let lines = [];
759       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
760           if (x >= width || msg[i] == "\n") {
761               if (in_link) {
762                   push_inner_link(y, chunk.length);
763                   url_start_x = 0;
764               };
765               lines.push(chunk);
766               chunk = "";
767               x = 0;
768               if (msg[i] == "\n") {
769                   x -= 1;
770               };
771               y += 1;
772           };
773           if (msg[i] != "\n") {
774               chunk += msg[i];
775           };
776           if (i in link_data) {
777               url_start_x = x;
778               url = link_data[i];
779               in_link = true;
780           } else if (url_ends.includes(i)) {
781               push_inner_link(y, x);
782               in_link = false;
783           }
784       }
785       lines.push(chunk);
786       if (in_link) {
787           push_inner_link(lines.length - 1, chunk.length);
788       }
789       return [lines, inner_links];
790   },
791   log_msg: function(msg) {
792       this.log.push(msg);
793       while (this.log.length > 100) {
794         this.log.shift();
795       };
796       this.full_refresh();
797   },
798   draw_map: function() {
799     let map_lines_split = [];
800     let line = [];
801     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
802         if (j == game.map_size[1]) {
803             map_lines_split.push(line);
804             line = [];
805             j = 0;
806         };
807         if (['edit', 'write', 'control_tile_draw',
808              'control_tile_type'].includes(this.mode.name)) {
809             line.push(game.map[i] + game.map_control[i]);
810         } else {
811             line.push(game.map[i] + ' ');
812         }
813     };
814     map_lines_split.push(line);
815     if (this.map_mode == 'terrain + annotations') {
816         for (const coordinate of explorer.info_hints) {
817             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
818         }
819     } else if (this.map_mode == 'terrain + things') {
820         for (const p in game.portals) {
821             let coordinate = p.split(',')
822             let original = map_lines_split[coordinate[0]][coordinate[1]];
823             map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
824         }
825         let used_positions = [];
826         for (const thing_id in game.things) {
827             let t = game.things[thing_id];
828             let symbol = game.thing_types[t.type_];
829             let meta_char = ' ';
830             if (t.player_char) {
831                 meta_char = t.player_char;
832             }
833             if (used_positions.includes(t.position.toString())) {
834                 meta_char = '+';
835             };
836             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
837             used_positions.push(t.position.toString());
838         };
839     }
840     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
841         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
842     }
843     let map_lines = []
844     if (game.map_geometry == 'Square') {
845         for (let line_split of map_lines_split) {
846             map_lines.push(line_split.join(''));
847         };
848     } else if (game.map_geometry == 'Hex') {
849         let indent = 0
850         for (let line_split of map_lines_split) {
851             map_lines.push(' '.repeat(indent) + line_split.join(''));
852             if (indent == 0) {
853                 indent = 1;
854             } else {
855                 indent = 0;
856             };
857         };
858     }
859     let window_center = [terminal.rows / 2, this.window_width / 2];
860     let player = game.things[game.player_id];
861     let center_position = [player.position[0], player.position[1]];
862     if (tui.mode.shows_info) {
863         center_position = [explorer.position[0], explorer.position[1]];
864     }
865     center_position[1] = center_position[1] * 2;
866     let offset = [center_position[0] - window_center[0],
867                   center_position[1] - window_center[1]]
868     if (game.map_geometry == 'Hex' && offset[0] % 2) {
869         offset[1] += 1;
870     };
871     let term_y = Math.max(0, -offset[0]);
872     let term_x = Math.max(0, -offset[1]);
873     let map_y = Math.max(0, offset[0]);
874     let map_x = Math.max(0, offset[1]);
875     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
876         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
877         terminal.write(term_y, term_x, to_draw);
878     }
879   },
880   draw_mode_line: function() {
881       let help = 'hit [' + this.keys.help + '] for help';
882       if (this.mode.has_input_prompt) {
883           help = 'enter /help for help';
884       }
885       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
886   },
887   draw_turn_line: function(n) {
888     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
889   },
890   draw_history: function() {
891       let log_display_lines = [];
892       let log_links = {};
893       let y_offset_in_log = 0;
894       for (let line of this.log) {
895           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
896                                                                     this.window_width)
897           log_display_lines = log_display_lines.concat(new_lines);
898           for (const y in link_data) {
899               const rel_y = y_offset_in_log + parseInt(y);
900               log_links[rel_y] = [];
901               for (let link of link_data[y]) {
902                   log_links[rel_y].push(link);
903               }
904           }
905           y_offset_in_log += new_lines.length;
906       };
907       let i = log_display_lines.length - 1;
908       for (let y = terminal.rows - 1 - this.height_input;
909            y >= this.height_header && i >= 0;
910            y--, i--) {
911           terminal.write(y, this.window_width, log_display_lines[i]);
912       }
913       for (const key of Object.keys(log_links)) {
914           if (parseInt(key) <= i) {
915               delete log_links[key];
916           }
917       }
918       let offset = [terminal.rows - this.height_input - log_display_lines.length,
919                     this.window_width];
920       this.offset_links(offset, log_links);
921   },
922   draw_info: function() {
923       let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
924                                                             this.window_width);
925       let offset = [this.height_header, this.window_width];
926       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
927         terminal.write(y, offset[1], lines[i]);
928       }
929       this.offset_links(offset, link_data);
930   },
931   draw_input: function() {
932     if (this.mode.has_input_prompt) {
933         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
934             terminal.write(y, this.window_width, this.input_lines[i]);
935         }
936     }
937   },
938   draw_help: function() {
939       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
940       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
941       if (this.mode.name == 'play') {
942           content += "Available actions:\n";
943           if (game.tasks.includes('MOVE')) {
944               content += "[" + movement_keys_desc + "] – move player\n";
945           }
946           if (game.tasks.includes('PICK_UP')) {
947               content += "[" + this.keys.take_thing + "] – pick up thing\n";
948           }
949           if (game.tasks.includes('DROP')) {
950               content += "[" + this.keys.drop_thing + "] – drop picked-up thing\n";
951           }
952           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
953           content += '\n';
954       } else if (this.mode.name == 'study') {
955           content += "Available actions:\n";
956           content += '[' + movement_keys_desc + '] – move question mark\n';
957           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
958           content += '\n';
959       } else if (this.mode.name == 'edit') {
960           content += "Available actions:\n";
961           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
962               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
963           }
964           content += '\n';
965       } else if (this.mode.name == 'control_tile_draw') {
966           content += "Available actions:\n";
967           content += "[" + tui.keys.toggle_tile_draw + "] – toggle tile control drawing\n";
968           content += '\n';
969       } else if (this.mode.name == 'chat') {
970           content += '/nick NAME – re-name yourself to NAME\n';
971           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
972           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
973           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
974           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
975       }
976       content += this.mode.list_available_modes();
977       let start_x = 0;
978       if (!this.mode.has_input_prompt) {
979           start_x = this.window_width
980       }
981       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
982       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
983       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
984           terminal.write(y, start_x, lines[i]);
985       }
986   },
987   toggle_tile_draw: function() {
988       if (tui.tile_draw) {
989           tui.tile_draw = false;
990       } else {
991           tui.tile_draw = true;
992       }
993   },
994   toggle_map_mode: function() {
995       if (tui.map_mode == 'terrain only') {
996           tui.map_mode = 'terrain + annotations';
997       } else if (tui.map_mode == 'terrain + annotations') {
998           tui.map_mode = 'terrain + things';
999       } else {
1000           tui.map_mode = 'terrain only';
1001       }
1002   },
1003   full_refresh: function() {
1004     this.links = {};
1005     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1006     if (this.mode.is_intro) {
1007         this.draw_history();
1008         this.draw_input();
1009     } else {
1010         if (game.turn_complete) {
1011             this.draw_map();
1012             this.draw_turn_line();
1013         }
1014         this.draw_mode_line();
1015         if (this.mode.shows_info) {
1016           this.draw_info();
1017         } else {
1018           this.draw_history();
1019         }
1020         this.draw_input();
1021     }
1022     if (this.show_help) {
1023         this.draw_help();
1024     }
1025     terminal.refresh();
1026   }
1027 }
1028
1029 let game = {
1030     init: function() {
1031         this.things = {};
1032         this.turn = -1;
1033         this.map = "";
1034         this.map_control = "";
1035         this.map_size = [0,0];
1036         this.player_id = -1;
1037         this.portals = {};
1038         this.tasks = {};
1039     },
1040     get_thing: function(id_, create_if_not_found=false) {
1041         if (id_ in game.things) {
1042             return game.things[id_];
1043         } else if (create_if_not_found) {
1044             let t = new Thing([0,0]);
1045             game.things[id_] = t;
1046             return t;
1047         };
1048     },
1049     move: function(start_position, direction) {
1050         let target = [start_position[0], start_position[1]];
1051         if (direction == 'LEFT') {
1052             target[1] -= 1;
1053         } else if (direction == 'RIGHT') {
1054             target[1] += 1;
1055         } else if (game.map_geometry == 'Square') {
1056             if (direction == 'UP') {
1057                 target[0] -= 1;
1058             } else if (direction == 'DOWN') {
1059                 target[0] += 1;
1060             };
1061         } else if (game.map_geometry == 'Hex') {
1062             let start_indented = start_position[0] % 2;
1063             if (direction == 'UPLEFT') {
1064                 target[0] -= 1;
1065                 if (!start_indented) {
1066                     target[1] -= 1;
1067                 }
1068             } else if (direction == 'UPRIGHT') {
1069                 target[0] -= 1;
1070                 if (start_indented) {
1071                     target[1] += 1;
1072                 }
1073             } else if (direction == 'DOWNLEFT') {
1074                 target[0] += 1;
1075                 if (!start_indented) {
1076                     target[1] -= 1;
1077                 }
1078             } else if (direction == 'DOWNRIGHT') {
1079                 target[0] += 1;
1080                 if (start_indented) {
1081                     target[1] += 1;
1082                 }
1083             };
1084         };
1085         if (target[0] < 0 || target[1] < 0 ||
1086             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1087             return null;
1088         };
1089         return target;
1090     },
1091     teleport: function() {
1092         let player = this.get_thing(game.player_id);
1093         if (player.position in this.portals) {
1094             server.reconnect_to(this.portals[player.position]);
1095         } else {
1096             terminal.blink_screen();
1097             tui.log_msg('? not standing on portal')
1098         }
1099     }
1100 }
1101
1102 game.init();
1103 tui.init();
1104 tui.full_refresh();
1105 server.init(websocket_location);
1106
1107 let explorer = {
1108     position: [0,0],
1109     info_db: {},
1110     info_hints: [],
1111     move: function(direction) {
1112         let target = game.move(this.position, direction);
1113         if (target) {
1114             this.position = target
1115             if (tui.mode.shows_info) {
1116                 this.query_info();
1117             } else if (tui.tile_draw) {
1118                 this.send_tile_control_command();
1119             }
1120         } else {
1121             terminal.blink_screen();
1122         };
1123     },
1124     update_info_db: function(yx, str) {
1125         this.info_db[yx] = str;
1126         if (tui.mode.name == 'study') {
1127             tui.full_refresh();
1128         }
1129     },
1130     empty_info_db: function() {
1131         this.info_db = {};
1132         this.info_hints = [];
1133         if (tui.mode.name == 'study') {
1134             tui.full_refresh();
1135         }
1136     },
1137     query_info: function() {
1138         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1139     },
1140     get_info: function() {
1141         let info = "MAP VIEW: " + tui.map_mode + "\n";
1142         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1143         if (game.fov[position_i] != '.') {
1144             return info + 'outside field of view';
1145         };
1146         let terrain_char = game.map[position_i]
1147         let terrain_desc = '?'
1148         if (game.terrains[terrain_char]) {
1149             terrain_desc = game.terrains[terrain_char];
1150         };
1151         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1152         let protection = game.map_control[position_i];
1153         if (protection == '.') {
1154             protection = 'unprotected';
1155         };
1156         info += 'PROTECTION: ' + protection + '\n';
1157         for (let t_id in game.things) {
1158              let t = game.things[t_id];
1159              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1160                  let symbol = game.thing_types[t.type_];
1161                  info += "THING: " + t.type_ + " / " + symbol;
1162                  if (t.player_char) {
1163                      info += t.player_char;
1164                  };
1165                  if (t.name_) {
1166                      info += " (" + t.name_ + ")";
1167                  }
1168                  info += "\n";
1169              }
1170         }
1171         if (this.position in game.portals) {
1172             info += "PORTAL: " + game.portals[this.position] + "\n";
1173         }
1174         if (this.position in this.info_db) {
1175             info += "ANNOTATIONS: " + this.info_db[this.position];
1176         } else {
1177             info += 'waiting …';
1178         }
1179         return info;
1180     },
1181     annotate: function(msg) {
1182         if (msg.length == 0) {
1183             msg = " ";  // triggers annotation deletion
1184         }
1185         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1186     },
1187     set_portal: function(msg) {
1188         if (msg.length == 0) {
1189             msg = " ";  // triggers portal deletion
1190         }
1191         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1192     },
1193     send_tile_control_command: function() {
1194         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1195     }
1196 }
1197
1198 tui.inputEl.addEventListener('input', (event) => {
1199     if (tui.mode.has_input_prompt) {
1200         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1201         if (tui.inputEl.value.length > max_length) {
1202             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1203         };
1204         tui.recalc_input_lines();
1205     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1206         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1207         tui.switch_mode('edit');
1208     }
1209     tui.full_refresh();
1210 }, false);
1211 document.onclick = function() {
1212     tui.show_help = false;
1213 };
1214 tui.inputEl.addEventListener('keydown', (event) => {
1215     tui.show_help = false;
1216     if (event.key == 'Enter') {
1217         event.preventDefault();
1218     }
1219     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1220         tui.show_help = true;
1221         tui.empty_input();
1222         tui.restore_input_values();
1223     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1224                && !tui.mode.is_single_char_entry) {
1225         tui.show_help = true;
1226     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1227         tui.login_name = tui.inputEl.value;
1228         server.send(['LOGIN', tui.inputEl.value]);
1229         tui.empty_input();
1230     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1231         if (tui.inputEl.value.length == 0) {
1232             tui.log_msg('@ aborted');
1233         } else {
1234             server.send(['SET_MAP_CONTROL_PASSWORD',
1235                         tui.tile_control_char, tui.inputEl.value]);
1236         }
1237         tui.switch_mode('admin');
1238     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1239         explorer.set_portal(tui.inputEl.value);
1240         tui.switch_mode('edit');
1241     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1242         explorer.annotate(tui.inputEl.value);
1243         tui.switch_mode('edit');
1244     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1245         if (tui.inputEl.value.length == 0) {
1246             tui.inputEl.value = " ";
1247         }
1248         tui.password = tui.inputEl.value
1249         tui.switch_mode('edit');
1250     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1251         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1252         tui.switch_mode('play');
1253     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1254         if (tui.inputEl.value.length != 1) {
1255             tui.log_msg('@ entered non-single-char, therefore aborted');
1256             tui.switch_mode('admin');
1257         } else {
1258             tui.tile_control_char = tui.inputEl.value[0];
1259             tui.switch_mode('control_pw_pw');
1260         }
1261     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1262         if (tui.inputEl.value.length != 1) {
1263             tui.log_msg('@ entered non-single-char, therefore aborted');
1264             tui.switch_mode('admin');
1265         } else {
1266             tui.tile_control_char = tui.inputEl.value[0];
1267             tui.switch_mode('control_tile_draw');
1268         }
1269     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1270         let tokens = parser.tokenize(tui.inputEl.value);
1271         if (tokens.length > 0 && tokens[0].length > 0) {
1272             if (tui.inputEl.value[0][0] == '/') {
1273                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1274                     tui.switch_mode('play');
1275                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1276                     tui.switch_mode('study');
1277                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1278                     tui.switch_mode('edit');
1279                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1280                     tui.switch_mode('admin_enter');
1281                 } else if (tokens[0].slice(1) == 'nick') {
1282                     if (tokens.length > 1) {
1283                         server.send(['NICK', tokens[1]]);
1284                     } else {
1285                         tui.log_msg('? need new name');
1286                     }
1287                 } else {
1288                     tui.log_msg('? unknown command');
1289                 }
1290             } else {
1291                     server.send(['ALL', tui.inputEl.value]);
1292             }
1293         } else if (tui.inputEl.valuelength > 0) {
1294                 server.send(['ALL', tui.inputEl.value]);
1295         }
1296         tui.empty_input();
1297     } else if (tui.mode.name == 'play') {
1298           if (tui.mode.mode_switch_on_key(event)) {
1299               null;
1300           } else if (event.key === tui.keys.take_thing
1301                      && game.tasks.includes('PICK_UP')) {
1302               server.send(["TASK:PICK_UP"]);
1303           } else if (event.key === tui.keys.drop_thing
1304                      && game.tasks.includes('DROP')) {
1305               server.send(["TASK:DROP"]);
1306           } else if (event.key in tui.movement_keys
1307                      && game.tasks.includes('MOVE')) {
1308               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1309           } else if (event.key === tui.keys.teleport) {
1310               game.teleport();
1311           };
1312     } else if (tui.mode.name == 'study') {
1313         if (tui.mode.mode_switch_on_key(event)) {
1314               null;
1315         } else if (event.key in tui.movement_keys) {
1316             explorer.move(tui.movement_keys[event.key]);
1317         } else if (event.key == tui.keys.toggle_map_mode) {
1318             tui.toggle_map_mode();
1319         };
1320     } else if (tui.mode.name == 'control_tile_draw') {
1321         if (tui.mode.mode_switch_on_key(event)) {
1322               null;
1323         } else if (event.key in tui.movement_keys) {
1324             explorer.move(tui.movement_keys[event.key]);
1325         } else if (event.key === tui.keys.toggle_tile_draw) {
1326             tui.toggle_tile_draw();
1327         };
1328     } else if (tui.mode.name == 'admin') {
1329         if (tui.mode.mode_switch_on_key(event)) {
1330               null;
1331         };
1332     } else if (tui.mode.name == 'edit') {
1333         if (tui.mode.mode_switch_on_key(event)) {
1334               null;
1335         } else if (event.key in tui.movement_keys
1336                    && game.tasks.includes('MOVE')) {
1337             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1338         } else if (event.key === tui.keys.flatten
1339                    && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1340             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1341         }
1342     }
1343     tui.full_refresh();
1344 }, false);
1345
1346 rows_selector.addEventListener('input', function() {
1347     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1348         return;
1349     }
1350     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1351     terminal.initialize();
1352     tui.full_refresh();
1353 }, false);
1354 cols_selector.addEventListener('input', function() {
1355     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1356         return;
1357     }
1358     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1359     terminal.initialize();
1360     tui.window_width = terminal.cols / 2,
1361     tui.full_refresh();
1362 }, false);
1363 for (let key_selector of key_selectors) {
1364     key_selector.addEventListener('input', function() {
1365         window.localStorage.setItem(key_selector.id, key_selector.value);
1366         tui.init_keys();
1367     }, false);
1368 }
1369 window.setInterval(function() {
1370     if (server.connected) {
1371         server.send(['PING']);
1372     } else {
1373         server.reconnect_to(server.url);
1374         tui.log_msg('@ attempting reconnect …')
1375     }
1376 }, 5000);
1377 document.getElementById("terminal").onclick = function() {
1378     tui.inputEl.focus();
1379 };
1380 document.getElementById("help").onclick = function() {
1381     tui.show_help = true;
1382     tui.full_refresh();
1383 };
1384 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1385     const mode = switchEl.id.slice("switch_to_".length);
1386     switchEl.onclick = function() {
1387         tui.switch_mode(mode);
1388         tui.full_refresh();
1389     }
1390 };
1391 document.getElementById("toggle_tile_draw").onclick = function() {
1392     tui.toggle_tile_draw();
1393 }
1394 document.getElementById("toggle_map_mode").onclick = function() {
1395     tui.toggle_map_mode();
1396     tui.full_refresh();
1397 };
1398 document.getElementById("take_thing").onclick = function() {
1399         server.send(['TASK:PICK_UP']);
1400 };
1401 document.getElementById("drop_thing").onclick = function() {
1402         server.send(['TASK:DROP']);
1403 };
1404 document.getElementById("flatten").onclick = function() {
1405     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1406 };
1407 document.getElementById("teleport").onclick = function() {
1408     game.teleport();
1409 };
1410 document.getElementById("move_upleft").onclick = function() {
1411     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1412         server.send(['TASK:MOVE', 'UPLEFT']);
1413     } else {
1414         explorer.move('UPLEFT');
1415     };
1416 };
1417 document.getElementById("move_left").onclick = function() {
1418     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1419         server.send(['TASK:MOVE', 'LEFT']);
1420     } else {
1421         explorer.move('LEFT');
1422     };
1423 };
1424 document.getElementById("move_downleft").onclick = function() {
1425     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1426         server.send(['TASK:MOVE', 'DOWNLEFT']);
1427     } else {
1428         explorer.move('DOWNLEFT');
1429     };
1430 };
1431 document.getElementById("move_down").onclick = function() {
1432     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1433         server.send(['TASK:MOVE', 'DOWN']);
1434     } else {
1435         explorer.move('DOWN');
1436     };
1437 };
1438 document.getElementById("move_up").onclick = function() {
1439     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1440         server.send(['TASK:MOVE', 'UP']);
1441     } else {
1442         explorer.move('UP');
1443     };
1444 };
1445 document.getElementById("move_upright").onclick = function() {
1446     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1447         server.send(['TASK:MOVE', 'UPRIGHT']);
1448     } else {
1449         explorer.move('UPRIGHT');
1450     };
1451 };
1452 document.getElementById("move_right").onclick = function() {
1453     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1454         server.send(['TASK:MOVE', 'RIGHT']);
1455     } else {
1456         explorer.move('RIGHT');
1457     };
1458 };
1459 document.getElementById("move_downright").onclick = function() {
1460     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1461         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1462     } else {
1463         explorer.move('DOWNRIGHT');
1464     };
1465 };
1466 </script>
1467 </body></html>