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="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>pick up thing: <input id="key_take_thing" type="text" value="z" />
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><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.'
146 'admin_thing_protect': {
147 'short': 'change thing protection',
148 'long': 'Change protection character for thing here.'
151 'short': 'change terrain',
152 '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.'
155 'short': 'change protection character password',
156 '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.'
159 'short': 'change protection character password',
160 '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.'
162 'control_tile_type': {
163 'short': 'change tiles protection',
164 '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.'
166 'control_tile_draw': {
167 'short': 'change tiles protection',
168 '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.'
171 'short': 'annotate tile',
172 '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.'
175 'short': 'edit portal',
176 '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.'
180 '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:'
184 'long': 'Enter your player name.'
186 'waiting_for_server': {
187 'short': 'waiting for server response',
188 'long': 'Waiting for a server response.'
191 'short': 'waiting for server response',
192 'long': 'Waiting for a server response.'
195 'short': 'set world edit password',
196 '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.'
199 'short': 'become admin',
200 'long': 'This mode allows you to become admin if you know an admin password.'
204 'long': 'This mode allows you access to actions limited to administrators.'
207 let key_descriptions = {
209 'flatten': 'flatten surroundings',
210 'teleport': 'teleport',
211 'take_thing': 'pick up thing',
212 'drop_thing': 'drop thing',
213 'door': 'open/close',
214 'consume': 'consume',
215 'toggle_map_mode': 'toggle map view',
216 'toggle_tile_draw': 'toggle protection character drawing',
217 'hex_move_upleft': 'up-left',
218 'hex_move_upright': 'up-right',
219 'hex_move_right': 'right',
220 'hex_move_left': 'left',
221 'hex_move_downleft': 'down-left',
222 'hex_move_downright': 'down-right',
223 'square_move_up': 'up',
224 'square_move_left': 'left',
225 'square_move_down': 'down',
226 'square_move_right': 'right',
228 for (const mode_name of Object.keys(mode_helps)) {
229 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
232 let rows_selector = document.getElementById("n_rows");
233 let cols_selector = document.getElementById("n_cols");
234 let key_selectors = document.querySelectorAll('[id^="key_"]');
236 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
237 const action = key_switch_selector.id.slice("key_switch_to_".length);
238 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
241 function restore_selector_value(selector) {
242 let stored_selection = window.localStorage.getItem(selector.id);
243 if (stored_selection) {
244 selector.value = stored_selection;
247 restore_selector_value(rows_selector);
248 restore_selector_value(cols_selector);
249 for (let key_selector of key_selectors) {
250 restore_selector_value(key_selector);
253 function escapeHTML(str) {
255 replace(/&/g, '&').
256 replace(/</g, '<').
257 replace(/>/g, '>').
258 replace(/'/g, ''').
259 replace(/"/g, '"');
263 initialize: function() {
264 this.rows = rows_selector.value;
265 this.cols = cols_selector.value;
266 this.pre_el = document.getElementById("terminal");
267 this.set_default_colors();
271 for (let y = 0, x = 0; y <= this.rows; x++) {
272 if (x == this.cols) {
275 this.content.push(line);
277 if (y == this.rows) {
284 apply_colors: function() {
285 this.pre_el.style.color = this.foreground;
286 this.pre_el.style.backgroundColor = this.background;
288 set_default_colors: function() {
289 this.foreground = 'white';
290 this.background = 'black';
293 set_random_colors: function() {
294 function rand(offset) {
295 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
297 this.foreground = '#' + rand(159) + rand(159) + rand(159);
298 this.background = '#' + rand(0) + rand(0) + rand(0);
301 blink_screen: function() {
302 this.pre_el.style.color = this.background;
303 this.pre_el.style.backgroundColor = this.foreground;
305 this.pre_el.style.color = this.foreground;
306 this.pre_el.style.backgroundColor = this.background;
309 refresh: function() {
310 let pre_content = '';
311 for (let y = 0; y < this.rows; y++) {
312 let line = this.content[y].join('');
314 if (y in tui.links) {
316 for (let span of tui.links[y]) {
317 chunks.push(escapeHTML(line.slice(start_x, span[0])));
318 chunks.push('<a target="_blank" href="');
319 chunks.push(escapeHTML(span[2]));
321 chunks.push(escapeHTML(line.slice(span[0], span[1])));
325 chunks.push(escapeHTML(line.slice(start_x)));
327 chunks = [escapeHTML(line)];
329 for (const chunk of chunks) {
330 pre_content += chunk;
334 this.pre_el.innerHTML = pre_content;
336 write: function(start_y, start_x, msg) {
337 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
338 this.content[start_y][x] = msg[i];
341 drawBox: function(start_y, start_x, height, width) {
342 let end_y = start_y + height;
343 let end_x = start_x + width;
344 for (let y = start_y, x = start_x; y < this.rows; x++) {
352 this.content[y][x] = ' ';
356 terminal.initialize();
359 tokenize: function(str) {
364 for (let i = 0; i < str.length; i++) {
370 } else if (c == '\\') {
372 } else if (c == '"') {
377 } else if (c == '"') {
379 } else if (c === ' ') {
380 if (token.length > 0) {
388 if (token.length > 0) {
393 parse_yx: function(position_string) {
394 let coordinate_strings = position_string.split(',')
395 let position = [0, 0];
396 position[0] = parseInt(coordinate_strings[0].slice(2));
397 position[1] = parseInt(coordinate_strings[1].slice(2));
409 init: function(url) {
411 this.websocket = new WebSocket(this.url);
412 this.websocket.onopen = function(event) {
413 server.connected = true;
414 game.thing_types = {};
416 server.send(['TASKS']);
417 server.send(['TERRAINS']);
418 server.send(['THING_TYPES']);
419 tui.log_msg("@ server connected! :)");
420 tui.switch_mode('login');
422 this.websocket.onclose = function(event) {
423 server.connected = false;
424 tui.switch_mode('waiting_for_server');
425 tui.log_msg("@ server disconnected :(");
427 this.websocket.onmessage = this.handle_event;
429 reconnect_to: function(url) {
430 this.websocket.close();
433 send: function(tokens) {
434 this.websocket.send(unparser.untokenize(tokens));
436 handle_event: function(event) {
437 let tokens = parser.tokenize(event.data);
438 if (tokens[0] === 'TURN') {
439 game.turn_complete = false;
440 explorer.empty_annotations();
444 game.turn = parseInt(tokens[1]);
445 } else if (tokens[0] === 'THING') {
446 let t = game.get_thing(tokens[4], true);
447 t.position = parser.parse_yx(tokens[1]);
449 t.protection = tokens[3];
450 } else if (tokens[0] === 'THING_NAME') {
451 let t = game.get_thing(tokens[1], false);
455 } else if (tokens[0] === 'THING_CHAR') {
456 let t = game.get_thing(tokens[1], false);
458 t.thing_char = tokens[2];
460 } else if (tokens[0] === 'TASKS') {
461 game.tasks = tokens[1].split(',');
462 tui.mode_write.legal = game.tasks.includes('WRITE');
463 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
464 } else if (tokens[0] === 'THING_TYPE') {
465 game.thing_types[tokens[1]] = tokens[2]
466 } else if (tokens[0] === 'TERRAIN') {
467 game.terrains[tokens[1]] = tokens[2]
468 } else if (tokens[0] === 'MAP') {
469 game.map_geometry = tokens[1];
471 game.map_size = parser.parse_yx(tokens[2]);
473 } else if (tokens[0] === 'FOV') {
475 } else if (tokens[0] === 'MAP_CONTROL') {
476 game.map_control = tokens[1]
477 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
478 game.turn_complete = true;
479 if (tui.mode.name == 'post_login_wait') {
480 tui.switch_mode('play');
482 explorer.info_cached = false;
484 } else if (tokens[0] === 'CHAT') {
485 tui.log_msg('# ' + tokens[1], 1);
486 } else if (tokens[0] === 'REPLY') {
487 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
488 } else if (tokens[0] === 'PLAYER_ID') {
489 game.player_id = parseInt(tokens[1]);
490 } else if (tokens[0] === 'LOGIN_OK') {
491 this.send(['GET_GAMESTATE']);
492 tui.switch_mode('post_login_wait');
493 } else if (tokens[0] === 'DEFAULT_COLORS') {
494 terminal.set_default_colors();
495 } else if (tokens[0] === 'RANDOM_COLORS') {
496 terminal.set_random_colors();
497 } else if (tokens[0] === 'ADMIN_OK') {
499 tui.log_msg('@ you now have admin rights');
500 tui.switch_mode('admin');
501 } else if (tokens[0] === 'PORTAL') {
502 let position = parser.parse_yx(tokens[1]);
503 game.portals[position] = tokens[2];
504 } else if (tokens[0] === 'ANNOTATION') {
505 let position = parser.parse_yx(tokens[1]);
506 explorer.update_annotations(position, tokens[2]);
508 } else if (tokens[0] === 'UNHANDLED_INPUT') {
509 tui.log_msg('? unknown command');
510 } else if (tokens[0] === 'PLAY_ERROR') {
511 tui.log_msg('? ' + tokens[1]);
512 terminal.blink_screen();
513 } else if (tokens[0] === 'ARGUMENT_ERROR') {
514 tui.log_msg('? syntax error: ' + tokens[1]);
515 } else if (tokens[0] === 'GAME_ERROR') {
516 tui.log_msg('? game error: ' + tokens[1]);
517 } else if (tokens[0] === 'PONG') {
520 tui.log_msg('? unhandled input: ' + event.data);
526 quote: function(str) {
528 for (let i = 0; i < str.length; i++) {
530 if (['"', '\\'].includes(c)) {
536 return quoted.join('');
538 to_yx: function(yx_coordinate) {
539 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
541 untokenize: function(tokens) {
542 let quoted_tokens = [];
543 for (let token of tokens) {
544 quoted_tokens.push(this.quote(token));
546 return quoted_tokens.join(" ");
551 constructor(name, has_input_prompt=false, shows_info=false,
552 is_intro=false, is_single_char_entry=false) {
554 this.short_desc = mode_helps[name].short;
555 this.available_modes = [];
556 this.available_actions = [];
557 this.has_input_prompt = has_input_prompt;
558 this.shows_info= shows_info;
559 this.is_intro = is_intro;
560 this.help_intro = mode_helps[name].long;
561 this.is_single_char_entry = is_single_char_entry;
564 *iter_available_modes() {
565 for (let mode_name of this.available_modes) {
566 let mode = tui['mode_' + mode_name];
570 let key = tui.keys['switch_to_' + mode.name];
574 list_available_modes() {
576 if (this.available_modes.length > 0) {
577 msg += 'Other modes available from here:\n';
578 for (let [mode, key] of this.iter_available_modes()) {
579 msg += '[' + key + '] – ' + mode.short_desc + '\n';
584 mode_switch_on_key(key_event) {
585 for (let [mode, key] of this.iter_available_modes()) {
586 if (key_event.key == key) {
587 event.preventDefault();
588 tui.switch_mode(mode.name);
600 window_width: terminal.cols / 2,
608 mode_waiting_for_server: new Mode('waiting_for_server',
610 mode_login: new Mode('login', true, false, true),
611 mode_post_login_wait: new Mode('post_login_wait'),
612 mode_chat: new Mode('chat', true),
613 mode_annotate: new Mode('annotate', true, true),
614 mode_play: new Mode('play'),
615 mode_study: new Mode('study', false, true),
616 mode_write: new Mode('write', false, false, false, true),
617 mode_edit: new Mode('edit'),
618 mode_control_pw_type: new Mode('control_pw_type', true),
619 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
620 mode_portal: new Mode('portal', true, true),
621 mode_password: new Mode('password', true),
622 mode_name_thing: new Mode('name_thing', true, true),
623 mode_command_thing: new Mode('command_thing', true),
624 mode_admin_enter: new Mode('admin_enter', true),
625 mode_admin: new Mode('admin'),
626 mode_control_pw_pw: new Mode('control_pw_pw', true),
627 mode_control_tile_type: new Mode('control_tile_type', true),
628 mode_control_tile_draw: new Mode('control_tile_draw'),
630 'flatten': 'FLATTEN_SURROUNDINGS',
631 'take_thing': 'PICK_UP',
632 'drop_thing': 'DROP',
635 'command': 'COMMAND',
636 'consume': 'INTOXICATE',
641 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
642 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
644 this.mode_play.available_actions = ["move", "take_thing", "drop_thing",
645 "teleport", "door", "consume"];
646 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
647 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
648 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
649 "control_tile_type", "chat",
650 "study", "play", "edit"]
651 this.mode_admin.available_actions = ["move"];
652 this.mode_control_tile_draw.available_modes = ["admin_enter"]
653 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
654 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
655 "password", "chat", "study", "play",
657 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
658 this.mode = this.mode_waiting_for_server;
659 this.inputEl = document.getElementById("input");
660 this.inputEl.focus();
661 this.recalc_input_lines();
662 this.height_header = this.height_turn_line + this.height_mode_line;
663 this.log_msg("@ waiting for server connection ...");
666 init_keys: function() {
667 document.getElementById("move_table").hidden = true;
669 for (let key_selector of key_selectors) {
670 this.keys[key_selector.id.slice(4)] = key_selector.value;
672 this.movement_keys = {};
673 let geometry_prefix = 'undefinedMapGeometry_';
674 if (game.map_geometry) {
675 geometry_prefix = game.map_geometry.toLowerCase() + '_';
677 for (const key_name of Object.keys(key_descriptions)) {
678 if (key_name.startsWith(geometry_prefix)) {
679 let direction = key_name.split('_')[2].toUpperCase();
680 let key = this.keys[key_name];
681 this.movement_keys[key] = direction;
684 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
685 if (move_button.id.startsWith('key_')) {
688 move_button.hidden = true;
690 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
691 document.getElementById("move_table").hidden = false;
692 move_button.hidden = false;
694 for (let el of document.getElementsByTagName("button")) {
695 let action_desc = key_descriptions[el.id];
696 let action_key = '[' + this.keys[el.id] + ']';
697 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
700 task_action_on: function(action) {
701 return game.tasks.includes(this.action_tasks[action]);
703 switch_mode: function(mode_name) {
704 if (this.mode.name == 'control_tile_draw') {
705 tui.log_msg('@ finished tile protection drawing.')
707 this.tile_draw = false;
708 if (mode_name == 'admin_enter' && this.is_admin) {
710 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
711 let player_position = game.things[game.player_id].position;
713 for (let t_id in game.things) {
714 if (t_id == game.player_id) {
717 let t = game.things[t_id];
718 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
724 terminal.blink_screen();
725 this.log_msg('? not standing over thing');
728 this.selected_thing_id = thing_id;
731 this.mode = this['mode_' + mode_name];
732 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
733 this.map_mode = 'protections';
734 } else if (this.mode.name != "edit") {
735 this.map_mode = 'terrain + things';
737 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
738 this.inputEl.focus();
740 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
741 explorer.position = game.things[game.player_id].position;
743 this.inputEl.value = "";
744 this.restore_input_values();
745 for (let el of document.getElementsByTagName("button")) {
748 document.getElementById("help").disabled = false;
749 for (const action of this.mode.available_actions) {
750 if (["move", "move_explorer"].includes(action)) {
751 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
752 move_key.disabled = false;
754 } else if (Object.keys(this.action_tasks).includes(action)) {
755 if (this.task_action_on(action)) {
756 document.getElementById(action).disabled = false;
759 document.getElementById(action).disabled = false;
762 for (const mode_name of this.mode.available_modes) {
763 document.getElementById('switch_to_' + mode_name).disabled = false;
765 if (this.mode.name == 'login') {
766 if (this.login_name) {
767 server.send(['LOGIN', this.login_name]);
769 this.log_msg("? need login name");
771 } else if (this.mode.is_single_char_entry) {
772 this.show_help = true;
773 } else if (this.mode.name == 'command_thing') {
774 server.send(['TASK:COMMAND', 'HELP']);
775 } else if (this.mode.name == 'admin_enter') {
776 this.log_msg('@ enter admin password:')
777 } else if (this.mode.name == 'control_pw_type') {
778 this.log_msg('@ enter protection character for which you want to change the password:')
779 } else if (this.mode.name == 'control_tile_type') {
780 this.log_msg('@ enter protection character which you want to draw:')
781 } else if (this.mode.name == 'admin_thing_protect') {
782 this.log_msg('@ enter thing protection character:')
783 } else if (this.mode.name == 'control_pw_pw') {
784 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
785 } else if (this.mode.name == 'control_tile_draw') {
786 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 + '].')
790 offset_links: function(offset, links) {
791 for (let y in links) {
792 let real_y = offset[0] + parseInt(y);
793 if (!this.links[real_y]) {
794 this.links[real_y] = [];
796 for (let link of links[y]) {
797 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
798 this.links[real_y].push(offset_link);
802 restore_input_values: function() {
803 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
804 let info = explorer.annotations[explorer.position];
805 if (info != "(none)") {
806 this.inputEl.value = info;
808 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
809 let portal = game.portals[explorer.position]
810 this.inputEl.value = portal;
811 } else if (this.mode.name == 'password') {
812 this.inputEl.value = this.password;
813 } else if (this.mode.name == 'name_thing') {
814 let t = game.get_thing(this.selected_thing_id);
816 this.inputEl.value = t.name_;
818 } else if (this.mode.name == 'admin_thing_protect') {
819 let t = game.get_thing(this.selected_thing_id);
820 if (t && t.protection) {
821 this.inputEl.value = t.protection;
825 recalc_input_lines: function() {
826 if (this.mode.has_input_prompt) {
828 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
830 this.input_lines = [];
832 this.height_input = this.input_lines.length;
834 msg_into_lines_of_width: function(msg, width) {
835 function push_inner_link(y, end_x) {
836 if (!inner_links[y]) {
839 inner_links[y].push([url_start_x, end_x, url]);
841 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
844 for (const match of matches) {
845 const url = match[0];
846 const url_start = match.index;
847 const url_end = match.index + match[0].length;
848 link_data[url_start] = url;
849 url_ends.push(url_end);
853 let inner_links = {};
857 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
858 if (x >= width || msg[i] == "\n") {
860 push_inner_link(y, chunk.length);
862 if (url_ends[0] == i) {
870 if (msg[i] == "\n") {
875 if (msg[i] != "\n") {
878 if (i in link_data) {
882 } else if (url_ends[0] == i) {
884 push_inner_link(y, x);
890 push_inner_link(lines.length - 1, chunk.length);
892 return [lines, inner_links];
894 log_msg: function(msg) {
896 while (this.log.length > 100) {
901 draw_map: function() {
902 if (!game.turn_complete && this.map_lines.length == 0) {
905 if (game.turn_complete) {
906 let map_lines_split = [];
908 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
909 if (j == game.map_size[1]) {
910 map_lines_split.push(line);
914 if (this.map_mode == 'protections') {
915 line.push(game.map_control[i] + ' ');
917 line.push(game.map[i] + ' ');
920 map_lines_split.push(line);
921 if (this.map_mode == 'terrain + annotations') {
922 for (const coordinate of explorer.info_hints) {
923 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
925 } else if (this.map_mode == 'terrain + things') {
926 for (const p in game.portals) {
927 let coordinate = p.split(',')
928 let original = map_lines_split[coordinate[0]][coordinate[1]];
929 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
931 let used_positions = [];
932 for (const thing_id in game.things) {
933 let t = game.things[thing_id];
934 let symbol = game.thing_types[t.type_];
937 meta_char = t.thing_char;
939 if (used_positions.includes(t.position.toString())) {
942 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
943 used_positions.push(t.position.toString());
946 let player = game.things[game.player_id];
947 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
948 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
949 } else if (tui.map_mode != 'terrain + things') {
950 map_lines_split[player.position[0]][player.position[1]] = '??';
953 if (game.map_geometry == 'Square') {
954 for (let line_split of map_lines_split) {
955 this.map_lines.push(line_split.join(''));
957 } else if (game.map_geometry == 'Hex') {
959 for (let line_split of map_lines_split) {
960 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
968 let window_center = [terminal.rows / 2, this.window_width / 2];
969 let center_position = [player.position[0], player.position[1]];
970 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
971 center_position = [explorer.position[0], explorer.position[1]];
973 center_position[1] = center_position[1] * 2;
974 this.offset = [center_position[0] - window_center[0],
975 center_position[1] - window_center[1]]
976 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
980 let term_y = Math.max(0, -this.offset[0]);
981 let term_x = Math.max(0, -this.offset[1]);
982 let map_y = Math.max(0, this.offset[0]);
983 let map_x = Math.max(0, this.offset[1]);
984 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
985 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
986 terminal.write(term_y, term_x, to_draw);
989 draw_mode_line: function() {
990 let help = 'hit [' + this.keys.help + '] for help';
991 if (this.mode.has_input_prompt) {
992 help = 'enter /help for help';
994 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
996 draw_turn_line: function(n) {
997 if (game.turn_complete) {
998 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1001 draw_history: function() {
1002 let log_display_lines = [];
1004 let y_offset_in_log = 0;
1005 for (let line of this.log) {
1006 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1008 log_display_lines = log_display_lines.concat(new_lines);
1009 for (const y in link_data) {
1010 const rel_y = y_offset_in_log + parseInt(y);
1011 log_links[rel_y] = [];
1012 for (let link of link_data[y]) {
1013 log_links[rel_y].push(link);
1016 y_offset_in_log += new_lines.length;
1018 let i = log_display_lines.length - 1;
1019 for (let y = terminal.rows - 1 - this.height_input;
1020 y >= this.height_header && i >= 0;
1022 terminal.write(y, this.window_width, log_display_lines[i]);
1024 for (const key of Object.keys(log_links)) {
1025 if (parseInt(key) <= i) {
1026 delete log_links[key];
1029 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1031 this.offset_links(offset, log_links);
1033 draw_info: function() {
1034 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1035 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1036 let offset = [this.height_header, this.window_width];
1037 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1038 terminal.write(y, offset[1], lines[i]);
1040 this.offset_links(offset, link_data);
1042 draw_input: function() {
1043 if (this.mode.has_input_prompt) {
1044 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1045 terminal.write(y, this.window_width, this.input_lines[i]);
1049 draw_help: function() {
1050 let movement_keys_desc = '';
1051 if (!this.mode.is_intro) {
1052 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1054 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1055 if (this.mode.name == 'chat') {
1056 content += '/nick NAME – re-name yourself to NAME\n';
1057 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1058 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1059 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1060 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1061 } else if (this.mode.available_actions.length > 0) {
1062 content += "Available actions:\n";
1063 for (let action of this.mode.available_actions) {
1064 if (Object.keys(this.action_tasks).includes(action)) {
1065 if (!this.task_action_on(action)) {
1069 if (action == 'move_explorer') {
1072 if (action == 'move') {
1073 content += "[" + movement_keys_desc + "] – move\n"
1075 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1080 content += this.mode.list_available_modes();
1082 if (!this.mode.has_input_prompt) {
1083 start_x = this.window_width
1085 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1086 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1087 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1088 terminal.write(y, start_x, lines[i]);
1091 toggle_tile_draw: function() {
1092 if (tui.tile_draw) {
1093 tui.tile_draw = false;
1095 tui.tile_draw = true;
1098 toggle_map_mode: function() {
1099 if (tui.map_mode == 'terrain only') {
1100 tui.map_mode = 'terrain + annotations';
1101 } else if (tui.map_mode == 'terrain + annotations') {
1102 tui.map_mode = 'terrain + things';
1103 } else if (tui.map_mode == 'terrain + things') {
1104 tui.map_mode = 'protections';
1105 } else if (tui.map_mode == 'protections') {
1106 tui.map_mode = 'terrain only';
1109 full_refresh: function() {
1111 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1112 this.recalc_input_lines();
1113 if (this.mode.is_intro) {
1114 this.draw_history();
1118 this.draw_turn_line();
1119 this.draw_mode_line();
1120 if (this.mode.shows_info) {
1123 this.draw_history();
1127 if (this.show_help) {
1139 this.map_control = "";
1140 this.map_size = [0,0];
1141 this.player_id = -1;
1145 get_thing: function(id_, create_if_not_found=false) {
1146 if (id_ in game.things) {
1147 return game.things[id_];
1148 } else if (create_if_not_found) {
1149 let t = new Thing([0,0]);
1150 game.things[id_] = t;
1154 move: function(start_position, direction) {
1155 let target = [start_position[0], start_position[1]];
1156 if (direction == 'LEFT') {
1158 } else if (direction == 'RIGHT') {
1160 } else if (game.map_geometry == 'Square') {
1161 if (direction == 'UP') {
1163 } else if (direction == 'DOWN') {
1166 } else if (game.map_geometry == 'Hex') {
1167 let start_indented = start_position[0] % 2;
1168 if (direction == 'UPLEFT') {
1170 if (!start_indented) {
1173 } else if (direction == 'UPRIGHT') {
1175 if (start_indented) {
1178 } else if (direction == 'DOWNLEFT') {
1180 if (!start_indented) {
1183 } else if (direction == 'DOWNRIGHT') {
1185 if (start_indented) {
1190 if (target[0] < 0 || target[1] < 0 ||
1191 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1196 teleport: function() {
1197 let player = this.get_thing(game.player_id);
1198 if (player.position in this.portals) {
1199 server.reconnect_to(this.portals[player.position]);
1201 terminal.blink_screen();
1202 tui.log_msg('? not standing on portal')
1210 server.init(websocket_location);
1216 move: function(direction) {
1217 let target = game.move(this.position, direction);
1219 this.position = target
1220 this.info_cached = false;
1221 if (tui.tile_draw) {
1222 this.send_tile_control_command();
1225 terminal.blink_screen();
1228 update_annotations: function(yx, str) {
1229 this.annotations[yx] = str;
1230 if (tui.mode.name == 'study') {
1234 empty_annotations: function() {
1235 this.annotations = {};
1236 if (tui.mode.name == 'study') {
1240 get_info: function() {
1241 if (this.info_cached) {
1242 return this.info_cached;
1244 let info_to_cache = '';
1245 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1246 if (game.fov[position_i] != '.') {
1247 info_to_cache += 'outside field of view';
1249 let terrain_char = game.map[position_i]
1250 let terrain_desc = '?'
1251 if (game.terrains[terrain_char]) {
1252 terrain_desc = game.terrains[terrain_char];
1254 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1255 let protection = game.map_control[position_i];
1256 if (protection == '.') {
1257 protection = 'unprotected';
1259 info_to_cache += 'PROTECTION: ' + protection + '\n';
1260 for (let t_id in game.things) {
1261 let t = game.things[t_id];
1262 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1263 let symbol = game.thing_types[t.type_];
1264 let protection = t.protection;
1265 if (protection == '.') {
1266 protection = 'none';
1268 info_to_cache += "THING: " + t.type_ + " / " + symbol;
1270 info_to_cache += t.thing_char;
1273 info_to_cache += " (" + t.name_ + ")";
1275 info_to_cache += " / protection: " + protection + "\n";
1278 if (this.position in game.portals) {
1279 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1281 if (this.position in this.annotations) {
1282 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1285 this.info_cached = info_to_cache;
1286 return this.info_cached;
1288 annotate: function(msg) {
1289 if (msg.length == 0) {
1290 msg = " "; // triggers annotation deletion
1292 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1294 set_portal: function(msg) {
1295 if (msg.length == 0) {
1296 msg = " "; // triggers portal deletion
1298 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1300 send_tile_control_command: function() {
1301 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1305 tui.inputEl.addEventListener('input', (event) => {
1306 if (tui.mode.has_input_prompt) {
1307 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1308 if (tui.inputEl.value.length > max_length) {
1309 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1311 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1312 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1313 tui.switch_mode('edit');
1317 document.onclick = function() {
1318 tui.show_help = false;
1320 tui.inputEl.addEventListener('keydown', (event) => {
1321 tui.show_help = false;
1322 if (event.key == 'Enter') {
1323 event.preventDefault();
1325 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1326 tui.show_help = true;
1327 tui.inputEl.value = "";
1328 tui.restore_input_values();
1329 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1330 && !tui.mode.is_single_char_entry) {
1331 tui.show_help = true;
1332 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1333 tui.login_name = tui.inputEl.value;
1334 server.send(['LOGIN', tui.inputEl.value]);
1335 tui.inputEl.value = "";
1336 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1337 if (tui.inputEl.value.length == 0) {
1338 tui.log_msg('@ aborted');
1339 tui.switch_mode('play');
1340 } else if (tui.task_action_on('command')) {
1341 server.send(['TASK:COMMAND', tui.inputEl.value]);
1342 tui.inputEl.value = "";
1344 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1345 if (tui.inputEl.value.length == 0) {
1346 tui.log_msg('@ aborted');
1348 server.send(['SET_MAP_CONTROL_PASSWORD',
1349 tui.tile_control_char, tui.inputEl.value]);
1350 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1352 tui.switch_mode('admin');
1353 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1354 explorer.set_portal(tui.inputEl.value);
1355 tui.switch_mode('edit');
1356 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1357 if (tui.inputEl.value.length == 0) {
1358 tui.inputEl.value = " ";
1360 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1362 tui.switch_mode('edit');
1363 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1364 explorer.annotate(tui.inputEl.value);
1365 tui.switch_mode('edit');
1366 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1367 if (tui.inputEl.value.length == 0) {
1368 tui.inputEl.value = " ";
1370 tui.password = tui.inputEl.value
1371 tui.switch_mode('edit');
1372 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1373 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1374 tui.switch_mode('play');
1375 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1376 if (tui.inputEl.value.length != 1) {
1377 tui.log_msg('@ entered non-single-char, therefore aborted');
1378 tui.switch_mode('admin');
1380 tui.tile_control_char = tui.inputEl.value[0];
1381 tui.switch_mode('control_pw_pw');
1383 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1384 if (tui.inputEl.value.length != 1) {
1385 tui.log_msg('@ entered non-single-char, therefore aborted');
1386 tui.switch_mode('admin');
1388 tui.tile_control_char = tui.inputEl.value[0];
1389 tui.switch_mode('control_tile_draw');
1391 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1392 if (tui.inputEl.value.length != 1) {
1393 tui.log_msg('@ entered non-single-char, therefore aborted');
1395 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1396 tui.log_msg('@ sent new protection character for thing');
1398 tui.switch_mode('admin');
1399 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1400 let tokens = parser.tokenize(tui.inputEl.value);
1401 if (tokens.length > 0 && tokens[0].length > 0) {
1402 if (tui.inputEl.value[0][0] == '/') {
1403 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1404 tui.switch_mode('play');
1405 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1406 tui.switch_mode('study');
1407 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1408 tui.switch_mode('edit');
1409 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1410 tui.switch_mode('admin_enter');
1411 } else if (tokens[0].slice(1) == 'nick') {
1412 if (tokens.length > 1) {
1413 server.send(['NICK', tokens[1]]);
1415 tui.log_msg('? need new name');
1418 tui.log_msg('? unknown command');
1421 server.send(['ALL', tui.inputEl.value]);
1423 } else if (tui.inputEl.valuelength > 0) {
1424 server.send(['ALL', tui.inputEl.value]);
1426 tui.inputEl.value = "";
1427 } else if (tui.mode.name == 'play') {
1428 if (tui.mode.mode_switch_on_key(event)) {
1430 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1431 server.send(["TASK:PICK_UP"]);
1432 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1433 server.send(["TASK:DROP"]);
1434 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1435 server.send(["TASK:INTOXICATE"]);
1436 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1437 server.send(["TASK:DOOR"]);
1438 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1439 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1440 } else if (event.key === tui.keys.teleport) {
1443 } else if (tui.mode.name == 'study') {
1444 if (tui.mode.mode_switch_on_key(event)) {
1446 } else if (event.key in tui.movement_keys) {
1447 explorer.move(tui.movement_keys[event.key]);
1448 } else if (event.key == tui.keys.toggle_map_mode) {
1449 tui.toggle_map_mode();
1451 } else if (tui.mode.name == 'control_tile_draw') {
1452 if (tui.mode.mode_switch_on_key(event)) {
1454 } else if (event.key in tui.movement_keys) {
1455 explorer.move(tui.movement_keys[event.key]);
1456 } else if (event.key === tui.keys.toggle_tile_draw) {
1457 tui.toggle_tile_draw();
1459 } else if (tui.mode.name == 'admin') {
1460 if (tui.mode.mode_switch_on_key(event)) {
1462 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1463 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1465 } else if (tui.mode.name == 'edit') {
1466 if (tui.mode.mode_switch_on_key(event)) {
1468 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1469 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1470 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1471 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1472 } else if (event.key == tui.keys.toggle_map_mode) {
1473 tui.toggle_map_mode();
1479 rows_selector.addEventListener('input', function() {
1480 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1483 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1484 terminal.initialize();
1487 cols_selector.addEventListener('input', function() {
1488 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1491 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1492 terminal.initialize();
1493 tui.window_width = terminal.cols / 2,
1496 for (let key_selector of key_selectors) {
1497 key_selector.addEventListener('input', function() {
1498 window.localStorage.setItem(key_selector.id, key_selector.value);
1502 window.setInterval(function() {
1503 if (server.connected) {
1504 server.send(['PING']);
1506 server.reconnect_to(server.url);
1507 tui.log_msg('@ attempting reconnect …')
1510 window.setInterval(function() {
1512 let span_decoration = "none";
1513 if (document.activeElement == tui.inputEl) {
1514 val = "on (click outside terminal to change)";
1516 val = "off (click into terminal to change)";
1517 span_decoration = "line-through";
1519 document.getElementById("keyboard_control").textContent = val;
1520 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1521 span.style.textDecoration = span_decoration;
1524 document.getElementById("terminal").onclick = function() {
1525 tui.inputEl.focus();
1527 document.getElementById("help").onclick = function() {
1528 tui.show_help = true;
1531 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1532 const mode = switchEl.id.slice("switch_to_".length);
1533 switchEl.onclick = function() {
1534 tui.switch_mode(mode);
1538 document.getElementById("toggle_tile_draw").onclick = function() {
1539 tui.toggle_tile_draw();
1541 document.getElementById("toggle_map_mode").onclick = function() {
1542 tui.toggle_map_mode();
1545 document.getElementById("take_thing").onclick = function() {
1546 server.send(['TASK:PICK_UP']);
1548 document.getElementById("drop_thing").onclick = function() {
1549 server.send(['TASK:DROP']);
1551 document.getElementById("flatten").onclick = function() {
1552 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1554 document.getElementById("door").onclick = function() {
1555 server.send(['TASK:DOOR']);
1557 document.getElementById("consume").onclick = function() {
1558 server.send(['TASK:INTOXICATE']);
1560 document.getElementById("teleport").onclick = function() {
1563 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1564 let direction = move_button.id.split('_')[2].toUpperCase();
1565 move_button.onclick = function() {
1566 if (tui.mode.available_actions.includes("move")
1567 || tui.mode.available_actions.includes("move_explorer")) {
1568 server.send(['TASK:MOVE', direction]);
1570 explorer.move(direction);