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