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