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