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