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