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