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