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