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>
60 <button id="install"></button>
64 <td><button id="switch_to_edit"></button></td>
66 <button id="switch_to_write"></button>
67 <button id="flatten"></button>
68 <button id="switch_to_annotate"></button>
69 <button id="switch_to_portal"></button>
70 <button id="switch_to_name_thing"></button>
71 <button id="switch_to_password"></button>
75 <td><button id="switch_to_admin_enter"></button></td>
77 <button id="switch_to_control_pw_type"></button>
78 <button id="switch_to_control_tile_type"></button>
79 <button id="switch_to_admin_thing_protect"></button>
80 <button id="toggle_tile_draw"></button>
85 <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 />
87 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
88 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
89 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
90 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
91 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
92 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
93 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
94 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
95 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
96 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
97 <li>help: <input id="key_help" type="text" value="h" />
98 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
99 <li>teleport: <input id="key_teleport" type="text" value="p" />
100 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
101 <li>open/close: <input id="key_door" type="text" value="D" />
102 <li>consume: <input id="key_consume" type="text" value="C" />
103 <li>install: <input id="key_install" type="text" value="I" />
104 <li><input id="key_switch_to_take_thing" type="text" value="z" />
105 <li><input id="key_switch_to_chat" type="text" value="t" />
106 <li><input id="key_switch_to_play" type="text" value="p" />
107 <li><input id="key_switch_to_study" type="text" value="?" />
108 <li><input id="key_switch_to_edit" type="text" value="E" />
109 <li><input id="key_switch_to_write" type="text" value="m" />
110 <li><input id="key_switch_to_name_thing" type="text" value="N" />
111 <li><input id="key_switch_to_command_thing" type="text" value="O" />
112 <li><input id="key_switch_to_password" type="text" value="P" />
113 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
114 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
115 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
116 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
117 <li><input id="key_switch_to_annotate" type="text" value="M" />
118 <li><input id="key_switch_to_portal" type="text" value="T" />
119 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
120 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
125 let websocket_location = "wss://plomlompom.com/rogue_chat/";
126 //let websocket_location = "ws://localhost:8001/";
132 'long': 'This mode allows you to interact with the map in various ways.'
137 '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.'},
139 'short': 'world edit',
141 '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.'
144 'short': 'name thing',
146 'long': 'Give name to/change name of thing here.'
149 'short': 'command thing',
151 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
154 'short': 'take thing',
155 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
156 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
158 'admin_thing_protect': {
159 'short': 'change thing protection',
160 'intro': '@ enter thing protection character:',
161 'long': 'Change protection character for thing here.'
164 'short': 'change terrain',
166 '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.'
169 'short': 'change protection character password',
170 'intro': '@ enter protection character for which you want to change the password:',
171 '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.'
174 'short': 'change protection character password',
176 '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.'
178 'control_tile_type': {
179 'short': 'change tiles protection',
180 'intro': '@ enter protection character which you want to draw:',
181 '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.'
183 'control_tile_draw': {
184 'short': 'change tiles protection',
186 '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.'
189 'short': 'annotate tile',
191 '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.'
194 'short': 'edit portal',
196 '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.'
201 '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'
206 'long': 'Enter your player name.'
208 'waiting_for_server': {
209 'short': 'waiting for server response',
210 'intro': '@ waiting for server …',
211 'long': 'Waiting for a server response.'
214 'short': 'waiting for server response',
216 'long': 'Waiting for a server response.'
219 'short': 'set world edit password',
221 '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.'
224 'short': 'become admin',
225 'intro': '@ enter admin password:',
226 'long': 'This mode allows you to become admin if you know an admin password.'
231 'long': 'This mode allows you access to actions limited to administrators.'
234 let key_descriptions = {
236 'flatten': 'flatten surroundings',
237 'teleport': 'teleport',
238 'drop_thing': 'drop thing',
239 'door': 'open/close',
240 'consume': 'consume',
241 'install': 'install',
242 'toggle_map_mode': 'toggle map view',
243 'toggle_tile_draw': 'toggle protection character drawing',
244 'hex_move_upleft': 'up-left',
245 'hex_move_upright': 'up-right',
246 'hex_move_right': 'right',
247 'hex_move_left': 'left',
248 'hex_move_downleft': 'down-left',
249 'hex_move_downright': 'down-right',
250 'square_move_up': 'up',
251 'square_move_left': 'left',
252 'square_move_down': 'down',
253 'square_move_right': 'right',
255 for (const mode_name of Object.keys(mode_helps)) {
256 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
259 let rows_selector = document.getElementById("n_rows");
260 let cols_selector = document.getElementById("n_cols");
261 let key_selectors = document.querySelectorAll('[id^="key_"]');
263 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
264 const action = key_switch_selector.id.slice("key_switch_to_".length);
265 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
268 function restore_selector_value(selector) {
269 let stored_selection = window.localStorage.getItem(selector.id);
270 if (stored_selection) {
271 selector.value = stored_selection;
274 restore_selector_value(rows_selector);
275 restore_selector_value(cols_selector);
276 for (let key_selector of key_selectors) {
277 restore_selector_value(key_selector);
280 function escapeHTML(str) {
282 replace(/&/g, '&').
283 replace(/</g, '<').
284 replace(/>/g, '>').
285 replace(/'/g, ''').
286 replace(/"/g, '"');
290 initialize: function() {
291 this.rows = rows_selector.value;
292 this.cols = cols_selector.value;
293 this.pre_el = document.getElementById("terminal");
294 this.set_default_colors();
298 for (let y = 0, x = 0; y <= this.rows; x++) {
299 if (x == this.cols) {
302 this.content.push(line);
304 if (y == this.rows) {
311 apply_colors: function() {
312 this.pre_el.style.color = this.foreground;
313 this.pre_el.style.backgroundColor = this.background;
315 set_default_colors: function() {
316 this.foreground = 'white';
317 this.background = 'black';
320 set_random_colors: function() {
321 function rand(offset) {
322 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
324 this.foreground = '#' + rand(159) + rand(159) + rand(159);
325 this.background = '#' + rand(0) + rand(0) + rand(0);
328 blink_screen: function() {
329 this.pre_el.style.color = this.background;
330 this.pre_el.style.backgroundColor = this.foreground;
332 this.pre_el.style.color = this.foreground;
333 this.pre_el.style.backgroundColor = this.background;
336 refresh: function() {
337 let pre_content = '';
338 for (let y = 0; y < this.rows; y++) {
339 let line = this.content[y].join('');
341 if (y in tui.links) {
343 for (let span of tui.links[y]) {
344 chunks.push(escapeHTML(line.slice(start_x, span[0])));
345 chunks.push('<a target="_blank" href="');
346 chunks.push(escapeHTML(span[2]));
348 chunks.push(escapeHTML(line.slice(span[0], span[1])));
352 chunks.push(escapeHTML(line.slice(start_x)));
354 chunks = [escapeHTML(line)];
356 for (const chunk of chunks) {
357 pre_content += chunk;
361 this.pre_el.innerHTML = pre_content;
363 write: function(start_y, start_x, msg) {
364 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
365 this.content[start_y][x] = msg[i];
368 drawBox: function(start_y, start_x, height, width) {
369 let end_y = start_y + height;
370 let end_x = start_x + width;
371 for (let y = start_y, x = start_x; y < this.rows; x++) {
379 this.content[y][x] = ' ';
383 terminal.initialize();
386 tokenize: function(str) {
391 for (let i = 0; i < str.length; i++) {
397 } else if (c == '\\') {
399 } else if (c == '"') {
404 } else if (c == '"') {
406 } else if (c === ' ') {
407 if (token.length > 0) {
415 if (token.length > 0) {
420 parse_yx: function(position_string) {
421 let coordinate_strings = position_string.split(',')
422 let position = [0, 0];
423 position[0] = parseInt(coordinate_strings[0].slice(2));
424 position[1] = parseInt(coordinate_strings[1].slice(2));
436 init: function(url) {
438 this.websocket = new WebSocket(this.url);
439 this.websocket.onopen = function(event) {
440 server.connected = true;
441 game.thing_types = {};
443 server.send(['TASKS']);
444 server.send(['TERRAINS']);
445 server.send(['THING_TYPES']);
446 tui.log_msg("@ server connected! :)");
447 tui.switch_mode('login');
449 this.websocket.onclose = function(event) {
450 server.connected = false;
451 tui.switch_mode('waiting_for_server');
452 tui.log_msg("@ server disconnected :(");
454 this.websocket.onmessage = this.handle_event;
456 reconnect_to: function(url) {
457 this.websocket.close();
460 send: function(tokens) {
461 this.websocket.send(unparser.untokenize(tokens));
463 handle_event: function(event) {
464 let tokens = parser.tokenize(event.data);
465 if (tokens[0] === 'TURN') {
466 game.turn_complete = false;
467 explorer.empty_annotations();
471 game.turn = parseInt(tokens[1]);
472 } else if (tokens[0] === 'THING') {
473 let t = game.get_thing(tokens[4], true);
474 t.position = parser.parse_yx(tokens[1]);
476 t.protection = tokens[3];
477 } else if (tokens[0] === 'THING_NAME') {
478 let t = game.get_thing(tokens[1], false);
482 } else if (tokens[0] === 'THING_CHAR') {
483 let t = game.get_thing(tokens[1], false);
485 t.thing_char = tokens[2];
487 } else if (tokens[0] === 'TASKS') {
488 game.tasks = tokens[1].split(',');
489 tui.mode_write.legal = game.tasks.includes('WRITE');
490 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
491 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
492 } else if (tokens[0] === 'THING_TYPE') {
493 game.thing_types[tokens[1]] = tokens[2]
494 } else if (tokens[0] === 'THING_CARRYING') {
495 let t = game.get_thing(tokens[1], false);
499 } else if (tokens[0] === 'TERRAIN') {
500 game.terrains[tokens[1]] = tokens[2]
501 } else if (tokens[0] === 'MAP') {
502 game.map_geometry = tokens[1];
504 game.map_size = parser.parse_yx(tokens[2]);
506 } else if (tokens[0] === 'FOV') {
508 } else if (tokens[0] === 'MAP_CONTROL') {
509 game.map_control = tokens[1]
510 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
511 game.turn_complete = true;
512 if (tui.mode.name == 'post_login_wait') {
513 tui.switch_mode('play');
515 explorer.info_cached = false;
517 } else if (tokens[0] === 'CHAT') {
518 tui.log_msg('# ' + tokens[1], 1);
519 } else if (tokens[0] === 'REPLY') {
520 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
521 } else if (tokens[0] === 'PLAYER_ID') {
522 game.player_id = parseInt(tokens[1]);
523 } else if (tokens[0] === 'LOGIN_OK') {
524 this.send(['GET_GAMESTATE']);
525 tui.switch_mode('post_login_wait');
526 } else if (tokens[0] === 'DEFAULT_COLORS') {
527 terminal.set_default_colors();
528 } else if (tokens[0] === 'RANDOM_COLORS') {
529 terminal.set_random_colors();
530 } else if (tokens[0] === 'ADMIN_OK') {
532 tui.log_msg('@ you now have admin rights');
533 tui.switch_mode('admin');
534 } else if (tokens[0] === 'PORTAL') {
535 let position = parser.parse_yx(tokens[1]);
536 game.portals[position] = tokens[2];
537 } else if (tokens[0] === 'ANNOTATION') {
538 let position = parser.parse_yx(tokens[1]);
539 explorer.update_annotations(position, tokens[2]);
541 } else if (tokens[0] === 'UNHANDLED_INPUT') {
542 tui.log_msg('? unknown command');
543 } else if (tokens[0] === 'PLAY_ERROR') {
544 tui.log_msg('? ' + tokens[1]);
545 terminal.blink_screen();
546 } else if (tokens[0] === 'ARGUMENT_ERROR') {
547 tui.log_msg('? syntax error: ' + tokens[1]);
548 } else if (tokens[0] === 'GAME_ERROR') {
549 tui.log_msg('? game error: ' + tokens[1]);
550 } else if (tokens[0] === 'PONG') {
553 tui.log_msg('? unhandled input: ' + event.data);
559 quote: function(str) {
561 for (let i = 0; i < str.length; i++) {
563 if (['"', '\\'].includes(c)) {
569 return quoted.join('');
571 to_yx: function(yx_coordinate) {
572 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
574 untokenize: function(tokens) {
575 let quoted_tokens = [];
576 for (let token of tokens) {
577 quoted_tokens.push(this.quote(token));
579 return quoted_tokens.join(" ");
584 constructor(name, has_input_prompt=false, shows_info=false,
585 is_intro=false, is_single_char_entry=false) {
587 this.short_desc = mode_helps[name].short;
588 this.available_modes = [];
589 this.available_actions = [];
590 this.has_input_prompt = has_input_prompt;
591 this.shows_info= shows_info;
592 this.is_intro = is_intro;
593 this.help_intro = mode_helps[name].long;
594 this.intro_msg = mode_helps[name].intro;
595 this.is_single_char_entry = is_single_char_entry;
598 *iter_available_modes() {
599 for (let mode_name of this.available_modes) {
600 let mode = tui['mode_' + mode_name];
604 let key = tui.keys['switch_to_' + mode.name];
608 list_available_modes() {
610 if (this.available_modes.length > 0) {
611 msg += 'Other modes available from here:\n';
612 for (let [mode, key] of this.iter_available_modes()) {
613 msg += '[' + key + '] – ' + mode.short_desc + '\n';
618 mode_switch_on_key(key_event) {
619 for (let [mode, key] of this.iter_available_modes()) {
620 if (key_event.key == key) {
621 event.preventDefault();
622 tui.switch_mode(mode.name);
634 window_width: terminal.cols / 2,
642 mode_waiting_for_server: new Mode('waiting_for_server',
644 mode_login: new Mode('login', true, false, true),
645 mode_post_login_wait: new Mode('post_login_wait'),
646 mode_chat: new Mode('chat', true),
647 mode_annotate: new Mode('annotate', true, true),
648 mode_play: new Mode('play'),
649 mode_study: new Mode('study', false, true),
650 mode_write: new Mode('write', false, false, false, true),
651 mode_edit: new Mode('edit'),
652 mode_control_pw_type: new Mode('control_pw_type', true),
653 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
654 mode_portal: new Mode('portal', true, true),
655 mode_password: new Mode('password', true),
656 mode_name_thing: new Mode('name_thing', true, true),
657 mode_command_thing: new Mode('command_thing', true),
658 mode_take_thing: new Mode('take_thing', true),
659 mode_admin_enter: new Mode('admin_enter', true),
660 mode_admin: new Mode('admin'),
661 mode_control_pw_pw: new Mode('control_pw_pw', true),
662 mode_control_tile_type: new Mode('control_tile_type', true),
663 mode_control_tile_draw: new Mode('control_tile_draw'),
665 'flatten': 'FLATTEN_SURROUNDINGS',
666 'take_thing': 'PICK_UP',
667 'drop_thing': 'DROP',
670 'install': 'INSTALL',
671 'command': 'COMMAND',
672 'consume': 'INTOXICATE',
678 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
679 "command_thing", "take_thing"]
680 this.mode_play.available_actions = ["move", "drop_thing",
681 "teleport", "door", "consume", "install"];
682 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
683 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
684 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
685 "control_tile_type", "chat",
686 "study", "play", "edit"]
687 this.mode_admin.available_actions = ["move"];
688 this.mode_control_tile_draw.available_modes = ["admin_enter"]
689 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
690 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
691 "password", "chat", "study", "play",
693 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
694 this.inputEl = document.getElementById("input");
695 this.inputEl.focus();
696 this.switch_mode('waiting_for_server');
697 this.recalc_input_lines();
698 this.height_header = this.height_turn_line + this.height_mode_line;
701 init_keys: function() {
702 document.getElementById("move_table").hidden = true;
704 for (let key_selector of key_selectors) {
705 this.keys[key_selector.id.slice(4)] = key_selector.value;
707 this.movement_keys = {};
708 let geometry_prefix = 'undefinedMapGeometry_';
709 if (game.map_geometry) {
710 geometry_prefix = game.map_geometry.toLowerCase() + '_';
712 for (const key_name of Object.keys(key_descriptions)) {
713 if (key_name.startsWith(geometry_prefix)) {
714 let direction = key_name.split('_')[2].toUpperCase();
715 let key = this.keys[key_name];
716 this.movement_keys[key] = direction;
719 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
720 if (move_button.id.startsWith('key_')) {
723 move_button.hidden = true;
725 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
726 document.getElementById("move_table").hidden = false;
727 move_button.hidden = false;
729 for (let el of document.getElementsByTagName("button")) {
730 let action_desc = key_descriptions[el.id];
731 let action_key = '[' + this.keys[el.id] + ']';
732 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
735 task_action_on: function(action) {
736 return game.tasks.includes(this.action_tasks[action]);
738 switch_mode: function(mode_name) {
739 if (this.mode && this.mode.name == 'control_tile_draw') {
740 tui.log_msg('@ finished tile protection drawing.')
742 this.tile_draw = false;
743 if (mode_name == 'admin_enter' && this.is_admin) {
745 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
746 let player_position = game.things[game.player_id].position;
748 for (let t_id in game.things) {
749 if (t_id == game.player_id) {
752 let t = game.things[t_id];
753 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
759 terminal.blink_screen();
760 this.log_msg('? not standing over thing');
763 this.selected_thing_id = thing_id;
766 this.mode = this['mode_' + mode_name];
767 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
768 this.map_mode = 'protections';
769 } else if (this.mode.name != "edit") {
770 this.map_mode = 'terrain + things';
772 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
773 this.inputEl.focus();
775 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
776 explorer.position = game.things[game.player_id].position;
778 this.inputEl.value = "";
779 this.restore_input_values();
780 for (let el of document.getElementsByTagName("button")) {
783 document.getElementById("help").disabled = false;
784 for (const action of this.mode.available_actions) {
785 if (["move", "move_explorer"].includes(action)) {
786 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
787 move_key.disabled = false;
789 } else if (Object.keys(this.action_tasks).includes(action)) {
790 if (this.task_action_on(action)) {
791 document.getElementById(action).disabled = false;
794 document.getElementById(action).disabled = false;
797 for (const mode_name of this.mode.available_modes) {
798 document.getElementById('switch_to_' + mode_name).disabled = false;
800 if (this.mode.intro_msg.length > 0) {
801 this.log_msg(this.mode.intro_msg);
803 if (this.mode.name == 'login') {
804 if (this.login_name) {
805 server.send(['LOGIN', this.login_name]);
807 this.log_msg("? need login name");
809 } else if (this.mode.is_single_char_entry) {
810 this.show_help = true;
811 } else if (this.mode.name == 'take_thing') {
812 this.log_msg("Things in reach for pick-up:");
813 const player = game.things[game.player_id];
814 const y = player.position[0]
815 const x = player.position[1]
816 let select_range = [y.toString() + ':' + x.toString(),
817 (y + 0).toString() + ':' + (x - 1).toString(),
818 (y + 0).toString() + ':' + (x + 1).toString(),
819 (y - 1).toString() + ':' + (x).toString(),
820 (y + 1).toString() + ':' + (x).toString()];
821 if (game.map_geometry == 'Hex') {
823 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
824 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
826 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
827 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
830 this.selectables = [];
831 for (const t_id in game.things) {
832 const t = game.things[t_id];
833 if (select_range.includes(t.position[0].toString()
834 + ':' + t.position[1].toString())
835 && t != player && t.type_ != 'Player') {
836 this.selectables.push([t_id, t]);
839 if (this.selectables.length == 0) {
842 for (let [i, t] of this.selectables.entries()) {
843 this.log_msg(i + ': ' + explorer.get_thing_info(t[1]));
846 } else if (this.mode.name == 'command_thing') {
847 server.send(['TASK:COMMAND', 'HELP']);
848 } else if (this.mode.name == 'control_pw_pw') {
849 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
850 } else if (this.mode.name == 'control_tile_draw') {
851 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 + '].')
855 offset_links: function(offset, links) {
856 for (let y in links) {
857 let real_y = offset[0] + parseInt(y);
858 if (!this.links[real_y]) {
859 this.links[real_y] = [];
861 for (let link of links[y]) {
862 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
863 this.links[real_y].push(offset_link);
867 restore_input_values: function() {
868 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
869 let info = explorer.annotations[explorer.position];
870 if (info != "(none)") {
871 this.inputEl.value = info;
873 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
874 let portal = game.portals[explorer.position]
875 this.inputEl.value = portal;
876 } else if (this.mode.name == 'password') {
877 this.inputEl.value = this.password;
878 } else if (this.mode.name == 'name_thing') {
879 let t = game.get_thing(this.selected_thing_id);
881 this.inputEl.value = t.name_;
883 } else if (this.mode.name == 'admin_thing_protect') {
884 let t = game.get_thing(this.selected_thing_id);
885 if (t && t.protection) {
886 this.inputEl.value = t.protection;
890 recalc_input_lines: function() {
891 if (this.mode.has_input_prompt) {
893 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
895 this.input_lines = [];
897 this.height_input = this.input_lines.length;
899 msg_into_lines_of_width: function(msg, width) {
900 function push_inner_link(y, end_x) {
901 if (!inner_links[y]) {
904 inner_links[y].push([url_start_x, end_x, url]);
906 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
909 for (const match of matches) {
910 const url = match[0];
911 const url_start = match.index;
912 const url_end = match.index + match[0].length;
913 link_data[url_start] = url;
914 url_ends.push(url_end);
918 let inner_links = {};
922 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
923 if (x >= width || msg[i] == "\n") {
925 push_inner_link(y, chunk.length);
927 if (url_ends[0] == i) {
935 if (msg[i] == "\n") {
940 if (msg[i] != "\n") {
943 if (i in link_data) {
947 } else if (url_ends[0] == i) {
949 push_inner_link(y, x);
955 push_inner_link(lines.length - 1, chunk.length);
957 return [lines, inner_links];
959 log_msg: function(msg) {
961 while (this.log.length > 100) {
966 draw_map: function() {
967 if (!game.turn_complete && this.map_lines.length == 0) {
970 if (game.turn_complete) {
971 let map_lines_split = [];
973 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
974 if (j == game.map_size[1]) {
975 map_lines_split.push(line);
979 if (this.map_mode == 'protections') {
980 line.push(game.map_control[i] + ' ');
982 line.push(game.map[i] + ' ');
985 map_lines_split.push(line);
986 if (this.map_mode == 'terrain + annotations') {
987 for (const coordinate of explorer.info_hints) {
988 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
990 } else if (this.map_mode == 'terrain + things') {
991 for (const p in game.portals) {
992 let coordinate = p.split(',')
993 let original = map_lines_split[coordinate[0]][coordinate[1]];
994 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
996 let used_positions = [];
997 function draw_thing(t, used_positions) {
998 let symbol = game.thing_types[t.type_];
1001 meta_char = t.thing_char;
1003 if (used_positions.includes(t.position.toString())) {
1009 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1010 used_positions.push(t.position.toString());
1012 for (const thing_id in game.things) {
1013 let t = game.things[thing_id];
1014 if (t.type_ != 'Player') {
1015 draw_thing(t, used_positions);
1018 for (const thing_id in game.things) {
1019 let t = game.things[thing_id];
1020 if (t.type_ == 'Player') {
1021 draw_thing(t, used_positions);
1025 let player = game.things[game.player_id];
1026 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1027 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1028 } else if (tui.map_mode != 'terrain + things') {
1029 map_lines_split[player.position[0]][player.position[1]] = '??';
1032 if (game.map_geometry == 'Square') {
1033 for (let line_split of map_lines_split) {
1034 this.map_lines.push(line_split.join(''));
1036 } else if (game.map_geometry == 'Hex') {
1038 for (let line_split of map_lines_split) {
1039 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1047 let window_center = [terminal.rows / 2, this.window_width / 2];
1048 let center_position = [player.position[0], player.position[1]];
1049 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1050 center_position = [explorer.position[0], explorer.position[1]];
1052 center_position[1] = center_position[1] * 2;
1053 this.offset = [center_position[0] - window_center[0],
1054 center_position[1] - window_center[1]]
1055 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1056 this.offset[1] += 1;
1059 let term_y = Math.max(0, -this.offset[0]);
1060 let term_x = Math.max(0, -this.offset[1]);
1061 let map_y = Math.max(0, this.offset[0]);
1062 let map_x = Math.max(0, this.offset[1]);
1063 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
1064 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1065 terminal.write(term_y, term_x, to_draw);
1068 draw_mode_line: function() {
1069 let help = 'hit [' + this.keys.help + '] for help';
1070 if (this.mode.has_input_prompt) {
1071 help = 'enter /help for help';
1073 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1075 draw_turn_line: function(n) {
1076 if (game.turn_complete) {
1077 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1080 draw_history: function() {
1081 let log_display_lines = [];
1083 let y_offset_in_log = 0;
1084 for (let line of this.log) {
1085 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1087 log_display_lines = log_display_lines.concat(new_lines);
1088 for (const y in link_data) {
1089 const rel_y = y_offset_in_log + parseInt(y);
1090 log_links[rel_y] = [];
1091 for (let link of link_data[y]) {
1092 log_links[rel_y].push(link);
1095 y_offset_in_log += new_lines.length;
1097 let i = log_display_lines.length - 1;
1098 for (let y = terminal.rows - 1 - this.height_input;
1099 y >= this.height_header && i >= 0;
1101 terminal.write(y, this.window_width, log_display_lines[i]);
1103 for (const key of Object.keys(log_links)) {
1104 if (parseInt(key) <= i) {
1105 delete log_links[key];
1108 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1110 this.offset_links(offset, log_links);
1112 draw_info: function() {
1113 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1114 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1115 let offset = [this.height_header, this.window_width];
1116 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1117 terminal.write(y, offset[1], lines[i]);
1119 this.offset_links(offset, link_data);
1121 draw_input: function() {
1122 if (this.mode.has_input_prompt) {
1123 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1124 terminal.write(y, this.window_width, this.input_lines[i]);
1128 draw_help: function() {
1129 let movement_keys_desc = '';
1130 if (!this.mode.is_intro) {
1131 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1133 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1134 if (this.mode.available_actions.length > 0) {
1135 content += "Available actions:\n";
1136 for (let action of this.mode.available_actions) {
1137 if (Object.keys(this.action_tasks).includes(action)) {
1138 if (!this.task_action_on(action)) {
1142 if (action == 'move_explorer') {
1145 if (action == 'move') {
1146 content += "[" + movement_keys_desc + "] – move\n"
1148 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1153 content += this.mode.list_available_modes();
1155 if (!this.mode.has_input_prompt) {
1156 start_x = this.window_width
1158 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1159 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1160 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1161 terminal.write(y, start_x, lines[i]);
1164 toggle_tile_draw: function() {
1165 if (tui.tile_draw) {
1166 tui.tile_draw = false;
1168 tui.tile_draw = true;
1171 toggle_map_mode: function() {
1172 if (tui.map_mode == 'terrain only') {
1173 tui.map_mode = 'terrain + annotations';
1174 } else if (tui.map_mode == 'terrain + annotations') {
1175 tui.map_mode = 'terrain + things';
1176 } else if (tui.map_mode == 'terrain + things') {
1177 tui.map_mode = 'protections';
1178 } else if (tui.map_mode == 'protections') {
1179 tui.map_mode = 'terrain only';
1182 full_refresh: function() {
1184 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1185 this.recalc_input_lines();
1186 if (this.mode.is_intro) {
1187 this.draw_history();
1191 this.draw_turn_line();
1192 this.draw_mode_line();
1193 if (this.mode.shows_info) {
1196 this.draw_history();
1200 if (this.show_help) {
1212 this.map_control = "";
1213 this.map_size = [0,0];
1214 this.player_id = -1;
1218 get_thing: function(id_, create_if_not_found=false) {
1219 if (id_ in game.things) {
1220 return game.things[id_];
1221 } else if (create_if_not_found) {
1222 let t = new Thing([0,0]);
1223 game.things[id_] = t;
1227 move: function(start_position, direction) {
1228 let target = [start_position[0], start_position[1]];
1229 if (direction == 'LEFT') {
1231 } else if (direction == 'RIGHT') {
1233 } else if (game.map_geometry == 'Square') {
1234 if (direction == 'UP') {
1236 } else if (direction == 'DOWN') {
1239 } else if (game.map_geometry == 'Hex') {
1240 let start_indented = start_position[0] % 2;
1241 if (direction == 'UPLEFT') {
1243 if (!start_indented) {
1246 } else if (direction == 'UPRIGHT') {
1248 if (start_indented) {
1251 } else if (direction == 'DOWNLEFT') {
1253 if (!start_indented) {
1256 } else if (direction == 'DOWNRIGHT') {
1258 if (start_indented) {
1263 if (target[0] < 0 || target[1] < 0 ||
1264 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1269 teleport: function() {
1270 let player = this.get_thing(game.player_id);
1271 if (player.position in this.portals) {
1272 server.reconnect_to(this.portals[player.position]);
1274 terminal.blink_screen();
1275 tui.log_msg('? not standing on portal')
1283 server.init(websocket_location);
1289 move: function(direction) {
1290 let target = game.move(this.position, direction);
1292 this.position = target
1293 this.info_cached = false;
1294 if (tui.tile_draw) {
1295 this.send_tile_control_command();
1298 terminal.blink_screen();
1301 update_annotations: function(yx, str) {
1302 this.annotations[yx] = str;
1303 if (tui.mode.name == 'study') {
1307 empty_annotations: function() {
1308 this.annotations = {};
1309 if (tui.mode.name == 'study') {
1313 get_info: function() {
1314 if (this.info_cached) {
1315 return this.info_cached;
1317 let info_to_cache = '';
1318 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1319 if (game.fov[position_i] != '.') {
1320 info_to_cache += 'outside field of view';
1322 let terrain_char = game.map[position_i]
1323 let terrain_desc = '?'
1324 if (game.terrains[terrain_char]) {
1325 terrain_desc = game.terrains[terrain_char];
1327 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1328 let protection = game.map_control[position_i];
1329 if (protection == '.') {
1330 protection = 'unprotected';
1332 info_to_cache += 'PROTECTION: ' + protection + '\n';
1333 for (let t_id in game.things) {
1334 let t = game.things[t_id];
1335 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1336 info_to_cache += "THING: " + this.get_thing_info(t);
1337 let protection = t.protection;
1338 if (protection == '.') {
1339 protection = 'none';
1341 info_to_cache += " / protection: " + protection + "\n";
1344 if (this.position in game.portals) {
1345 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1347 if (this.position in this.annotations) {
1348 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1351 this.info_cached = info_to_cache;
1352 return this.info_cached;
1354 get_thing_info: function(t) {
1355 const symbol = game.thing_types[t.type_];
1356 let info = t.type_ + " / " + symbol;
1358 info += t.thing_char;
1361 info += " (" + t.name_ + ")";
1365 annotate: function(msg) {
1366 if (msg.length == 0) {
1367 msg = " "; // triggers annotation deletion
1369 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1371 set_portal: function(msg) {
1372 if (msg.length == 0) {
1373 msg = " "; // triggers portal deletion
1375 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1377 send_tile_control_command: function() {
1378 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1382 tui.inputEl.addEventListener('input', (event) => {
1383 if (tui.mode.has_input_prompt) {
1384 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1385 if (tui.inputEl.value.length > max_length) {
1386 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1388 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1389 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1390 tui.switch_mode('edit');
1394 document.onclick = function() {
1395 tui.show_help = false;
1397 tui.inputEl.addEventListener('keydown', (event) => {
1398 tui.show_help = false;
1399 if (event.key == 'Enter') {
1400 event.preventDefault();
1402 if (tui.mode.has_input_prompt && event.key == 'Enter'
1403 && tui.inputEl.value.length == 0
1404 && ['chat', 'command_thing', 'take_thing',
1405 'admin_enter'].includes(tui.mode.name)) {
1406 if (tui.mode.name != 'chat') {
1407 tui.log_msg('@ aborted');
1409 tui.switch_mode('play');
1410 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1411 tui.show_help = true;
1412 tui.inputEl.value = "";
1413 tui.restore_input_values();
1414 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1415 && !tui.mode.is_single_char_entry) {
1416 tui.show_help = true;
1417 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1418 tui.login_name = tui.inputEl.value;
1419 server.send(['LOGIN', tui.inputEl.value]);
1420 tui.inputEl.value = "";
1421 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1422 if (tui.task_action_on('command')) {
1423 server.send(['TASK:COMMAND', tui.inputEl.value]);
1424 tui.inputEl.value = "";
1426 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1427 const i = parseInt(tui.inputEl.value);
1428 if (isNaN(i) || i < 0 || i >= tui.selectables.length) {
1429 tui.log_msg('? invalid index, aborted');
1431 server.send(['TASK:PICK_UP', tui.selectables[i][0]]);
1433 tui.inputEl.value = "";
1434 tui.switch_mode('play');
1435 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1436 if (tui.inputEl.value.length == 0) {
1437 tui.log_msg('@ aborted');
1439 server.send(['SET_MAP_CONTROL_PASSWORD',
1440 tui.tile_control_char, tui.inputEl.value]);
1441 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1443 tui.switch_mode('admin');
1444 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1445 explorer.set_portal(tui.inputEl.value);
1446 tui.switch_mode('edit');
1447 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1448 if (tui.inputEl.value.length == 0) {
1449 tui.inputEl.value = " ";
1451 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1453 tui.switch_mode('edit');
1454 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1455 explorer.annotate(tui.inputEl.value);
1456 tui.switch_mode('edit');
1457 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1458 if (tui.inputEl.value.length == 0) {
1459 tui.inputEl.value = " ";
1461 tui.password = tui.inputEl.value
1462 tui.switch_mode('edit');
1463 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1464 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1465 tui.switch_mode('play');
1466 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1467 if (tui.inputEl.value.length != 1) {
1468 tui.log_msg('@ entered non-single-char, therefore aborted');
1469 tui.switch_mode('admin');
1471 tui.tile_control_char = tui.inputEl.value[0];
1472 tui.switch_mode('control_pw_pw');
1474 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1475 if (tui.inputEl.value.length != 1) {
1476 tui.log_msg('@ entered non-single-char, therefore aborted');
1477 tui.switch_mode('admin');
1479 tui.tile_control_char = tui.inputEl.value[0];
1480 tui.switch_mode('control_tile_draw');
1482 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1483 if (tui.inputEl.value.length != 1) {
1484 tui.log_msg('@ entered non-single-char, therefore aborted');
1486 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1487 tui.log_msg('@ sent new protection character for thing');
1489 tui.switch_mode('admin');
1490 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1491 let tokens = parser.tokenize(tui.inputEl.value);
1492 if (tokens.length > 0 && tokens[0].length > 0) {
1493 if (tui.inputEl.value[0][0] == '/') {
1494 if (tokens[0].slice(1) == 'nick') {
1495 if (tokens.length > 1) {
1496 server.send(['NICK', tokens[1]]);
1498 tui.log_msg('? need new name');
1501 tui.log_msg('? unknown command');
1504 server.send(['ALL', tui.inputEl.value]);
1506 } else if (tui.inputEl.valuelength > 0) {
1507 server.send(['ALL', tui.inputEl.value]);
1509 tui.inputEl.value = "";
1510 } else if (tui.mode.name == 'play') {
1511 if (tui.mode.mode_switch_on_key(event)) {
1513 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1514 server.send(["TASK:DROP"]);
1515 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1516 server.send(["TASK:INTOXICATE"]);
1517 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1518 server.send(["TASK:DOOR"]);
1519 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1520 server.send(["TASK:INSTALL"]);
1521 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1522 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1523 } else if (event.key === tui.keys.teleport) {
1526 } else if (tui.mode.name == 'study') {
1527 if (tui.mode.mode_switch_on_key(event)) {
1529 } else if (event.key in tui.movement_keys) {
1530 explorer.move(tui.movement_keys[event.key]);
1531 } else if (event.key == tui.keys.toggle_map_mode) {
1532 tui.toggle_map_mode();
1534 } else if (tui.mode.name == 'control_tile_draw') {
1535 if (tui.mode.mode_switch_on_key(event)) {
1537 } else if (event.key in tui.movement_keys) {
1538 explorer.move(tui.movement_keys[event.key]);
1539 } else if (event.key === tui.keys.toggle_tile_draw) {
1540 tui.toggle_tile_draw();
1542 } else if (tui.mode.name == 'admin') {
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]]);
1548 } else if (tui.mode.name == 'edit') {
1549 if (tui.mode.mode_switch_on_key(event)) {
1551 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1552 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1553 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1554 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1555 } else if (event.key == tui.keys.toggle_map_mode) {
1556 tui.toggle_map_mode();
1562 rows_selector.addEventListener('input', function() {
1563 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1566 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1567 terminal.initialize();
1570 cols_selector.addEventListener('input', function() {
1571 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1574 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1575 terminal.initialize();
1576 tui.window_width = terminal.cols / 2,
1579 for (let key_selector of key_selectors) {
1580 key_selector.addEventListener('input', function() {
1581 window.localStorage.setItem(key_selector.id, key_selector.value);
1585 window.setInterval(function() {
1586 if (server.connected) {
1587 server.send(['PING']);
1589 server.reconnect_to(server.url);
1590 tui.log_msg('@ attempting reconnect …')
1593 window.setInterval(function() {
1595 let span_decoration = "none";
1596 if (document.activeElement == tui.inputEl) {
1597 val = "on (click outside terminal to change)";
1599 val = "off (click into terminal to change)";
1600 span_decoration = "line-through";
1602 document.getElementById("keyboard_control").textContent = val;
1603 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1604 span.style.textDecoration = span_decoration;
1607 document.getElementById("terminal").onclick = function() {
1608 tui.inputEl.focus();
1610 document.getElementById("help").onclick = function() {
1611 tui.show_help = true;
1614 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1615 const mode = switchEl.id.slice("switch_to_".length);
1616 switchEl.onclick = function() {
1617 tui.switch_mode(mode);
1621 document.getElementById("toggle_tile_draw").onclick = function() {
1622 tui.toggle_tile_draw();
1624 document.getElementById("toggle_map_mode").onclick = function() {
1625 tui.toggle_map_mode();
1628 document.getElementById("drop_thing").onclick = function() {
1629 server.send(['TASK:DROP']);
1631 document.getElementById("flatten").onclick = function() {
1632 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1634 document.getElementById("door").onclick = function() {
1635 server.send(['TASK:DOOR']);
1637 document.getElementById("consume").onclick = function() {
1638 server.send(['TASK:INTOXICATE']);
1640 document.getElementById("install").onclick = function() {
1641 server.send(['TASK:INSTALL']);
1643 document.getElementById("teleport").onclick = function() {
1646 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1647 let direction = move_button.id.split('_')[2].toUpperCase();
1648 move_button.onclick = function() {
1649 if (tui.mode.available_actions.includes("move")
1650 || tui.mode.available_actions.includes("move_explorer")) {
1651 server.send(['TASK:MOVE', direction]);
1653 explorer.move(direction);