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