home · contact · privacy
Replace annotation polling with annotation push, cache info display.
[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) {
903         let map_lines_split = [];
904         let line = [];
905         for (let i = 0, j = 0; i < game.map.length; i++, j++) {
906             if (j == game.map_size[1]) {
907                 map_lines_split.push(line);
908                 line = [];
909                 j = 0;
910             };
911             if (this.map_mode == 'protections') {
912                 line.push(game.map_control[i] + ' ');
913             } else {
914                 line.push(game.map[i] + ' ');
915             }
916         };
917         map_lines_split.push(line);
918         if (this.map_mode == 'terrain + annotations') {
919             for (const coordinate of explorer.info_hints) {
920                 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
921             }
922         } else if (this.map_mode == 'terrain + things') {
923             for (const p in game.portals) {
924                 let coordinate = p.split(',')
925                 let original = map_lines_split[coordinate[0]][coordinate[1]];
926                 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
927             }
928             let used_positions = [];
929             for (const thing_id in game.things) {
930                 let t = game.things[thing_id];
931                 let symbol = game.thing_types[t.type_];
932                 let meta_char = ' ';
933                 if (t.thing_char) {
934                     meta_char = t.thing_char;
935                 }
936                 if (used_positions.includes(t.position.toString())) {
937                     meta_char = '+';
938                 };
939                 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
940                 used_positions.push(t.position.toString());
941             };
942         }
943         let player = game.things[game.player_id];
944         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
945             map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
946         } else if (tui.map_mode != 'terrain + things') {
947             map_lines_split[player.position[0]][player.position[1]] = '??';
948         }
949         this.map_lines = []
950         if (game.map_geometry == 'Square') {
951             for (let line_split of map_lines_split) {
952                 this.map_lines.push(line_split.join(''));
953             };
954         } else if (game.map_geometry == 'Hex') {
955             let indent = 0
956             for (let line_split of map_lines_split) {
957                 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
958                 if (indent == 0) {
959                     indent = 1;
960                 } else {
961                     indent = 0;
962                 };
963             };
964         }
965         let window_center = [terminal.rows / 2, this.window_width / 2];
966         let center_position = [player.position[0], player.position[1]];
967         if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
968             center_position = [explorer.position[0], explorer.position[1]];
969         }
970         center_position[1] = center_position[1] * 2;
971         this.offset = [center_position[0] - window_center[0],
972                        center_position[1] - window_center[1]]
973         if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
974             this.offset[1] += 1;
975         };
976     };
977     let term_y = Math.max(0, -this.offset[0]);
978     let term_x = Math.max(0, -this.offset[1]);
979     let map_y = Math.max(0, this.offset[0]);
980     let map_x = Math.max(0, this.offset[1]);
981     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
982         let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
983         terminal.write(term_y, term_x, to_draw);
984     }
985   },
986   draw_mode_line: function() {
987       let help = 'hit [' + this.keys.help + '] for help';
988       if (this.mode.has_input_prompt) {
989           help = 'enter /help for help';
990       }
991       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
992   },
993   draw_turn_line: function(n) {
994       if (game.turn_complete) {
995           terminal.write(1, this.window_width, 'TURN: ' + game.turn);
996       }
997   },
998   draw_history: function() {
999       let log_display_lines = [];
1000       let log_links = {};
1001       let y_offset_in_log = 0;
1002       for (let line of this.log) {
1003           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1004                                                                     this.window_width)
1005           log_display_lines = log_display_lines.concat(new_lines);
1006           for (const y in link_data) {
1007               const rel_y = y_offset_in_log + parseInt(y);
1008               log_links[rel_y] = [];
1009               for (let link of link_data[y]) {
1010                   log_links[rel_y].push(link);
1011               }
1012           }
1013           y_offset_in_log += new_lines.length;
1014       };
1015       let i = log_display_lines.length - 1;
1016       for (let y = terminal.rows - 1 - this.height_input;
1017            y >= this.height_header && i >= 0;
1018            y--, i--) {
1019           terminal.write(y, this.window_width, log_display_lines[i]);
1020       }
1021       for (const key of Object.keys(log_links)) {
1022           if (parseInt(key) <= i) {
1023               delete log_links[key];
1024           }
1025       }
1026       let offset = [terminal.rows - this.height_input - log_display_lines.length,
1027                     this.window_width];
1028       this.offset_links(offset, log_links);
1029   },
1030   draw_info: function() {
1031       const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1032       let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1033       let offset = [this.height_header, this.window_width];
1034       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1035         terminal.write(y, offset[1], lines[i]);
1036       }
1037       this.offset_links(offset, link_data);
1038   },
1039   draw_input: function() {
1040     if (this.mode.has_input_prompt) {
1041         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1042             terminal.write(y, this.window_width, this.input_lines[i]);
1043         }
1044     }
1045   },
1046   draw_help: function() {
1047       let movement_keys_desc = '';
1048       if (!this.mode.is_intro) {
1049           movement_keys_desc = Object.keys(this.movement_keys).join(',');
1050       }
1051       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1052       if (this.mode.name == 'chat') {
1053           content += '/nick NAME – re-name yourself to NAME\n';
1054           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1055           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1056           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1057           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1058       } else if (this.mode.available_actions.length > 0) {
1059           content += "Available actions:\n";
1060           for (let action of this.mode.available_actions) {
1061               if (Object.keys(this.action_tasks).includes(action)) {
1062                   if (!this.task_action_on(action)) {
1063                       continue;
1064                   }
1065               }
1066               if (action == 'move_explorer') {
1067                   action = 'move';
1068               }
1069               if (action == 'move') {
1070                   content += "[" + movement_keys_desc + "] – move\n"
1071               } else {
1072                   content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1073               }
1074           }
1075           content += '\n';
1076       }
1077       content += this.mode.list_available_modes();
1078       let start_x = 0;
1079       if (!this.mode.has_input_prompt) {
1080           start_x = this.window_width
1081       }
1082       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1083       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1084       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1085           terminal.write(y, start_x, lines[i]);
1086       }
1087   },
1088   toggle_tile_draw: function() {
1089       if (tui.tile_draw) {
1090           tui.tile_draw = false;
1091       } else {
1092           tui.tile_draw = true;
1093       }
1094   },
1095   toggle_map_mode: function() {
1096       if (tui.map_mode == 'terrain only') {
1097           tui.map_mode = 'terrain + annotations';
1098       } else if (tui.map_mode == 'terrain + annotations') {
1099           tui.map_mode = 'terrain + things';
1100       } else if (tui.map_mode == 'terrain + things') {
1101           tui.map_mode = 'protections';
1102       } else if (tui.map_mode == 'protections') {
1103           tui.map_mode = 'terrain only';
1104       }
1105   },
1106   full_refresh: function() {
1107     this.links = {};
1108     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1109     this.recalc_input_lines();
1110     if (this.mode.is_intro) {
1111         this.draw_history();
1112         this.draw_input();
1113     } else {
1114         this.draw_map();
1115         this.draw_turn_line();
1116         this.draw_mode_line();
1117         if (this.mode.shows_info) {
1118           this.draw_info();
1119         } else {
1120           this.draw_history();
1121         }
1122         this.draw_input();
1123     }
1124     if (this.show_help) {
1125         this.draw_help();
1126     }
1127     terminal.refresh();
1128   }
1129 }
1130
1131 let game = {
1132     init: function() {
1133         this.things = {};
1134         this.turn = -1;
1135         this.map = "";
1136         this.map_control = "";
1137         this.map_size = [0,0];
1138         this.player_id = -1;
1139         this.portals = {};
1140         this.tasks = {};
1141     },
1142     get_thing: function(id_, create_if_not_found=false) {
1143         if (id_ in game.things) {
1144             return game.things[id_];
1145         } else if (create_if_not_found) {
1146             let t = new Thing([0,0]);
1147             game.things[id_] = t;
1148             return t;
1149         };
1150     },
1151     move: function(start_position, direction) {
1152         let target = [start_position[0], start_position[1]];
1153         if (direction == 'LEFT') {
1154             target[1] -= 1;
1155         } else if (direction == 'RIGHT') {
1156             target[1] += 1;
1157         } else if (game.map_geometry == 'Square') {
1158             if (direction == 'UP') {
1159                 target[0] -= 1;
1160             } else if (direction == 'DOWN') {
1161                 target[0] += 1;
1162             };
1163         } else if (game.map_geometry == 'Hex') {
1164             let start_indented = start_position[0] % 2;
1165             if (direction == 'UPLEFT') {
1166                 target[0] -= 1;
1167                 if (!start_indented) {
1168                     target[1] -= 1;
1169                 }
1170             } else if (direction == 'UPRIGHT') {
1171                 target[0] -= 1;
1172                 if (start_indented) {
1173                     target[1] += 1;
1174                 }
1175             } else if (direction == 'DOWNLEFT') {
1176                 target[0] += 1;
1177                 if (!start_indented) {
1178                     target[1] -= 1;
1179                 }
1180             } else if (direction == 'DOWNRIGHT') {
1181                 target[0] += 1;
1182                 if (start_indented) {
1183                     target[1] += 1;
1184                 }
1185             };
1186         };
1187         if (target[0] < 0 || target[1] < 0 ||
1188             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1189             return null;
1190         };
1191         return target;
1192     },
1193     teleport: function() {
1194         let player = this.get_thing(game.player_id);
1195         if (player.position in this.portals) {
1196             server.reconnect_to(this.portals[player.position]);
1197         } else {
1198             terminal.blink_screen();
1199             tui.log_msg('? not standing on portal')
1200         }
1201     }
1202 }
1203
1204 game.init();
1205 tui.init();
1206 tui.full_refresh();
1207 server.init(websocket_location);
1208
1209 let explorer = {
1210     position: [0,0],
1211     annotations: {},
1212     info_cached: false,
1213     move: function(direction) {
1214         let target = game.move(this.position, direction);
1215         if (target) {
1216             this.position = target
1217             this.info_cached = false;
1218             if (tui.tile_draw) {
1219                 this.send_tile_control_command();
1220             }
1221         } else {
1222             terminal.blink_screen();
1223         };
1224     },
1225     update_annotations: function(yx, str) {
1226         this.annotations[yx] = str;
1227         if (tui.mode.name == 'study') {
1228             tui.full_refresh();
1229         }
1230     },
1231     empty_annotations: function() {
1232         this.annotations = {};
1233         if (tui.mode.name == 'study') {
1234             tui.full_refresh();
1235         }
1236     },
1237     get_info: function() {
1238         if (this.info_cached) {
1239             return this.info_cached;
1240         }
1241         let info_to_cache = '';
1242         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1243         if (game.fov[position_i] != '.') {
1244             info_to_cache += 'outside field of view';
1245         } else {
1246             let terrain_char = game.map[position_i]
1247             let terrain_desc = '?'
1248             if (game.terrains[terrain_char]) {
1249                 terrain_desc = game.terrains[terrain_char];
1250             };
1251             info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1252             let protection = game.map_control[position_i];
1253             if (protection == '.') {
1254                 protection = 'unprotected';
1255             };
1256             info_to_cache += 'PROTECTION: ' + protection + '\n';
1257             for (let t_id in game.things) {
1258                  let t = game.things[t_id];
1259                  if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1260                      let symbol = game.thing_types[t.type_];
1261                      let protection = t.protection;
1262                      if (protection == '.') {
1263                          protection = 'none';
1264                      }
1265                      info_to_cache += "THING: " + t.type_ + " / " + symbol;
1266                      if (t.thing_char) {
1267                          info_to_cache += t.thing_char;
1268                      };
1269                      if (t.name_) {
1270                          info_to_cache += " (" + t.name_ + ")";
1271                      }
1272                      info_to_cache += " / protection: " + protection + "\n";
1273                  }
1274             }
1275             if (this.position in game.portals) {
1276                 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1277             }
1278             if (this.position in this.annotations) {
1279                 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1280             }
1281         }
1282         this.info_cached = info_to_cache;
1283         return this.info_cached;
1284     },
1285     annotate: function(msg) {
1286         if (msg.length == 0) {
1287             msg = " ";  // triggers annotation deletion
1288         }
1289         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1290     },
1291     set_portal: function(msg) {
1292         if (msg.length == 0) {
1293             msg = " ";  // triggers portal deletion
1294         }
1295         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1296     },
1297     send_tile_control_command: function() {
1298         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1299     }
1300 }
1301
1302 tui.inputEl.addEventListener('input', (event) => {
1303     if (tui.mode.has_input_prompt) {
1304         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1305         if (tui.inputEl.value.length > max_length) {
1306             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1307         };
1308     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1309         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1310         tui.switch_mode('edit');
1311     }
1312     tui.full_refresh();
1313 }, false);
1314 document.onclick = function() {
1315     tui.show_help = false;
1316 };
1317 tui.inputEl.addEventListener('keydown', (event) => {
1318     tui.show_help = false;
1319     if (event.key == 'Enter') {
1320         event.preventDefault();
1321     }
1322     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1323         tui.show_help = true;
1324         tui.inputEl.value = "";
1325         tui.restore_input_values();
1326     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1327                && !tui.mode.is_single_char_entry) {
1328         tui.show_help = true;
1329     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1330         tui.login_name = tui.inputEl.value;
1331         server.send(['LOGIN', tui.inputEl.value]);
1332         tui.inputEl.value = "";
1333     } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1334         if (tui.inputEl.value.length == 0) {
1335             tui.log_msg('@ aborted');
1336             tui.switch_mode('play');
1337         } else if (tui.task_action_on('command')) {
1338             server.send(['TASK:COMMAND', tui.inputEl.value]);
1339             tui.inputEl.value = "";
1340         }
1341     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1342         if (tui.inputEl.value.length == 0) {
1343             tui.log_msg('@ aborted');
1344         } else {
1345             server.send(['SET_MAP_CONTROL_PASSWORD',
1346                         tui.tile_control_char, tui.inputEl.value]);
1347             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1348         }
1349         tui.switch_mode('admin');
1350     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1351         explorer.set_portal(tui.inputEl.value);
1352         tui.switch_mode('edit');
1353     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1354         if (tui.inputEl.value.length == 0) {
1355             tui.inputEl.value = " ";
1356         }
1357         server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1358                      tui.password]);
1359         tui.switch_mode('edit');
1360     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1361         explorer.annotate(tui.inputEl.value);
1362         tui.switch_mode('edit');
1363     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1364         if (tui.inputEl.value.length == 0) {
1365             tui.inputEl.value = " ";
1366         }
1367         tui.password = tui.inputEl.value
1368         tui.switch_mode('edit');
1369     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1370         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1371         tui.switch_mode('play');
1372     } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1373         if (tui.inputEl.value.length != 1) {
1374             tui.log_msg('@ entered non-single-char, therefore aborted');
1375             tui.switch_mode('admin');
1376         } else {
1377             tui.tile_control_char = tui.inputEl.value[0];
1378             tui.switch_mode('control_pw_pw');
1379         }
1380     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1381         if (tui.inputEl.value.length != 1) {
1382             tui.log_msg('@ entered non-single-char, therefore aborted');
1383             tui.switch_mode('admin');
1384         } else {
1385             tui.tile_control_char = tui.inputEl.value[0];
1386             tui.switch_mode('control_tile_draw');
1387         }
1388     } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1389         if (tui.inputEl.value.length != 1) {
1390             tui.log_msg('@ entered non-single-char, therefore aborted');
1391         } else {
1392             server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1393             tui.log_msg('@ sent new protection character for thing');
1394         }
1395         tui.switch_mode('admin');
1396     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1397         let tokens = parser.tokenize(tui.inputEl.value);
1398         if (tokens.length > 0 && tokens[0].length > 0) {
1399             if (tui.inputEl.value[0][0] == '/') {
1400                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1401                     tui.switch_mode('play');
1402                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1403                     tui.switch_mode('study');
1404                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1405                     tui.switch_mode('edit');
1406                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1407                     tui.switch_mode('admin_enter');
1408                 } else if (tokens[0].slice(1) == 'nick') {
1409                     if (tokens.length > 1) {
1410                         server.send(['NICK', tokens[1]]);
1411                     } else {
1412                         tui.log_msg('? need new name');
1413                     }
1414                 } else {
1415                     tui.log_msg('? unknown command');
1416                 }
1417             } else {
1418                     server.send(['ALL', tui.inputEl.value]);
1419             }
1420         } else if (tui.inputEl.valuelength > 0) {
1421                 server.send(['ALL', tui.inputEl.value]);
1422         }
1423         tui.inputEl.value = "";
1424     } else if (tui.mode.name == 'play') {
1425           if (tui.mode.mode_switch_on_key(event)) {
1426               null;
1427           } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1428               server.send(["TASK:PICK_UP"]);
1429           } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1430               server.send(["TASK:DROP"]);
1431           } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1432               server.send(["TASK:INTOXICATE"]);
1433           } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1434               server.send(["TASK:DOOR"]);
1435           } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1436               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1437           } else if (event.key === tui.keys.teleport) {
1438               game.teleport();
1439           };
1440     } else if (tui.mode.name == 'study') {
1441         if (tui.mode.mode_switch_on_key(event)) {
1442               null;
1443         } else if (event.key in tui.movement_keys) {
1444             explorer.move(tui.movement_keys[event.key]);
1445         } else if (event.key == tui.keys.toggle_map_mode) {
1446             tui.toggle_map_mode();
1447         };
1448     } else if (tui.mode.name == 'control_tile_draw') {
1449         if (tui.mode.mode_switch_on_key(event)) {
1450             null;
1451         } else if (event.key in tui.movement_keys) {
1452             explorer.move(tui.movement_keys[event.key]);
1453         } else if (event.key === tui.keys.toggle_tile_draw) {
1454             tui.toggle_tile_draw();
1455         };
1456     } else if (tui.mode.name == 'admin') {
1457         if (tui.mode.mode_switch_on_key(event)) {
1458               null;
1459         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1460             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1461         };
1462     } else if (tui.mode.name == 'edit') {
1463         if (tui.mode.mode_switch_on_key(event)) {
1464               null;
1465         } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1466             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1467         } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1468             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1469         } else if (event.key == tui.keys.toggle_map_mode) {
1470             tui.toggle_map_mode();
1471         }
1472     }
1473     tui.full_refresh();
1474 }, false);
1475
1476 rows_selector.addEventListener('input', function() {
1477     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1478         return;
1479     }
1480     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1481     terminal.initialize();
1482     tui.full_refresh();
1483 }, false);
1484 cols_selector.addEventListener('input', function() {
1485     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1486         return;
1487     }
1488     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1489     terminal.initialize();
1490     tui.window_width = terminal.cols / 2,
1491     tui.full_refresh();
1492 }, false);
1493 for (let key_selector of key_selectors) {
1494     key_selector.addEventListener('input', function() {
1495         window.localStorage.setItem(key_selector.id, key_selector.value);
1496         tui.init_keys();
1497     }, false);
1498 }
1499 window.setInterval(function() {
1500     if (server.connected) {
1501         server.send(['PING']);
1502     } else {
1503         server.reconnect_to(server.url);
1504         tui.log_msg('@ attempting reconnect …')
1505     }
1506 }, 5000);
1507 window.setInterval(function() {
1508     let val = "?";
1509     let span_decoration = "none";
1510     if (document.activeElement == tui.inputEl) {
1511         val = "on (click outside terminal to change)";
1512     } else {
1513         val = "off (click into terminal to change)";
1514         span_decoration = "line-through";
1515     };
1516     document.getElementById("keyboard_control").textContent = val;
1517     for (const span of document.querySelectorAll('.keyboard_controlled')) {
1518         span.style.textDecoration = span_decoration;
1519     }
1520 }, 100);
1521 document.getElementById("terminal").onclick = function() {
1522     tui.inputEl.focus();
1523 };
1524 document.getElementById("help").onclick = function() {
1525     tui.show_help = true;
1526     tui.full_refresh();
1527 };
1528 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1529     const mode = switchEl.id.slice("switch_to_".length);
1530     switchEl.onclick = function() {
1531         tui.switch_mode(mode);
1532         tui.full_refresh();
1533     }
1534 };
1535 document.getElementById("toggle_tile_draw").onclick = function() {
1536     tui.toggle_tile_draw();
1537 }
1538 document.getElementById("toggle_map_mode").onclick = function() {
1539     tui.toggle_map_mode();
1540     tui.full_refresh();
1541 };
1542 document.getElementById("take_thing").onclick = function() {
1543         server.send(['TASK:PICK_UP']);
1544 };
1545 document.getElementById("drop_thing").onclick = function() {
1546         server.send(['TASK:DROP']);
1547 };
1548 document.getElementById("flatten").onclick = function() {
1549     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1550 };
1551 document.getElementById("door").onclick = function() {
1552     server.send(['TASK:DOOR']);
1553 };
1554 document.getElementById("consume").onclick = function() {
1555     server.send(['TASK:INTOXICATE']);
1556 };
1557 document.getElementById("teleport").onclick = function() {
1558     game.teleport();
1559 };
1560 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1561     let direction = move_button.id.split('_')[2].toUpperCase();
1562     move_button.onclick = function() {
1563         if (tui.mode.available_actions.includes("move")
1564             || tui.mode.available_actions.includes("move_explorer")) {
1565             server.send(['TASK:MOVE', direction]);
1566         } else {
1567             explorer.move(direction);
1568         };
1569     };
1570 };
1571 </script>
1572 </body></html>