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