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