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