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