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