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 style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft">up-left</button></td>
26 <td style="text-align: center"><button id="square_move_up">up</button></td>
27 <td><button id="hex_move_upright">up-right</button></td>
30 <td style="text-align: right;"><button id="square_move_left">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">right</button><button id="hex_move_right">right</button></td>
35 <td><button id="hex_move_downleft">down-left</button></td>
36 <td style="text-align: center"><button id="square_move_down">down</button></td>
37 <td><button id="hex_move_downright">down-right</button></td>
42 <td><button id="help">help</button></td>
45 <td><button id="switch_to_chat">chat mode</button><br /></td>
48 <td><button id="switch_to_study">study mode</button></td>
49 <td><button id="toggle_map_mode">toggle map view</button>
52 <td><button id="switch_to_play">play mode</button></td>
54 <button id="take_thing">pick up thing</button>
55 <button id="drop_thing">drop thing</button>
56 <button id="teleport">teleport</button>
60 <td><button id="switch_to_edit">world edit mode</button></td>
62 <button id="switch_to_write">change terrain</button>
63 <button id="flatten">flatten surroundings</button>
64 <button id="switch_to_annotate">annotate tile</button>
65 <button id="switch_to_portal">edit portal</button>
66 <button id="switch_to_name_thing">name thing</button>
67 <button id="switch_to_password">enter world edit password</button>
71 <td><button id="switch_to_admin_enter">admin mode</button></td>
73 <button id="switch_to_control_pw_type">change protection character password</button>
74 <button id="switch_to_control_tile_type">change protection areas</button>
75 <button id="switch_to_admin_thing_protect">change thing protection</button>
76 <button id="toggle_tile_draw">toggle protection character drawing</button>
81 <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 />
83 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
84 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
85 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
86 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
87 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
88 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
89 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
90 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
91 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
92 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
93 <li>help: <input id="key_help" type="text" value="h" />
94 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
95 <li>teleport: <input id="key_teleport" type="text" value="p" />
96 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
97 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
98 <li><input id="key_switch_to_chat" type="text" value="t" />
99 <li><input id="key_switch_to_play" type="text" value="p" />
100 <li><input id="key_switch_to_study" type="text" value="?" />
101 <li><input id="key_switch_to_edit" type="text" value="E" />
102 <li><input id="key_switch_to_write" type="text" value="m" />
103 <li><input id="key_switch_to_name_thing" type="text" value="N" />
104 <li><input id="key_switch_to_password" type="text" value="P" />
105 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
106 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
107 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
108 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
109 <li><input id="key_switch_to_annotate" type="text" value="M" />
110 <li><input id="key_switch_to_portal" type="text" value="T" />
111 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
112 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
117 let websocket_location = "wss://plomlompom.com/rogue_chat/";
118 //let websocket_location = "ws://localhost:8000/";
123 'long': 'This mode allows you to interact with the map in various ways.'
127 '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.'},
129 'short': 'world edit',
130 '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.'
133 'short': 'name thing',
134 'long': 'Give name to/change name of thing here.'
136 'admin_thing_protect': {
137 'short': 'change thing protection',
138 'long': 'Change protection character for thing here.'
141 'short': 'change terrain',
142 '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.'
145 'short': 'change protection character password',
146 '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.'
149 'short': 'change protection character password',
150 '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.'
152 'control_tile_type': {
153 'short': 'change tiles protection',
154 '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.'
156 'control_tile_draw': {
157 'short': 'change tiles protection',
158 '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.'
161 'short': 'annotate tile',
162 '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.'
165 'short': 'edit portal',
166 '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.'
170 '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:'
174 'long': 'Enter your player name.'
176 'waiting_for_server': {
177 'short': 'waiting for server response',
178 'long': 'Waiting for a server response.'
181 'short': 'waiting for server response',
182 'long': 'Waiting for a server response.'
185 'short': 'set world edit password',
186 '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.'
189 'short': 'become admin',
190 'long': 'This mode allows you to become admin if you know an admin password.'
194 'long': 'This mode allows you access to actions limited to administrators.'
197 let key_descriptions = {
199 'flatten': 'flatten surroundings',
200 'teleport': 'teleport',
201 'take_thing': 'pick up thing',
202 'drop_thing': 'drop thing',
203 'toggle_map_mode': 'toggle map view',
204 'toggle_tile_draw': 'toggle protection character drawing',
205 'hex_move_upleft': 'up-left',
206 'hex_move_upright': 'up-right',
207 'hex_move_right': 'right',
208 'hex_move_left': 'left',
209 'hex_move_downleft': 'down-left',
210 'hex_move_downright': 'down-right',
211 'square_move_right': 'right',
212 'square_move_left': 'left',
213 'square_move_up': 'up',
214 'square_move_down': 'down',
216 for (const mode_name of Object.keys(mode_helps)) {
217 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
220 let rows_selector = document.getElementById("n_rows");
221 let cols_selector = document.getElementById("n_cols");
222 let key_selectors = document.querySelectorAll('[id^="key_"]');
224 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
225 const action = key_switch_selector.id.slice("key_switch_to_".length);
226 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
229 function restore_selector_value(selector) {
230 let stored_selection = window.localStorage.getItem(selector.id);
231 if (stored_selection) {
232 selector.value = stored_selection;
235 restore_selector_value(rows_selector);
236 restore_selector_value(cols_selector);
237 for (let key_selector of key_selectors) {
238 restore_selector_value(key_selector);
241 function escapeHTML(str) {
243 replace(/&/g, '&').
244 replace(/</g, '<').
245 replace(/>/g, '>').
246 replace(/'/g, ''').
247 replace(/"/g, '"');
253 initialize: function() {
254 this.rows = rows_selector.value;
255 this.cols = cols_selector.value;
256 this.pre_el = document.getElementById("terminal");
257 this.pre_el.style.color = this.foreground;
258 this.pre_el.style.backgroundColor = this.background;
261 for (let y = 0, x = 0; y <= this.rows; x++) {
262 if (x == this.cols) {
265 this.content.push(line);
267 if (y == this.rows) {
274 blink_screen: function() {
275 this.pre_el.style.color = this.background;
276 this.pre_el.style.backgroundColor = this.foreground;
278 this.pre_el.style.color = this.foreground;
279 this.pre_el.style.backgroundColor = this.background;
282 refresh: function() {
283 let pre_content = '';
284 for (let y = 0; y < this.rows; y++) {
285 let line = this.content[y].join('');
287 if (y in tui.links) {
289 for (let span of tui.links[y]) {
290 chunks.push(escapeHTML(line.slice(start_x, span[0])));
291 chunks.push('<a target="_blank" href="');
292 chunks.push(escapeHTML(span[2]));
294 chunks.push(escapeHTML(line.slice(span[0], span[1])));
298 chunks.push(escapeHTML(line.slice(start_x)));
300 chunks = [escapeHTML(line)];
302 for (const chunk of chunks) {
303 pre_content += chunk;
307 this.pre_el.innerHTML = pre_content;
309 write: function(start_y, start_x, msg) {
310 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
311 this.content[start_y][x] = msg[i];
314 drawBox: function(start_y, start_x, height, width) {
315 let end_y = start_y + height;
316 let end_x = start_x + width;
317 for (let y = start_y, x = start_x; y < this.rows; x++) {
325 this.content[y][x] = ' ';
329 terminal.initialize();
332 tokenize: function(str) {
337 for (let i = 0; i < str.length; i++) {
343 } else if (c == '\\') {
345 } else if (c == '"') {
350 } else if (c == '"') {
352 } else if (c === ' ') {
353 if (token.length > 0) {
361 if (token.length > 0) {
366 parse_yx: function(position_string) {
367 let coordinate_strings = position_string.split(',')
368 let position = [0, 0];
369 position[0] = parseInt(coordinate_strings[0].slice(2));
370 position[1] = parseInt(coordinate_strings[1].slice(2));
382 init: function(url) {
384 this.websocket = new WebSocket(this.url);
385 this.websocket.onopen = function(event) {
386 server.connected = true;
387 game.thing_types = {};
389 server.send(['TASKS']);
390 server.send(['TERRAINS']);
391 server.send(['THING_TYPES']);
392 tui.log_msg("@ server connected! :)");
393 tui.switch_mode('login');
395 this.websocket.onclose = function(event) {
396 server.connected = false;
397 tui.switch_mode('waiting_for_server');
398 tui.log_msg("@ server disconnected :(");
400 this.websocket.onmessage = this.handle_event;
402 reconnect_to: function(url) {
403 this.websocket.close();
406 send: function(tokens) {
407 this.websocket.send(unparser.untokenize(tokens));
409 handle_event: function(event) {
410 let tokens = parser.tokenize(event.data);
411 if (tokens[0] === 'TURN') {
412 game.turn_complete = false;
413 explorer.empty_info_db();
416 game.turn = parseInt(tokens[1]);
417 } else if (tokens[0] === 'THING') {
418 let t = game.get_thing(tokens[4], true);
419 t.position = parser.parse_yx(tokens[1]);
421 t.protection = tokens[3];
422 } else if (tokens[0] === 'THING_NAME') {
423 let t = game.get_thing(tokens[1], false);
427 } else if (tokens[0] === 'THING_CHAR') {
428 let t = game.get_thing(tokens[1], false);
430 t.player_char = tokens[2];
432 } else if (tokens[0] === 'TASKS') {
433 game.tasks = tokens[1].split(',');
434 tui.mode_write.legal = game.tasks.includes('WRITE');
435 } else if (tokens[0] === 'THING_TYPE') {
436 game.thing_types[tokens[1]] = tokens[2]
437 } else if (tokens[0] === 'TERRAIN') {
438 game.terrains[tokens[1]] = tokens[2]
439 } else if (tokens[0] === 'MAP') {
440 game.map_geometry = tokens[1];
442 game.map_size = parser.parse_yx(tokens[2]);
444 } else if (tokens[0] === 'FOV') {
446 } else if (tokens[0] === 'MAP_CONTROL') {
447 game.map_control = tokens[1]
448 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
449 game.turn_complete = true;
450 if (tui.mode.name == 'post_login_wait') {
451 tui.switch_mode('play');
452 } else if (tui.mode.name == 'study') {
453 explorer.query_info();
456 } else if (tokens[0] === 'CHAT') {
457 tui.log_msg('# ' + tokens[1], 1);
458 } else if (tokens[0] === 'PLAYER_ID') {
459 game.player_id = parseInt(tokens[1]);
460 } else if (tokens[0] === 'LOGIN_OK') {
461 this.send(['GET_GAMESTATE']);
462 tui.switch_mode('post_login_wait');
463 } else if (tokens[0] === 'ADMIN_OK') {
465 tui.log_msg('@ you now have admin rights');
466 tui.switch_mode('admin');
467 } else if (tokens[0] === 'PORTAL') {
468 let position = parser.parse_yx(tokens[1]);
469 game.portals[position] = tokens[2];
470 } else if (tokens[0] === 'ANNOTATION_HINT') {
471 let position = parser.parse_yx(tokens[1]);
472 explorer.info_hints = explorer.info_hints.concat([position]);
473 } else if (tokens[0] === 'ANNOTATION') {
474 let position = parser.parse_yx(tokens[1]);
475 explorer.update_info_db(position, tokens[2]);
477 } else if (tokens[0] === 'UNHANDLED_INPUT') {
478 tui.log_msg('? unknown command');
479 } else if (tokens[0] === 'PLAY_ERROR') {
480 tui.log_msg('? ' + tokens[1]);
481 terminal.blink_screen();
482 } else if (tokens[0] === 'ARGUMENT_ERROR') {
483 tui.log_msg('? syntax error: ' + tokens[1]);
484 } else if (tokens[0] === 'GAME_ERROR') {
485 tui.log_msg('? game error: ' + tokens[1]);
486 } else if (tokens[0] === 'PONG') {
489 tui.log_msg('? unhandled input: ' + event.data);
495 quote: function(str) {
497 for (let i = 0; i < str.length; i++) {
499 if (['"', '\\'].includes(c)) {
505 return quoted.join('');
507 to_yx: function(yx_coordinate) {
508 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
510 untokenize: function(tokens) {
511 let quoted_tokens = [];
512 for (let token of tokens) {
513 quoted_tokens.push(this.quote(token));
515 return quoted_tokens.join(" ");
520 constructor(name, has_input_prompt=false, shows_info=false,
521 is_intro=false, is_single_char_entry=false) {
523 this.short_desc = mode_helps[name].short;
524 this.available_modes = [];
525 this.has_input_prompt = has_input_prompt;
526 this.shows_info= shows_info;
527 this.is_intro = is_intro;
528 this.help_intro = mode_helps[name].long;
529 this.is_single_char_entry = is_single_char_entry;
532 *iter_available_modes() {
533 for (let mode_name of this.available_modes) {
534 let mode = tui['mode_' + mode_name];
538 let key = tui.keys['switch_to_' + mode.name];
542 list_available_modes() {
544 if (this.available_modes.length > 0) {
545 msg += 'Other modes available from here:\n';
546 for (let [mode, key] of this.iter_available_modes()) {
547 msg += '[' + key + '] – ' + mode.short_desc + '\n';
552 mode_switch_on_key(key_event) {
553 for (let [mode, key] of this.iter_available_modes()) {
554 if (key_event.key == key) {
555 event.preventDefault();
556 tui.switch_mode(mode.name);
568 window_width: terminal.cols / 2,
576 mode_waiting_for_server: new Mode('waiting_for_server',
578 mode_login: new Mode('login', true, false, true),
579 mode_post_login_wait: new Mode('post_login_wait'),
580 mode_chat: new Mode('chat', true),
581 mode_annotate: new Mode('annotate', true, true),
582 mode_play: new Mode('play'),
583 mode_study: new Mode('study', false, true),
584 mode_write: new Mode('write', false, false, false, true),
585 mode_edit: new Mode('edit'),
586 mode_control_pw_type: new Mode('control_pw_type', true),
587 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
588 mode_portal: new Mode('portal', true, true),
589 mode_password: new Mode('password', true),
590 mode_name_thing: new Mode('name_thing', true, true),
591 mode_admin_enter: new Mode('admin_enter', true),
592 mode_admin: new Mode('admin'),
593 mode_control_pw_pw: new Mode('control_pw_pw', true),
594 mode_control_tile_type: new Mode('control_tile_type', true),
595 mode_control_tile_draw: new Mode('control_tile_draw'),
597 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
598 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
599 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
600 "control_tile_type", "chat",
601 "study", "play", "edit"]
602 this.mode_control_tile_draw.available_modes = ["admin_enter"]
603 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
604 "password", "chat", "study", "play",
606 this.mode = this.mode_waiting_for_server;
607 this.inputEl = document.getElementById("input");
608 this.inputEl.focus();
609 this.recalc_input_lines();
610 this.height_header = this.height_turn_line + this.height_mode_line;
611 this.log_msg("@ waiting for server connection ...");
614 init_keys: function() {
616 for (let key_selector of key_selectors) {
617 this.keys[key_selector.id.slice(4)] = key_selector.value;
619 this.movement_keys = {};
620 if (!game.map_geometry) {
623 let geometry_prefix = game.map_geometry.toLowerCase() + '_';
624 for (const key_name of Object.keys(key_descriptions)) {
625 if (key_name.startsWith(geometry_prefix)) {
626 let direction = key_name.split('_')[2].toUpperCase();
627 let key = this.keys[key_name];
628 this.movement_keys[key] = direction;
631 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
632 move_button.hidden = true;
634 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
635 move_button.hidden = false;
637 for (let el of document.getElementsByTagName("button")) {
638 let action_desc = key_descriptions[el.id];
639 let action_key = '[' + this.keys[el.id] + ']';
640 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
643 switch_mode: function(mode_name) {
644 if (this.mode.name == 'control_tile_draw') {
645 tui.log_msg('@ finished tile protection drawing.')
647 this.tile_draw = false;
648 if (mode_name == 'admin_enter' && this.is_admin) {
650 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
651 let player_position = game.things[game.player_id].position;
653 for (let t_id in game.things) {
654 if (t_id == game.player_id) {
657 let t = game.things[t_id];
658 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
664 terminal.blink_screen();
665 this.log_msg('? not standing over thing');
668 this.selected_thing_id = thing_id;
671 this.mode = this['mode_' + mode_name];
672 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
673 this.map_mode = 'protections';
674 } else if (this.mode.name != "edit") {
675 this.map_mode = 'terrain + things';
677 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
678 this.inputEl.focus();
680 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
681 explorer.position = game.things[game.player_id].position;
682 if (this.mode.shows_info) {
683 explorer.query_info();
686 this.inputEl.value = "";
687 this.restore_input_values();
688 for (let el of document.getElementsByTagName("button")) {
691 document.getElementById("help").disabled = false;
692 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit' || this.mode.name == 'admin') {
693 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
694 move_key.disabled = false;
697 if (!this.mode.is_intro && this.mode.name != 'play') {
698 document.getElementById("switch_to_play").disabled = false;
700 if (!this.mode.is_intro && this.mode.name != 'study') {
701 document.getElementById("switch_to_study").disabled = false;
703 if (!this.mode.is_intro && this.mode.name != 'chat') {
704 document.getElementById("switch_to_chat").disabled = false;
706 if (!this.mode.is_intro && this.mode.name != 'edit') {
707 document.getElementById("switch_to_edit").disabled = false;
709 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
710 document.getElementById("switch_to_admin_enter").disabled = false;
712 if (this.mode.name == 'login') {
713 if (this.login_name) {
714 server.send(['LOGIN', this.login_name]);
716 this.log_msg("? need login name");
718 } else if (this.mode.name == 'play') {
719 if (game.tasks.includes('PICK_UP')) {
720 document.getElementById("take_thing").disabled = false;
722 if (game.tasks.includes('DROP')) {
723 document.getElementById("drop_thing").disabled = false;
725 if (game.tasks.includes('MOVE')) {
727 document.getElementById("teleport").disabled = false;
728 } else if (this.mode.name == 'edit') {
729 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
730 document.getElementById("flatten").disabled = false;
732 document.getElementById("switch_to_annotate").disabled = false;
733 document.getElementById("switch_to_write").disabled = false;
734 document.getElementById("switch_to_portal").disabled = false;
735 document.getElementById("switch_to_password").disabled = false;
736 document.getElementById("switch_to_name_thing").disabled = false;
737 document.getElementById("toggle_map_mode").disabled = false;
738 } else if (this.mode.name == 'admin') {
739 document.getElementById("switch_to_control_pw_type").disabled = false;
740 document.getElementById("switch_to_control_tile_type").disabled = false;
741 document.getElementById("switch_to_admin_thing_protect").disabled = false;
742 } else if (this.mode.name == 'study') {
743 document.getElementById("toggle_map_mode").disabled = false;
744 } else if (this.mode.is_single_char_entry) {
745 this.show_help = true;
746 } else if (this.mode.name == 'admin_enter') {
747 this.log_msg('@ enter admin password:')
748 } else if (this.mode.name == 'control_pw_type') {
749 this.log_msg('@ enter protection character for which you want to change the password:')
750 } else if (this.mode.name == 'control_tile_type') {
751 this.log_msg('@ enter protection character which you want to draw:')
752 } else if (this.mode.name == 'admin_thing_protect') {
753 this.log_msg('@ enter thing protection character:')
754 } else if (this.mode.name == 'control_pw_pw') {
755 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
756 } else if (this.mode.name == 'control_tile_draw') {
757 document.getElementById("toggle_tile_draw").disabled = false;
758 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 + '].')
762 offset_links: function(offset, links) {
763 for (let y in links) {
764 let real_y = offset[0] + parseInt(y);
765 if (!this.links[real_y]) {
766 this.links[real_y] = [];
768 for (let link of links[y]) {
769 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
770 this.links[real_y].push(offset_link);
774 restore_input_values: function() {
775 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
776 let info = explorer.info_db[explorer.position];
777 if (info != "(none)") {
778 this.inputEl.value = info;
780 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
781 let portal = game.portals[explorer.position]
782 this.inputEl.value = portal;
783 } else if (this.mode.name == 'password') {
784 this.inputEl.value = this.password;
785 } else if (this.mode.name == 'name_thing') {
786 let t = game.get_thing(this.selected_thing_id);
788 this.inputEl.value = t.name_;
790 } else if (this.mode.name == 'admin_thing_protect') {
791 let t = game.get_thing(this.selected_thing_id);
792 if (t && t.protection) {
793 this.inputEl.value = t.protection;
797 recalc_input_lines: function() {
798 if (this.mode.has_input_prompt) {
800 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
802 this.input_lines = [];
804 this.height_input = this.input_lines.length;
806 msg_into_lines_of_width: function(msg, width) {
807 function push_inner_link(y, end_x) {
808 if (!inner_links[y]) {
811 inner_links[y].push([url_start_x, end_x, url]);
813 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
816 for (const match of matches) {
817 const url = match[0];
818 const url_start = match.index;
819 const url_end = match.index + match[0].length;
820 link_data[url_start] = url;
821 url_ends.push(url_end);
825 let inner_links = {};
829 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
830 if (x >= width || msg[i] == "\n") {
832 push_inner_link(y, chunk.length);
834 if (url_ends[0] == i) {
842 if (msg[i] == "\n") {
847 if (msg[i] != "\n") {
850 if (i in link_data) {
854 } else if (url_ends[0] == i) {
856 push_inner_link(y, x);
862 push_inner_link(lines.length - 1, chunk.length);
864 return [lines, inner_links];
866 log_msg: function(msg) {
868 while (this.log.length > 100) {
873 draw_map: function() {
874 let map_lines_split = [];
876 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
877 if (j == game.map_size[1]) {
878 map_lines_split.push(line);
882 if (this.map_mode == 'protections') {
883 line.push(game.map_control[i] + ' ');
885 line.push(game.map[i] + ' ');
888 map_lines_split.push(line);
889 if (this.map_mode == 'terrain + annotations') {
890 for (const coordinate of explorer.info_hints) {
891 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
893 } else if (this.map_mode == 'terrain + things') {
894 for (const p in game.portals) {
895 let coordinate = p.split(',')
896 let original = map_lines_split[coordinate[0]][coordinate[1]];
897 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
899 let used_positions = [];
900 for (const thing_id in game.things) {
901 let t = game.things[thing_id];
902 let symbol = game.thing_types[t.type_];
905 meta_char = t.player_char;
907 if (used_positions.includes(t.position.toString())) {
910 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
911 used_positions.push(t.position.toString());
914 let player = game.things[game.player_id];
915 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
916 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
917 } else if (tui.map_mode != 'terrain + things') {
918 map_lines_split[player.position[0]][player.position[1]] = '??';
921 if (game.map_geometry == 'Square') {
922 for (let line_split of map_lines_split) {
923 map_lines.push(line_split.join(''));
925 } else if (game.map_geometry == 'Hex') {
927 for (let line_split of map_lines_split) {
928 map_lines.push(' '.repeat(indent) + line_split.join(''));
936 let window_center = [terminal.rows / 2, this.window_width / 2];
937 let center_position = [player.position[0], player.position[1]];
938 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
939 center_position = [explorer.position[0], explorer.position[1]];
941 center_position[1] = center_position[1] * 2;
942 let offset = [center_position[0] - window_center[0],
943 center_position[1] - window_center[1]]
944 if (game.map_geometry == 'Hex' && offset[0] % 2) {
947 let term_y = Math.max(0, -offset[0]);
948 let term_x = Math.max(0, -offset[1]);
949 let map_y = Math.max(0, offset[0]);
950 let map_x = Math.max(0, offset[1]);
951 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
952 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
953 terminal.write(term_y, term_x, to_draw);
956 draw_mode_line: function() {
957 let help = 'hit [' + this.keys.help + '] for help';
958 if (this.mode.has_input_prompt) {
959 help = 'enter /help for help';
961 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
963 draw_turn_line: function(n) {
964 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
966 draw_history: function() {
967 let log_display_lines = [];
969 let y_offset_in_log = 0;
970 for (let line of this.log) {
971 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
973 log_display_lines = log_display_lines.concat(new_lines);
974 for (const y in link_data) {
975 const rel_y = y_offset_in_log + parseInt(y);
976 log_links[rel_y] = [];
977 for (let link of link_data[y]) {
978 log_links[rel_y].push(link);
981 y_offset_in_log += new_lines.length;
983 let i = log_display_lines.length - 1;
984 for (let y = terminal.rows - 1 - this.height_input;
985 y >= this.height_header && i >= 0;
987 terminal.write(y, this.window_width, log_display_lines[i]);
989 for (const key of Object.keys(log_links)) {
990 if (parseInt(key) <= i) {
991 delete log_links[key];
994 let offset = [terminal.rows - this.height_input - log_display_lines.length,
996 this.offset_links(offset, log_links);
998 draw_info: function() {
999 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
1001 let offset = [this.height_header, this.window_width];
1002 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1003 terminal.write(y, offset[1], lines[i]);
1005 this.offset_links(offset, link_data);
1007 draw_input: function() {
1008 if (this.mode.has_input_prompt) {
1009 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1010 terminal.write(y, this.window_width, this.input_lines[i]);
1014 draw_help: function() {
1015 let movement_keys_desc = '';
1016 if (!this.mode.is_intro) {
1017 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1019 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1020 if (this.mode.name == 'play') {
1021 content += "Available actions:\n";
1022 if (game.tasks.includes('MOVE')) {
1023 content += "[" + movement_keys_desc + "] – move player\n";
1025 if (game.tasks.includes('PICK_UP')) {
1026 content += "[" + this.keys.take_thing + "] – pick up thing\n";
1028 if (game.tasks.includes('DROP')) {
1029 content += "[" + this.keys.drop_thing + "] – drop thing\n";
1031 content += "[" + tui.keys.teleport + "] – teleport\n";
1033 } else if (this.mode.name == 'study') {
1034 content += "Available actions:\n";
1035 content += '[' + movement_keys_desc + '] – move question mark\n';
1036 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1038 } else if (this.mode.name == 'edit') {
1039 content += "Available actions:\n";
1040 if (game.tasks.includes('MOVE')) {
1041 content += "[" + movement_keys_desc + "] – move player\n";
1043 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1044 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
1046 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1048 } else if (this.mode.name == 'control_tile_draw') {
1049 content += "Available actions:\n";
1050 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
1052 } else if (this.mode.name == 'chat') {
1053 content += '/nick NAME – re-name yourself to NAME\n';
1054 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1055 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1056 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1057 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1058 } else if (this.mode.name == 'admin') {
1059 content += "Available actions:\n";
1060 if (game.tasks.includes('MOVE')) {
1061 content += "[" + movement_keys_desc + "] – move player\n";
1065 content += this.mode.list_available_modes();
1067 if (!this.mode.has_input_prompt) {
1068 start_x = this.window_width
1070 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1071 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1072 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1073 terminal.write(y, start_x, lines[i]);
1076 toggle_tile_draw: function() {
1077 if (tui.tile_draw) {
1078 tui.tile_draw = false;
1080 tui.tile_draw = true;
1083 toggle_map_mode: function() {
1084 if (tui.map_mode == 'terrain only') {
1085 tui.map_mode = 'terrain + annotations';
1086 } else if (tui.map_mode == 'terrain + annotations') {
1087 tui.map_mode = 'terrain + things';
1088 } else if (tui.map_mode == 'terrain + things') {
1089 tui.map_mode = 'protections';
1090 } else if (tui.map_mode == 'protections') {
1091 tui.map_mode = 'terrain only';
1094 full_refresh: function() {
1096 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1097 this.recalc_input_lines();
1098 if (this.mode.is_intro) {
1099 this.draw_history();
1102 if (game.turn_complete) {
1104 this.draw_turn_line();
1106 this.draw_mode_line();
1107 if (this.mode.shows_info) {
1110 this.draw_history();
1114 if (this.show_help) {
1126 this.map_control = "";
1127 this.map_size = [0,0];
1128 this.player_id = -1;
1132 get_thing: function(id_, create_if_not_found=false) {
1133 if (id_ in game.things) {
1134 return game.things[id_];
1135 } else if (create_if_not_found) {
1136 let t = new Thing([0,0]);
1137 game.things[id_] = t;
1141 move: function(start_position, direction) {
1142 let target = [start_position[0], start_position[1]];
1143 if (direction == 'LEFT') {
1145 } else if (direction == 'RIGHT') {
1147 } else if (game.map_geometry == 'Square') {
1148 if (direction == 'UP') {
1150 } else if (direction == 'DOWN') {
1153 } else if (game.map_geometry == 'Hex') {
1154 let start_indented = start_position[0] % 2;
1155 if (direction == 'UPLEFT') {
1157 if (!start_indented) {
1160 } else if (direction == 'UPRIGHT') {
1162 if (start_indented) {
1165 } else if (direction == 'DOWNLEFT') {
1167 if (!start_indented) {
1170 } else if (direction == 'DOWNRIGHT') {
1172 if (start_indented) {
1177 if (target[0] < 0 || target[1] < 0 ||
1178 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1183 teleport: function() {
1184 let player = this.get_thing(game.player_id);
1185 if (player.position in this.portals) {
1186 server.reconnect_to(this.portals[player.position]);
1188 terminal.blink_screen();
1189 tui.log_msg('? not standing on portal')
1197 server.init(websocket_location);
1203 move: function(direction) {
1204 let target = game.move(this.position, direction);
1206 this.position = target
1207 if (tui.mode.shows_info) {
1209 } else if (tui.tile_draw) {
1210 this.send_tile_control_command();
1213 terminal.blink_screen();
1216 update_info_db: function(yx, str) {
1217 this.info_db[yx] = str;
1218 if (tui.mode.name == 'study') {
1222 empty_info_db: function() {
1224 this.info_hints = [];
1225 if (tui.mode.name == 'study') {
1229 query_info: function() {
1230 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1232 get_info: function() {
1233 let info = "MAP VIEW: " + tui.map_mode + "\n";
1234 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1235 if (game.fov[position_i] != '.') {
1236 return info + 'outside field of view';
1238 let terrain_char = game.map[position_i]
1239 let terrain_desc = '?'
1240 if (game.terrains[terrain_char]) {
1241 terrain_desc = game.terrains[terrain_char];
1243 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1244 let protection = game.map_control[position_i];
1245 if (protection == '.') {
1246 protection = 'unprotected';
1248 info += 'PROTECTION: ' + protection + '\n';
1249 for (let t_id in game.things) {
1250 let t = game.things[t_id];
1251 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1252 let symbol = game.thing_types[t.type_];
1253 let protection = t.protection;
1254 if (protection == '.') {
1255 protection = 'unprotected';
1257 info += "THING: " + t.type_ + " / protection: " + protection + " / " + symbol;
1258 if (t.player_char) {
1259 info += t.player_char;
1262 info += " (" + t.name_ + ")";
1267 if (this.position in game.portals) {
1268 info += "PORTAL: " + game.portals[this.position] + "\n";
1270 if (this.position in this.info_db) {
1271 info += "ANNOTATIONS: " + this.info_db[this.position];
1273 info += 'waiting …';
1277 annotate: function(msg) {
1278 if (msg.length == 0) {
1279 msg = " "; // triggers annotation deletion
1281 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1283 set_portal: function(msg) {
1284 if (msg.length == 0) {
1285 msg = " "; // triggers portal deletion
1287 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1289 send_tile_control_command: function() {
1290 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1294 tui.inputEl.addEventListener('input', (event) => {
1295 if (tui.mode.has_input_prompt) {
1296 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1297 if (tui.inputEl.value.length > max_length) {
1298 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1300 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1301 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1302 tui.switch_mode('edit');
1306 document.onclick = function() {
1307 tui.show_help = false;
1309 tui.inputEl.addEventListener('keydown', (event) => {
1310 tui.show_help = false;
1311 if (event.key == 'Enter') {
1312 event.preventDefault();
1314 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1315 tui.show_help = true;
1316 tui.inputEl.value = "";
1317 tui.restore_input_values();
1318 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1319 && !tui.mode.is_single_char_entry) {
1320 tui.show_help = true;
1321 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1322 tui.login_name = tui.inputEl.value;
1323 server.send(['LOGIN', tui.inputEl.value]);
1324 tui.inputEl.value = "";
1325 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1326 if (tui.inputEl.value.length == 0) {
1327 tui.log_msg('@ aborted');
1329 server.send(['SET_MAP_CONTROL_PASSWORD',
1330 tui.tile_control_char, tui.inputEl.value]);
1331 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1333 tui.switch_mode('admin');
1334 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1335 explorer.set_portal(tui.inputEl.value);
1336 tui.switch_mode('edit');
1337 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1338 if (tui.inputEl.value.length == 0) {
1339 tui.inputEl.value = " ";
1341 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1343 tui.switch_mode('edit');
1344 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1345 explorer.annotate(tui.inputEl.value);
1346 tui.switch_mode('edit');
1347 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1348 if (tui.inputEl.value.length == 0) {
1349 tui.inputEl.value = " ";
1351 tui.password = tui.inputEl.value
1352 tui.switch_mode('edit');
1353 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1354 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1355 tui.switch_mode('play');
1356 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1357 if (tui.inputEl.value.length != 1) {
1358 tui.log_msg('@ entered non-single-char, therefore aborted');
1359 tui.switch_mode('admin');
1361 tui.tile_control_char = tui.inputEl.value[0];
1362 tui.switch_mode('control_pw_pw');
1364 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1365 if (tui.inputEl.value.length != 1) {
1366 tui.log_msg('@ entered non-single-char, therefore aborted');
1367 tui.switch_mode('admin');
1369 tui.tile_control_char = tui.inputEl.value[0];
1370 tui.switch_mode('control_tile_draw');
1372 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1373 if (tui.inputEl.value.length != 1) {
1374 tui.log_msg('@ entered non-single-char, therefore aborted');
1376 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1377 tui.log_msg('@ sent new protection character for thing');
1379 tui.switch_mode('admin');
1380 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1381 let tokens = parser.tokenize(tui.inputEl.value);
1382 if (tokens.length > 0 && tokens[0].length > 0) {
1383 if (tui.inputEl.value[0][0] == '/') {
1384 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1385 tui.switch_mode('play');
1386 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1387 tui.switch_mode('study');
1388 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1389 tui.switch_mode('edit');
1390 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1391 tui.switch_mode('admin_enter');
1392 } else if (tokens[0].slice(1) == 'nick') {
1393 if (tokens.length > 1) {
1394 server.send(['NICK', tokens[1]]);
1396 tui.log_msg('? need new name');
1399 tui.log_msg('? unknown command');
1402 server.send(['ALL', tui.inputEl.value]);
1404 } else if (tui.inputEl.valuelength > 0) {
1405 server.send(['ALL', tui.inputEl.value]);
1407 tui.inputEl.value = "";
1408 } else if (tui.mode.name == 'play') {
1409 if (tui.mode.mode_switch_on_key(event)) {
1411 } else if (event.key === tui.keys.take_thing
1412 && game.tasks.includes('PICK_UP')) {
1413 server.send(["TASK:PICK_UP"]);
1414 } else if (event.key === tui.keys.drop_thing
1415 && game.tasks.includes('DROP')) {
1416 server.send(["TASK:DROP"]);
1417 } else if (event.key in tui.movement_keys
1418 && game.tasks.includes('MOVE')) {
1419 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1420 } else if (event.key === tui.keys.teleport) {
1423 } else if (tui.mode.name == 'study') {
1424 if (tui.mode.mode_switch_on_key(event)) {
1426 } else if (event.key in tui.movement_keys) {
1427 explorer.move(tui.movement_keys[event.key]);
1428 } else if (event.key == tui.keys.toggle_map_mode) {
1429 tui.toggle_map_mode();
1431 } else if (tui.mode.name == 'control_tile_draw') {
1432 if (tui.mode.mode_switch_on_key(event)) {
1434 } else if (event.key in tui.movement_keys) {
1435 explorer.move(tui.movement_keys[event.key]);
1436 } else if (event.key === tui.keys.toggle_tile_draw) {
1437 tui.toggle_tile_draw();
1439 } else if (tui.mode.name == 'admin') {
1440 if (tui.mode.mode_switch_on_key(event)) {
1442 } else if (event.key in tui.movement_keys
1443 && game.tasks.includes('MOVE')) {
1444 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1446 } else if (tui.mode.name == 'edit') {
1447 if (tui.mode.mode_switch_on_key(event)) {
1449 } else if (event.key in tui.movement_keys
1450 && game.tasks.includes('MOVE')) {
1451 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1452 } else if (event.key === tui.keys.flatten
1453 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1454 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1455 } else if (event.key == tui.keys.toggle_map_mode) {
1456 tui.toggle_map_mode();
1462 rows_selector.addEventListener('input', function() {
1463 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1466 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1467 terminal.initialize();
1470 cols_selector.addEventListener('input', function() {
1471 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1474 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1475 terminal.initialize();
1476 tui.window_width = terminal.cols / 2,
1479 for (let key_selector of key_selectors) {
1480 key_selector.addEventListener('input', function() {
1481 window.localStorage.setItem(key_selector.id, key_selector.value);
1485 window.setInterval(function() {
1486 if (server.connected) {
1487 server.send(['PING']);
1489 server.reconnect_to(server.url);
1490 tui.log_msg('@ attempting reconnect …')
1493 window.setInterval(function() {
1495 let span_decoration = "none";
1496 if (document.activeElement == tui.inputEl) {
1497 val = "on (click outside terminal to change)";
1499 val = "off (click into terminal to change)";
1500 span_decoration = "line-through";
1502 document.getElementById("keyboard_control").textContent = val;
1503 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1504 span.style.textDecoration = span_decoration;
1507 document.getElementById("terminal").onclick = function() {
1508 tui.inputEl.focus();
1510 document.getElementById("help").onclick = function() {
1511 tui.show_help = true;
1514 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1515 const mode = switchEl.id.slice("switch_to_".length);
1516 switchEl.onclick = function() {
1517 tui.switch_mode(mode);
1521 document.getElementById("toggle_tile_draw").onclick = function() {
1522 tui.toggle_tile_draw();
1524 document.getElementById("toggle_map_mode").onclick = function() {
1525 tui.toggle_map_mode();
1528 document.getElementById("take_thing").onclick = function() {
1529 server.send(['TASK:PICK_UP']);
1531 document.getElementById("drop_thing").onclick = function() {
1532 server.send(['TASK:DROP']);
1534 document.getElementById("flatten").onclick = function() {
1535 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1537 document.getElementById("teleport").onclick = function() {
1540 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1541 let direction = move_button.id.split('_')[2].toUpperCase();
1542 move_button.onclick = function() {
1543 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1544 server.send(['TASK:MOVE', direction]);
1546 explorer.move(direction);