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