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