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