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/";
129 'long': 'This mode allows you to interact with the map in various ways.'
133 '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.'},
135 'short': 'world edit',
136 '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.'
139 'short': 'name thing',
140 'long': 'Give name to/change name of thing here.'
143 'short': 'command thing',
144 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
147 'short': 'take thing',
148 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
150 'admin_thing_protect': {
151 'short': 'change thing protection',
152 'long': 'Change protection character for thing here.'
155 'short': 'change terrain',
156 '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.'
159 'short': 'change protection character password',
160 '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.'
163 'short': 'change protection character password',
164 '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.'
166 'control_tile_type': {
167 'short': 'change tiles protection',
168 '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.'
170 'control_tile_draw': {
171 'short': 'change tiles protection',
172 '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.'
175 'short': 'annotate tile',
176 '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.'
179 'short': 'edit portal',
180 '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.'
184 '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:'
188 'long': 'Enter your player name.'
190 'waiting_for_server': {
191 'short': 'waiting for server response',
192 'long': 'Waiting for a server response.'
195 'short': 'waiting for server response',
196 'long': 'Waiting for a server response.'
199 'short': 'set world edit password',
200 '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.'
203 'short': 'become admin',
204 'long': 'This mode allows you to become admin if you know an admin password.'
208 'long': 'This mode allows you access to actions limited to administrators.'
211 let key_descriptions = {
213 'flatten': 'flatten surroundings',
214 'teleport': 'teleport',
215 'drop_thing': 'drop thing',
216 'door': 'open/close',
217 'consume': 'consume',
218 'toggle_map_mode': 'toggle map view',
219 'toggle_tile_draw': 'toggle protection character drawing',
220 'hex_move_upleft': 'up-left',
221 'hex_move_upright': 'up-right',
222 'hex_move_right': 'right',
223 'hex_move_left': 'left',
224 'hex_move_downleft': 'down-left',
225 'hex_move_downright': 'down-right',
226 'square_move_up': 'up',
227 'square_move_left': 'left',
228 'square_move_down': 'down',
229 'square_move_right': 'right',
231 for (const mode_name of Object.keys(mode_helps)) {
232 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
235 let rows_selector = document.getElementById("n_rows");
236 let cols_selector = document.getElementById("n_cols");
237 let key_selectors = document.querySelectorAll('[id^="key_"]');
239 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
240 const action = key_switch_selector.id.slice("key_switch_to_".length);
241 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
244 function restore_selector_value(selector) {
245 let stored_selection = window.localStorage.getItem(selector.id);
246 if (stored_selection) {
247 selector.value = stored_selection;
250 restore_selector_value(rows_selector);
251 restore_selector_value(cols_selector);
252 for (let key_selector of key_selectors) {
253 restore_selector_value(key_selector);
256 function escapeHTML(str) {
258 replace(/&/g, '&').
259 replace(/</g, '<').
260 replace(/>/g, '>').
261 replace(/'/g, ''').
262 replace(/"/g, '"');
266 initialize: function() {
267 this.rows = rows_selector.value;
268 this.cols = cols_selector.value;
269 this.pre_el = document.getElementById("terminal");
270 this.set_default_colors();
274 for (let y = 0, x = 0; y <= this.rows; x++) {
275 if (x == this.cols) {
278 this.content.push(line);
280 if (y == this.rows) {
287 apply_colors: function() {
288 this.pre_el.style.color = this.foreground;
289 this.pre_el.style.backgroundColor = this.background;
291 set_default_colors: function() {
292 this.foreground = 'white';
293 this.background = 'black';
296 set_random_colors: function() {
297 function rand(offset) {
298 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
300 this.foreground = '#' + rand(159) + rand(159) + rand(159);
301 this.background = '#' + rand(0) + rand(0) + rand(0);
304 blink_screen: function() {
305 this.pre_el.style.color = this.background;
306 this.pre_el.style.backgroundColor = this.foreground;
308 this.pre_el.style.color = this.foreground;
309 this.pre_el.style.backgroundColor = this.background;
312 refresh: function() {
313 let pre_content = '';
314 for (let y = 0; y < this.rows; y++) {
315 let line = this.content[y].join('');
317 if (y in tui.links) {
319 for (let span of tui.links[y]) {
320 chunks.push(escapeHTML(line.slice(start_x, span[0])));
321 chunks.push('<a target="_blank" href="');
322 chunks.push(escapeHTML(span[2]));
324 chunks.push(escapeHTML(line.slice(span[0], span[1])));
328 chunks.push(escapeHTML(line.slice(start_x)));
330 chunks = [escapeHTML(line)];
332 for (const chunk of chunks) {
333 pre_content += chunk;
337 this.pre_el.innerHTML = pre_content;
339 write: function(start_y, start_x, msg) {
340 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
341 this.content[start_y][x] = msg[i];
344 drawBox: function(start_y, start_x, height, width) {
345 let end_y = start_y + height;
346 let end_x = start_x + width;
347 for (let y = start_y, x = start_x; y < this.rows; x++) {
355 this.content[y][x] = ' ';
359 terminal.initialize();
362 tokenize: function(str) {
367 for (let i = 0; i < str.length; i++) {
373 } else if (c == '\\') {
375 } else if (c == '"') {
380 } else if (c == '"') {
382 } else if (c === ' ') {
383 if (token.length > 0) {
391 if (token.length > 0) {
396 parse_yx: function(position_string) {
397 let coordinate_strings = position_string.split(',')
398 let position = [0, 0];
399 position[0] = parseInt(coordinate_strings[0].slice(2));
400 position[1] = parseInt(coordinate_strings[1].slice(2));
412 init: function(url) {
414 this.websocket = new WebSocket(this.url);
415 this.websocket.onopen = function(event) {
416 server.connected = true;
417 game.thing_types = {};
419 server.send(['TASKS']);
420 server.send(['TERRAINS']);
421 server.send(['THING_TYPES']);
422 tui.log_msg("@ server connected! :)");
423 tui.switch_mode('login');
425 this.websocket.onclose = function(event) {
426 server.connected = false;
427 tui.switch_mode('waiting_for_server');
428 tui.log_msg("@ server disconnected :(");
430 this.websocket.onmessage = this.handle_event;
432 reconnect_to: function(url) {
433 this.websocket.close();
436 send: function(tokens) {
437 this.websocket.send(unparser.untokenize(tokens));
439 handle_event: function(event) {
440 let tokens = parser.tokenize(event.data);
441 if (tokens[0] === 'TURN') {
442 game.turn_complete = false;
443 explorer.empty_annotations();
447 game.turn = parseInt(tokens[1]);
448 } else if (tokens[0] === 'THING') {
449 let t = game.get_thing(tokens[4], true);
450 t.position = parser.parse_yx(tokens[1]);
452 t.protection = tokens[3];
453 } else if (tokens[0] === 'THING_NAME') {
454 let t = game.get_thing(tokens[1], false);
458 } else if (tokens[0] === 'THING_CHAR') {
459 let t = game.get_thing(tokens[1], false);
461 t.thing_char = tokens[2];
463 } else if (tokens[0] === 'TASKS') {
464 game.tasks = tokens[1].split(',');
465 tui.mode_write.legal = game.tasks.includes('WRITE');
466 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
467 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
468 } else if (tokens[0] === 'THING_TYPE') {
469 game.thing_types[tokens[1]] = tokens[2]
470 } else if (tokens[0] === 'TERRAIN') {
471 game.terrains[tokens[1]] = tokens[2]
472 } else if (tokens[0] === 'MAP') {
473 game.map_geometry = tokens[1];
475 game.map_size = parser.parse_yx(tokens[2]);
477 } else if (tokens[0] === 'FOV') {
479 } else if (tokens[0] === 'MAP_CONTROL') {
480 game.map_control = tokens[1]
481 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
482 game.turn_complete = true;
483 if (tui.mode.name == 'post_login_wait') {
484 tui.switch_mode('play');
486 explorer.info_cached = false;
488 } else if (tokens[0] === 'CHAT') {
489 tui.log_msg('# ' + tokens[1], 1);
490 } else if (tokens[0] === 'REPLY') {
491 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
492 } else if (tokens[0] === 'PLAYER_ID') {
493 game.player_id = parseInt(tokens[1]);
494 } else if (tokens[0] === 'LOGIN_OK') {
495 this.send(['GET_GAMESTATE']);
496 tui.switch_mode('post_login_wait');
497 } else if (tokens[0] === 'DEFAULT_COLORS') {
498 terminal.set_default_colors();
499 } else if (tokens[0] === 'RANDOM_COLORS') {
500 terminal.set_random_colors();
501 } else if (tokens[0] === 'ADMIN_OK') {
503 tui.log_msg('@ you now have admin rights');
504 tui.switch_mode('admin');
505 } else if (tokens[0] === 'PORTAL') {
506 let position = parser.parse_yx(tokens[1]);
507 game.portals[position] = tokens[2];
508 } else if (tokens[0] === 'ANNOTATION') {
509 let position = parser.parse_yx(tokens[1]);
510 explorer.update_annotations(position, tokens[2]);
512 } else if (tokens[0] === 'UNHANDLED_INPUT') {
513 tui.log_msg('? unknown command');
514 } else if (tokens[0] === 'PLAY_ERROR') {
515 tui.log_msg('? ' + tokens[1]);
516 terminal.blink_screen();
517 } else if (tokens[0] === 'ARGUMENT_ERROR') {
518 tui.log_msg('? syntax error: ' + tokens[1]);
519 } else if (tokens[0] === 'GAME_ERROR') {
520 tui.log_msg('? game error: ' + tokens[1]);
521 } else if (tokens[0] === 'PONG') {
524 tui.log_msg('? unhandled input: ' + event.data);
530 quote: function(str) {
532 for (let i = 0; i < str.length; i++) {
534 if (['"', '\\'].includes(c)) {
540 return quoted.join('');
542 to_yx: function(yx_coordinate) {
543 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
545 untokenize: function(tokens) {
546 let quoted_tokens = [];
547 for (let token of tokens) {
548 quoted_tokens.push(this.quote(token));
550 return quoted_tokens.join(" ");
555 constructor(name, has_input_prompt=false, shows_info=false,
556 is_intro=false, is_single_char_entry=false) {
558 this.short_desc = mode_helps[name].short;
559 this.available_modes = [];
560 this.available_actions = [];
561 this.has_input_prompt = has_input_prompt;
562 this.shows_info= shows_info;
563 this.is_intro = is_intro;
564 this.help_intro = mode_helps[name].long;
565 this.is_single_char_entry = is_single_char_entry;
568 *iter_available_modes() {
569 for (let mode_name of this.available_modes) {
570 let mode = tui['mode_' + mode_name];
574 let key = tui.keys['switch_to_' + mode.name];
578 list_available_modes() {
580 if (this.available_modes.length > 0) {
581 msg += 'Other modes available from here:\n';
582 for (let [mode, key] of this.iter_available_modes()) {
583 msg += '[' + key + '] – ' + mode.short_desc + '\n';
588 mode_switch_on_key(key_event) {
589 for (let [mode, key] of this.iter_available_modes()) {
590 if (key_event.key == key) {
591 event.preventDefault();
592 tui.switch_mode(mode.name);
604 window_width: terminal.cols / 2,
612 mode_waiting_for_server: new Mode('waiting_for_server',
614 mode_login: new Mode('login', true, false, true),
615 mode_post_login_wait: new Mode('post_login_wait'),
616 mode_chat: new Mode('chat', true),
617 mode_annotate: new Mode('annotate', true, true),
618 mode_play: new Mode('play'),
619 mode_study: new Mode('study', false, true),
620 mode_write: new Mode('write', false, false, false, true),
621 mode_edit: new Mode('edit'),
622 mode_control_pw_type: new Mode('control_pw_type', true),
623 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
624 mode_portal: new Mode('portal', true, true),
625 mode_password: new Mode('password', true),
626 mode_name_thing: new Mode('name_thing', true, true),
627 mode_command_thing: new Mode('command_thing', true),
628 mode_take_thing: new Mode('take_thing', true),
629 mode_admin_enter: new Mode('admin_enter', true),
630 mode_admin: new Mode('admin'),
631 mode_control_pw_pw: new Mode('control_pw_pw', true),
632 mode_control_tile_type: new Mode('control_tile_type', true),
633 mode_control_tile_draw: new Mode('control_tile_draw'),
635 'flatten': 'FLATTEN_SURROUNDINGS',
636 'take_thing': 'PICK_UP',
637 'drop_thing': 'DROP',
640 'command': 'COMMAND',
641 'consume': 'INTOXICATE',
646 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
647 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
648 "command_thing", "take_thing"]
649 this.mode_play.available_actions = ["move", "drop_thing",
650 "teleport", "door", "consume"];
651 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
652 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
653 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
654 "control_tile_type", "chat",
655 "study", "play", "edit"]
656 this.mode_admin.available_actions = ["move"];
657 this.mode_control_tile_draw.available_modes = ["admin_enter"]
658 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
659 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
660 "password", "chat", "study", "play",
662 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
663 this.mode = this.mode_waiting_for_server;
664 this.inputEl = document.getElementById("input");
665 this.inputEl.focus();
666 this.recalc_input_lines();
667 this.height_header = this.height_turn_line + this.height_mode_line;
668 this.log_msg("@ waiting for server connection ...");
671 init_keys: function() {
672 document.getElementById("move_table").hidden = true;
674 for (let key_selector of key_selectors) {
675 this.keys[key_selector.id.slice(4)] = key_selector.value;
677 this.movement_keys = {};
678 let geometry_prefix = 'undefinedMapGeometry_';
679 if (game.map_geometry) {
680 geometry_prefix = game.map_geometry.toLowerCase() + '_';
682 for (const key_name of Object.keys(key_descriptions)) {
683 if (key_name.startsWith(geometry_prefix)) {
684 let direction = key_name.split('_')[2].toUpperCase();
685 let key = this.keys[key_name];
686 this.movement_keys[key] = direction;
689 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
690 if (move_button.id.startsWith('key_')) {
693 move_button.hidden = true;
695 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
696 document.getElementById("move_table").hidden = false;
697 move_button.hidden = false;
699 for (let el of document.getElementsByTagName("button")) {
700 let action_desc = key_descriptions[el.id];
701 let action_key = '[' + this.keys[el.id] + ']';
702 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
705 task_action_on: function(action) {
706 return game.tasks.includes(this.action_tasks[action]);
708 switch_mode: function(mode_name) {
709 if (this.mode.name == 'control_tile_draw') {
710 tui.log_msg('@ finished tile protection drawing.')
712 this.tile_draw = false;
713 if (mode_name == 'admin_enter' && this.is_admin) {
715 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
716 let player_position = game.things[game.player_id].position;
718 for (let t_id in game.things) {
719 if (t_id == game.player_id) {
722 let t = game.things[t_id];
723 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
729 terminal.blink_screen();
730 this.log_msg('? not standing over thing');
733 this.selected_thing_id = thing_id;
736 this.mode = this['mode_' + mode_name];
737 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
738 this.map_mode = 'protections';
739 } else if (this.mode.name != "edit") {
740 this.map_mode = 'terrain + things';
742 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
743 this.inputEl.focus();
745 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
746 explorer.position = game.things[game.player_id].position;
748 this.inputEl.value = "";
749 this.restore_input_values();
750 for (let el of document.getElementsByTagName("button")) {
753 document.getElementById("help").disabled = false;
754 for (const action of this.mode.available_actions) {
755 if (["move", "move_explorer"].includes(action)) {
756 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
757 move_key.disabled = false;
759 } else if (Object.keys(this.action_tasks).includes(action)) {
760 if (this.task_action_on(action)) {
761 document.getElementById(action).disabled = false;
764 document.getElementById(action).disabled = false;
767 for (const mode_name of this.mode.available_modes) {
768 document.getElementById('switch_to_' + mode_name).disabled = false;
770 if (this.mode.name == 'login') {
771 if (this.login_name) {
772 server.send(['LOGIN', this.login_name]);
774 this.log_msg("? need login name");
776 } else if (this.mode.is_single_char_entry) {
777 this.show_help = true;
778 } else if (this.mode.name == 'take_thing') {
779 this.log_msg("selectable things:");
780 const player = game.things[game.player_id];
781 let selectables = [];
782 for (const t_id in game.things) {
783 const t = game.things[t_id];
784 if (t.position[0] == player.position[0]
785 && t.position[1] == player.position[1]
786 && t != player && t.type_ != 'Player') {
787 selectables.push([t_id, t]);
790 if (selectables.length == 0) {
793 for (const t of selectables) {
794 this.log_msg(t[0] + ' ' + explorer.get_thing_info(t[1]));
797 } else if (this.mode.name == 'command_thing') {
798 server.send(['TASK:COMMAND', 'HELP']);
799 } else if (this.mode.name == 'admin_enter') {
800 this.log_msg('@ enter admin password:')
801 } else if (this.mode.name == 'control_pw_type') {
802 this.log_msg('@ enter protection character for which you want to change the password:')
803 } else if (this.mode.name == 'control_tile_type') {
804 this.log_msg('@ enter protection character which you want to draw:')
805 } else if (this.mode.name == 'admin_thing_protect') {
806 this.log_msg('@ enter thing protection character:')
807 } else if (this.mode.name == 'control_pw_pw') {
808 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
809 } else if (this.mode.name == 'control_tile_draw') {
810 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 + '].')
814 offset_links: function(offset, links) {
815 for (let y in links) {
816 let real_y = offset[0] + parseInt(y);
817 if (!this.links[real_y]) {
818 this.links[real_y] = [];
820 for (let link of links[y]) {
821 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
822 this.links[real_y].push(offset_link);
826 restore_input_values: function() {
827 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
828 let info = explorer.annotations[explorer.position];
829 if (info != "(none)") {
830 this.inputEl.value = info;
832 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
833 let portal = game.portals[explorer.position]
834 this.inputEl.value = portal;
835 } else if (this.mode.name == 'password') {
836 this.inputEl.value = this.password;
837 } else if (this.mode.name == 'name_thing') {
838 let t = game.get_thing(this.selected_thing_id);
840 this.inputEl.value = t.name_;
842 } else if (this.mode.name == 'admin_thing_protect') {
843 let t = game.get_thing(this.selected_thing_id);
844 if (t && t.protection) {
845 this.inputEl.value = t.protection;
849 recalc_input_lines: function() {
850 if (this.mode.has_input_prompt) {
852 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
854 this.input_lines = [];
856 this.height_input = this.input_lines.length;
858 msg_into_lines_of_width: function(msg, width) {
859 function push_inner_link(y, end_x) {
860 if (!inner_links[y]) {
863 inner_links[y].push([url_start_x, end_x, url]);
865 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
868 for (const match of matches) {
869 const url = match[0];
870 const url_start = match.index;
871 const url_end = match.index + match[0].length;
872 link_data[url_start] = url;
873 url_ends.push(url_end);
877 let inner_links = {};
881 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
882 if (x >= width || msg[i] == "\n") {
884 push_inner_link(y, chunk.length);
886 if (url_ends[0] == i) {
894 if (msg[i] == "\n") {
899 if (msg[i] != "\n") {
902 if (i in link_data) {
906 } else if (url_ends[0] == i) {
908 push_inner_link(y, x);
914 push_inner_link(lines.length - 1, chunk.length);
916 return [lines, inner_links];
918 log_msg: function(msg) {
920 while (this.log.length > 100) {
925 draw_map: function() {
926 if (!game.turn_complete && this.map_lines.length == 0) {
929 if (game.turn_complete) {
930 let map_lines_split = [];
932 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
933 if (j == game.map_size[1]) {
934 map_lines_split.push(line);
938 if (this.map_mode == 'protections') {
939 line.push(game.map_control[i] + ' ');
941 line.push(game.map[i] + ' ');
944 map_lines_split.push(line);
945 if (this.map_mode == 'terrain + annotations') {
946 for (const coordinate of explorer.info_hints) {
947 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
949 } else if (this.map_mode == 'terrain + things') {
950 for (const p in game.portals) {
951 let coordinate = p.split(',')
952 let original = map_lines_split[coordinate[0]][coordinate[1]];
953 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
955 let used_positions = [];
956 function draw_thing(t, used_positions) {
957 let symbol = game.thing_types[t.type_];
960 meta_char = t.thing_char;
962 if (used_positions.includes(t.position.toString())) {
965 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
966 used_positions.push(t.position.toString());
968 for (const thing_id in game.things) {
969 let t = game.things[thing_id];
970 if (t.type_ != 'Player') {
971 draw_thing(t, used_positions);
974 for (const thing_id in game.things) {
975 let t = game.things[thing_id];
976 if (t.type_ == 'Player') {
977 draw_thing(t, used_positions);
981 let player = game.things[game.player_id];
982 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
983 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
984 } else if (tui.map_mode != 'terrain + things') {
985 map_lines_split[player.position[0]][player.position[1]] = '??';
988 if (game.map_geometry == 'Square') {
989 for (let line_split of map_lines_split) {
990 this.map_lines.push(line_split.join(''));
992 } else if (game.map_geometry == 'Hex') {
994 for (let line_split of map_lines_split) {
995 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1003 let window_center = [terminal.rows / 2, this.window_width / 2];
1004 let center_position = [player.position[0], player.position[1]];
1005 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1006 center_position = [explorer.position[0], explorer.position[1]];
1008 center_position[1] = center_position[1] * 2;
1009 this.offset = [center_position[0] - window_center[0],
1010 center_position[1] - window_center[1]]
1011 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1012 this.offset[1] += 1;
1015 let term_y = Math.max(0, -this.offset[0]);
1016 let term_x = Math.max(0, -this.offset[1]);
1017 let map_y = Math.max(0, this.offset[0]);
1018 let map_x = Math.max(0, this.offset[1]);
1019 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
1020 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1021 terminal.write(term_y, term_x, to_draw);
1024 draw_mode_line: function() {
1025 let help = 'hit [' + this.keys.help + '] for help';
1026 if (this.mode.has_input_prompt) {
1027 help = 'enter /help for help';
1029 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1031 draw_turn_line: function(n) {
1032 if (game.turn_complete) {
1033 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1036 draw_history: function() {
1037 let log_display_lines = [];
1039 let y_offset_in_log = 0;
1040 for (let line of this.log) {
1041 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1043 log_display_lines = log_display_lines.concat(new_lines);
1044 for (const y in link_data) {
1045 const rel_y = y_offset_in_log + parseInt(y);
1046 log_links[rel_y] = [];
1047 for (let link of link_data[y]) {
1048 log_links[rel_y].push(link);
1051 y_offset_in_log += new_lines.length;
1053 let i = log_display_lines.length - 1;
1054 for (let y = terminal.rows - 1 - this.height_input;
1055 y >= this.height_header && i >= 0;
1057 terminal.write(y, this.window_width, log_display_lines[i]);
1059 for (const key of Object.keys(log_links)) {
1060 if (parseInt(key) <= i) {
1061 delete log_links[key];
1064 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1066 this.offset_links(offset, log_links);
1068 draw_info: function() {
1069 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1070 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1071 let offset = [this.height_header, this.window_width];
1072 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1073 terminal.write(y, offset[1], lines[i]);
1075 this.offset_links(offset, link_data);
1077 draw_input: function() {
1078 if (this.mode.has_input_prompt) {
1079 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1080 terminal.write(y, this.window_width, this.input_lines[i]);
1084 draw_help: function() {
1085 let movement_keys_desc = '';
1086 if (!this.mode.is_intro) {
1087 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1089 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1090 if (this.mode.name == 'chat') {
1091 content += '/nick NAME – re-name yourself to NAME\n';
1092 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1093 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1094 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1095 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1096 } else if (this.mode.available_actions.length > 0) {
1097 content += "Available actions:\n";
1098 for (let action of this.mode.available_actions) {
1099 if (Object.keys(this.action_tasks).includes(action)) {
1100 if (!this.task_action_on(action)) {
1104 if (action == 'move_explorer') {
1107 if (action == 'move') {
1108 content += "[" + movement_keys_desc + "] – move\n"
1110 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1115 content += this.mode.list_available_modes();
1117 if (!this.mode.has_input_prompt) {
1118 start_x = this.window_width
1120 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1121 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1122 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1123 terminal.write(y, start_x, lines[i]);
1126 toggle_tile_draw: function() {
1127 if (tui.tile_draw) {
1128 tui.tile_draw = false;
1130 tui.tile_draw = true;
1133 toggle_map_mode: function() {
1134 if (tui.map_mode == 'terrain only') {
1135 tui.map_mode = 'terrain + annotations';
1136 } else if (tui.map_mode == 'terrain + annotations') {
1137 tui.map_mode = 'terrain + things';
1138 } else if (tui.map_mode == 'terrain + things') {
1139 tui.map_mode = 'protections';
1140 } else if (tui.map_mode == 'protections') {
1141 tui.map_mode = 'terrain only';
1144 full_refresh: function() {
1146 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1147 this.recalc_input_lines();
1148 if (this.mode.is_intro) {
1149 this.draw_history();
1153 this.draw_turn_line();
1154 this.draw_mode_line();
1155 if (this.mode.shows_info) {
1158 this.draw_history();
1162 if (this.show_help) {
1174 this.map_control = "";
1175 this.map_size = [0,0];
1176 this.player_id = -1;
1180 get_thing: function(id_, create_if_not_found=false) {
1181 if (id_ in game.things) {
1182 return game.things[id_];
1183 } else if (create_if_not_found) {
1184 let t = new Thing([0,0]);
1185 game.things[id_] = t;
1189 move: function(start_position, direction) {
1190 let target = [start_position[0], start_position[1]];
1191 if (direction == 'LEFT') {
1193 } else if (direction == 'RIGHT') {
1195 } else if (game.map_geometry == 'Square') {
1196 if (direction == 'UP') {
1198 } else if (direction == 'DOWN') {
1201 } else if (game.map_geometry == 'Hex') {
1202 let start_indented = start_position[0] % 2;
1203 if (direction == 'UPLEFT') {
1205 if (!start_indented) {
1208 } else if (direction == 'UPRIGHT') {
1210 if (start_indented) {
1213 } else if (direction == 'DOWNLEFT') {
1215 if (!start_indented) {
1218 } else if (direction == 'DOWNRIGHT') {
1220 if (start_indented) {
1225 if (target[0] < 0 || target[1] < 0 ||
1226 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1231 teleport: function() {
1232 let player = this.get_thing(game.player_id);
1233 if (player.position in this.portals) {
1234 server.reconnect_to(this.portals[player.position]);
1236 terminal.blink_screen();
1237 tui.log_msg('? not standing on portal')
1245 server.init(websocket_location);
1251 move: function(direction) {
1252 let target = game.move(this.position, direction);
1254 this.position = target
1255 this.info_cached = false;
1256 if (tui.tile_draw) {
1257 this.send_tile_control_command();
1260 terminal.blink_screen();
1263 update_annotations: function(yx, str) {
1264 this.annotations[yx] = str;
1265 if (tui.mode.name == 'study') {
1269 empty_annotations: function() {
1270 this.annotations = {};
1271 if (tui.mode.name == 'study') {
1275 get_info: function() {
1276 if (this.info_cached) {
1277 return this.info_cached;
1279 let info_to_cache = '';
1280 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1281 if (game.fov[position_i] != '.') {
1282 info_to_cache += 'outside field of view';
1284 let terrain_char = game.map[position_i]
1285 let terrain_desc = '?'
1286 if (game.terrains[terrain_char]) {
1287 terrain_desc = game.terrains[terrain_char];
1289 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1290 let protection = game.map_control[position_i];
1291 if (protection == '.') {
1292 protection = 'unprotected';
1294 info_to_cache += 'PROTECTION: ' + protection + '\n';
1295 for (let t_id in game.things) {
1296 let t = game.things[t_id];
1297 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1298 info_to_cache += "THING: " + this.get_thing_info(t);
1299 let protection = t.protection;
1300 if (protection == '.') {
1301 protection = 'none';
1303 info_to_cache += " / protection: " + protection + "\n";
1306 if (this.position in game.portals) {
1307 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1309 if (this.position in this.annotations) {
1310 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1313 this.info_cached = info_to_cache;
1314 return this.info_cached;
1316 get_thing_info: function(t) {
1317 const symbol = game.thing_types[t.type_];
1318 let info = t.type_ + " / " + symbol;
1320 info += t.thing_char;
1323 info += " (" + t.name_ + ")";
1327 annotate: function(msg) {
1328 if (msg.length == 0) {
1329 msg = " "; // triggers annotation deletion
1331 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1333 set_portal: function(msg) {
1334 if (msg.length == 0) {
1335 msg = " "; // triggers portal deletion
1337 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1339 send_tile_control_command: function() {
1340 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1344 tui.inputEl.addEventListener('input', (event) => {
1345 if (tui.mode.has_input_prompt) {
1346 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1347 if (tui.inputEl.value.length > max_length) {
1348 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1350 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1351 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1352 tui.switch_mode('edit');
1356 document.onclick = function() {
1357 tui.show_help = false;
1359 tui.inputEl.addEventListener('keydown', (event) => {
1360 tui.show_help = false;
1361 if (event.key == 'Enter') {
1362 event.preventDefault();
1364 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1365 tui.show_help = true;
1366 tui.inputEl.value = "";
1367 tui.restore_input_values();
1368 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1369 && !tui.mode.is_single_char_entry) {
1370 tui.show_help = true;
1371 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1372 tui.login_name = tui.inputEl.value;
1373 server.send(['LOGIN', tui.inputEl.value]);
1374 tui.inputEl.value = "";
1375 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1376 if (tui.inputEl.value.length == 0) {
1377 tui.log_msg('@ aborted');
1378 tui.switch_mode('play');
1379 } else if (tui.task_action_on('command')) {
1380 server.send(['TASK:COMMAND', tui.inputEl.value]);
1381 tui.inputEl.value = "";
1383 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1384 if (tui.inputEl.value.length == 0) {
1385 tui.log_msg('@ aborted');
1387 server.send(['TASK:PICK_UP', tui.inputEl.value]);
1389 tui.inputEl.value = "";
1390 tui.switch_mode('play');
1391 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1392 if (tui.inputEl.value.length == 0) {
1393 tui.log_msg('@ aborted');
1395 server.send(['SET_MAP_CONTROL_PASSWORD',
1396 tui.tile_control_char, tui.inputEl.value]);
1397 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1399 tui.switch_mode('admin');
1400 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1401 explorer.set_portal(tui.inputEl.value);
1402 tui.switch_mode('edit');
1403 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1404 if (tui.inputEl.value.length == 0) {
1405 tui.inputEl.value = " ";
1407 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1409 tui.switch_mode('edit');
1410 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1411 explorer.annotate(tui.inputEl.value);
1412 tui.switch_mode('edit');
1413 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1414 if (tui.inputEl.value.length == 0) {
1415 tui.inputEl.value = " ";
1417 tui.password = tui.inputEl.value
1418 tui.switch_mode('edit');
1419 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1420 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1421 tui.switch_mode('play');
1422 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1423 if (tui.inputEl.value.length != 1) {
1424 tui.log_msg('@ entered non-single-char, therefore aborted');
1425 tui.switch_mode('admin');
1427 tui.tile_control_char = tui.inputEl.value[0];
1428 tui.switch_mode('control_pw_pw');
1430 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1431 if (tui.inputEl.value.length != 1) {
1432 tui.log_msg('@ entered non-single-char, therefore aborted');
1433 tui.switch_mode('admin');
1435 tui.tile_control_char = tui.inputEl.value[0];
1436 tui.switch_mode('control_tile_draw');
1438 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1439 if (tui.inputEl.value.length != 1) {
1440 tui.log_msg('@ entered non-single-char, therefore aborted');
1442 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1443 tui.log_msg('@ sent new protection character for thing');
1445 tui.switch_mode('admin');
1446 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1447 let tokens = parser.tokenize(tui.inputEl.value);
1448 if (tokens.length > 0 && tokens[0].length > 0) {
1449 if (tui.inputEl.value[0][0] == '/') {
1450 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1451 tui.switch_mode('play');
1452 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1453 tui.switch_mode('study');
1454 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1455 tui.switch_mode('edit');
1456 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1457 tui.switch_mode('admin_enter');
1458 } else if (tokens[0].slice(1) == 'nick') {
1459 if (tokens.length > 1) {
1460 server.send(['NICK', tokens[1]]);
1462 tui.log_msg('? need new name');
1465 tui.log_msg('? unknown command');
1468 server.send(['ALL', tui.inputEl.value]);
1470 } else if (tui.inputEl.valuelength > 0) {
1471 server.send(['ALL', tui.inputEl.value]);
1473 tui.inputEl.value = "";
1474 } else if (tui.mode.name == 'play') {
1475 if (tui.mode.mode_switch_on_key(event)) {
1477 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1478 server.send(["TASK:DROP"]);
1479 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1480 server.send(["TASK:INTOXICATE"]);
1481 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1482 server.send(["TASK:DOOR"]);
1483 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1484 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1485 } else if (event.key === tui.keys.teleport) {
1488 } else if (tui.mode.name == 'study') {
1489 if (tui.mode.mode_switch_on_key(event)) {
1491 } else if (event.key in tui.movement_keys) {
1492 explorer.move(tui.movement_keys[event.key]);
1493 } else if (event.key == tui.keys.toggle_map_mode) {
1494 tui.toggle_map_mode();
1496 } else if (tui.mode.name == 'control_tile_draw') {
1497 if (tui.mode.mode_switch_on_key(event)) {
1499 } else if (event.key in tui.movement_keys) {
1500 explorer.move(tui.movement_keys[event.key]);
1501 } else if (event.key === tui.keys.toggle_tile_draw) {
1502 tui.toggle_tile_draw();
1504 } else if (tui.mode.name == 'admin') {
1505 if (tui.mode.mode_switch_on_key(event)) {
1507 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1508 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1510 } else if (tui.mode.name == 'edit') {
1511 if (tui.mode.mode_switch_on_key(event)) {
1513 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1514 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1515 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1516 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1517 } else if (event.key == tui.keys.toggle_map_mode) {
1518 tui.toggle_map_mode();
1524 rows_selector.addEventListener('input', function() {
1525 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1528 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1529 terminal.initialize();
1532 cols_selector.addEventListener('input', function() {
1533 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1536 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1537 terminal.initialize();
1538 tui.window_width = terminal.cols / 2,
1541 for (let key_selector of key_selectors) {
1542 key_selector.addEventListener('input', function() {
1543 window.localStorage.setItem(key_selector.id, key_selector.value);
1547 window.setInterval(function() {
1548 if (server.connected) {
1549 server.send(['PING']);
1551 server.reconnect_to(server.url);
1552 tui.log_msg('@ attempting reconnect …')
1555 window.setInterval(function() {
1557 let span_decoration = "none";
1558 if (document.activeElement == tui.inputEl) {
1559 val = "on (click outside terminal to change)";
1561 val = "off (click into terminal to change)";
1562 span_decoration = "line-through";
1564 document.getElementById("keyboard_control").textContent = val;
1565 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1566 span.style.textDecoration = span_decoration;
1569 document.getElementById("terminal").onclick = function() {
1570 tui.inputEl.focus();
1572 document.getElementById("help").onclick = function() {
1573 tui.show_help = true;
1576 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1577 const mode = switchEl.id.slice("switch_to_".length);
1578 switchEl.onclick = function() {
1579 tui.switch_mode(mode);
1583 document.getElementById("toggle_tile_draw").onclick = function() {
1584 tui.toggle_tile_draw();
1586 document.getElementById("toggle_map_mode").onclick = function() {
1587 tui.toggle_map_mode();
1590 document.getElementById("drop_thing").onclick = function() {
1591 server.send(['TASK:DROP']);
1593 document.getElementById("flatten").onclick = function() {
1594 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1596 document.getElementById("door").onclick = function() {
1597 server.send(['TASK:DOOR']);
1599 document.getElementById("consume").onclick = function() {
1600 server.send(['TASK:INTOXICATE']);
1602 document.getElementById("teleport").onclick = function() {
1605 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1606 let direction = move_button.id.split('_')[2].toUpperCase();
1607 move_button.onclick = function() {
1608 if (tui.mode.available_actions.includes("move")
1609 || tui.mode.available_actions.includes("move_explorer")) {
1610 server.send(['TASK:MOVE', direction]);
1612 explorer.move(direction);