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