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