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