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