home · contact · privacy
Make doors installable/uninstallable.
[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         } else if (tokens[0] === 'THING_NAME') {
478             let t = game.get_thing(tokens[1], false);
479             if (t) {
480                 t.name_ = tokens[2];
481             };
482         } else if (tokens[0] === 'THING_CHAR') {
483             let t = game.get_thing(tokens[1], false);
484             if (t) {
485                 t.thing_char = tokens[2];
486             };
487         } else if (tokens[0] === 'TASKS') {
488             game.tasks = tokens[1].split(',');
489             tui.mode_write.legal = game.tasks.includes('WRITE');
490             tui.mode_command_thing.legal = game.tasks.includes('WRITE');
491             tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
492         } else if (tokens[0] === 'THING_TYPE') {
493             game.thing_types[tokens[1]] = tokens[2]
494         } else if (tokens[0] === 'THING_CARRYING') {
495             let t = game.get_thing(tokens[1], false);
496             if (t) {
497                 t.carrying = true;
498             };
499         } else if (tokens[0] === 'TERRAIN') {
500             game.terrains[tokens[1]] = tokens[2]
501         } else if (tokens[0] === 'MAP') {
502             game.map_geometry = tokens[1];
503             tui.init_keys();
504             game.map_size = parser.parse_yx(tokens[2]);
505             game.map = tokens[3]
506         } else if (tokens[0] === 'FOV') {
507             game.fov = tokens[1]
508         } else if (tokens[0] === 'MAP_CONTROL') {
509             game.map_control = tokens[1]
510         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
511             game.turn_complete = true;
512             if (tui.mode.name == 'post_login_wait') {
513                 tui.switch_mode('play');
514             }
515             explorer.info_cached = false;
516             tui.full_refresh();
517         } else if (tokens[0] === 'CHAT') {
518              tui.log_msg('# ' + tokens[1], 1);
519         } else if (tokens[0] === 'REPLY') {
520              tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
521         } else if (tokens[0] === 'PLAYER_ID') {
522             game.player_id = parseInt(tokens[1]);
523         } else if (tokens[0] === 'LOGIN_OK') {
524             this.send(['GET_GAMESTATE']);
525             tui.switch_mode('post_login_wait');
526         } else if (tokens[0] === 'DEFAULT_COLORS') {
527             terminal.set_default_colors();
528         } else if (tokens[0] === 'RANDOM_COLORS') {
529             terminal.set_random_colors();
530         } else if (tokens[0] === 'ADMIN_OK') {
531             tui.is_admin = true;
532             tui.log_msg('@ you now have admin rights');
533             tui.switch_mode('admin');
534         } else if (tokens[0] === 'PORTAL') {
535             let position = parser.parse_yx(tokens[1]);
536             game.portals[position] = tokens[2];
537         } else if (tokens[0] === 'ANNOTATION') {
538             let position = parser.parse_yx(tokens[1]);
539             explorer.update_annotations(position, tokens[2]);
540             tui.full_refresh();
541         } else if (tokens[0] === 'UNHANDLED_INPUT') {
542             tui.log_msg('? unknown command');
543         } else if (tokens[0] === 'PLAY_ERROR') {
544             tui.log_msg('? ' + tokens[1]);
545             terminal.blink_screen();
546         } else if (tokens[0] === 'ARGUMENT_ERROR') {
547             tui.log_msg('? syntax error: ' + tokens[1]);
548         } else if (tokens[0] === 'GAME_ERROR') {
549             tui.log_msg('? game error: ' + tokens[1]);
550         } else if (tokens[0] === 'PONG') {
551             ;
552         } else {
553             tui.log_msg('? unhandled input: ' + event.data);
554         }
555     }
556 }
557
558 let unparser = {
559     quote: function(str) {
560         let quoted = ['"'];
561         for (let i = 0; i < str.length; i++) {
562             let c = str[i];
563             if (['"', '\\'].includes(c)) {
564                 quoted.push('\\');
565             };
566             quoted.push(c);
567         }
568         quoted.push('"');
569         return quoted.join('');
570     },
571     to_yx: function(yx_coordinate) {
572         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
573     },
574     untokenize: function(tokens) {
575         let quoted_tokens = [];
576         for (let token of tokens) {
577             quoted_tokens.push(this.quote(token));
578         }
579         return quoted_tokens.join(" ");
580     }
581 }
582
583 class Mode {
584     constructor(name, has_input_prompt=false, shows_info=false,
585                 is_intro=false, is_single_char_entry=false) {
586         this.name = name;
587         this.short_desc = mode_helps[name].short;
588         this.available_modes = [];
589         this.available_actions = [];
590         this.has_input_prompt = has_input_prompt;
591         this.shows_info= shows_info;
592         this.is_intro = is_intro;
593         this.help_intro = mode_helps[name].long;
594         this.intro_msg = mode_helps[name].intro;
595         this.is_single_char_entry = is_single_char_entry;
596         this.legal = true;
597     }
598     *iter_available_modes() {
599         for (let mode_name of this.available_modes) {
600             let mode = tui['mode_' + mode_name];
601             if (!mode.legal) {
602                 continue;
603             }
604             let key = tui.keys['switch_to_' + mode.name];
605             yield [mode, key]
606         }
607     }
608     list_available_modes() {
609         let msg = ''
610         if (this.available_modes.length > 0) {
611             msg += 'Other modes available from here:\n';
612             for (let [mode, key] of this.iter_available_modes()) {
613                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
614             }
615         }
616         return msg;
617     }
618     mode_switch_on_key(key_event) {
619         for (let [mode, key] of this.iter_available_modes()) {
620             if (key_event.key == key) {
621                 event.preventDefault();
622                 tui.switch_mode(mode.name);
623                 return true;
624             };
625         }
626         return false;
627     }
628 }
629 let tui = {
630   links: {},
631   log: [],
632   input_prompt: '> ',
633   input_lines: [],
634   window_width: terminal.cols / 2,
635   height_turn_line: 1,
636   height_mode_line: 1,
637   height_input: 1,
638   password: 'foo',
639   show_help: false,
640   is_admin: false,
641   tile_draw: false,
642   mode_waiting_for_server: new Mode('waiting_for_server',
643                                      false, false, true),
644   mode_login: new Mode('login', true, false, true),
645   mode_post_login_wait: new Mode('post_login_wait'),
646   mode_chat: new Mode('chat', true),
647   mode_annotate: new Mode('annotate', true, true),
648   mode_play: new Mode('play'),
649   mode_study: new Mode('study', false, true),
650   mode_write: new Mode('write', false, false, false, true),
651   mode_edit: new Mode('edit'),
652   mode_control_pw_type: new Mode('control_pw_type', true),
653   mode_admin_thing_protect: new Mode('admin_thing_protect', true),
654   mode_portal: new Mode('portal', true, true),
655   mode_password: new Mode('password', true),
656   mode_name_thing: new Mode('name_thing', true, true),
657   mode_command_thing: new Mode('command_thing', true),
658   mode_take_thing: new Mode('take_thing', true),
659   mode_admin_enter: new Mode('admin_enter', true),
660   mode_admin: new Mode('admin'),
661   mode_control_pw_pw: new Mode('control_pw_pw', true),
662   mode_control_tile_type: new Mode('control_tile_type', true),
663   mode_control_tile_draw: new Mode('control_tile_draw'),
664   action_tasks: {
665       'flatten': 'FLATTEN_SURROUNDINGS',
666       'take_thing': 'PICK_UP',
667       'drop_thing': 'DROP',
668       'move': 'MOVE',
669       'door': 'DOOR',
670       'install': 'INSTALL',
671       'command': 'COMMAND',
672       'consume': 'INTOXICATE',
673   },
674   offset: [0,0],
675   map_lines: [],
676   selectables: [],
677   init: function() {
678       this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
679                                         "command_thing", "take_thing"]
680       this.mode_play.available_actions = ["move", "drop_thing",
681                                           "teleport", "door", "consume", "install"];
682       this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
683       this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
684       this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
685                                          "control_tile_type", "chat",
686                                          "study", "play", "edit"]
687       this.mode_admin.available_actions = ["move"];
688       this.mode_control_tile_draw.available_modes = ["admin_enter"]
689       this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
690       this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
691                                         "password", "chat", "study", "play",
692                                         "admin_enter"]
693       this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
694       this.inputEl = document.getElementById("input");
695       this.inputEl.focus();
696       this.switch_mode('waiting_for_server');
697       this.recalc_input_lines();
698       this.height_header = this.height_turn_line + this.height_mode_line;
699       this.init_keys();
700   },
701   init_keys: function() {
702     document.getElementById("move_table").hidden = true;
703     this.keys = {};
704     for (let key_selector of key_selectors) {
705         this.keys[key_selector.id.slice(4)] = key_selector.value;
706     }
707     this.movement_keys = {};
708     let geometry_prefix = 'undefinedMapGeometry_';
709     if (game.map_geometry) {
710         geometry_prefix = game.map_geometry.toLowerCase() + '_';
711     }
712     for (const key_name of Object.keys(key_descriptions)) {
713         if (key_name.startsWith(geometry_prefix)) {
714             let direction = key_name.split('_')[2].toUpperCase();
715             let key = this.keys[key_name];
716             this.movement_keys[key] = direction;
717         }
718     };
719     for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
720         if (move_button.id.startsWith('key_')) {
721             continue;
722         }
723         move_button.hidden = true;
724     };
725     for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
726         document.getElementById("move_table").hidden = false;
727         move_button.hidden = false;
728     };
729     for (let el of document.getElementsByTagName("button")) {
730       let action_desc = key_descriptions[el.id];
731       let action_key = '[' + this.keys[el.id] + ']';
732       el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
733     }
734   },
735   task_action_on: function(action) {
736       return game.tasks.includes(this.action_tasks[action]);
737   },
738   switch_mode: function(mode_name) {
739     if (this.mode && this.mode.name == 'control_tile_draw') {
740         tui.log_msg('@ finished tile protection drawing.')
741     }
742     this.tile_draw = false;
743     if (mode_name == 'admin_enter' && this.is_admin) {
744         mode_name = 'admin';
745     } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
746         let player_position = game.things[game.player_id].position;
747         let thing_id = null;
748         for (let t_id in game.things) {
749             if (t_id == game.player_id) {
750                 continue;
751             }
752             let t = game.things[t_id];
753             if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
754                 thing_id = t_id;
755                 break;
756             }
757         }
758         if (!thing_id) {
759             terminal.blink_screen();
760             this.log_msg('? not standing over thing');
761             return;
762         } else {
763             this.selected_thing_id = thing_id;
764         }
765     };
766     this.mode = this['mode_' + mode_name];
767     if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
768         this.map_mode = 'protections';
769     } else if (this.mode.name != "edit") {
770         this.map_mode = 'terrain + things';
771     };
772     if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
773         this.inputEl.focus();
774     }
775     if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
776         explorer.position = game.things[game.player_id].position;
777     }
778     this.inputEl.value = "";
779     this.restore_input_values();
780     for (let el of document.getElementsByTagName("button")) {
781         el.disabled = true;
782     }
783     document.getElementById("help").disabled = false;
784     for (const action of this.mode.available_actions) {
785         if (["move", "move_explorer"].includes(action)) {
786             for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
787                 move_key.disabled = false;
788             }
789         } else if (Object.keys(this.action_tasks).includes(action)) {
790             if (this.task_action_on(action)) {
791                 document.getElementById(action).disabled = false;
792             }
793         } else {
794             document.getElementById(action).disabled = false;
795         };
796     }
797     for (const mode_name of this.mode.available_modes) {
798             document.getElementById('switch_to_' + mode_name).disabled = false;
799     }
800     if (this.mode.intro_msg.length > 0) {
801         this.log_msg(this.mode.intro_msg);
802     }
803     if (this.mode.name == 'login') {
804         if (this.login_name) {
805             server.send(['LOGIN', this.login_name]);
806         } else {
807             this.log_msg("? need login name");
808         }
809     } else if (this.mode.is_single_char_entry) {
810         this.show_help = true;
811     } else if (this.mode.name == 'take_thing') {
812         this.log_msg("Things in reach for pick-up:");
813         const player = game.things[game.player_id];
814         const y = player.position[0]
815         const x = player.position[1]
816         let select_range = [y.toString() + ':' + x.toString(),
817                             (y + 0).toString() + ':' + (x - 1).toString(),
818                             (y + 0).toString() + ':' + (x + 1).toString(),
819                             (y - 1).toString() + ':' + (x).toString(),
820                             (y + 1).toString() + ':' + (x).toString()];
821         if (game.map_geometry == 'Hex') {
822             if (y % 2) {
823                 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
824                 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
825             } else {
826                 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
827                 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
828             }
829         };
830         this.selectables = [];
831         for (const t_id in game.things) {
832             const t = game.things[t_id];
833             if (select_range.includes(t.position[0].toString()
834                                       + ':' + t.position[1].toString())
835                 && t != player && t.type_ != 'Player') {
836                 this.selectables.push([t_id, t]);
837             }
838         };
839         if (this.selectables.length == 0) {
840             this.log_msg('none')
841         } else {
842             for (let [i, t] of this.selectables.entries()) {
843                 this.log_msg(i + ': ' + explorer.get_thing_info(t[1]));
844             }
845         }
846     } else if (this.mode.name == 'command_thing') {
847         server.send(['TASK:COMMAND', 'HELP']);
848     } else if (this.mode.name == 'control_pw_pw') {
849         this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
850     } else if (this.mode.name == 'control_tile_draw') {
851         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 + '].')
852     }
853     this.full_refresh();
854   },
855   offset_links: function(offset, links) {
856       for (let y in links) {
857           let real_y = offset[0] + parseInt(y);
858           if (!this.links[real_y]) {
859               this.links[real_y] = [];
860           }
861           for (let link of links[y]) {
862               const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
863               this.links[real_y].push(offset_link);
864           }
865       }
866   },
867   restore_input_values: function() {
868       if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
869           let info = explorer.annotations[explorer.position];
870           if (info != "(none)") {
871               this.inputEl.value = info;
872           }
873       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
874           let portal = game.portals[explorer.position]
875           this.inputEl.value = portal;
876       } else if (this.mode.name == 'password') {
877           this.inputEl.value = this.password;
878       } else if (this.mode.name == 'name_thing') {
879           let t = game.get_thing(this.selected_thing_id);
880           if (t && t.name_) {
881               this.inputEl.value = t.name_;
882           }
883       } else if (this.mode.name == 'admin_thing_protect') {
884           let t = game.get_thing(this.selected_thing_id);
885           if (t && t.protection) {
886               this.inputEl.value = t.protection;
887           }
888       }
889   },
890   recalc_input_lines: function() {
891       if (this.mode.has_input_prompt) {
892           let _ = null;
893           [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
894       } else {
895           this.input_lines = [];
896       }
897       this.height_input = this.input_lines.length;
898   },
899   msg_into_lines_of_width: function(msg, width) {
900       function push_inner_link(y, end_x) {
901           if (!inner_links[y]) {
902               inner_links[y] = [];
903           };
904           inner_links[y].push([url_start_x, end_x, url]);
905       };
906       const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
907       let link_data = {};
908       let url_ends = [];
909       for (const match of matches) {
910           const url = match[0];
911           const url_start = match.index;
912           const url_end = match.index + match[0].length;
913           link_data[url_start] = url;
914           url_ends.push(url_end);
915       }
916       let url_start_x = 0;
917       let url = '';
918       let inner_links = {};
919       let in_link = false;
920       let chunk = "";
921       let lines = [];
922       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
923           if (x >= width || msg[i] == "\n") {
924               if (in_link) {
925                   push_inner_link(y, chunk.length);
926                   url_start_x = 0;
927                   if (url_ends[0] == i) {
928                       in_link = false;
929                       url_ends.shift();
930                   }
931               };
932               lines.push(chunk);
933               chunk = "";
934               x = 0;
935               if (msg[i] == "\n") {
936                   x -= 1;
937               };
938               y += 1;
939           };
940           if (msg[i] != "\n") {
941               chunk += msg[i];
942           };
943           if (i in link_data) {
944               url_start_x = x;
945               url = link_data[i];
946               in_link = true;
947           } else if (url_ends[0] == i) {
948               url_ends.shift();
949               push_inner_link(y, x);
950               in_link = false;
951           }
952       }
953       lines.push(chunk);
954       if (in_link) {
955           push_inner_link(lines.length - 1, chunk.length);
956       }
957       return [lines, inner_links];
958   },
959   log_msg: function(msg) {
960       this.log.push(msg);
961       while (this.log.length > 100) {
962         this.log.shift();
963       };
964       this.full_refresh();
965   },
966   draw_map: function() {
967     if (!game.turn_complete && this.map_lines.length == 0) {
968         return;
969     }
970     if (game.turn_complete) {
971         let map_lines_split = [];
972         let line = [];
973         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
974             if (j == game.map_size[1]) {
975                 map_lines_split.push(line);
976                 line = [];
977                 j = 0;
978             };
979             if (this.map_mode == 'protections') {
980                 line.push(game.map_control[i] + ' ');
981             } else {
982                 line.push(game.map[i] + ' ');
983             }
984         };
985         map_lines_split.push(line);
986         if (this.map_mode == 'terrain + annotations') {
987             for (const coordinate of explorer.info_hints) {
988                 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
989             }
990         } else if (this.map_mode == 'terrain + things') {
991             for (const p in game.portals) {
992                 let coordinate = p.split(',')
993                 let original = map_lines_split[coordinate[0]][coordinate[1]];
994                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
995             }
996             let used_positions = [];
997             function draw_thing(t, used_positions) {
998                 let symbol = game.thing_types[t.type_];
999                 let meta_char = ' ';
1000                 if (t.thing_char) {
1001                     meta_char = t.thing_char;
1002                 }
1003                 if (used_positions.includes(t.position.toString())) {
1004                     meta_char = '+';
1005                 };
1006                 if (t.carrying) {
1007                     meta_char = '$';
1008                 }
1009                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1010                 used_positions.push(t.position.toString());
1011             }
1012             for (const thing_id in game.things) {
1013                 let t = game.things[thing_id];
1014                 if (t.type_ != 'Player') {
1015                     draw_thing(t, used_positions);
1016                 }
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         }
1025         let player = game.things[game.player_id];
1026         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1027             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1028         } else if (tui.map_mode != 'terrain + things') {
1029             map_lines_split[player.position[0]][player.position[1]] = '??';
1030         }
1031         this.map_lines = []
1032         if (game.map_geometry == 'Square') {
1033             for (let line_split of map_lines_split) {
1034                 this.map_lines.push(line_split.join(''));
1035             };
1036         } else if (game.map_geometry == 'Hex') {
1037             let indent = 0
1038             for (let line_split of map_lines_split) {
1039                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1040                 if (indent == 0) {
1041                     indent = 1;
1042                 } else {
1043                     indent = 0;
1044                 };
1045             };
1046         }
1047         let window_center = [terminal.rows / 2, this.window_width / 2];
1048         let center_position = [player.position[0], player.position[1]];
1049         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1050             center_position = [explorer.position[0], explorer.position[1]];
1051         }
1052         center_position[1] = center_position[1] * 2;
1053         this.offset = [center_position[0] - window_center[0],
1054                        center_position[1] - window_center[1]]
1055         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1056             this.offset[1] += 1;
1057         };
1058     };
1059     let term_y = Math.max(0, -this.offset[0]);
1060     let term_x = Math.max(0, -this.offset[1]);
1061     let map_y = Math.max(0, this.offset[0]);
1062     let map_x = Math.max(0, this.offset[1]);
1063     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
1064         let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1065         terminal.write(term_y, term_x, to_draw);
1066     }
1067   },
1068   draw_mode_line: function() {
1069       let help = 'hit [' + this.keys.help + '] for help';
1070       if (this.mode.has_input_prompt) {
1071           help = 'enter /help for help';
1072       }
1073       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1074   },
1075   draw_turn_line: function(n) {
1076       if (game.turn_complete) {
1077           terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1078       }
1079   },
1080   draw_history: function() {
1081       let log_display_lines = [];
1082       let log_links = {};
1083       let y_offset_in_log = 0;
1084       for (let line of this.log) {
1085           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1086                                                                     this.window_width)
1087           log_display_lines = log_display_lines.concat(new_lines);
1088           for (const y in link_data) {
1089               const rel_y = y_offset_in_log + parseInt(y);
1090               log_links[rel_y] = [];
1091               for (let link of link_data[y]) {
1092                   log_links[rel_y].push(link);
1093               }
1094           }
1095           y_offset_in_log += new_lines.length;
1096       };
1097       let i = log_display_lines.length - 1;
1098       for (let y = terminal.rows - 1 - this.height_input;
1099            y >= this.height_header && i >= 0;
1100            y--, i--) {
1101           terminal.write(y, this.window_width, log_display_lines[i]);
1102       }
1103       for (const key of Object.keys(log_links)) {
1104           if (parseInt(key) <= i) {
1105               delete log_links[key];
1106           }
1107       }
1108       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1109                     this.window_width];
1110       this.offset_links(offset, log_links);
1111   },
1112   draw_info: function() {
1113       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1114       let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1115       let offset = [this.height_header, this.window_width];
1116       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1117         terminal.write(y, offset[1], lines[i]);
1118       }
1119       this.offset_links(offset, link_data);
1120   },
1121   draw_input: function() {
1122     if (this.mode.has_input_prompt) {
1123         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1124             terminal.write(y, this.window_width, this.input_lines[i]);
1125         }
1126     }
1127   },
1128   draw_help: function() {
1129       let movement_keys_desc = '';
1130       if (!this.mode.is_intro) {
1131           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1132       }
1133       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1134       if (this.mode.available_actions.length > 0) {
1135           content += "Available actions:\n";
1136           for (let action of this.mode.available_actions) {
1137               if (Object.keys(this.action_tasks).includes(action)) {
1138                   if (!this.task_action_on(action)) {
1139                       continue;
1140                   }
1141               }
1142               if (action == 'move_explorer') {
1143                   action = 'move';
1144               }
1145               if (action == 'move') {
1146                   content += "[" + movement_keys_desc + "] – move\n"
1147               } else {
1148                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1149               }
1150           }
1151           content += '\n';
1152       }
1153       content += this.mode.list_available_modes();
1154       let start_x = 0;
1155       if (!this.mode.has_input_prompt) {
1156           start_x = this.window_width
1157       }
1158       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1159       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1160       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1161           terminal.write(y, start_x, lines[i]);
1162       }
1163   },
1164   toggle_tile_draw: function() {
1165       if (tui.tile_draw) {
1166           tui.tile_draw = false;
1167       } else {
1168           tui.tile_draw = true;
1169       }
1170   },
1171   toggle_map_mode: function() {
1172       if (tui.map_mode == 'terrain only') {
1173           tui.map_mode = 'terrain + annotations';
1174       } else if (tui.map_mode == 'terrain + annotations') {
1175           tui.map_mode = 'terrain + things';
1176       } else if (tui.map_mode == 'terrain + things') {
1177           tui.map_mode = 'protections';
1178       } else if (tui.map_mode == 'protections') {
1179           tui.map_mode = 'terrain only';
1180       }
1181   },
1182   full_refresh: function() {
1183     this.links = {};
1184     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1185     this.recalc_input_lines();
1186     if (this.mode.is_intro) {
1187         this.draw_history();
1188         this.draw_input();
1189     } else {
1190         this.draw_map();
1191         this.draw_turn_line();
1192         this.draw_mode_line();
1193         if (this.mode.shows_info) {
1194           this.draw_info();
1195         } else {
1196           this.draw_history();
1197         }
1198         this.draw_input();
1199     }
1200     if (this.show_help) {
1201         this.draw_help();
1202     }
1203     terminal.refresh();
1204   }
1205 }
1206
1207 let game = {
1208     init: function() {
1209         this.things = {};
1210         this.turn = -1;
1211         this.map = "";
1212         this.map_control = "";
1213         this.map_size = [0,0];
1214         this.player_id = -1;
1215         this.portals = {};
1216         this.tasks = {};
1217     },
1218     get_thing: function(id_, create_if_not_found=false) {
1219         if (id_ in game.things) {
1220             return game.things[id_];
1221         } else if (create_if_not_found) {
1222             let t = new Thing([0,0]);
1223             game.things[id_] = t;
1224             return t;
1225         };
1226     },
1227     move: function(start_position, direction) {
1228         let target = [start_position[0], start_position[1]];
1229         if (direction == 'LEFT') {
1230             target[1] -= 1;
1231         } else if (direction == 'RIGHT') {
1232             target[1] += 1;
1233         } else if (game.map_geometry == 'Square') {
1234             if (direction == 'UP') {
1235                 target[0] -= 1;
1236             } else if (direction == 'DOWN') {
1237                 target[0] += 1;
1238             };
1239         } else if (game.map_geometry == 'Hex') {
1240             let start_indented = start_position[0] % 2;
1241             if (direction == 'UPLEFT') {
1242                 target[0] -= 1;
1243                 if (!start_indented) {
1244                     target[1] -= 1;
1245                 }
1246             } else if (direction == 'UPRIGHT') {
1247                 target[0] -= 1;
1248                 if (start_indented) {
1249                     target[1] += 1;
1250                 }
1251             } else if (direction == 'DOWNLEFT') {
1252                 target[0] += 1;
1253                 if (!start_indented) {
1254                     target[1] -= 1;
1255                 }
1256             } else if (direction == 'DOWNRIGHT') {
1257                 target[0] += 1;
1258                 if (start_indented) {
1259                     target[1] += 1;
1260                 }
1261             };
1262         };
1263         if (target[0] < 0 || target[1] < 0 ||
1264             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1265             return null;
1266         };
1267         return target;
1268     },
1269     teleport: function() {
1270         let player = this.get_thing(game.player_id);
1271         if (player.position in this.portals) {
1272             server.reconnect_to(this.portals[player.position]);
1273         } else {
1274             terminal.blink_screen();
1275             tui.log_msg('? not standing on portal')
1276         }
1277     }
1278 }
1279
1280 game.init();
1281 tui.init();
1282 tui.full_refresh();
1283 server.init(websocket_location);
1284
1285 let explorer = {
1286     position: [0,0],
1287     annotations: {},
1288     info_cached: false,
1289     move: function(direction) {
1290         let target = game.move(this.position, direction);
1291         if (target) {
1292             this.position = target
1293             this.info_cached = false;
1294             if (tui.tile_draw) {
1295                 this.send_tile_control_command();
1296             }
1297         } else {
1298             terminal.blink_screen();
1299         };
1300     },
1301     update_annotations: function(yx, str) {
1302         this.annotations[yx] = str;
1303         if (tui.mode.name == 'study') {
1304             tui.full_refresh();
1305         }
1306     },
1307     empty_annotations: function() {
1308         this.annotations = {};
1309         if (tui.mode.name == 'study') {
1310             tui.full_refresh();
1311         }
1312     },
1313     get_info: function() {
1314         if (this.info_cached) {
1315             return this.info_cached;
1316         }
1317         let info_to_cache = '';
1318         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1319         if (game.fov[position_i] != '.') {
1320             info_to_cache += 'outside field of view';
1321         } else {
1322             let terrain_char = game.map[position_i]
1323             let terrain_desc = '?'
1324             if (game.terrains[terrain_char]) {
1325                 terrain_desc = game.terrains[terrain_char];
1326             };
1327             info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1328             let protection = game.map_control[position_i];
1329             if (protection == '.') {
1330                 protection = 'unprotected';
1331             };
1332             info_to_cache += 'PROTECTION: ' + protection + '\n';
1333             for (let t_id in game.things) {
1334                  let t = game.things[t_id];
1335                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1336                      info_to_cache += "THING: " + this.get_thing_info(t);
1337                      let protection = t.protection;
1338                      if (protection == '.') {
1339                          protection = 'none';
1340                      }
1341                      info_to_cache += " / protection: " + protection + "\n";
1342                  }
1343             }
1344             if (this.position in game.portals) {
1345                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1346             }
1347             if (this.position in this.annotations) {
1348                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1349             }
1350         }
1351         this.info_cached = info_to_cache;
1352         return this.info_cached;
1353     },
1354     get_thing_info: function(t) {
1355         const symbol = game.thing_types[t.type_];
1356         let info = t.type_ + " / " + symbol;
1357          if (t.thing_char) {
1358              info += t.thing_char;
1359          };
1360          if (t.name_) {
1361              info += " (" + t.name_ + ")";
1362          }
1363         return info;
1364     },
1365     annotate: function(msg) {
1366         if (msg.length == 0) {
1367             msg = " ";  // triggers annotation deletion
1368         }
1369         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1370     },
1371     set_portal: function(msg) {
1372         if (msg.length == 0) {
1373             msg = " ";  // triggers portal deletion
1374         }
1375         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1376     },
1377     send_tile_control_command: function() {
1378         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1379     }
1380 }
1381
1382 tui.inputEl.addEventListener('input', (event) => {
1383     if (tui.mode.has_input_prompt) {
1384         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1385         if (tui.inputEl.value.length > max_length) {
1386             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1387         };
1388     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1389         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1390         tui.switch_mode('edit');
1391     }
1392     tui.full_refresh();
1393 }, false);
1394 document.onclick = function() {
1395     tui.show_help = false;
1396 };
1397 tui.inputEl.addEventListener('keydown', (event) => {
1398     tui.show_help = false;
1399     if (event.key == 'Enter') {
1400         event.preventDefault();
1401     }
1402     if (tui.mode.has_input_prompt && event.key == 'Enter'
1403         && tui.inputEl.value.length == 0
1404         && ['chat', 'command_thing', 'take_thing',
1405             'admin_enter'].includes(tui.mode.name)) {
1406         if (tui.mode.name != 'chat') {
1407             tui.log_msg('@ aborted');
1408         }
1409         tui.switch_mode('play');
1410     } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1411         tui.show_help = true;
1412         tui.inputEl.value = "";
1413         tui.restore_input_values();
1414     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1415                && !tui.mode.is_single_char_entry) {
1416         tui.show_help = true;
1417     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1418         tui.login_name = tui.inputEl.value;
1419         server.send(['LOGIN', tui.inputEl.value]);
1420         tui.inputEl.value = "";
1421     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1422         if (tui.task_action_on('command')) {
1423             server.send(['TASK:COMMAND', tui.inputEl.value]);
1424             tui.inputEl.value = "";
1425         }
1426     } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1427         const i = parseInt(tui.inputEl.value);
1428         if (isNaN(i) || i < 0 || i >= tui.selectables.length) {
1429             tui.log_msg('? invalid index, aborted');
1430         } else {
1431             server.send(['TASK:PICK_UP', tui.selectables[i][0]]);
1432         }
1433         tui.inputEl.value = "";
1434         tui.switch_mode('play');
1435     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1436         if (tui.inputEl.value.length == 0) {
1437             tui.log_msg('@ aborted');
1438         } else {
1439             server.send(['SET_MAP_CONTROL_PASSWORD',
1440                         tui.tile_control_char, tui.inputEl.value]);
1441             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1442         }
1443         tui.switch_mode('admin');
1444     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1445         explorer.set_portal(tui.inputEl.value);
1446         tui.switch_mode('edit');
1447     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1448         if (tui.inputEl.value.length == 0) {
1449             tui.inputEl.value = " ";
1450         }
1451         server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1452                      tui.password]);
1453         tui.switch_mode('edit');
1454     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1455         explorer.annotate(tui.inputEl.value);
1456         tui.switch_mode('edit');
1457     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1458         if (tui.inputEl.value.length == 0) {
1459             tui.inputEl.value = " ";
1460         }
1461         tui.password = tui.inputEl.value
1462         tui.switch_mode('edit');
1463     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1464         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1465         tui.switch_mode('play');
1466     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1467         if (tui.inputEl.value.length != 1) {
1468             tui.log_msg('@ entered non-single-char, therefore aborted');
1469             tui.switch_mode('admin');
1470         } else {
1471             tui.tile_control_char = tui.inputEl.value[0];
1472             tui.switch_mode('control_pw_pw');
1473         }
1474     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1475         if (tui.inputEl.value.length != 1) {
1476             tui.log_msg('@ entered non-single-char, therefore aborted');
1477             tui.switch_mode('admin');
1478         } else {
1479             tui.tile_control_char = tui.inputEl.value[0];
1480             tui.switch_mode('control_tile_draw');
1481         }
1482     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1483         if (tui.inputEl.value.length != 1) {
1484             tui.log_msg('@ entered non-single-char, therefore aborted');
1485         } else {
1486             server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1487             tui.log_msg('@ sent new protection character for thing');
1488         }
1489         tui.switch_mode('admin');
1490     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1491         let tokens = parser.tokenize(tui.inputEl.value);
1492         if (tokens.length > 0 && tokens[0].length > 0) {
1493             if (tui.inputEl.value[0][0] == '/') {
1494                 if (tokens[0].slice(1) == 'nick') {
1495                     if (tokens.length > 1) {
1496                         server.send(['NICK', tokens[1]]);
1497                     } else {
1498                         tui.log_msg('? need new name');
1499                     }
1500                 } else {
1501                     tui.log_msg('? unknown command');
1502                 }
1503             } else {
1504                     server.send(['ALL', tui.inputEl.value]);
1505             }
1506         } else if (tui.inputEl.valuelength > 0) {
1507                 server.send(['ALL', tui.inputEl.value]);
1508         }
1509         tui.inputEl.value = "";
1510     } else if (tui.mode.name == 'play') {
1511           if (tui.mode.mode_switch_on_key(event)) {
1512               null;
1513           } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1514               server.send(["TASK:DROP"]);
1515           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1516               server.send(["TASK:INTOXICATE"]);
1517           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1518               server.send(["TASK:DOOR"]);
1519           } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1520               server.send(["TASK:INSTALL"]);
1521           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1522               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1523           } else if (event.key === tui.keys.teleport) {
1524               game.teleport();
1525           };
1526     } else if (tui.mode.name == 'study') {
1527         if (tui.mode.mode_switch_on_key(event)) {
1528               null;
1529         } else if (event.key in tui.movement_keys) {
1530             explorer.move(tui.movement_keys[event.key]);
1531         } else if (event.key == tui.keys.toggle_map_mode) {
1532             tui.toggle_map_mode();
1533         };
1534     } else if (tui.mode.name == 'control_tile_draw') {
1535         if (tui.mode.mode_switch_on_key(event)) {
1536             null;
1537         } else if (event.key in tui.movement_keys) {
1538             explorer.move(tui.movement_keys[event.key]);
1539         } else if (event.key === tui.keys.toggle_tile_draw) {
1540             tui.toggle_tile_draw();
1541         };
1542     } else if (tui.mode.name == 'admin') {
1543         if (tui.mode.mode_switch_on_key(event)) {
1544               null;
1545         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1546             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1547         };
1548     } else if (tui.mode.name == 'edit') {
1549         if (tui.mode.mode_switch_on_key(event)) {
1550               null;
1551         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1552             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1553         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1554             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1555         } else if (event.key == tui.keys.toggle_map_mode) {
1556             tui.toggle_map_mode();
1557         }
1558     }
1559     tui.full_refresh();
1560 }, false);
1561
1562 rows_selector.addEventListener('input', function() {
1563     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1564         return;
1565     }
1566     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1567     terminal.initialize();
1568     tui.full_refresh();
1569 }, false);
1570 cols_selector.addEventListener('input', function() {
1571     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1572         return;
1573     }
1574     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1575     terminal.initialize();
1576     tui.window_width = terminal.cols / 2,
1577     tui.full_refresh();
1578 }, false);
1579 for (let key_selector of key_selectors) {
1580     key_selector.addEventListener('input', function() {
1581         window.localStorage.setItem(key_selector.id, key_selector.value);
1582         tui.init_keys();
1583     }, false);
1584 }
1585 window.setInterval(function() {
1586     if (server.connected) {
1587         server.send(['PING']);
1588     } else {
1589         server.reconnect_to(server.url);
1590         tui.log_msg('@ attempting reconnect …')
1591     }
1592 }, 5000);
1593 window.setInterval(function() {
1594     let val = "?";
1595     let span_decoration = "none";
1596     if (document.activeElement == tui.inputEl) {
1597         val = "on (click outside terminal to change)";
1598     } else {
1599         val = "off (click into terminal to change)";
1600         span_decoration = "line-through";
1601     };
1602     document.getElementById("keyboard_control").textContent = val;
1603     for (const span of document.querySelectorAll('.keyboard_controlled')) {
1604         span.style.textDecoration = span_decoration;
1605     }
1606 }, 100);
1607 document.getElementById("terminal").onclick = function() {
1608     tui.inputEl.focus();
1609 };
1610 document.getElementById("help").onclick = function() {
1611     tui.show_help = true;
1612     tui.full_refresh();
1613 };
1614 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1615     const mode = switchEl.id.slice("switch_to_".length);
1616     switchEl.onclick = function() {
1617         tui.switch_mode(mode);
1618         tui.full_refresh();
1619     }
1620 };
1621 document.getElementById("toggle_tile_draw").onclick = function() {
1622     tui.toggle_tile_draw();
1623 }
1624 document.getElementById("toggle_map_mode").onclick = function() {
1625     tui.toggle_map_mode();
1626     tui.full_refresh();
1627 };
1628 document.getElementById("drop_thing").onclick = function() {
1629     server.send(['TASK:DROP']);
1630 };
1631 document.getElementById("flatten").onclick = function() {
1632     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1633 };
1634 document.getElementById("door").onclick = function() {
1635     server.send(['TASK:DOOR']);
1636 };
1637 document.getElementById("consume").onclick = function() {
1638     server.send(['TASK:INTOXICATE']);
1639 };
1640 document.getElementById("install").onclick = function() {
1641     server.send(['TASK:INSTALL']);
1642 };
1643 document.getElementById("teleport").onclick = function() {
1644     game.teleport();
1645 };
1646 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1647     let direction = move_button.id.split('_')[2].toUpperCase();
1648     move_button.onclick = function() {
1649         if (tui.mode.available_actions.includes("move")
1650             || tui.mode.available_actions.includes("move_explorer")) {
1651             server.send(['TASK:MOVE', direction]);
1652         } else {
1653             explorer.move(direction);
1654         };
1655     };
1656 };
1657 </script>
1658 </body></html>