home · contact · privacy
Add thing name editing.
[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.empty_input();
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               this.recalc_input_lines();
752           }
753       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
754           let portal = game.portals[explorer.position]
755           this.inputEl.value = portal;
756           this.recalc_input_lines();
757       } else if (this.mode.name == 'password') {
758           this.inputEl.value = this.password;
759           this.recalc_input_lines();
760       } else if (this.mode.name == 'name_thing') {
761           let t = game.get_thing(this.selected_thing_id);
762           if (t && t.name_) {
763               this.inputEl.value = t.name_;
764               this.recalc_input_lines();
765           }
766       }
767   },
768   empty_input: function(str) {
769       this.inputEl.value = "";
770       if (this.mode.has_input_prompt) {
771           this.recalc_input_lines();
772       } else {
773           this.height_input = 0;
774       }
775   },
776   recalc_input_lines: function() {
777       let _ = null;
778       [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
779       this.height_input = this.input_lines.length;
780   },
781   msg_into_lines_of_width: function(msg, width) {
782       function push_inner_link(y, end_x) {
783           if (!inner_links[y]) {
784               inner_links[y] = [];
785           };
786           inner_links[y].push([url_start_x, end_x, url]);
787       };
788       const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
789       let link_data = {};
790       let url_ends = [];
791       for (const match of matches) {
792           const url = match[0];
793           const url_start = match.index;
794           const url_end = match.index + match[0].length;
795           link_data[url_start] = url;
796           url_ends.push(url_end);
797       }
798       let url_start_x = 0;
799       let url = '';
800       let inner_links = {};
801       let in_link = false;
802       let chunk = "";
803       let lines = [];
804       for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
805           if (x >= width || msg[i] == "\n") {
806               if (in_link) {
807                   push_inner_link(y, chunk.length);
808                   url_start_x = 0;
809               };
810               lines.push(chunk);
811               chunk = "";
812               x = 0;
813               if (msg[i] == "\n") {
814                   x -= 1;
815               };
816               y += 1;
817           };
818           if (msg[i] != "\n") {
819               chunk += msg[i];
820           };
821           if (i in link_data) {
822               url_start_x = x;
823               url = link_data[i];
824               in_link = true;
825           } else if (url_ends.includes(i)) {
826               push_inner_link(y, x);
827               in_link = false;
828           }
829       }
830       lines.push(chunk);
831       if (in_link) {
832           push_inner_link(lines.length - 1, chunk.length);
833       }
834       return [lines, inner_links];
835   },
836   log_msg: function(msg) {
837       this.log.push(msg);
838       while (this.log.length > 100) {
839         this.log.shift();
840       };
841       this.full_refresh();
842   },
843   draw_map: function() {
844     let map_lines_split = [];
845     let line = [];
846     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
847         if (j == game.map_size[1]) {
848             map_lines_split.push(line);
849             line = [];
850             j = 0;
851         };
852         if (this.map_mode == 'protections') {
853             line.push(game.map_control[i] + ' ');
854         } else {
855             line.push(game.map[i] + ' ');
856         }
857     };
858     map_lines_split.push(line);
859     if (this.map_mode == 'terrain + annotations') {
860         for (const coordinate of explorer.info_hints) {
861             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
862         }
863     } else if (this.map_mode == 'terrain + things') {
864         for (const p in game.portals) {
865             let coordinate = p.split(',')
866             let original = map_lines_split[coordinate[0]][coordinate[1]];
867             map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
868         }
869         let used_positions = [];
870         for (const thing_id in game.things) {
871             let t = game.things[thing_id];
872             let symbol = game.thing_types[t.type_];
873             let meta_char = ' ';
874             if (t.player_char) {
875                 meta_char = t.player_char;
876             }
877             if (used_positions.includes(t.position.toString())) {
878                 meta_char = '+';
879             };
880             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
881             used_positions.push(t.position.toString());
882         };
883     }
884     let player = game.things[game.player_id];
885     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
886         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
887     } else if (tui.map_mode != 'terrain + things') {
888         map_lines_split[player.position[0]][player.position[1]] = '??';
889     }
890     let map_lines = []
891     if (game.map_geometry == 'Square') {
892         for (let line_split of map_lines_split) {
893             map_lines.push(line_split.join(''));
894         };
895     } else if (game.map_geometry == 'Hex') {
896         let indent = 0
897         for (let line_split of map_lines_split) {
898             map_lines.push(' '.repeat(indent) + line_split.join(''));
899             if (indent == 0) {
900                 indent = 1;
901             } else {
902                 indent = 0;
903             };
904         };
905     }
906     let window_center = [terminal.rows / 2, this.window_width / 2];
907     let center_position = [player.position[0], player.position[1]];
908     if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
909         center_position = [explorer.position[0], explorer.position[1]];
910     }
911     center_position[1] = center_position[1] * 2;
912     let offset = [center_position[0] - window_center[0],
913                   center_position[1] - window_center[1]]
914     if (game.map_geometry == 'Hex' && offset[0] % 2) {
915         offset[1] += 1;
916     };
917     let term_y = Math.max(0, -offset[0]);
918     let term_x = Math.max(0, -offset[1]);
919     let map_y = Math.max(0, offset[0]);
920     let map_x = Math.max(0, offset[1]);
921     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
922         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
923         terminal.write(term_y, term_x, to_draw);
924     }
925   },
926   draw_mode_line: function() {
927       let help = 'hit [' + this.keys.help + '] for help';
928       if (this.mode.has_input_prompt) {
929           help = 'enter /help for help';
930       }
931       terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
932   },
933   draw_turn_line: function(n) {
934     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
935   },
936   draw_history: function() {
937       let log_display_lines = [];
938       let log_links = {};
939       let y_offset_in_log = 0;
940       for (let line of this.log) {
941           let [new_lines, link_data] = this.msg_into_lines_of_width(line,
942                                                                     this.window_width)
943           log_display_lines = log_display_lines.concat(new_lines);
944           for (const y in link_data) {
945               const rel_y = y_offset_in_log + parseInt(y);
946               log_links[rel_y] = [];
947               for (let link of link_data[y]) {
948                   log_links[rel_y].push(link);
949               }
950           }
951           y_offset_in_log += new_lines.length;
952       };
953       let i = log_display_lines.length - 1;
954       for (let y = terminal.rows - 1 - this.height_input;
955            y >= this.height_header && i >= 0;
956            y--, i--) {
957           terminal.write(y, this.window_width, log_display_lines[i]);
958       }
959       for (const key of Object.keys(log_links)) {
960           if (parseInt(key) <= i) {
961               delete log_links[key];
962           }
963       }
964       let offset = [terminal.rows - this.height_input - log_display_lines.length,
965                     this.window_width];
966       this.offset_links(offset, log_links);
967   },
968   draw_info: function() {
969       let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
970                                                             this.window_width);
971       let offset = [this.height_header, this.window_width];
972       for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
973         terminal.write(y, offset[1], lines[i]);
974       }
975       this.offset_links(offset, link_data);
976   },
977   draw_input: function() {
978     if (this.mode.has_input_prompt) {
979         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
980             terminal.write(y, this.window_width, this.input_lines[i]);
981         }
982     }
983   },
984   draw_help: function() {
985       let movement_keys_desc = '';
986       if (!this.mode.is_intro) {
987           movement_keys_desc = Object.keys(this.movement_keys).join(',');
988       }
989       let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
990       if (this.mode.name == 'play') {
991           content += "Available actions:\n";
992           if (game.tasks.includes('MOVE')) {
993               content += "[" + movement_keys_desc + "] – move player\n";
994           }
995           if (game.tasks.includes('PICK_UP')) {
996               content += "[" + this.keys.take_thing + "] – pick up thing\n";
997           }
998           if (game.tasks.includes('DROP')) {
999               content += "[" + this.keys.drop_thing + "] – drop thing\n";
1000           }
1001           content += "[" + tui.keys.teleport + "] – teleport\n";
1002           content += '\n';
1003       } else if (this.mode.name == 'study') {
1004           content += "Available actions:\n";
1005           content += '[' + movement_keys_desc + '] – move question mark\n';
1006           content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1007           content += '\n';
1008       } else if (this.mode.name == 'edit') {
1009           content += "Available actions:\n";
1010           if (game.tasks.includes('MOVE')) {
1011               content += "[" + movement_keys_desc + "] – move player\n";
1012           }
1013           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1014               content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
1015           }
1016           content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1017           content += '\n';
1018       } else if (this.mode.name == 'control_tile_draw') {
1019           content += "Available actions:\n";
1020           content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
1021           content += '\n';
1022       } else if (this.mode.name == 'chat') {
1023           content += '/nick NAME – re-name yourself to NAME\n';
1024           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1025           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1026           content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
1027           content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1028       }
1029       content += this.mode.list_available_modes();
1030       let start_x = 0;
1031       if (!this.mode.has_input_prompt) {
1032           start_x = this.window_width
1033       }
1034       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1035       let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1036       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1037           terminal.write(y, start_x, lines[i]);
1038       }
1039   },
1040   toggle_tile_draw: function() {
1041       if (tui.tile_draw) {
1042           tui.tile_draw = false;
1043       } else {
1044           tui.tile_draw = true;
1045       }
1046   },
1047   toggle_map_mode: function() {
1048       if (tui.map_mode == 'terrain only') {
1049           tui.map_mode = 'terrain + annotations';
1050       } else if (tui.map_mode == 'terrain + annotations') {
1051           tui.map_mode = 'terrain + things';
1052       } else if (tui.map_mode == 'terrain + things') {
1053           tui.map_mode = 'protections';
1054       } else if (tui.map_mode == 'protections') {
1055           tui.map_mode = 'terrain only';
1056       }
1057   },
1058   full_refresh: function() {
1059     this.links = {};
1060     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1061     if (this.mode.is_intro) {
1062         this.draw_history();
1063         this.draw_input();
1064     } else {
1065         if (game.turn_complete) {
1066             this.draw_map();
1067             this.draw_turn_line();
1068         }
1069         this.draw_mode_line();
1070         if (this.mode.shows_info) {
1071           this.draw_info();
1072         } else {
1073           this.draw_history();
1074         }
1075         this.draw_input();
1076     }
1077     if (this.show_help) {
1078         this.draw_help();
1079     }
1080     terminal.refresh();
1081   }
1082 }
1083
1084 let game = {
1085     init: function() {
1086         this.things = {};
1087         this.turn = -1;
1088         this.map = "";
1089         this.map_control = "";
1090         this.map_size = [0,0];
1091         this.player_id = -1;
1092         this.portals = {};
1093         this.tasks = {};
1094     },
1095     get_thing: function(id_, create_if_not_found=false) {
1096         if (id_ in game.things) {
1097             return game.things[id_];
1098         } else if (create_if_not_found) {
1099             let t = new Thing([0,0]);
1100             game.things[id_] = t;
1101             return t;
1102         };
1103     },
1104     move: function(start_position, direction) {
1105         let target = [start_position[0], start_position[1]];
1106         if (direction == 'LEFT') {
1107             target[1] -= 1;
1108         } else if (direction == 'RIGHT') {
1109             target[1] += 1;
1110         } else if (game.map_geometry == 'Square') {
1111             if (direction == 'UP') {
1112                 target[0] -= 1;
1113             } else if (direction == 'DOWN') {
1114                 target[0] += 1;
1115             };
1116         } else if (game.map_geometry == 'Hex') {
1117             let start_indented = start_position[0] % 2;
1118             if (direction == 'UPLEFT') {
1119                 target[0] -= 1;
1120                 if (!start_indented) {
1121                     target[1] -= 1;
1122                 }
1123             } else if (direction == 'UPRIGHT') {
1124                 target[0] -= 1;
1125                 if (start_indented) {
1126                     target[1] += 1;
1127                 }
1128             } else if (direction == 'DOWNLEFT') {
1129                 target[0] += 1;
1130                 if (!start_indented) {
1131                     target[1] -= 1;
1132                 }
1133             } else if (direction == 'DOWNRIGHT') {
1134                 target[0] += 1;
1135                 if (start_indented) {
1136                     target[1] += 1;
1137                 }
1138             };
1139         };
1140         if (target[0] < 0 || target[1] < 0 ||
1141             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1142             return null;
1143         };
1144         return target;
1145     },
1146     teleport: function() {
1147         let player = this.get_thing(game.player_id);
1148         if (player.position in this.portals) {
1149             server.reconnect_to(this.portals[player.position]);
1150         } else {
1151             terminal.blink_screen();
1152             tui.log_msg('? not standing on portal')
1153         }
1154     }
1155 }
1156
1157 game.init();
1158 tui.init();
1159 tui.full_refresh();
1160 server.init(websocket_location);
1161
1162 let explorer = {
1163     position: [0,0],
1164     info_db: {},
1165     info_hints: [],
1166     move: function(direction) {
1167         let target = game.move(this.position, direction);
1168         if (target) {
1169             this.position = target
1170             if (tui.mode.shows_info) {
1171                 this.query_info();
1172             } else if (tui.tile_draw) {
1173                 this.send_tile_control_command();
1174             }
1175         } else {
1176             terminal.blink_screen();
1177         };
1178     },
1179     update_info_db: function(yx, str) {
1180         this.info_db[yx] = str;
1181         if (tui.mode.name == 'study') {
1182             tui.full_refresh();
1183         }
1184     },
1185     empty_info_db: function() {
1186         this.info_db = {};
1187         this.info_hints = [];
1188         if (tui.mode.name == 'study') {
1189             tui.full_refresh();
1190         }
1191     },
1192     query_info: function() {
1193         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1194     },
1195     get_info: function() {
1196         let info = "MAP VIEW: " + tui.map_mode + "\n";
1197         let position_i = this.position[0] * game.map_size[1] + this.position[1];
1198         if (game.fov[position_i] != '.') {
1199             return info + 'outside field of view';
1200         };
1201         let terrain_char = game.map[position_i]
1202         let terrain_desc = '?'
1203         if (game.terrains[terrain_char]) {
1204             terrain_desc = game.terrains[terrain_char];
1205         };
1206         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1207         let protection = game.map_control[position_i];
1208         if (protection == '.') {
1209             protection = 'unprotected';
1210         };
1211         info += 'PROTECTION: ' + protection + '\n';
1212         for (let t_id in game.things) {
1213              let t = game.things[t_id];
1214              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1215                  let symbol = game.thing_types[t.type_];
1216                  info += "THING: " + t.type_ + " / " + symbol;
1217                  if (t.player_char) {
1218                      info += t.player_char;
1219                  };
1220                  if (t.name_) {
1221                      info += " (" + t.name_ + ")";
1222                  }
1223                  info += "\n";
1224              }
1225         }
1226         if (this.position in game.portals) {
1227             info += "PORTAL: " + game.portals[this.position] + "\n";
1228         }
1229         if (this.position in this.info_db) {
1230             info += "ANNOTATIONS: " + this.info_db[this.position];
1231         } else {
1232             info += 'waiting …';
1233         }
1234         return info;
1235     },
1236     annotate: function(msg) {
1237         if (msg.length == 0) {
1238             msg = " ";  // triggers annotation deletion
1239         }
1240         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1241     },
1242     set_portal: function(msg) {
1243         if (msg.length == 0) {
1244             msg = " ";  // triggers portal deletion
1245         }
1246         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1247     },
1248     send_tile_control_command: function() {
1249         server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1250     }
1251 }
1252
1253 tui.inputEl.addEventListener('input', (event) => {
1254     if (tui.mode.has_input_prompt) {
1255         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1256         if (tui.inputEl.value.length > max_length) {
1257             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1258         };
1259         tui.recalc_input_lines();
1260     } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1261         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1262         tui.switch_mode('edit');
1263     }
1264     tui.full_refresh();
1265 }, false);
1266 document.onclick = function() {
1267     tui.show_help = false;
1268 };
1269 tui.inputEl.addEventListener('keydown', (event) => {
1270     tui.show_help = false;
1271     if (event.key == 'Enter') {
1272         event.preventDefault();
1273     }
1274     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1275         tui.show_help = true;
1276         tui.empty_input();
1277         tui.restore_input_values();
1278     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1279                && !tui.mode.is_single_char_entry) {
1280         tui.show_help = true;
1281     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1282         tui.login_name = tui.inputEl.value;
1283         server.send(['LOGIN', tui.inputEl.value]);
1284         tui.empty_input();
1285     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1286         if (tui.inputEl.value.length == 0) {
1287             tui.log_msg('@ aborted');
1288         } else {
1289             server.send(['SET_MAP_CONTROL_PASSWORD',
1290                         tui.tile_control_char, tui.inputEl.value]);
1291             tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1292         }
1293         tui.switch_mode('admin');
1294     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1295         explorer.set_portal(tui.inputEl.value);
1296         tui.switch_mode('edit');
1297     } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1298         if (tui.inputEl.value.length == 0) {
1299             tui.inputEl.value = " ";
1300         }
1301         server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value]);
1302         tui.switch_mode('edit');
1303     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1304         explorer.annotate(tui.inputEl.value);
1305         tui.switch_mode('edit');
1306     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1307         if (tui.inputEl.value.length == 0) {
1308             tui.inputEl.value = " ";
1309         }
1310         tui.password = tui.inputEl.value
1311         tui.switch_mode('edit');
1312     } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1313         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1314         tui.switch_mode('play');
1315     } else if (tui.mode.name == 'control_pw_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_pw_pw');
1322         }
1323     } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1324         if (tui.inputEl.value.length != 1) {
1325             tui.log_msg('@ entered non-single-char, therefore aborted');
1326             tui.switch_mode('admin');
1327         } else {
1328             tui.tile_control_char = tui.inputEl.value[0];
1329             tui.switch_mode('control_tile_draw');
1330         }
1331     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1332         let tokens = parser.tokenize(tui.inputEl.value);
1333         if (tokens.length > 0 && tokens[0].length > 0) {
1334             if (tui.inputEl.value[0][0] == '/') {
1335                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1336                     tui.switch_mode('play');
1337                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1338                     tui.switch_mode('study');
1339                 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1340                     tui.switch_mode('edit');
1341                 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1342                     tui.switch_mode('admin_enter');
1343                 } else if (tokens[0].slice(1) == 'nick') {
1344                     if (tokens.length > 1) {
1345                         server.send(['NICK', tokens[1]]);
1346                     } else {
1347                         tui.log_msg('? need new name');
1348                     }
1349                 } else {
1350                     tui.log_msg('? unknown command');
1351                 }
1352             } else {
1353                     server.send(['ALL', tui.inputEl.value]);
1354             }
1355         } else if (tui.inputEl.valuelength > 0) {
1356                 server.send(['ALL', tui.inputEl.value]);
1357         }
1358         tui.empty_input();
1359     } else if (tui.mode.name == 'play') {
1360           if (tui.mode.mode_switch_on_key(event)) {
1361               null;
1362           } else if (event.key === tui.keys.take_thing
1363                      && game.tasks.includes('PICK_UP')) {
1364               server.send(["TASK:PICK_UP"]);
1365           } else if (event.key === tui.keys.drop_thing
1366                      && game.tasks.includes('DROP')) {
1367               server.send(["TASK:DROP"]);
1368           } else if (event.key in tui.movement_keys
1369                      && game.tasks.includes('MOVE')) {
1370               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1371           } else if (event.key === tui.keys.teleport) {
1372               game.teleport();
1373           };
1374     } else if (tui.mode.name == 'study') {
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_map_mode) {
1380             tui.toggle_map_mode();
1381         };
1382     } else if (tui.mode.name == 'control_tile_draw') {
1383         if (tui.mode.mode_switch_on_key(event)) {
1384             null;
1385         } else if (event.key in tui.movement_keys) {
1386             explorer.move(tui.movement_keys[event.key]);
1387         } else if (event.key === tui.keys.toggle_tile_draw) {
1388             tui.toggle_tile_draw();
1389         };
1390     } else if (tui.mode.name == 'admin') {
1391         if (tui.mode.mode_switch_on_key(event)) {
1392               null;
1393         };
1394     } else if (tui.mode.name == 'edit') {
1395         if (tui.mode.mode_switch_on_key(event)) {
1396               null;
1397         } else if (event.key in tui.movement_keys
1398                    && game.tasks.includes('MOVE')) {
1399             server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1400         } else if (event.key === tui.keys.flatten
1401                    && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1402             server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1403         } else if (event.key == tui.keys.toggle_map_mode) {
1404             tui.toggle_map_mode();
1405         }
1406     }
1407     tui.full_refresh();
1408 }, false);
1409
1410 rows_selector.addEventListener('input', function() {
1411     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1412         return;
1413     }
1414     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1415     terminal.initialize();
1416     tui.full_refresh();
1417 }, false);
1418 cols_selector.addEventListener('input', function() {
1419     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1420         return;
1421     }
1422     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1423     terminal.initialize();
1424     tui.window_width = terminal.cols / 2,
1425     tui.full_refresh();
1426 }, false);
1427 for (let key_selector of key_selectors) {
1428     key_selector.addEventListener('input', function() {
1429         window.localStorage.setItem(key_selector.id, key_selector.value);
1430         tui.init_keys();
1431     }, false);
1432 }
1433 window.setInterval(function() {
1434     if (server.connected) {
1435         server.send(['PING']);
1436     } else {
1437         server.reconnect_to(server.url);
1438         tui.log_msg('@ attempting reconnect …')
1439     }
1440 }, 5000);
1441 window.setInterval(function() {
1442     let val = "?";
1443     if (document.activeElement == tui.inputEl) {
1444         val = "on (click outside terminal to change)";
1445     } else {
1446         val = "off (click into terminal to change)";
1447     };
1448     document.getElementById("keyboard_control").textContent = val;
1449 }, 100);
1450 document.getElementById("terminal").onclick = function() {
1451     tui.inputEl.focus();
1452 };
1453 document.getElementById("help").onclick = function() {
1454     tui.show_help = true;
1455     tui.full_refresh();
1456 };
1457 for (const switchEl  of document.querySelectorAll('[id^="switch_to_"]')) {
1458     const mode = switchEl.id.slice("switch_to_".length);
1459     switchEl.onclick = function() {
1460         tui.switch_mode(mode);
1461         tui.full_refresh();
1462     }
1463 };
1464 document.getElementById("toggle_tile_draw").onclick = function() {
1465     tui.toggle_tile_draw();
1466 }
1467 document.getElementById("toggle_map_mode").onclick = function() {
1468     tui.toggle_map_mode();
1469     tui.full_refresh();
1470 };
1471 document.getElementById("take_thing").onclick = function() {
1472         server.send(['TASK:PICK_UP']);
1473 };
1474 document.getElementById("drop_thing").onclick = function() {
1475         server.send(['TASK:DROP']);
1476 };
1477 document.getElementById("flatten").onclick = function() {
1478     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1479 };
1480 document.getElementById("teleport").onclick = function() {
1481     game.teleport();
1482 };
1483 document.getElementById("move_upleft").onclick = function() {
1484     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1485         server.send(['TASK:MOVE', 'UPLEFT']);
1486     } else {
1487         explorer.move('UPLEFT');
1488     };
1489 };
1490 document.getElementById("move_left").onclick = function() {
1491     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1492         server.send(['TASK:MOVE', 'LEFT']);
1493     } else {
1494         explorer.move('LEFT');
1495     };
1496 };
1497 document.getElementById("move_downleft").onclick = function() {
1498     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1499         server.send(['TASK:MOVE', 'DOWNLEFT']);
1500     } else {
1501         explorer.move('DOWNLEFT');
1502     };
1503 };
1504 document.getElementById("move_down").onclick = function() {
1505     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1506         server.send(['TASK:MOVE', 'DOWN']);
1507     } else {
1508         explorer.move('DOWN');
1509     };
1510 };
1511 document.getElementById("move_up").onclick = function() {
1512     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1513         server.send(['TASK:MOVE', 'UP']);
1514     } else {
1515         explorer.move('UP');
1516     };
1517 };
1518 document.getElementById("move_upright").onclick = function() {
1519     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1520         server.send(['TASK:MOVE', 'UPRIGHT']);
1521     } else {
1522         explorer.move('UPRIGHT');
1523     };
1524 };
1525 document.getElementById("move_right").onclick = function() {
1526     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1527         server.send(['TASK:MOVE', 'RIGHT']);
1528     } else {
1529         explorer.move('RIGHT');
1530     };
1531 };
1532 document.getElementById("move_downright").onclick = function() {
1533     if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1534         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1535     } else {
1536         explorer.move('DOWNRIGHT');
1537     };
1538 };
1539 </script>
1540 </body></html>