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="teleport"></button>
62 <td><button id="switch_to_edit"></button></td>
64 <button id="switch_to_write"></button>
65 <button id="flatten"></button>
66 <button id="switch_to_annotate"></button>
67 <button id="switch_to_portal"></button>
68 <button id="switch_to_name_thing"></button>
69 <button id="switch_to_password"></button>
73 <td><button id="switch_to_admin_enter"></button></td>
75 <button id="switch_to_control_pw_type"></button>
76 <button id="switch_to_control_tile_type"></button>
77 <button id="switch_to_admin_thing_protect"></button>
78 <button id="toggle_tile_draw"></button>
83 <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 />
85 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
86 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
87 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
88 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
89 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
90 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
91 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
92 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
93 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
94 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
95 <li>help: <input id="key_help" type="text" value="h" />
96 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
97 <li>teleport: <input id="key_teleport" type="text" value="p" />
98 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
99 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
100 <li>open/close: <input id="key_door" type="text" value="D" />
101 <li>consume: <input id="key_consume" type="text" value="C" />
102 <li><input id="key_switch_to_chat" type="text" value="t" />
103 <li><input id="key_switch_to_play" type="text" value="p" />
104 <li><input id="key_switch_to_study" type="text" value="?" />
105 <li><input id="key_switch_to_edit" type="text" value="E" />
106 <li><input id="key_switch_to_write" type="text" value="m" />
107 <li><input id="key_switch_to_name_thing" type="text" value="N" />
108 <li><input id="key_switch_to_password" type="text" value="P" />
109 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
110 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
111 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
112 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
113 <li><input id="key_switch_to_annotate" type="text" value="M" />
114 <li><input id="key_switch_to_portal" type="text" value="T" />
115 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
116 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
121 let websocket_location = "wss://plomlompom.com/rogue_chat/";
122 //let websocket_location = "ws://localhost:8000/";
127 'long': 'This mode allows you to interact with the map in various ways.'
131 '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.'},
133 'short': 'world edit',
134 '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.'
137 'short': 'name thing',
138 'long': 'Give name to/change name of thing here.'
140 'admin_thing_protect': {
141 'short': 'change thing protection',
142 'long': 'Change protection character for thing here.'
145 'short': 'change terrain',
146 '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.'
149 'short': 'change protection character password',
150 '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.'
153 'short': 'change protection character password',
154 '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.'
156 'control_tile_type': {
157 'short': 'change tiles protection',
158 '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.'
160 'control_tile_draw': {
161 'short': 'change tiles protection',
162 '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.'
165 'short': 'annotate tile',
166 '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.'
169 'short': 'edit portal',
170 '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.'
174 '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:'
178 'long': 'Enter your player name.'
180 'waiting_for_server': {
181 'short': 'waiting for server response',
182 'long': 'Waiting for a server response.'
185 'short': 'waiting for server response',
186 'long': 'Waiting for a server response.'
189 'short': 'set world edit password',
190 '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.'
193 'short': 'become admin',
194 'long': 'This mode allows you to become admin if you know an admin password.'
198 'long': 'This mode allows you access to actions limited to administrators.'
201 let key_descriptions = {
203 'flatten': 'flatten surroundings',
204 'teleport': 'teleport',
205 'take_thing': 'pick up thing',
206 'drop_thing': 'drop thing',
207 'door': 'open/close',
208 'consume': 'consume',
209 'toggle_map_mode': 'toggle map view',
210 'toggle_tile_draw': 'toggle protection character drawing',
211 'hex_move_upleft': 'up-left',
212 'hex_move_upright': 'up-right',
213 'hex_move_right': 'right',
214 'hex_move_left': 'left',
215 'hex_move_downleft': 'down-left',
216 'hex_move_downright': 'down-right',
217 'square_move_up': 'up',
218 'square_move_left': 'left',
219 'square_move_down': 'down',
220 'square_move_right': 'right',
222 for (const mode_name of Object.keys(mode_helps)) {
223 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
226 let rows_selector = document.getElementById("n_rows");
227 let cols_selector = document.getElementById("n_cols");
228 let key_selectors = document.querySelectorAll('[id^="key_"]');
230 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
231 const action = key_switch_selector.id.slice("key_switch_to_".length);
232 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
235 function restore_selector_value(selector) {
236 let stored_selection = window.localStorage.getItem(selector.id);
237 if (stored_selection) {
238 selector.value = stored_selection;
241 restore_selector_value(rows_selector);
242 restore_selector_value(cols_selector);
243 for (let key_selector of key_selectors) {
244 restore_selector_value(key_selector);
247 function escapeHTML(str) {
249 replace(/&/g, '&').
250 replace(/</g, '<').
251 replace(/>/g, '>').
252 replace(/'/g, ''').
253 replace(/"/g, '"');
257 initialize: function() {
258 this.rows = rows_selector.value;
259 this.cols = cols_selector.value;
260 this.pre_el = document.getElementById("terminal");
261 this.set_default_colors();
265 for (let y = 0, x = 0; y <= this.rows; x++) {
266 if (x == this.cols) {
269 this.content.push(line);
271 if (y == this.rows) {
278 apply_colors: function() {
279 this.pre_el.style.color = this.foreground;
280 this.pre_el.style.backgroundColor = this.background;
282 set_default_colors: function() {
283 this.foreground = 'white';
284 this.background = 'black';
287 set_random_colors: function() {
288 function rand(offset) {
289 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
291 this.foreground = '#' + rand(159) + rand(159) + rand(159);
292 this.background = '#' + rand(0) + rand(0) + rand(0);
295 blink_screen: function() {
296 this.pre_el.style.color = this.background;
297 this.pre_el.style.backgroundColor = this.foreground;
299 this.pre_el.style.color = this.foreground;
300 this.pre_el.style.backgroundColor = this.background;
303 refresh: function() {
304 let pre_content = '';
305 for (let y = 0; y < this.rows; y++) {
306 let line = this.content[y].join('');
308 if (y in tui.links) {
310 for (let span of tui.links[y]) {
311 chunks.push(escapeHTML(line.slice(start_x, span[0])));
312 chunks.push('<a target="_blank" href="');
313 chunks.push(escapeHTML(span[2]));
315 chunks.push(escapeHTML(line.slice(span[0], span[1])));
319 chunks.push(escapeHTML(line.slice(start_x)));
321 chunks = [escapeHTML(line)];
323 for (const chunk of chunks) {
324 pre_content += chunk;
328 this.pre_el.innerHTML = pre_content;
330 write: function(start_y, start_x, msg) {
331 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
332 this.content[start_y][x] = msg[i];
335 drawBox: function(start_y, start_x, height, width) {
336 let end_y = start_y + height;
337 let end_x = start_x + width;
338 for (let y = start_y, x = start_x; y < this.rows; x++) {
346 this.content[y][x] = ' ';
350 terminal.initialize();
353 tokenize: function(str) {
358 for (let i = 0; i < str.length; i++) {
364 } else if (c == '\\') {
366 } else if (c == '"') {
371 } else if (c == '"') {
373 } else if (c === ' ') {
374 if (token.length > 0) {
382 if (token.length > 0) {
387 parse_yx: function(position_string) {
388 let coordinate_strings = position_string.split(',')
389 let position = [0, 0];
390 position[0] = parseInt(coordinate_strings[0].slice(2));
391 position[1] = parseInt(coordinate_strings[1].slice(2));
403 init: function(url) {
405 this.websocket = new WebSocket(this.url);
406 this.websocket.onopen = function(event) {
407 server.connected = true;
408 game.thing_types = {};
410 server.send(['TASKS']);
411 server.send(['TERRAINS']);
412 server.send(['THING_TYPES']);
413 tui.log_msg("@ server connected! :)");
414 tui.switch_mode('login');
416 this.websocket.onclose = function(event) {
417 server.connected = false;
418 tui.switch_mode('waiting_for_server');
419 tui.log_msg("@ server disconnected :(");
421 this.websocket.onmessage = this.handle_event;
423 reconnect_to: function(url) {
424 this.websocket.close();
427 send: function(tokens) {
428 this.websocket.send(unparser.untokenize(tokens));
430 handle_event: function(event) {
431 let tokens = parser.tokenize(event.data);
432 if (tokens[0] === 'TURN') {
433 game.turn_complete = false;
434 explorer.empty_info_db();
437 game.turn = parseInt(tokens[1]);
438 } else if (tokens[0] === 'THING') {
439 let t = game.get_thing(tokens[4], true);
440 t.position = parser.parse_yx(tokens[1]);
442 t.protection = tokens[3];
443 } else if (tokens[0] === 'THING_NAME') {
444 let t = game.get_thing(tokens[1], false);
448 } else if (tokens[0] === 'THING_CHAR') {
449 let t = game.get_thing(tokens[1], false);
451 t.thing_char = tokens[2];
453 } else if (tokens[0] === 'TASKS') {
454 game.tasks = tokens[1].split(',');
455 tui.mode_write.legal = game.tasks.includes('WRITE');
456 } else if (tokens[0] === 'THING_TYPE') {
457 game.thing_types[tokens[1]] = tokens[2]
458 } else if (tokens[0] === 'TERRAIN') {
459 game.terrains[tokens[1]] = tokens[2]
460 } else if (tokens[0] === 'MAP') {
461 game.map_geometry = tokens[1];
463 game.map_size = parser.parse_yx(tokens[2]);
465 } else if (tokens[0] === 'FOV') {
467 } else if (tokens[0] === 'MAP_CONTROL') {
468 game.map_control = tokens[1]
469 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
470 game.turn_complete = true;
471 if (tui.mode.name == 'post_login_wait') {
472 tui.switch_mode('play');
473 } else if (tui.mode.name == 'study') {
474 explorer.query_info();
477 } else if (tokens[0] === 'CHAT') {
478 tui.log_msg('# ' + tokens[1], 1);
479 } else if (tokens[0] === 'PLAYER_ID') {
480 game.player_id = parseInt(tokens[1]);
481 } else if (tokens[0] === 'LOGIN_OK') {
482 this.send(['GET_GAMESTATE']);
483 tui.switch_mode('post_login_wait');
484 } else if (tokens[0] === 'DEFAULT_COLORS') {
485 terminal.set_default_colors();
486 } else if (tokens[0] === 'RANDOM_COLORS') {
487 terminal.set_random_colors();
488 } else if (tokens[0] === 'ADMIN_OK') {
490 tui.log_msg('@ you now have admin rights');
491 tui.switch_mode('admin');
492 } else if (tokens[0] === 'PORTAL') {
493 let position = parser.parse_yx(tokens[1]);
494 game.portals[position] = tokens[2];
495 } else if (tokens[0] === 'ANNOTATION_HINT') {
496 let position = parser.parse_yx(tokens[1]);
497 explorer.info_hints = explorer.info_hints.concat([position]);
498 } else if (tokens[0] === 'ANNOTATION') {
499 let position = parser.parse_yx(tokens[1]);
500 explorer.update_info_db(position, tokens[2]);
502 } else if (tokens[0] === 'UNHANDLED_INPUT') {
503 tui.log_msg('? unknown command');
504 } else if (tokens[0] === 'PLAY_ERROR') {
505 tui.log_msg('? ' + tokens[1]);
506 terminal.blink_screen();
507 } else if (tokens[0] === 'ARGUMENT_ERROR') {
508 tui.log_msg('? syntax error: ' + tokens[1]);
509 } else if (tokens[0] === 'GAME_ERROR') {
510 tui.log_msg('? game error: ' + tokens[1]);
511 } else if (tokens[0] === 'PONG') {
514 tui.log_msg('? unhandled input: ' + event.data);
520 quote: function(str) {
522 for (let i = 0; i < str.length; i++) {
524 if (['"', '\\'].includes(c)) {
530 return quoted.join('');
532 to_yx: function(yx_coordinate) {
533 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
535 untokenize: function(tokens) {
536 let quoted_tokens = [];
537 for (let token of tokens) {
538 quoted_tokens.push(this.quote(token));
540 return quoted_tokens.join(" ");
545 constructor(name, has_input_prompt=false, shows_info=false,
546 is_intro=false, is_single_char_entry=false) {
548 this.short_desc = mode_helps[name].short;
549 this.available_modes = [];
550 this.available_actions = [];
551 this.has_input_prompt = has_input_prompt;
552 this.shows_info= shows_info;
553 this.is_intro = is_intro;
554 this.help_intro = mode_helps[name].long;
555 this.is_single_char_entry = is_single_char_entry;
558 *iter_available_modes() {
559 for (let mode_name of this.available_modes) {
560 let mode = tui['mode_' + mode_name];
564 let key = tui.keys['switch_to_' + mode.name];
568 list_available_modes() {
570 if (this.available_modes.length > 0) {
571 msg += 'Other modes available from here:\n';
572 for (let [mode, key] of this.iter_available_modes()) {
573 msg += '[' + key + '] – ' + mode.short_desc + '\n';
578 mode_switch_on_key(key_event) {
579 for (let [mode, key] of this.iter_available_modes()) {
580 if (key_event.key == key) {
581 event.preventDefault();
582 tui.switch_mode(mode.name);
594 window_width: terminal.cols / 2,
602 mode_waiting_for_server: new Mode('waiting_for_server',
604 mode_login: new Mode('login', true, false, true),
605 mode_post_login_wait: new Mode('post_login_wait'),
606 mode_chat: new Mode('chat', true),
607 mode_annotate: new Mode('annotate', true, true),
608 mode_play: new Mode('play'),
609 mode_study: new Mode('study', false, true),
610 mode_write: new Mode('write', false, false, false, true),
611 mode_edit: new Mode('edit'),
612 mode_control_pw_type: new Mode('control_pw_type', true),
613 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
614 mode_portal: new Mode('portal', true, true),
615 mode_password: new Mode('password', true),
616 mode_name_thing: new Mode('name_thing', true, true),
617 mode_admin_enter: new Mode('admin_enter', true),
618 mode_admin: new Mode('admin'),
619 mode_control_pw_pw: new Mode('control_pw_pw', true),
620 mode_control_tile_type: new Mode('control_tile_type', true),
621 mode_control_tile_draw: new Mode('control_tile_draw'),
623 'flatten': 'FLATTEN_SURROUNDINGS',
624 'take_thing': 'PICK_UP',
625 'drop_thing': 'DROP',
628 'consume': 'INTOXICATE',
631 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
632 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
633 this.mode_play.available_actions = ["move", "take_thing", "drop_thing",
634 "teleport", "door", "consume"];
635 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
636 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
637 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
638 "control_tile_type", "chat",
639 "study", "play", "edit"]
640 this.mode_admin.available_actions = ["move"];
641 this.mode_control_tile_draw.available_modes = ["admin_enter"]
642 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
643 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
644 "password", "chat", "study", "play",
646 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
647 this.mode = this.mode_waiting_for_server;
648 this.inputEl = document.getElementById("input");
649 this.inputEl.focus();
650 this.recalc_input_lines();
651 this.height_header = this.height_turn_line + this.height_mode_line;
652 this.log_msg("@ waiting for server connection ...");
655 init_keys: function() {
656 document.getElementById("move_table").hidden = true;
658 for (let key_selector of key_selectors) {
659 this.keys[key_selector.id.slice(4)] = key_selector.value;
661 this.movement_keys = {};
662 let geometry_prefix = 'undefinedMapGeometry_';
663 if (game.map_geometry) {
664 geometry_prefix = game.map_geometry.toLowerCase() + '_';
666 for (const key_name of Object.keys(key_descriptions)) {
667 if (key_name.startsWith(geometry_prefix)) {
668 let direction = key_name.split('_')[2].toUpperCase();
669 let key = this.keys[key_name];
670 this.movement_keys[key] = direction;
673 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
674 if (move_button.id.startsWith('key_')) {
677 move_button.hidden = true;
679 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
680 document.getElementById("move_table").hidden = false;
681 move_button.hidden = false;
683 for (let el of document.getElementsByTagName("button")) {
684 let action_desc = key_descriptions[el.id];
685 let action_key = '[' + this.keys[el.id] + ']';
686 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
689 task_action_on: function(action) {
690 return game.tasks.includes(this.action_tasks[action]);
692 switch_mode: function(mode_name) {
693 if (this.mode.name == 'control_tile_draw') {
694 tui.log_msg('@ finished tile protection drawing.')
696 this.tile_draw = false;
697 if (mode_name == 'admin_enter' && this.is_admin) {
699 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
700 let player_position = game.things[game.player_id].position;
702 for (let t_id in game.things) {
703 if (t_id == game.player_id) {
706 let t = game.things[t_id];
707 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
713 terminal.blink_screen();
714 this.log_msg('? not standing over thing');
717 this.selected_thing_id = thing_id;
720 this.mode = this['mode_' + mode_name];
721 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
722 this.map_mode = 'protections';
723 } else if (this.mode.name != "edit") {
724 this.map_mode = 'terrain + things';
726 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
727 this.inputEl.focus();
729 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
730 explorer.position = game.things[game.player_id].position;
731 if (this.mode.shows_info) {
732 explorer.query_info();
735 this.inputEl.value = "";
736 this.restore_input_values();
737 for (let el of document.getElementsByTagName("button")) {
740 document.getElementById("help").disabled = false;
741 for (const action of this.mode.available_actions) {
742 if (["move", "move_explorer"].includes(action)) {
743 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
744 move_key.disabled = false;
746 } else if (Object.keys(this.action_tasks).includes(action)) {
747 if (this.task_action_on(action)) {
748 document.getElementById(action).disabled = false;
751 document.getElementById(action).disabled = false;
754 for (const mode_name of this.mode.available_modes) {
755 document.getElementById('switch_to_' + mode_name).disabled = false;
757 if (this.mode.name == 'login') {
758 if (this.login_name) {
759 server.send(['LOGIN', this.login_name]);
761 this.log_msg("? need login name");
763 } else if (this.mode.is_single_char_entry) {
764 this.show_help = true;
765 } else if (this.mode.name == 'admin_enter') {
766 this.log_msg('@ enter admin password:')
767 } else if (this.mode.name == 'control_pw_type') {
768 this.log_msg('@ enter protection character for which you want to change the password:')
769 } else if (this.mode.name == 'control_tile_type') {
770 this.log_msg('@ enter protection character which you want to draw:')
771 } else if (this.mode.name == 'admin_thing_protect') {
772 this.log_msg('@ enter thing protection character:')
773 } else if (this.mode.name == 'control_pw_pw') {
774 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
775 } else if (this.mode.name == 'control_tile_draw') {
776 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 + '].')
780 offset_links: function(offset, links) {
781 for (let y in links) {
782 let real_y = offset[0] + parseInt(y);
783 if (!this.links[real_y]) {
784 this.links[real_y] = [];
786 for (let link of links[y]) {
787 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
788 this.links[real_y].push(offset_link);
792 restore_input_values: function() {
793 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
794 let info = explorer.info_db[explorer.position];
795 if (info != "(none)") {
796 this.inputEl.value = info;
798 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
799 let portal = game.portals[explorer.position]
800 this.inputEl.value = portal;
801 } else if (this.mode.name == 'password') {
802 this.inputEl.value = this.password;
803 } else if (this.mode.name == 'name_thing') {
804 let t = game.get_thing(this.selected_thing_id);
806 this.inputEl.value = t.name_;
808 } else if (this.mode.name == 'admin_thing_protect') {
809 let t = game.get_thing(this.selected_thing_id);
810 if (t && t.protection) {
811 this.inputEl.value = t.protection;
815 recalc_input_lines: function() {
816 if (this.mode.has_input_prompt) {
818 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
820 this.input_lines = [];
822 this.height_input = this.input_lines.length;
824 msg_into_lines_of_width: function(msg, width) {
825 function push_inner_link(y, end_x) {
826 if (!inner_links[y]) {
829 inner_links[y].push([url_start_x, end_x, url]);
831 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
834 for (const match of matches) {
835 const url = match[0];
836 const url_start = match.index;
837 const url_end = match.index + match[0].length;
838 link_data[url_start] = url;
839 url_ends.push(url_end);
843 let inner_links = {};
847 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
848 if (x >= width || msg[i] == "\n") {
850 push_inner_link(y, chunk.length);
852 if (url_ends[0] == i) {
860 if (msg[i] == "\n") {
865 if (msg[i] != "\n") {
868 if (i in link_data) {
872 } else if (url_ends[0] == i) {
874 push_inner_link(y, x);
880 push_inner_link(lines.length - 1, chunk.length);
882 return [lines, inner_links];
884 log_msg: function(msg) {
886 while (this.log.length > 100) {
891 draw_map: function() {
892 let map_lines_split = [];
894 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
895 if (j == game.map_size[1]) {
896 map_lines_split.push(line);
900 if (this.map_mode == 'protections') {
901 line.push(game.map_control[i] + ' ');
903 line.push(game.map[i] + ' ');
906 map_lines_split.push(line);
907 if (this.map_mode == 'terrain + annotations') {
908 for (const coordinate of explorer.info_hints) {
909 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
911 } else if (this.map_mode == 'terrain + things') {
912 for (const p in game.portals) {
913 let coordinate = p.split(',')
914 let original = map_lines_split[coordinate[0]][coordinate[1]];
915 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
917 let used_positions = [];
918 for (const thing_id in game.things) {
919 let t = game.things[thing_id];
920 let symbol = game.thing_types[t.type_];
923 meta_char = t.thing_char;
925 if (used_positions.includes(t.position.toString())) {
928 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
929 used_positions.push(t.position.toString());
932 let player = game.things[game.player_id];
933 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
934 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
935 } else if (tui.map_mode != 'terrain + things') {
936 map_lines_split[player.position[0]][player.position[1]] = '??';
939 if (game.map_geometry == 'Square') {
940 for (let line_split of map_lines_split) {
941 map_lines.push(line_split.join(''));
943 } else if (game.map_geometry == 'Hex') {
945 for (let line_split of map_lines_split) {
946 map_lines.push(' '.repeat(indent) + line_split.join(''));
954 let window_center = [terminal.rows / 2, this.window_width / 2];
955 let center_position = [player.position[0], player.position[1]];
956 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
957 center_position = [explorer.position[0], explorer.position[1]];
959 center_position[1] = center_position[1] * 2;
960 let offset = [center_position[0] - window_center[0],
961 center_position[1] - window_center[1]]
962 if (game.map_geometry == 'Hex' && offset[0] % 2) {
965 let term_y = Math.max(0, -offset[0]);
966 let term_x = Math.max(0, -offset[1]);
967 let map_y = Math.max(0, offset[0]);
968 let map_x = Math.max(0, offset[1]);
969 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
970 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
971 terminal.write(term_y, term_x, to_draw);
974 draw_mode_line: function() {
975 let help = 'hit [' + this.keys.help + '] for help';
976 if (this.mode.has_input_prompt) {
977 help = 'enter /help for help';
979 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
981 draw_turn_line: function(n) {
982 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
984 draw_history: function() {
985 let log_display_lines = [];
987 let y_offset_in_log = 0;
988 for (let line of this.log) {
989 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
991 log_display_lines = log_display_lines.concat(new_lines);
992 for (const y in link_data) {
993 const rel_y = y_offset_in_log + parseInt(y);
994 log_links[rel_y] = [];
995 for (let link of link_data[y]) {
996 log_links[rel_y].push(link);
999 y_offset_in_log += new_lines.length;
1001 let i = log_display_lines.length - 1;
1002 for (let y = terminal.rows - 1 - this.height_input;
1003 y >= this.height_header && i >= 0;
1005 terminal.write(y, this.window_width, log_display_lines[i]);
1007 for (const key of Object.keys(log_links)) {
1008 if (parseInt(key) <= i) {
1009 delete log_links[key];
1012 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1014 this.offset_links(offset, log_links);
1016 draw_info: function() {
1017 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
1019 let offset = [this.height_header, this.window_width];
1020 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1021 terminal.write(y, offset[1], lines[i]);
1023 this.offset_links(offset, link_data);
1025 draw_input: function() {
1026 if (this.mode.has_input_prompt) {
1027 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1028 terminal.write(y, this.window_width, this.input_lines[i]);
1032 draw_help: function() {
1033 let movement_keys_desc = '';
1034 if (!this.mode.is_intro) {
1035 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1037 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1038 if (this.mode.name == 'chat') {
1039 content += '/nick NAME – re-name yourself to NAME\n';
1040 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1041 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1042 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1043 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1044 } else if (this.mode.available_actions.length > 0) {
1045 content += "Available actions:\n";
1046 for (let action of this.mode.available_actions) {
1047 if (Object.keys(this.action_tasks).includes(action)) {
1048 if (!this.task_action_on(action)) {
1052 if (action == 'move_explorer') {
1055 if (action == 'move') {
1056 content += "[" + movement_keys_desc + "] – move\n"
1058 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1063 content += this.mode.list_available_modes();
1065 if (!this.mode.has_input_prompt) {
1066 start_x = this.window_width
1068 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1069 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1070 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1071 terminal.write(y, start_x, lines[i]);
1074 toggle_tile_draw: function() {
1075 if (tui.tile_draw) {
1076 tui.tile_draw = false;
1078 tui.tile_draw = true;
1081 toggle_map_mode: function() {
1082 if (tui.map_mode == 'terrain only') {
1083 tui.map_mode = 'terrain + annotations';
1084 } else if (tui.map_mode == 'terrain + annotations') {
1085 tui.map_mode = 'terrain + things';
1086 } else if (tui.map_mode == 'terrain + things') {
1087 tui.map_mode = 'protections';
1088 } else if (tui.map_mode == 'protections') {
1089 tui.map_mode = 'terrain only';
1092 full_refresh: function() {
1094 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1095 this.recalc_input_lines();
1096 if (this.mode.is_intro) {
1097 this.draw_history();
1100 if (game.turn_complete) {
1102 this.draw_turn_line();
1104 this.draw_mode_line();
1105 if (this.mode.shows_info) {
1108 this.draw_history();
1112 if (this.show_help) {
1124 this.map_control = "";
1125 this.map_size = [0,0];
1126 this.player_id = -1;
1130 get_thing: function(id_, create_if_not_found=false) {
1131 if (id_ in game.things) {
1132 return game.things[id_];
1133 } else if (create_if_not_found) {
1134 let t = new Thing([0,0]);
1135 game.things[id_] = t;
1139 move: function(start_position, direction) {
1140 let target = [start_position[0], start_position[1]];
1141 if (direction == 'LEFT') {
1143 } else if (direction == 'RIGHT') {
1145 } else if (game.map_geometry == 'Square') {
1146 if (direction == 'UP') {
1148 } else if (direction == 'DOWN') {
1151 } else if (game.map_geometry == 'Hex') {
1152 let start_indented = start_position[0] % 2;
1153 if (direction == 'UPLEFT') {
1155 if (!start_indented) {
1158 } else if (direction == 'UPRIGHT') {
1160 if (start_indented) {
1163 } else if (direction == 'DOWNLEFT') {
1165 if (!start_indented) {
1168 } else if (direction == 'DOWNRIGHT') {
1170 if (start_indented) {
1175 if (target[0] < 0 || target[1] < 0 ||
1176 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1181 teleport: function() {
1182 let player = this.get_thing(game.player_id);
1183 if (player.position in this.portals) {
1184 server.reconnect_to(this.portals[player.position]);
1186 terminal.blink_screen();
1187 tui.log_msg('? not standing on portal')
1195 server.init(websocket_location);
1201 move: function(direction) {
1202 let target = game.move(this.position, direction);
1204 this.position = target
1205 if (tui.mode.shows_info) {
1207 } else if (tui.tile_draw) {
1208 this.send_tile_control_command();
1211 terminal.blink_screen();
1214 update_info_db: function(yx, str) {
1215 this.info_db[yx] = str;
1216 if (tui.mode.name == 'study') {
1220 empty_info_db: function() {
1222 this.info_hints = [];
1223 if (tui.mode.name == 'study') {
1227 query_info: function() {
1228 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1230 get_info: function() {
1231 let info = "MAP VIEW: " + tui.map_mode + "\n";
1232 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1233 if (game.fov[position_i] != '.') {
1234 return info + 'outside field of view';
1236 let terrain_char = game.map[position_i]
1237 let terrain_desc = '?'
1238 if (game.terrains[terrain_char]) {
1239 terrain_desc = game.terrains[terrain_char];
1241 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1242 let protection = game.map_control[position_i];
1243 if (protection == '.') {
1244 protection = 'unprotected';
1246 info += 'PROTECTION: ' + protection + '\n';
1247 for (let t_id in game.things) {
1248 let t = game.things[t_id];
1249 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1250 let symbol = game.thing_types[t.type_];
1251 let protection = t.protection;
1252 if (protection == '.') {
1253 protection = 'none';
1255 info += "THING: " + t.type_ + " / " + symbol;
1257 info += t.thing_char;
1260 info += " (" + t.name_ + ")";
1262 info += " / protection: " + protection + "\n";
1265 if (this.position in game.portals) {
1266 info += "PORTAL: " + game.portals[this.position] + "\n";
1268 if (this.position in this.info_db) {
1269 info += "ANNOTATIONS: " + this.info_db[this.position];
1271 info += 'waiting …';
1275 annotate: function(msg) {
1276 if (msg.length == 0) {
1277 msg = " "; // triggers annotation deletion
1279 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1281 set_portal: function(msg) {
1282 if (msg.length == 0) {
1283 msg = " "; // triggers portal deletion
1285 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1287 send_tile_control_command: function() {
1288 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1292 tui.inputEl.addEventListener('input', (event) => {
1293 if (tui.mode.has_input_prompt) {
1294 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1295 if (tui.inputEl.value.length > max_length) {
1296 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1298 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1299 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1300 tui.switch_mode('edit');
1304 document.onclick = function() {
1305 tui.show_help = false;
1307 tui.inputEl.addEventListener('keydown', (event) => {
1308 tui.show_help = false;
1309 if (event.key == 'Enter') {
1310 event.preventDefault();
1312 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1313 tui.show_help = true;
1314 tui.inputEl.value = "";
1315 tui.restore_input_values();
1316 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1317 && !tui.mode.is_single_char_entry) {
1318 tui.show_help = true;
1319 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1320 tui.login_name = tui.inputEl.value;
1321 server.send(['LOGIN', tui.inputEl.value]);
1322 tui.inputEl.value = "";
1323 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1324 if (tui.inputEl.value.length == 0) {
1325 tui.log_msg('@ aborted');
1327 server.send(['SET_MAP_CONTROL_PASSWORD',
1328 tui.tile_control_char, tui.inputEl.value]);
1329 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1331 tui.switch_mode('admin');
1332 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1333 explorer.set_portal(tui.inputEl.value);
1334 tui.switch_mode('edit');
1335 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1336 if (tui.inputEl.value.length == 0) {
1337 tui.inputEl.value = " ";
1339 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1341 tui.switch_mode('edit');
1342 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1343 explorer.annotate(tui.inputEl.value);
1344 tui.switch_mode('edit');
1345 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1346 if (tui.inputEl.value.length == 0) {
1347 tui.inputEl.value = " ";
1349 tui.password = tui.inputEl.value
1350 tui.switch_mode('edit');
1351 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1352 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1353 tui.switch_mode('play');
1354 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1355 if (tui.inputEl.value.length != 1) {
1356 tui.log_msg('@ entered non-single-char, therefore aborted');
1357 tui.switch_mode('admin');
1359 tui.tile_control_char = tui.inputEl.value[0];
1360 tui.switch_mode('control_pw_pw');
1362 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1363 if (tui.inputEl.value.length != 1) {
1364 tui.log_msg('@ entered non-single-char, therefore aborted');
1365 tui.switch_mode('admin');
1367 tui.tile_control_char = tui.inputEl.value[0];
1368 tui.switch_mode('control_tile_draw');
1370 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1371 if (tui.inputEl.value.length != 1) {
1372 tui.log_msg('@ entered non-single-char, therefore aborted');
1374 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1375 tui.log_msg('@ sent new protection character for thing');
1377 tui.switch_mode('admin');
1378 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1379 let tokens = parser.tokenize(tui.inputEl.value);
1380 if (tokens.length > 0 && tokens[0].length > 0) {
1381 if (tui.inputEl.value[0][0] == '/') {
1382 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1383 tui.switch_mode('play');
1384 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1385 tui.switch_mode('study');
1386 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1387 tui.switch_mode('edit');
1388 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1389 tui.switch_mode('admin_enter');
1390 } else if (tokens[0].slice(1) == 'nick') {
1391 if (tokens.length > 1) {
1392 server.send(['NICK', tokens[1]]);
1394 tui.log_msg('? need new name');
1397 tui.log_msg('? unknown command');
1400 server.send(['ALL', tui.inputEl.value]);
1402 } else if (tui.inputEl.valuelength > 0) {
1403 server.send(['ALL', tui.inputEl.value]);
1405 tui.inputEl.value = "";
1406 } else if (tui.mode.name == 'play') {
1407 if (tui.mode.mode_switch_on_key(event)) {
1409 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1410 server.send(["TASK:PICK_UP"]);
1411 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1412 server.send(["TASK:DROP"]);
1413 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1414 server.send(["TASK:INTOXICATE"]);
1415 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1416 server.send(["TASK:DOOR"]);
1417 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1418 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1419 } else if (event.key === tui.keys.teleport) {
1422 } else if (tui.mode.name == 'study') {
1423 if (tui.mode.mode_switch_on_key(event)) {
1425 } else if (event.key in tui.movement_keys) {
1426 explorer.move(tui.movement_keys[event.key]);
1427 } else if (event.key == tui.keys.toggle_map_mode) {
1428 tui.toggle_map_mode();
1430 } else if (tui.mode.name == 'control_tile_draw') {
1431 if (tui.mode.mode_switch_on_key(event)) {
1433 } else if (event.key in tui.movement_keys) {
1434 explorer.move(tui.movement_keys[event.key]);
1435 } else if (event.key === tui.keys.toggle_tile_draw) {
1436 tui.toggle_tile_draw();
1438 } else if (tui.mode.name == 'admin') {
1439 if (tui.mode.mode_switch_on_key(event)) {
1441 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1442 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1444 } else if (tui.mode.name == 'edit') {
1445 if (tui.mode.mode_switch_on_key(event)) {
1447 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1448 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1449 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1450 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1451 } else if (event.key == tui.keys.toggle_map_mode) {
1452 tui.toggle_map_mode();
1458 rows_selector.addEventListener('input', function() {
1459 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1462 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1463 terminal.initialize();
1466 cols_selector.addEventListener('input', function() {
1467 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1470 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1471 terminal.initialize();
1472 tui.window_width = terminal.cols / 2,
1475 for (let key_selector of key_selectors) {
1476 key_selector.addEventListener('input', function() {
1477 window.localStorage.setItem(key_selector.id, key_selector.value);
1481 window.setInterval(function() {
1482 if (server.connected) {
1483 server.send(['PING']);
1485 server.reconnect_to(server.url);
1486 tui.log_msg('@ attempting reconnect …')
1489 window.setInterval(function() {
1491 let span_decoration = "none";
1492 if (document.activeElement == tui.inputEl) {
1493 val = "on (click outside terminal to change)";
1495 val = "off (click into terminal to change)";
1496 span_decoration = "line-through";
1498 document.getElementById("keyboard_control").textContent = val;
1499 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1500 span.style.textDecoration = span_decoration;
1503 document.getElementById("terminal").onclick = function() {
1504 tui.inputEl.focus();
1506 document.getElementById("help").onclick = function() {
1507 tui.show_help = true;
1510 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1511 const mode = switchEl.id.slice("switch_to_".length);
1512 switchEl.onclick = function() {
1513 tui.switch_mode(mode);
1517 document.getElementById("toggle_tile_draw").onclick = function() {
1518 tui.toggle_tile_draw();
1520 document.getElementById("toggle_map_mode").onclick = function() {
1521 tui.toggle_map_mode();
1524 document.getElementById("take_thing").onclick = function() {
1525 server.send(['TASK:PICK_UP']);
1527 document.getElementById("drop_thing").onclick = function() {
1528 server.send(['TASK:DROP']);
1530 document.getElementById("flatten").onclick = function() {
1531 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1533 document.getElementById("door").onclick = function() {
1534 server.send(['TASK:DOOR']);
1536 document.getElementById("consume").onclick = function() {
1537 server.send(['TASK:INTOXICATE']);
1539 document.getElementById("teleport").onclick = function() {
1542 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1543 let direction = move_button.id.split('_')[2].toUpperCase();
1544 move_button.onclick = function() {
1545 if (tui.mode.available_actions.includes("move")
1546 || tui.mode.available_actions.includes("move_explorer")) {
1547 server.send(['TASK:MOVE', direction]);
1549 explorer.move(direction);