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