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