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