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