13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table id="move_table" style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
26 <td style="text-align: center"><button id="square_move_up"></button></td>
27 <td><button id="hex_move_upright"></button></td>
30 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
35 <td><button id="hex_move_downleft"></button></td>
36 <td style="text-align: center"><button id="square_move_down"></button></td>
37 <td><button id="hex_move_downright"></button></td>
42 <td><button id="help"></button></td>
45 <td><button id="switch_to_chat"></button><br /></td>
48 <td><button id="switch_to_study"></button></td>
49 <td><button id="toggle_map_mode"></button>
52 <td><button id="switch_to_play"></button></td>
54 <button id="switch_to_take_thing"></button>
55 <button id="drop_thing"></button>
56 <button id="door"></button>
57 <button id="consume"></button>
58 <button id="switch_to_command_thing"></button>
59 <button id="teleport"></button>
63 <td><button id="switch_to_edit"></button></td>
65 <button id="switch_to_write"></button>
66 <button id="flatten"></button>
67 <button id="switch_to_annotate"></button>
68 <button id="switch_to_portal"></button>
69 <button id="switch_to_name_thing"></button>
70 <button id="switch_to_password"></button>
74 <td><button id="switch_to_admin_enter"></button></td>
76 <button id="switch_to_control_pw_type"></button>
77 <button id="switch_to_control_tile_type"></button>
78 <button id="switch_to_admin_thing_protect"></button>
79 <button id="toggle_tile_draw"></button>
84 <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 />
86 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
87 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
88 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
89 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
90 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
91 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
92 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
93 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
94 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
95 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
96 <li>help: <input id="key_help" type="text" value="h" />
97 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
98 <li>teleport: <input id="key_teleport" type="text" value="p" />
99 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
100 <li>open/close: <input id="key_door" type="text" value="D" />
101 <li>consume: <input id="key_consume" type="text" value="C" />
102 <li><input id="key_switch_to_take_thing" type="text" value="z" />
103 <li><input id="key_switch_to_chat" type="text" value="t" />
104 <li><input id="key_switch_to_play" type="text" value="p" />
105 <li><input id="key_switch_to_study" type="text" value="?" />
106 <li><input id="key_switch_to_edit" type="text" value="E" />
107 <li><input id="key_switch_to_write" type="text" value="m" />
108 <li><input id="key_switch_to_name_thing" type="text" value="N" />
109 <li><input id="key_switch_to_command_thing" type="text" value="O" />
110 <li><input id="key_switch_to_password" type="text" value="P" />
111 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
112 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
113 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
114 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
115 <li><input id="key_switch_to_annotate" type="text" value="M" />
116 <li><input id="key_switch_to_portal" type="text" value="T" />
117 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
118 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
123 let websocket_location = "wss://plomlompom.com/rogue_chat/";
124 //let websocket_location = "ws://localhost:8001/";
130 'long': 'This mode allows you to interact with the map in various ways.'
135 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
137 'short': 'world edit',
139 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
142 'short': 'name thing',
144 'long': 'Give name to/change name of thing here.'
147 'short': 'command thing',
149 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
152 'short': 'take thing',
153 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
154 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
156 'admin_thing_protect': {
157 'short': 'change thing protection',
158 'intro': '@ enter thing protection character:',
159 'long': 'Change protection character for thing here.'
162 'short': 'change terrain',
164 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
167 'short': 'change protection character password',
168 'intro': '@ enter protection character for which you want to change the password:',
169 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
172 'short': 'change protection character password',
174 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
176 'control_tile_type': {
177 'short': 'change tiles protection',
178 'intro': '@ enter protection character which you want to draw:',
179 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
181 'control_tile_draw': {
182 'short': 'change tiles protection',
184 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
187 'short': 'annotate tile',
189 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
192 'short': 'edit portal',
194 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world 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.'
199 '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:\n\n/nick NAME – re-name yourself to NAME'
204 'long': 'Enter your player name.'
206 'waiting_for_server': {
207 'short': 'waiting for server response',
208 'intro': '@ waiting for server …',
209 'long': 'Waiting for a server response.'
212 'short': 'waiting for server response',
214 'long': 'Waiting for a server response.'
217 'short': 'set world edit password',
219 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
222 'short': 'become admin',
223 'intro': '@ enter admin password:',
224 'long': 'This mode allows you to become admin if you know an admin password.'
229 'long': 'This mode allows you access to actions limited to administrators.'
232 let key_descriptions = {
234 'flatten': 'flatten surroundings',
235 'teleport': 'teleport',
236 'drop_thing': 'drop thing',
237 'door': 'open/close',
238 'consume': 'consume',
239 'toggle_map_mode': 'toggle map view',
240 'toggle_tile_draw': 'toggle protection character drawing',
241 'hex_move_upleft': 'up-left',
242 'hex_move_upright': 'up-right',
243 'hex_move_right': 'right',
244 'hex_move_left': 'left',
245 'hex_move_downleft': 'down-left',
246 'hex_move_downright': 'down-right',
247 'square_move_up': 'up',
248 'square_move_left': 'left',
249 'square_move_down': 'down',
250 'square_move_right': 'right',
252 for (const mode_name of Object.keys(mode_helps)) {
253 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
256 let rows_selector = document.getElementById("n_rows");
257 let cols_selector = document.getElementById("n_cols");
258 let key_selectors = document.querySelectorAll('[id^="key_"]');
260 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
261 const action = key_switch_selector.id.slice("key_switch_to_".length);
262 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
265 function restore_selector_value(selector) {
266 let stored_selection = window.localStorage.getItem(selector.id);
267 if (stored_selection) {
268 selector.value = stored_selection;
271 restore_selector_value(rows_selector);
272 restore_selector_value(cols_selector);
273 for (let key_selector of key_selectors) {
274 restore_selector_value(key_selector);
277 function escapeHTML(str) {
279 replace(/&/g, '&').
280 replace(/</g, '<').
281 replace(/>/g, '>').
282 replace(/'/g, ''').
283 replace(/"/g, '"');
287 initialize: function() {
288 this.rows = rows_selector.value;
289 this.cols = cols_selector.value;
290 this.pre_el = document.getElementById("terminal");
291 this.set_default_colors();
295 for (let y = 0, x = 0; y <= this.rows; x++) {
296 if (x == this.cols) {
299 this.content.push(line);
301 if (y == this.rows) {
308 apply_colors: function() {
309 this.pre_el.style.color = this.foreground;
310 this.pre_el.style.backgroundColor = this.background;
312 set_default_colors: function() {
313 this.foreground = 'white';
314 this.background = 'black';
317 set_random_colors: function() {
318 function rand(offset) {
319 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
321 this.foreground = '#' + rand(159) + rand(159) + rand(159);
322 this.background = '#' + rand(0) + rand(0) + rand(0);
325 blink_screen: function() {
326 this.pre_el.style.color = this.background;
327 this.pre_el.style.backgroundColor = this.foreground;
329 this.pre_el.style.color = this.foreground;
330 this.pre_el.style.backgroundColor = this.background;
333 refresh: function() {
334 let pre_content = '';
335 for (let y = 0; y < this.rows; y++) {
336 let line = this.content[y].join('');
338 if (y in tui.links) {
340 for (let span of tui.links[y]) {
341 chunks.push(escapeHTML(line.slice(start_x, span[0])));
342 chunks.push('<a target="_blank" href="');
343 chunks.push(escapeHTML(span[2]));
345 chunks.push(escapeHTML(line.slice(span[0], span[1])));
349 chunks.push(escapeHTML(line.slice(start_x)));
351 chunks = [escapeHTML(line)];
353 for (const chunk of chunks) {
354 pre_content += chunk;
358 this.pre_el.innerHTML = pre_content;
360 write: function(start_y, start_x, msg) {
361 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
362 this.content[start_y][x] = msg[i];
365 drawBox: function(start_y, start_x, height, width) {
366 let end_y = start_y + height;
367 let end_x = start_x + width;
368 for (let y = start_y, x = start_x; y < this.rows; x++) {
376 this.content[y][x] = ' ';
380 terminal.initialize();
383 tokenize: function(str) {
388 for (let i = 0; i < str.length; i++) {
394 } else if (c == '\\') {
396 } else if (c == '"') {
401 } else if (c == '"') {
403 } else if (c === ' ') {
404 if (token.length > 0) {
412 if (token.length > 0) {
417 parse_yx: function(position_string) {
418 let coordinate_strings = position_string.split(',')
419 let position = [0, 0];
420 position[0] = parseInt(coordinate_strings[0].slice(2));
421 position[1] = parseInt(coordinate_strings[1].slice(2));
433 init: function(url) {
435 this.websocket = new WebSocket(this.url);
436 this.websocket.onopen = function(event) {
437 server.connected = true;
438 game.thing_types = {};
440 server.send(['TASKS']);
441 server.send(['TERRAINS']);
442 server.send(['THING_TYPES']);
443 tui.log_msg("@ server connected! :)");
444 tui.switch_mode('login');
446 this.websocket.onclose = function(event) {
447 server.connected = false;
448 tui.switch_mode('waiting_for_server');
449 tui.log_msg("@ server disconnected :(");
451 this.websocket.onmessage = this.handle_event;
453 reconnect_to: function(url) {
454 this.websocket.close();
457 send: function(tokens) {
458 this.websocket.send(unparser.untokenize(tokens));
460 handle_event: function(event) {
461 let tokens = parser.tokenize(event.data);
462 if (tokens[0] === 'TURN') {
463 game.turn_complete = false;
464 explorer.empty_annotations();
468 game.turn = parseInt(tokens[1]);
469 } else if (tokens[0] === 'THING') {
470 let t = game.get_thing(tokens[4], true);
471 t.position = parser.parse_yx(tokens[1]);
473 t.protection = tokens[3];
474 } else if (tokens[0] === 'THING_NAME') {
475 let t = game.get_thing(tokens[1], false);
479 } else if (tokens[0] === 'THING_CHAR') {
480 let t = game.get_thing(tokens[1], false);
482 t.thing_char = tokens[2];
484 } else if (tokens[0] === 'TASKS') {
485 game.tasks = tokens[1].split(',');
486 tui.mode_write.legal = game.tasks.includes('WRITE');
487 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
488 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
489 } else if (tokens[0] === 'THING_TYPE') {
490 game.thing_types[tokens[1]] = tokens[2]
491 } else if (tokens[0] === 'THING_CARRYING') {
492 let t = game.get_thing(tokens[1], false);
496 } else if (tokens[0] === 'TERRAIN') {
497 game.terrains[tokens[1]] = tokens[2]
498 } else if (tokens[0] === 'MAP') {
499 game.map_geometry = tokens[1];
501 game.map_size = parser.parse_yx(tokens[2]);
503 } else if (tokens[0] === 'FOV') {
505 } else if (tokens[0] === 'MAP_CONTROL') {
506 game.map_control = tokens[1]
507 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
508 game.turn_complete = true;
509 if (tui.mode.name == 'post_login_wait') {
510 tui.switch_mode('play');
512 explorer.info_cached = false;
514 } else if (tokens[0] === 'CHAT') {
515 tui.log_msg('# ' + tokens[1], 1);
516 } else if (tokens[0] === 'REPLY') {
517 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
518 } else if (tokens[0] === 'PLAYER_ID') {
519 game.player_id = parseInt(tokens[1]);
520 } else if (tokens[0] === 'LOGIN_OK') {
521 this.send(['GET_GAMESTATE']);
522 tui.switch_mode('post_login_wait');
523 } else if (tokens[0] === 'DEFAULT_COLORS') {
524 terminal.set_default_colors();
525 } else if (tokens[0] === 'RANDOM_COLORS') {
526 terminal.set_random_colors();
527 } else if (tokens[0] === 'ADMIN_OK') {
529 tui.log_msg('@ you now have admin rights');
530 tui.switch_mode('admin');
531 } else if (tokens[0] === 'PORTAL') {
532 let position = parser.parse_yx(tokens[1]);
533 game.portals[position] = tokens[2];
534 } else if (tokens[0] === 'ANNOTATION') {
535 let position = parser.parse_yx(tokens[1]);
536 explorer.update_annotations(position, tokens[2]);
538 } else if (tokens[0] === 'UNHANDLED_INPUT') {
539 tui.log_msg('? unknown command');
540 } else if (tokens[0] === 'PLAY_ERROR') {
541 tui.log_msg('? ' + tokens[1]);
542 terminal.blink_screen();
543 } else if (tokens[0] === 'ARGUMENT_ERROR') {
544 tui.log_msg('? syntax error: ' + tokens[1]);
545 } else if (tokens[0] === 'GAME_ERROR') {
546 tui.log_msg('? game error: ' + tokens[1]);
547 } else if (tokens[0] === 'PONG') {
550 tui.log_msg('? unhandled input: ' + event.data);
556 quote: function(str) {
558 for (let i = 0; i < str.length; i++) {
560 if (['"', '\\'].includes(c)) {
566 return quoted.join('');
568 to_yx: function(yx_coordinate) {
569 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
571 untokenize: function(tokens) {
572 let quoted_tokens = [];
573 for (let token of tokens) {
574 quoted_tokens.push(this.quote(token));
576 return quoted_tokens.join(" ");
581 constructor(name, has_input_prompt=false, shows_info=false,
582 is_intro=false, is_single_char_entry=false) {
584 this.short_desc = mode_helps[name].short;
585 this.available_modes = [];
586 this.available_actions = [];
587 this.has_input_prompt = has_input_prompt;
588 this.shows_info= shows_info;
589 this.is_intro = is_intro;
590 this.help_intro = mode_helps[name].long;
591 this.intro_msg = mode_helps[name].intro;
592 this.is_single_char_entry = is_single_char_entry;
595 *iter_available_modes() {
596 for (let mode_name of this.available_modes) {
597 let mode = tui['mode_' + mode_name];
601 let key = tui.keys['switch_to_' + mode.name];
605 list_available_modes() {
607 if (this.available_modes.length > 0) {
608 msg += 'Other modes available from here:\n';
609 for (let [mode, key] of this.iter_available_modes()) {
610 msg += '[' + key + '] – ' + mode.short_desc + '\n';
615 mode_switch_on_key(key_event) {
616 for (let [mode, key] of this.iter_available_modes()) {
617 if (key_event.key == key) {
618 event.preventDefault();
619 tui.switch_mode(mode.name);
631 window_width: terminal.cols / 2,
639 mode_waiting_for_server: new Mode('waiting_for_server',
641 mode_login: new Mode('login', true, false, true),
642 mode_post_login_wait: new Mode('post_login_wait'),
643 mode_chat: new Mode('chat', true),
644 mode_annotate: new Mode('annotate', true, true),
645 mode_play: new Mode('play'),
646 mode_study: new Mode('study', false, true),
647 mode_write: new Mode('write', false, false, false, true),
648 mode_edit: new Mode('edit'),
649 mode_control_pw_type: new Mode('control_pw_type', true),
650 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
651 mode_portal: new Mode('portal', true, true),
652 mode_password: new Mode('password', true),
653 mode_name_thing: new Mode('name_thing', true, true),
654 mode_command_thing: new Mode('command_thing', true),
655 mode_take_thing: new Mode('take_thing', true),
656 mode_admin_enter: new Mode('admin_enter', true),
657 mode_admin: new Mode('admin'),
658 mode_control_pw_pw: new Mode('control_pw_pw', true),
659 mode_control_tile_type: new Mode('control_tile_type', true),
660 mode_control_tile_draw: new Mode('control_tile_draw'),
662 'flatten': 'FLATTEN_SURROUNDINGS',
663 'take_thing': 'PICK_UP',
664 'drop_thing': 'DROP',
667 'command': 'COMMAND',
668 'consume': 'INTOXICATE',
674 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
675 "command_thing", "take_thing"]
676 this.mode_play.available_actions = ["move", "drop_thing",
677 "teleport", "door", "consume"];
678 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
679 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
680 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
681 "control_tile_type", "chat",
682 "study", "play", "edit"]
683 this.mode_admin.available_actions = ["move"];
684 this.mode_control_tile_draw.available_modes = ["admin_enter"]
685 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
686 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
687 "password", "chat", "study", "play",
689 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
690 this.inputEl = document.getElementById("input");
691 this.inputEl.focus();
692 this.switch_mode('waiting_for_server');
693 this.recalc_input_lines();
694 this.height_header = this.height_turn_line + this.height_mode_line;
697 init_keys: function() {
698 document.getElementById("move_table").hidden = true;
700 for (let key_selector of key_selectors) {
701 this.keys[key_selector.id.slice(4)] = key_selector.value;
703 this.movement_keys = {};
704 let geometry_prefix = 'undefinedMapGeometry_';
705 if (game.map_geometry) {
706 geometry_prefix = game.map_geometry.toLowerCase() + '_';
708 for (const key_name of Object.keys(key_descriptions)) {
709 if (key_name.startsWith(geometry_prefix)) {
710 let direction = key_name.split('_')[2].toUpperCase();
711 let key = this.keys[key_name];
712 this.movement_keys[key] = direction;
715 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
716 if (move_button.id.startsWith('key_')) {
719 move_button.hidden = true;
721 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
722 document.getElementById("move_table").hidden = false;
723 move_button.hidden = false;
725 for (let el of document.getElementsByTagName("button")) {
726 let action_desc = key_descriptions[el.id];
727 let action_key = '[' + this.keys[el.id] + ']';
728 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
731 task_action_on: function(action) {
732 return game.tasks.includes(this.action_tasks[action]);
734 switch_mode: function(mode_name) {
735 if (this.mode && this.mode.name == 'control_tile_draw') {
736 tui.log_msg('@ finished tile protection drawing.')
738 this.tile_draw = false;
739 if (mode_name == 'admin_enter' && this.is_admin) {
741 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
742 let player_position = game.things[game.player_id].position;
744 for (let t_id in game.things) {
745 if (t_id == game.player_id) {
748 let t = game.things[t_id];
749 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
755 terminal.blink_screen();
756 this.log_msg('? not standing over thing');
759 this.selected_thing_id = thing_id;
762 this.mode = this['mode_' + mode_name];
763 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
764 this.map_mode = 'protections';
765 } else if (this.mode.name != "edit") {
766 this.map_mode = 'terrain + things';
768 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
769 this.inputEl.focus();
771 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
772 explorer.position = game.things[game.player_id].position;
774 this.inputEl.value = "";
775 this.restore_input_values();
776 for (let el of document.getElementsByTagName("button")) {
779 document.getElementById("help").disabled = false;
780 for (const action of this.mode.available_actions) {
781 if (["move", "move_explorer"].includes(action)) {
782 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
783 move_key.disabled = false;
785 } else if (Object.keys(this.action_tasks).includes(action)) {
786 if (this.task_action_on(action)) {
787 document.getElementById(action).disabled = false;
790 document.getElementById(action).disabled = false;
793 for (const mode_name of this.mode.available_modes) {
794 document.getElementById('switch_to_' + mode_name).disabled = false;
796 if (this.mode.intro_msg.length > 0) {
797 this.log_msg(this.mode.intro_msg);
799 if (this.mode.name == 'login') {
800 if (this.login_name) {
801 server.send(['LOGIN', this.login_name]);
803 this.log_msg("? need login name");
805 } else if (this.mode.is_single_char_entry) {
806 this.show_help = true;
807 } else if (this.mode.name == 'take_thing') {
808 this.log_msg("Things in reach for pick-up:");
809 const player = game.things[game.player_id];
810 const y = player.position[0]
811 const x = player.position[1]
812 let select_range = [y.toString() + ':' + x.toString(),
813 (y + 0).toString() + ':' + (x - 1).toString(),
814 (y + 0).toString() + ':' + (x + 1).toString(),
815 (y - 1).toString() + ':' + (x).toString(),
816 (y + 1).toString() + ':' + (x).toString()];
817 if (game.map_geometry == 'Hex') {
819 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
820 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
822 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
823 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
826 this.selectables = [];
827 for (const t_id in game.things) {
828 const t = game.things[t_id];
829 if (select_range.includes(t.position[0].toString()
830 + ':' + t.position[1].toString())
831 && t != player && t.type_ != 'Player') {
832 this.selectables.push([t_id, t]);
835 if (this.selectables.length == 0) {
838 for (let [i, t] of this.selectables.entries()) {
839 this.log_msg(i + ': ' + explorer.get_thing_info(t[1]));
842 } else if (this.mode.name == 'command_thing') {
843 server.send(['TASK:COMMAND', 'HELP']);
844 } else if (this.mode.name == 'control_pw_pw') {
845 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
846 } else if (this.mode.name == 'control_tile_draw') {
847 this.log_msg('@ can draw protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
851 offset_links: function(offset, links) {
852 for (let y in links) {
853 let real_y = offset[0] + parseInt(y);
854 if (!this.links[real_y]) {
855 this.links[real_y] = [];
857 for (let link of links[y]) {
858 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
859 this.links[real_y].push(offset_link);
863 restore_input_values: function() {
864 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
865 let info = explorer.annotations[explorer.position];
866 if (info != "(none)") {
867 this.inputEl.value = info;
869 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
870 let portal = game.portals[explorer.position]
871 this.inputEl.value = portal;
872 } else if (this.mode.name == 'password') {
873 this.inputEl.value = this.password;
874 } else if (this.mode.name == 'name_thing') {
875 let t = game.get_thing(this.selected_thing_id);
877 this.inputEl.value = t.name_;
879 } else if (this.mode.name == 'admin_thing_protect') {
880 let t = game.get_thing(this.selected_thing_id);
881 if (t && t.protection) {
882 this.inputEl.value = t.protection;
886 recalc_input_lines: function() {
887 if (this.mode.has_input_prompt) {
889 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
891 this.input_lines = [];
893 this.height_input = this.input_lines.length;
895 msg_into_lines_of_width: function(msg, width) {
896 function push_inner_link(y, end_x) {
897 if (!inner_links[y]) {
900 inner_links[y].push([url_start_x, end_x, url]);
902 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
905 for (const match of matches) {
906 const url = match[0];
907 const url_start = match.index;
908 const url_end = match.index + match[0].length;
909 link_data[url_start] = url;
910 url_ends.push(url_end);
914 let inner_links = {};
918 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
919 if (x >= width || msg[i] == "\n") {
921 push_inner_link(y, chunk.length);
923 if (url_ends[0] == i) {
931 if (msg[i] == "\n") {
936 if (msg[i] != "\n") {
939 if (i in link_data) {
943 } else if (url_ends[0] == i) {
945 push_inner_link(y, x);
951 push_inner_link(lines.length - 1, chunk.length);
953 return [lines, inner_links];
955 log_msg: function(msg) {
957 while (this.log.length > 100) {
962 draw_map: function() {
963 if (!game.turn_complete && this.map_lines.length == 0) {
966 if (game.turn_complete) {
967 let map_lines_split = [];
969 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
970 if (j == game.map_size[1]) {
971 map_lines_split.push(line);
975 if (this.map_mode == 'protections') {
976 line.push(game.map_control[i] + ' ');
978 line.push(game.map[i] + ' ');
981 map_lines_split.push(line);
982 if (this.map_mode == 'terrain + annotations') {
983 for (const coordinate of explorer.info_hints) {
984 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
986 } else if (this.map_mode == 'terrain + things') {
987 for (const p in game.portals) {
988 let coordinate = p.split(',')
989 let original = map_lines_split[coordinate[0]][coordinate[1]];
990 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
992 let used_positions = [];
993 function draw_thing(t, used_positions) {
994 let symbol = game.thing_types[t.type_];
997 meta_char = t.thing_char;
999 if (used_positions.includes(t.position.toString())) {
1005 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1006 used_positions.push(t.position.toString());
1008 for (const thing_id in game.things) {
1009 let t = game.things[thing_id];
1010 if (t.type_ != 'Player') {
1011 draw_thing(t, used_positions);
1014 for (const thing_id in game.things) {
1015 let t = game.things[thing_id];
1016 if (t.type_ == 'Player') {
1017 draw_thing(t, used_positions);
1021 let player = game.things[game.player_id];
1022 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1023 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1024 } else if (tui.map_mode != 'terrain + things') {
1025 map_lines_split[player.position[0]][player.position[1]] = '??';
1028 if (game.map_geometry == 'Square') {
1029 for (let line_split of map_lines_split) {
1030 this.map_lines.push(line_split.join(''));
1032 } else if (game.map_geometry == 'Hex') {
1034 for (let line_split of map_lines_split) {
1035 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1043 let window_center = [terminal.rows / 2, this.window_width / 2];
1044 let center_position = [player.position[0], player.position[1]];
1045 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1046 center_position = [explorer.position[0], explorer.position[1]];
1048 center_position[1] = center_position[1] * 2;
1049 this.offset = [center_position[0] - window_center[0],
1050 center_position[1] - window_center[1]]
1051 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1052 this.offset[1] += 1;
1055 let term_y = Math.max(0, -this.offset[0]);
1056 let term_x = Math.max(0, -this.offset[1]);
1057 let map_y = Math.max(0, this.offset[0]);
1058 let map_x = Math.max(0, this.offset[1]);
1059 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
1060 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1061 terminal.write(term_y, term_x, to_draw);
1064 draw_mode_line: function() {
1065 let help = 'hit [' + this.keys.help + '] for help';
1066 if (this.mode.has_input_prompt) {
1067 help = 'enter /help for help';
1069 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1071 draw_turn_line: function(n) {
1072 if (game.turn_complete) {
1073 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1076 draw_history: function() {
1077 let log_display_lines = [];
1079 let y_offset_in_log = 0;
1080 for (let line of this.log) {
1081 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1083 log_display_lines = log_display_lines.concat(new_lines);
1084 for (const y in link_data) {
1085 const rel_y = y_offset_in_log + parseInt(y);
1086 log_links[rel_y] = [];
1087 for (let link of link_data[y]) {
1088 log_links[rel_y].push(link);
1091 y_offset_in_log += new_lines.length;
1093 let i = log_display_lines.length - 1;
1094 for (let y = terminal.rows - 1 - this.height_input;
1095 y >= this.height_header && i >= 0;
1097 terminal.write(y, this.window_width, log_display_lines[i]);
1099 for (const key of Object.keys(log_links)) {
1100 if (parseInt(key) <= i) {
1101 delete log_links[key];
1104 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1106 this.offset_links(offset, log_links);
1108 draw_info: function() {
1109 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1110 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1111 let offset = [this.height_header, this.window_width];
1112 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1113 terminal.write(y, offset[1], lines[i]);
1115 this.offset_links(offset, link_data);
1117 draw_input: function() {
1118 if (this.mode.has_input_prompt) {
1119 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1120 terminal.write(y, this.window_width, this.input_lines[i]);
1124 draw_help: function() {
1125 let movement_keys_desc = '';
1126 if (!this.mode.is_intro) {
1127 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1129 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1130 if (this.mode.available_actions.length > 0) {
1131 content += "Available actions:\n";
1132 for (let action of this.mode.available_actions) {
1133 if (Object.keys(this.action_tasks).includes(action)) {
1134 if (!this.task_action_on(action)) {
1138 if (action == 'move_explorer') {
1141 if (action == 'move') {
1142 content += "[" + movement_keys_desc + "] – move\n"
1144 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1149 content += this.mode.list_available_modes();
1151 if (!this.mode.has_input_prompt) {
1152 start_x = this.window_width
1154 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1155 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1156 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1157 terminal.write(y, start_x, lines[i]);
1160 toggle_tile_draw: function() {
1161 if (tui.tile_draw) {
1162 tui.tile_draw = false;
1164 tui.tile_draw = true;
1167 toggle_map_mode: function() {
1168 if (tui.map_mode == 'terrain only') {
1169 tui.map_mode = 'terrain + annotations';
1170 } else if (tui.map_mode == 'terrain + annotations') {
1171 tui.map_mode = 'terrain + things';
1172 } else if (tui.map_mode == 'terrain + things') {
1173 tui.map_mode = 'protections';
1174 } else if (tui.map_mode == 'protections') {
1175 tui.map_mode = 'terrain only';
1178 full_refresh: function() {
1180 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1181 this.recalc_input_lines();
1182 if (this.mode.is_intro) {
1183 this.draw_history();
1187 this.draw_turn_line();
1188 this.draw_mode_line();
1189 if (this.mode.shows_info) {
1192 this.draw_history();
1196 if (this.show_help) {
1208 this.map_control = "";
1209 this.map_size = [0,0];
1210 this.player_id = -1;
1214 get_thing: function(id_, create_if_not_found=false) {
1215 if (id_ in game.things) {
1216 return game.things[id_];
1217 } else if (create_if_not_found) {
1218 let t = new Thing([0,0]);
1219 game.things[id_] = t;
1223 move: function(start_position, direction) {
1224 let target = [start_position[0], start_position[1]];
1225 if (direction == 'LEFT') {
1227 } else if (direction == 'RIGHT') {
1229 } else if (game.map_geometry == 'Square') {
1230 if (direction == 'UP') {
1232 } else if (direction == 'DOWN') {
1235 } else if (game.map_geometry == 'Hex') {
1236 let start_indented = start_position[0] % 2;
1237 if (direction == 'UPLEFT') {
1239 if (!start_indented) {
1242 } else if (direction == 'UPRIGHT') {
1244 if (start_indented) {
1247 } else if (direction == 'DOWNLEFT') {
1249 if (!start_indented) {
1252 } else if (direction == 'DOWNRIGHT') {
1254 if (start_indented) {
1259 if (target[0] < 0 || target[1] < 0 ||
1260 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1265 teleport: function() {
1266 let player = this.get_thing(game.player_id);
1267 if (player.position in this.portals) {
1268 server.reconnect_to(this.portals[player.position]);
1270 terminal.blink_screen();
1271 tui.log_msg('? not standing on portal')
1279 server.init(websocket_location);
1285 move: function(direction) {
1286 let target = game.move(this.position, direction);
1288 this.position = target
1289 this.info_cached = false;
1290 if (tui.tile_draw) {
1291 this.send_tile_control_command();
1294 terminal.blink_screen();
1297 update_annotations: function(yx, str) {
1298 this.annotations[yx] = str;
1299 if (tui.mode.name == 'study') {
1303 empty_annotations: function() {
1304 this.annotations = {};
1305 if (tui.mode.name == 'study') {
1309 get_info: function() {
1310 if (this.info_cached) {
1311 return this.info_cached;
1313 let info_to_cache = '';
1314 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1315 if (game.fov[position_i] != '.') {
1316 info_to_cache += 'outside field of view';
1318 let terrain_char = game.map[position_i]
1319 let terrain_desc = '?'
1320 if (game.terrains[terrain_char]) {
1321 terrain_desc = game.terrains[terrain_char];
1323 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1324 let protection = game.map_control[position_i];
1325 if (protection == '.') {
1326 protection = 'unprotected';
1328 info_to_cache += 'PROTECTION: ' + protection + '\n';
1329 for (let t_id in game.things) {
1330 let t = game.things[t_id];
1331 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1332 info_to_cache += "THING: " + this.get_thing_info(t);
1333 let protection = t.protection;
1334 if (protection == '.') {
1335 protection = 'none';
1337 info_to_cache += " / protection: " + protection + "\n";
1340 if (this.position in game.portals) {
1341 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1343 if (this.position in this.annotations) {
1344 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1347 this.info_cached = info_to_cache;
1348 return this.info_cached;
1350 get_thing_info: function(t) {
1351 const symbol = game.thing_types[t.type_];
1352 let info = t.type_ + " / " + symbol;
1354 info += t.thing_char;
1357 info += " (" + t.name_ + ")";
1361 annotate: function(msg) {
1362 if (msg.length == 0) {
1363 msg = " "; // triggers annotation deletion
1365 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1367 set_portal: function(msg) {
1368 if (msg.length == 0) {
1369 msg = " "; // triggers portal deletion
1371 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1373 send_tile_control_command: function() {
1374 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1378 tui.inputEl.addEventListener('input', (event) => {
1379 if (tui.mode.has_input_prompt) {
1380 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1381 if (tui.inputEl.value.length > max_length) {
1382 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1384 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1385 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1386 tui.switch_mode('edit');
1390 document.onclick = function() {
1391 tui.show_help = false;
1393 tui.inputEl.addEventListener('keydown', (event) => {
1394 tui.show_help = false;
1395 if (event.key == 'Enter') {
1396 event.preventDefault();
1398 if (tui.mode.has_input_prompt && event.key == 'Enter'
1399 && tui.inputEl.value.length == 0
1400 && ['chat', 'command_thing', 'take_thing',
1401 'admin_enter'].includes(tui.mode.name)) {
1402 if (tui.mode.name != 'chat') {
1403 tui.log_msg('@ aborted');
1405 tui.switch_mode('play');
1406 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1407 tui.show_help = true;
1408 tui.inputEl.value = "";
1409 tui.restore_input_values();
1410 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1411 && !tui.mode.is_single_char_entry) {
1412 tui.show_help = true;
1413 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1414 tui.login_name = tui.inputEl.value;
1415 server.send(['LOGIN', tui.inputEl.value]);
1416 tui.inputEl.value = "";
1417 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1418 if (tui.task_action_on('command')) {
1419 server.send(['TASK:COMMAND', tui.inputEl.value]);
1420 tui.inputEl.value = "";
1422 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1423 const i = parseInt(tui.inputEl.value);
1424 if (isNaN(i) || i < 0 || i >= tui.selectables.length) {
1425 tui.log_msg('? invalid index, aborted');
1427 server.send(['TASK:PICK_UP', tui.selectables[i][0]]);
1429 tui.inputEl.value = "";
1430 tui.switch_mode('play');
1431 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1432 if (tui.inputEl.value.length == 0) {
1433 tui.log_msg('@ aborted');
1435 server.send(['SET_MAP_CONTROL_PASSWORD',
1436 tui.tile_control_char, tui.inputEl.value]);
1437 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1439 tui.switch_mode('admin');
1440 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1441 explorer.set_portal(tui.inputEl.value);
1442 tui.switch_mode('edit');
1443 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1444 if (tui.inputEl.value.length == 0) {
1445 tui.inputEl.value = " ";
1447 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1449 tui.switch_mode('edit');
1450 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1451 explorer.annotate(tui.inputEl.value);
1452 tui.switch_mode('edit');
1453 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1454 if (tui.inputEl.value.length == 0) {
1455 tui.inputEl.value = " ";
1457 tui.password = tui.inputEl.value
1458 tui.switch_mode('edit');
1459 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1460 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1461 tui.switch_mode('play');
1462 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1463 if (tui.inputEl.value.length != 1) {
1464 tui.log_msg('@ entered non-single-char, therefore aborted');
1465 tui.switch_mode('admin');
1467 tui.tile_control_char = tui.inputEl.value[0];
1468 tui.switch_mode('control_pw_pw');
1470 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1471 if (tui.inputEl.value.length != 1) {
1472 tui.log_msg('@ entered non-single-char, therefore aborted');
1473 tui.switch_mode('admin');
1475 tui.tile_control_char = tui.inputEl.value[0];
1476 tui.switch_mode('control_tile_draw');
1478 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1479 if (tui.inputEl.value.length != 1) {
1480 tui.log_msg('@ entered non-single-char, therefore aborted');
1482 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1483 tui.log_msg('@ sent new protection character for thing');
1485 tui.switch_mode('admin');
1486 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1487 let tokens = parser.tokenize(tui.inputEl.value);
1488 if (tokens.length > 0 && tokens[0].length > 0) {
1489 if (tui.inputEl.value[0][0] == '/') {
1490 if (tokens[0].slice(1) == 'nick') {
1491 if (tokens.length > 1) {
1492 server.send(['NICK', tokens[1]]);
1494 tui.log_msg('? need new name');
1497 tui.log_msg('? unknown command');
1500 server.send(['ALL', tui.inputEl.value]);
1502 } else if (tui.inputEl.valuelength > 0) {
1503 server.send(['ALL', tui.inputEl.value]);
1505 tui.inputEl.value = "";
1506 } else if (tui.mode.name == 'play') {
1507 if (tui.mode.mode_switch_on_key(event)) {
1509 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1510 server.send(["TASK:DROP"]);
1511 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1512 server.send(["TASK:INTOXICATE"]);
1513 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1514 server.send(["TASK:DOOR"]);
1515 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1516 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1517 } else if (event.key === tui.keys.teleport) {
1520 } else if (tui.mode.name == 'study') {
1521 if (tui.mode.mode_switch_on_key(event)) {
1523 } else if (event.key in tui.movement_keys) {
1524 explorer.move(tui.movement_keys[event.key]);
1525 } else if (event.key == tui.keys.toggle_map_mode) {
1526 tui.toggle_map_mode();
1528 } else if (tui.mode.name == 'control_tile_draw') {
1529 if (tui.mode.mode_switch_on_key(event)) {
1531 } else if (event.key in tui.movement_keys) {
1532 explorer.move(tui.movement_keys[event.key]);
1533 } else if (event.key === tui.keys.toggle_tile_draw) {
1534 tui.toggle_tile_draw();
1536 } else if (tui.mode.name == 'admin') {
1537 if (tui.mode.mode_switch_on_key(event)) {
1539 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1540 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1542 } else if (tui.mode.name == 'edit') {
1543 if (tui.mode.mode_switch_on_key(event)) {
1545 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1546 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1547 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1548 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1549 } else if (event.key == tui.keys.toggle_map_mode) {
1550 tui.toggle_map_mode();
1556 rows_selector.addEventListener('input', function() {
1557 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1560 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1561 terminal.initialize();
1564 cols_selector.addEventListener('input', function() {
1565 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1568 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1569 terminal.initialize();
1570 tui.window_width = terminal.cols / 2,
1573 for (let key_selector of key_selectors) {
1574 key_selector.addEventListener('input', function() {
1575 window.localStorage.setItem(key_selector.id, key_selector.value);
1579 window.setInterval(function() {
1580 if (server.connected) {
1581 server.send(['PING']);
1583 server.reconnect_to(server.url);
1584 tui.log_msg('@ attempting reconnect …')
1587 window.setInterval(function() {
1589 let span_decoration = "none";
1590 if (document.activeElement == tui.inputEl) {
1591 val = "on (click outside terminal to change)";
1593 val = "off (click into terminal to change)";
1594 span_decoration = "line-through";
1596 document.getElementById("keyboard_control").textContent = val;
1597 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1598 span.style.textDecoration = span_decoration;
1601 document.getElementById("terminal").onclick = function() {
1602 tui.inputEl.focus();
1604 document.getElementById("help").onclick = function() {
1605 tui.show_help = true;
1608 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1609 const mode = switchEl.id.slice("switch_to_".length);
1610 switchEl.onclick = function() {
1611 tui.switch_mode(mode);
1615 document.getElementById("toggle_tile_draw").onclick = function() {
1616 tui.toggle_tile_draw();
1618 document.getElementById("toggle_map_mode").onclick = function() {
1619 tui.toggle_map_mode();
1622 document.getElementById("drop_thing").onclick = function() {
1623 server.send(['TASK:DROP']);
1625 document.getElementById("flatten").onclick = function() {
1626 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1628 document.getElementById("door").onclick = function() {
1629 server.send(['TASK:DOOR']);
1631 document.getElementById("consume").onclick = function() {
1632 server.send(['TASK:INTOXICATE']);
1634 document.getElementById("teleport").onclick = function() {
1637 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1638 let direction = move_button.id.split('_')[2].toUpperCase();
1639 move_button.onclick = function() {
1640 if (tui.mode.available_actions.includes("move")
1641 || tui.mode.available_actions.includes("move_explorer")) {
1642 server.send(['TASK:MOVE', direction]);
1644 explorer.move(direction);