13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table id="move_table" style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
26 <td style="text-align: center"><button id="square_move_up"></button></td>
27 <td><button id="hex_move_upright"></button></td>
30 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
35 <td><button id="hex_move_downleft"></button></td>
36 <td style="text-align: center"><button id="square_move_down"></button></td>
37 <td><button id="hex_move_downright"></button></td>
42 <td><button id="help"></button></td>
45 <td><button id="switch_to_chat"></button><br /></td>
48 <td><button id="switch_to_study"></button></td>
49 <td><button id="toggle_map_mode"></button>
52 <td><button id="switch_to_play"></button></td>
54 <button id="take_thing"></button>
55 <button id="drop_thing"></button>
56 <button id="door"></button>
57 <button id="consume"></button>
58 <button id="switch_to_command_thing"></button>
59 <button id="teleport"></button>
63 <td><button id="switch_to_edit"></button></td>
65 <button id="switch_to_write"></button>
66 <button id="flatten"></button>
67 <button id="switch_to_annotate"></button>
68 <button id="switch_to_portal"></button>
69 <button id="switch_to_name_thing"></button>
70 <button id="switch_to_password"></button>
74 <td><button id="switch_to_admin_enter"></button></td>
76 <button id="switch_to_control_pw_type"></button>
77 <button id="switch_to_control_tile_type"></button>
78 <button id="switch_to_admin_thing_protect"></button>
79 <button id="toggle_tile_draw"></button>
84 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
86 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
87 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
88 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
89 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
90 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
91 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
92 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
93 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
94 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
95 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
96 <li>help: <input id="key_help" type="text" value="h" />
97 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
98 <li>teleport: <input id="key_teleport" type="text" value="p" />
99 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
100 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
101 <li>open/close: <input id="key_door" type="text" value="D" />
102 <li>consume: <input id="key_consume" type="text" value="C" />
103 <li><input id="key_switch_to_chat" type="text" value="t" />
104 <li><input id="key_switch_to_play" type="text" value="p" />
105 <li><input id="key_switch_to_study" type="text" value="?" />
106 <li><input id="key_switch_to_edit" type="text" value="E" />
107 <li><input id="key_switch_to_write" type="text" value="m" />
108 <li><input id="key_switch_to_name_thing" type="text" value="N" />
109 <li><input id="key_switch_to_command_thing" type="text" value="O" />
110 <li><input id="key_switch_to_password" type="text" value="P" />
111 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
112 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
113 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
114 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
115 <li><input id="key_switch_to_annotate" type="text" value="M" />
116 <li><input id="key_switch_to_portal" type="text" value="T" />
117 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
118 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
123 let websocket_location = "wss://plomlompom.com/rogue_chat/";
124 //let websocket_location = "ws://localhost:8001/";
129 'long': 'This mode allows you to interact with the map in various ways.'
133 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
135 'short': 'world edit',
136 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
139 'short': 'name thing',
140 'long': 'Give name to/change name of thing here.'
143 'short': 'command thing',
144 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
146 'admin_thing_protect': {
147 'short': 'change thing protection',
148 'long': 'Change protection character for thing here.'
151 'short': 'change terrain',
152 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
155 'short': 'change protection character password',
156 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
159 'short': 'change protection character password',
160 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
162 'control_tile_type': {
163 'short': 'change tiles protection',
164 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
166 'control_tile_draw': {
167 'short': 'change tiles protection',
168 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
171 'short': 'annotate tile',
172 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
175 'short': 'edit portal',
176 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
180 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:'
184 'long': 'Enter your player name.'
186 'waiting_for_server': {
187 'short': 'waiting for server response',
188 'long': 'Waiting for a server response.'
191 'short': 'waiting for server response',
192 'long': 'Waiting for a server response.'
195 'short': 'set world edit password',
196 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
199 'short': 'become admin',
200 'long': 'This mode allows you to become admin if you know an admin password.'
204 'long': 'This mode allows you access to actions limited to administrators.'
207 let key_descriptions = {
209 'flatten': 'flatten surroundings',
210 'teleport': 'teleport',
211 'take_thing': 'pick up thing',
212 'drop_thing': 'drop thing',
213 'door': 'open/close',
214 'consume': 'consume',
215 'toggle_map_mode': 'toggle map view',
216 'toggle_tile_draw': 'toggle protection character drawing',
217 'hex_move_upleft': 'up-left',
218 'hex_move_upright': 'up-right',
219 'hex_move_right': 'right',
220 'hex_move_left': 'left',
221 'hex_move_downleft': 'down-left',
222 'hex_move_downright': 'down-right',
223 'square_move_up': 'up',
224 'square_move_left': 'left',
225 'square_move_down': 'down',
226 'square_move_right': 'right',
228 for (const mode_name of Object.keys(mode_helps)) {
229 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
232 let rows_selector = document.getElementById("n_rows");
233 let cols_selector = document.getElementById("n_cols");
234 let key_selectors = document.querySelectorAll('[id^="key_"]');
236 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
237 const action = key_switch_selector.id.slice("key_switch_to_".length);
238 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
241 function restore_selector_value(selector) {
242 let stored_selection = window.localStorage.getItem(selector.id);
243 if (stored_selection) {
244 selector.value = stored_selection;
247 restore_selector_value(rows_selector);
248 restore_selector_value(cols_selector);
249 for (let key_selector of key_selectors) {
250 restore_selector_value(key_selector);
253 function escapeHTML(str) {
255 replace(/&/g, '&').
256 replace(/</g, '<').
257 replace(/>/g, '>').
258 replace(/'/g, ''').
259 replace(/"/g, '"');
263 initialize: function() {
264 this.rows = rows_selector.value;
265 this.cols = cols_selector.value;
266 this.pre_el = document.getElementById("terminal");
267 this.set_default_colors();
271 for (let y = 0, x = 0; y <= this.rows; x++) {
272 if (x == this.cols) {
275 this.content.push(line);
277 if (y == this.rows) {
284 apply_colors: function() {
285 this.pre_el.style.color = this.foreground;
286 this.pre_el.style.backgroundColor = this.background;
288 set_default_colors: function() {
289 this.foreground = 'white';
290 this.background = 'black';
293 set_random_colors: function() {
294 function rand(offset) {
295 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
297 this.foreground = '#' + rand(159) + rand(159) + rand(159);
298 this.background = '#' + rand(0) + rand(0) + rand(0);
301 blink_screen: function() {
302 this.pre_el.style.color = this.background;
303 this.pre_el.style.backgroundColor = this.foreground;
305 this.pre_el.style.color = this.foreground;
306 this.pre_el.style.backgroundColor = this.background;
309 refresh: function() {
310 let pre_content = '';
311 for (let y = 0; y < this.rows; y++) {
312 let line = this.content[y].join('');
314 if (y in tui.links) {
316 for (let span of tui.links[y]) {
317 chunks.push(escapeHTML(line.slice(start_x, span[0])));
318 chunks.push('<a target="_blank" href="');
319 chunks.push(escapeHTML(span[2]));
321 chunks.push(escapeHTML(line.slice(span[0], span[1])));
325 chunks.push(escapeHTML(line.slice(start_x)));
327 chunks = [escapeHTML(line)];
329 for (const chunk of chunks) {
330 pre_content += chunk;
334 this.pre_el.innerHTML = pre_content;
336 write: function(start_y, start_x, msg) {
337 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
338 this.content[start_y][x] = msg[i];
341 drawBox: function(start_y, start_x, height, width) {
342 let end_y = start_y + height;
343 let end_x = start_x + width;
344 for (let y = start_y, x = start_x; y < this.rows; x++) {
352 this.content[y][x] = ' ';
356 terminal.initialize();
359 tokenize: function(str) {
364 for (let i = 0; i < str.length; i++) {
370 } else if (c == '\\') {
372 } else if (c == '"') {
377 } else if (c == '"') {
379 } else if (c === ' ') {
380 if (token.length > 0) {
388 if (token.length > 0) {
393 parse_yx: function(position_string) {
394 let coordinate_strings = position_string.split(',')
395 let position = [0, 0];
396 position[0] = parseInt(coordinate_strings[0].slice(2));
397 position[1] = parseInt(coordinate_strings[1].slice(2));
409 init: function(url) {
411 this.websocket = new WebSocket(this.url);
412 this.websocket.onopen = function(event) {
413 server.connected = true;
414 game.thing_types = {};
416 server.send(['TASKS']);
417 server.send(['TERRAINS']);
418 server.send(['THING_TYPES']);
419 tui.log_msg("@ server connected! :)");
420 tui.switch_mode('login');
422 this.websocket.onclose = function(event) {
423 server.connected = false;
424 tui.switch_mode('waiting_for_server');
425 tui.log_msg("@ server disconnected :(");
427 this.websocket.onmessage = this.handle_event;
429 reconnect_to: function(url) {
430 this.websocket.close();
433 send: function(tokens) {
434 this.websocket.send(unparser.untokenize(tokens));
436 handle_event: function(event) {
437 let tokens = parser.tokenize(event.data);
438 if (tokens[0] === 'TURN') {
439 game.turn_complete = false;
440 explorer.empty_annotations();
444 game.turn = parseInt(tokens[1]);
445 } else if (tokens[0] === 'THING') {
446 let t = game.get_thing(tokens[4], true);
447 t.position = parser.parse_yx(tokens[1]);
449 t.protection = tokens[3];
450 } else if (tokens[0] === 'THING_NAME') {
451 let t = game.get_thing(tokens[1], false);
455 } else if (tokens[0] === 'THING_CHAR') {
456 let t = game.get_thing(tokens[1], false);
458 t.thing_char = tokens[2];
460 } else if (tokens[0] === 'TASKS') {
461 game.tasks = tokens[1].split(',');
462 tui.mode_write.legal = game.tasks.includes('WRITE');
463 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
464 } else if (tokens[0] === 'THING_TYPE') {
465 game.thing_types[tokens[1]] = tokens[2]
466 } else if (tokens[0] === 'TERRAIN') {
467 game.terrains[tokens[1]] = tokens[2]
468 } else if (tokens[0] === 'MAP') {
469 game.map_geometry = tokens[1];
471 game.map_size = parser.parse_yx(tokens[2]);
473 } else if (tokens[0] === 'FOV') {
475 } else if (tokens[0] === 'MAP_CONTROL') {
476 game.map_control = tokens[1]
477 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
478 game.turn_complete = true;
479 if (tui.mode.name == 'post_login_wait') {
480 tui.switch_mode('play');
482 explorer.info_cached = false;
484 } else if (tokens[0] === 'CHAT') {
485 tui.log_msg('# ' + tokens[1], 1);
486 } else if (tokens[0] === 'REPLY') {
487 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
488 } else if (tokens[0] === 'PLAYER_ID') {
489 game.player_id = parseInt(tokens[1]);
490 } else if (tokens[0] === 'LOGIN_OK') {
491 this.send(['GET_GAMESTATE']);
492 tui.switch_mode('post_login_wait');
493 } else if (tokens[0] === 'DEFAULT_COLORS') {
494 terminal.set_default_colors();
495 } else if (tokens[0] === 'RANDOM_COLORS') {
496 terminal.set_random_colors();
497 } else if (tokens[0] === 'ADMIN_OK') {
499 tui.log_msg('@ you now have admin rights');
500 tui.switch_mode('admin');
501 } else if (tokens[0] === 'PORTAL') {
502 let position = parser.parse_yx(tokens[1]);
503 game.portals[position] = tokens[2];
504 } else if (tokens[0] === 'ANNOTATION') {
505 let position = parser.parse_yx(tokens[1]);
506 explorer.update_annotations(position, tokens[2]);
508 } else if (tokens[0] === 'UNHANDLED_INPUT') {
509 tui.log_msg('? unknown command');
510 } else if (tokens[0] === 'PLAY_ERROR') {
511 tui.log_msg('? ' + tokens[1]);
512 terminal.blink_screen();
513 } else if (tokens[0] === 'ARGUMENT_ERROR') {
514 tui.log_msg('? syntax error: ' + tokens[1]);
515 } else if (tokens[0] === 'GAME_ERROR') {
516 tui.log_msg('? game error: ' + tokens[1]);
517 } else if (tokens[0] === 'PONG') {
520 tui.log_msg('? unhandled input: ' + event.data);
526 quote: function(str) {
528 for (let i = 0; i < str.length; i++) {
530 if (['"', '\\'].includes(c)) {
536 return quoted.join('');
538 to_yx: function(yx_coordinate) {
539 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
541 untokenize: function(tokens) {
542 let quoted_tokens = [];
543 for (let token of tokens) {
544 quoted_tokens.push(this.quote(token));
546 return quoted_tokens.join(" ");
551 constructor(name, has_input_prompt=false, shows_info=false,
552 is_intro=false, is_single_char_entry=false) {
554 this.short_desc = mode_helps[name].short;
555 this.available_modes = [];
556 this.available_actions = [];
557 this.has_input_prompt = has_input_prompt;
558 this.shows_info= shows_info;
559 this.is_intro = is_intro;
560 this.help_intro = mode_helps[name].long;
561 this.is_single_char_entry = is_single_char_entry;
564 *iter_available_modes() {
565 for (let mode_name of this.available_modes) {
566 let mode = tui['mode_' + mode_name];
570 let key = tui.keys['switch_to_' + mode.name];
574 list_available_modes() {
576 if (this.available_modes.length > 0) {
577 msg += 'Other modes available from here:\n';
578 for (let [mode, key] of this.iter_available_modes()) {
579 msg += '[' + key + '] – ' + mode.short_desc + '\n';
584 mode_switch_on_key(key_event) {
585 for (let [mode, key] of this.iter_available_modes()) {
586 if (key_event.key == key) {
587 event.preventDefault();
588 tui.switch_mode(mode.name);
600 window_width: terminal.cols / 2,
608 mode_waiting_for_server: new Mode('waiting_for_server',
610 mode_login: new Mode('login', true, false, true),
611 mode_post_login_wait: new Mode('post_login_wait'),
612 mode_chat: new Mode('chat', true),
613 mode_annotate: new Mode('annotate', true, true),
614 mode_play: new Mode('play'),
615 mode_study: new Mode('study', false, true),
616 mode_write: new Mode('write', false, false, false, true),
617 mode_edit: new Mode('edit'),
618 mode_control_pw_type: new Mode('control_pw_type', true),
619 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
620 mode_portal: new Mode('portal', true, true),
621 mode_password: new Mode('password', true),
622 mode_name_thing: new Mode('name_thing', true, true),
623 mode_command_thing: new Mode('command_thing', true),
624 mode_admin_enter: new Mode('admin_enter', true),
625 mode_admin: new Mode('admin'),
626 mode_control_pw_pw: new Mode('control_pw_pw', true),
627 mode_control_tile_type: new Mode('control_tile_type', true),
628 mode_control_tile_draw: new Mode('control_tile_draw'),
630 'flatten': 'FLATTEN_SURROUNDINGS',
631 'take_thing': 'PICK_UP',
632 'drop_thing': 'DROP',
635 'command': 'COMMAND',
636 'consume': 'INTOXICATE',
641 this.mode_chat.available_modes = ["play", "study", "edit", "admin_enter"]
642 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
644 this.mode_play.available_actions = ["move", "take_thing", "drop_thing",
645 "teleport", "door", "consume"];
646 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
647 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
648 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
649 "control_tile_type", "chat",
650 "study", "play", "edit"]
651 this.mode_admin.available_actions = ["move"];
652 this.mode_control_tile_draw.available_modes = ["admin_enter"]
653 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
654 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
655 "password", "chat", "study", "play",
657 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
658 this.mode = this.mode_waiting_for_server;
659 this.inputEl = document.getElementById("input");
660 this.inputEl.focus();
661 this.recalc_input_lines();
662 this.height_header = this.height_turn_line + this.height_mode_line;
663 this.log_msg("@ waiting for server connection ...");
666 init_keys: function() {
667 document.getElementById("move_table").hidden = true;
669 for (let key_selector of key_selectors) {
670 this.keys[key_selector.id.slice(4)] = key_selector.value;
672 this.movement_keys = {};
673 let geometry_prefix = 'undefinedMapGeometry_';
674 if (game.map_geometry) {
675 geometry_prefix = game.map_geometry.toLowerCase() + '_';
677 for (const key_name of Object.keys(key_descriptions)) {
678 if (key_name.startsWith(geometry_prefix)) {
679 let direction = key_name.split('_')[2].toUpperCase();
680 let key = this.keys[key_name];
681 this.movement_keys[key] = direction;
684 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
685 if (move_button.id.startsWith('key_')) {
688 move_button.hidden = true;
690 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
691 document.getElementById("move_table").hidden = false;
692 move_button.hidden = false;
694 for (let el of document.getElementsByTagName("button")) {
695 let action_desc = key_descriptions[el.id];
696 let action_key = '[' + this.keys[el.id] + ']';
697 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
700 task_action_on: function(action) {
701 return game.tasks.includes(this.action_tasks[action]);
703 switch_mode: function(mode_name) {
704 if (this.mode.name == 'control_tile_draw') {
705 tui.log_msg('@ finished tile protection drawing.')
707 this.tile_draw = false;
708 if (mode_name == 'admin_enter' && this.is_admin) {
710 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
711 let player_position = game.things[game.player_id].position;
713 for (let t_id in game.things) {
714 if (t_id == game.player_id) {
717 let t = game.things[t_id];
718 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
724 terminal.blink_screen();
725 this.log_msg('? not standing over thing');
728 this.selected_thing_id = thing_id;
731 this.mode = this['mode_' + mode_name];
732 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
733 this.map_mode = 'protections';
734 } else if (this.mode.name != "edit") {
735 this.map_mode = 'terrain + things';
737 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
738 this.inputEl.focus();
740 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
741 explorer.position = game.things[game.player_id].position;
743 this.inputEl.value = "";
744 this.restore_input_values();
745 for (let el of document.getElementsByTagName("button")) {
748 document.getElementById("help").disabled = false;
749 for (const action of this.mode.available_actions) {
750 if (["move", "move_explorer"].includes(action)) {
751 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
752 move_key.disabled = false;
754 } else if (Object.keys(this.action_tasks).includes(action)) {
755 if (this.task_action_on(action)) {
756 document.getElementById(action).disabled = false;
759 document.getElementById(action).disabled = false;
762 for (const mode_name of this.mode.available_modes) {
763 document.getElementById('switch_to_' + mode_name).disabled = false;
765 if (this.mode.name == 'login') {
766 if (this.login_name) {
767 server.send(['LOGIN', this.login_name]);
769 this.log_msg("? need login name");
771 } else if (this.mode.is_single_char_entry) {
772 this.show_help = true;
773 } else if (this.mode.name == 'command_thing') {
774 server.send(['TASK:COMMAND', 'HELP']);
775 } else if (this.mode.name == 'admin_enter') {
776 this.log_msg('@ enter admin password:')
777 } else if (this.mode.name == 'control_pw_type') {
778 this.log_msg('@ enter protection character for which you want to change the password:')
779 } else if (this.mode.name == 'control_tile_type') {
780 this.log_msg('@ enter protection character which you want to draw:')
781 } else if (this.mode.name == 'admin_thing_protect') {
782 this.log_msg('@ enter thing protection character:')
783 } else if (this.mode.name == 'control_pw_pw') {
784 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
785 } else if (this.mode.name == 'control_tile_draw') {
786 this.log_msg('@ can draw protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
790 offset_links: function(offset, links) {
791 for (let y in links) {
792 let real_y = offset[0] + parseInt(y);
793 if (!this.links[real_y]) {
794 this.links[real_y] = [];
796 for (let link of links[y]) {
797 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
798 this.links[real_y].push(offset_link);
802 restore_input_values: function() {
803 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
804 let info = explorer.annotations[explorer.position];
805 if (info != "(none)") {
806 this.inputEl.value = info;
808 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
809 let portal = game.portals[explorer.position]
810 this.inputEl.value = portal;
811 } else if (this.mode.name == 'password') {
812 this.inputEl.value = this.password;
813 } else if (this.mode.name == 'name_thing') {
814 let t = game.get_thing(this.selected_thing_id);
816 this.inputEl.value = t.name_;
818 } else if (this.mode.name == 'admin_thing_protect') {
819 let t = game.get_thing(this.selected_thing_id);
820 if (t && t.protection) {
821 this.inputEl.value = t.protection;
825 recalc_input_lines: function() {
826 if (this.mode.has_input_prompt) {
828 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
830 this.input_lines = [];
832 this.height_input = this.input_lines.length;
834 msg_into_lines_of_width: function(msg, width) {
835 function push_inner_link(y, end_x) {
836 if (!inner_links[y]) {
839 inner_links[y].push([url_start_x, end_x, url]);
841 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
844 for (const match of matches) {
845 const url = match[0];
846 const url_start = match.index;
847 const url_end = match.index + match[0].length;
848 link_data[url_start] = url;
849 url_ends.push(url_end);
853 let inner_links = {};
857 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
858 if (x >= width || msg[i] == "\n") {
860 push_inner_link(y, chunk.length);
862 if (url_ends[0] == i) {
870 if (msg[i] == "\n") {
875 if (msg[i] != "\n") {
878 if (i in link_data) {
882 } else if (url_ends[0] == i) {
884 push_inner_link(y, x);
890 push_inner_link(lines.length - 1, chunk.length);
892 return [lines, inner_links];
894 log_msg: function(msg) {
896 while (this.log.length > 100) {
901 draw_map: function() {
902 if (!game.turn_complete && this.map_lines.length == 0) {
905 if (game.turn_complete) {
906 let map_lines_split = [];
908 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
909 if (j == game.map_size[1]) {
910 map_lines_split.push(line);
914 if (this.map_mode == 'protections') {
915 line.push(game.map_control[i] + ' ');
917 line.push(game.map[i] + ' ');
920 map_lines_split.push(line);
921 if (this.map_mode == 'terrain + annotations') {
922 for (const coordinate of explorer.info_hints) {
923 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
925 } else if (this.map_mode == 'terrain + things') {
926 for (const p in game.portals) {
927 let coordinate = p.split(',')
928 let original = map_lines_split[coordinate[0]][coordinate[1]];
929 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
931 let used_positions = [];
932 function draw_thing(t, used_positions) {
933 let symbol = game.thing_types[t.type_];
936 meta_char = t.thing_char;
938 if (used_positions.includes(t.position.toString())) {
941 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
942 used_positions.push(t.position.toString());
944 for (const thing_id in game.things) {
945 let t = game.things[thing_id];
946 if (t.type_ != 'Player') {
947 draw_thing(t, used_positions);
950 for (const thing_id in game.things) {
951 let t = game.things[thing_id];
952 if (t.type_ == 'Player') {
953 draw_thing(t, used_positions);
957 let player = game.things[game.player_id];
958 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
959 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
960 } else if (tui.map_mode != 'terrain + things') {
961 map_lines_split[player.position[0]][player.position[1]] = '??';
964 if (game.map_geometry == 'Square') {
965 for (let line_split of map_lines_split) {
966 this.map_lines.push(line_split.join(''));
968 } else if (game.map_geometry == 'Hex') {
970 for (let line_split of map_lines_split) {
971 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
979 let window_center = [terminal.rows / 2, this.window_width / 2];
980 let center_position = [player.position[0], player.position[1]];
981 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
982 center_position = [explorer.position[0], explorer.position[1]];
984 center_position[1] = center_position[1] * 2;
985 this.offset = [center_position[0] - window_center[0],
986 center_position[1] - window_center[1]]
987 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
991 let term_y = Math.max(0, -this.offset[0]);
992 let term_x = Math.max(0, -this.offset[1]);
993 let map_y = Math.max(0, this.offset[0]);
994 let map_x = Math.max(0, this.offset[1]);
995 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
996 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
997 terminal.write(term_y, term_x, to_draw);
1000 draw_mode_line: function() {
1001 let help = 'hit [' + this.keys.help + '] for help';
1002 if (this.mode.has_input_prompt) {
1003 help = 'enter /help for help';
1005 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1007 draw_turn_line: function(n) {
1008 if (game.turn_complete) {
1009 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1012 draw_history: function() {
1013 let log_display_lines = [];
1015 let y_offset_in_log = 0;
1016 for (let line of this.log) {
1017 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1019 log_display_lines = log_display_lines.concat(new_lines);
1020 for (const y in link_data) {
1021 const rel_y = y_offset_in_log + parseInt(y);
1022 log_links[rel_y] = [];
1023 for (let link of link_data[y]) {
1024 log_links[rel_y].push(link);
1027 y_offset_in_log += new_lines.length;
1029 let i = log_display_lines.length - 1;
1030 for (let y = terminal.rows - 1 - this.height_input;
1031 y >= this.height_header && i >= 0;
1033 terminal.write(y, this.window_width, log_display_lines[i]);
1035 for (const key of Object.keys(log_links)) {
1036 if (parseInt(key) <= i) {
1037 delete log_links[key];
1040 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1042 this.offset_links(offset, log_links);
1044 draw_info: function() {
1045 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1046 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1047 let offset = [this.height_header, this.window_width];
1048 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1049 terminal.write(y, offset[1], lines[i]);
1051 this.offset_links(offset, link_data);
1053 draw_input: function() {
1054 if (this.mode.has_input_prompt) {
1055 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1056 terminal.write(y, this.window_width, this.input_lines[i]);
1060 draw_help: function() {
1061 let movement_keys_desc = '';
1062 if (!this.mode.is_intro) {
1063 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1065 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1066 if (this.mode.name == 'chat') {
1067 content += '/nick NAME – re-name yourself to NAME\n';
1068 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1069 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1070 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to world edit mode\n';
1071 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1072 } else if (this.mode.available_actions.length > 0) {
1073 content += "Available actions:\n";
1074 for (let action of this.mode.available_actions) {
1075 if (Object.keys(this.action_tasks).includes(action)) {
1076 if (!this.task_action_on(action)) {
1080 if (action == 'move_explorer') {
1083 if (action == 'move') {
1084 content += "[" + movement_keys_desc + "] – move\n"
1086 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1091 content += this.mode.list_available_modes();
1093 if (!this.mode.has_input_prompt) {
1094 start_x = this.window_width
1096 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1097 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1098 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1099 terminal.write(y, start_x, lines[i]);
1102 toggle_tile_draw: function() {
1103 if (tui.tile_draw) {
1104 tui.tile_draw = false;
1106 tui.tile_draw = true;
1109 toggle_map_mode: function() {
1110 if (tui.map_mode == 'terrain only') {
1111 tui.map_mode = 'terrain + annotations';
1112 } else if (tui.map_mode == 'terrain + annotations') {
1113 tui.map_mode = 'terrain + things';
1114 } else if (tui.map_mode == 'terrain + things') {
1115 tui.map_mode = 'protections';
1116 } else if (tui.map_mode == 'protections') {
1117 tui.map_mode = 'terrain only';
1120 full_refresh: function() {
1122 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1123 this.recalc_input_lines();
1124 if (this.mode.is_intro) {
1125 this.draw_history();
1129 this.draw_turn_line();
1130 this.draw_mode_line();
1131 if (this.mode.shows_info) {
1134 this.draw_history();
1138 if (this.show_help) {
1150 this.map_control = "";
1151 this.map_size = [0,0];
1152 this.player_id = -1;
1156 get_thing: function(id_, create_if_not_found=false) {
1157 if (id_ in game.things) {
1158 return game.things[id_];
1159 } else if (create_if_not_found) {
1160 let t = new Thing([0,0]);
1161 game.things[id_] = t;
1165 move: function(start_position, direction) {
1166 let target = [start_position[0], start_position[1]];
1167 if (direction == 'LEFT') {
1169 } else if (direction == 'RIGHT') {
1171 } else if (game.map_geometry == 'Square') {
1172 if (direction == 'UP') {
1174 } else if (direction == 'DOWN') {
1177 } else if (game.map_geometry == 'Hex') {
1178 let start_indented = start_position[0] % 2;
1179 if (direction == 'UPLEFT') {
1181 if (!start_indented) {
1184 } else if (direction == 'UPRIGHT') {
1186 if (start_indented) {
1189 } else if (direction == 'DOWNLEFT') {
1191 if (!start_indented) {
1194 } else if (direction == 'DOWNRIGHT') {
1196 if (start_indented) {
1201 if (target[0] < 0 || target[1] < 0 ||
1202 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1207 teleport: function() {
1208 let player = this.get_thing(game.player_id);
1209 if (player.position in this.portals) {
1210 server.reconnect_to(this.portals[player.position]);
1212 terminal.blink_screen();
1213 tui.log_msg('? not standing on portal')
1221 server.init(websocket_location);
1227 move: function(direction) {
1228 let target = game.move(this.position, direction);
1230 this.position = target
1231 this.info_cached = false;
1232 if (tui.tile_draw) {
1233 this.send_tile_control_command();
1236 terminal.blink_screen();
1239 update_annotations: function(yx, str) {
1240 this.annotations[yx] = str;
1241 if (tui.mode.name == 'study') {
1245 empty_annotations: function() {
1246 this.annotations = {};
1247 if (tui.mode.name == 'study') {
1251 get_info: function() {
1252 if (this.info_cached) {
1253 return this.info_cached;
1255 let info_to_cache = '';
1256 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1257 if (game.fov[position_i] != '.') {
1258 info_to_cache += 'outside field of view';
1260 let terrain_char = game.map[position_i]
1261 let terrain_desc = '?'
1262 if (game.terrains[terrain_char]) {
1263 terrain_desc = game.terrains[terrain_char];
1265 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1266 let protection = game.map_control[position_i];
1267 if (protection == '.') {
1268 protection = 'unprotected';
1270 info_to_cache += 'PROTECTION: ' + protection + '\n';
1271 for (let t_id in game.things) {
1272 let t = game.things[t_id];
1273 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1274 let symbol = game.thing_types[t.type_];
1275 let protection = t.protection;
1276 if (protection == '.') {
1277 protection = 'none';
1279 info_to_cache += "THING: " + t.type_ + " / " + symbol;
1281 info_to_cache += t.thing_char;
1284 info_to_cache += " (" + t.name_ + ")";
1286 info_to_cache += " / protection: " + protection + "\n";
1289 if (this.position in game.portals) {
1290 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1292 if (this.position in this.annotations) {
1293 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1296 this.info_cached = info_to_cache;
1297 return this.info_cached;
1299 annotate: function(msg) {
1300 if (msg.length == 0) {
1301 msg = " "; // triggers annotation deletion
1303 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1305 set_portal: function(msg) {
1306 if (msg.length == 0) {
1307 msg = " "; // triggers portal deletion
1309 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1311 send_tile_control_command: function() {
1312 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1316 tui.inputEl.addEventListener('input', (event) => {
1317 if (tui.mode.has_input_prompt) {
1318 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1319 if (tui.inputEl.value.length > max_length) {
1320 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1322 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1323 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1324 tui.switch_mode('edit');
1328 document.onclick = function() {
1329 tui.show_help = false;
1331 tui.inputEl.addEventListener('keydown', (event) => {
1332 tui.show_help = false;
1333 if (event.key == 'Enter') {
1334 event.preventDefault();
1336 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1337 tui.show_help = true;
1338 tui.inputEl.value = "";
1339 tui.restore_input_values();
1340 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1341 && !tui.mode.is_single_char_entry) {
1342 tui.show_help = true;
1343 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1344 tui.login_name = tui.inputEl.value;
1345 server.send(['LOGIN', tui.inputEl.value]);
1346 tui.inputEl.value = "";
1347 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1348 if (tui.inputEl.value.length == 0) {
1349 tui.log_msg('@ aborted');
1350 tui.switch_mode('play');
1351 } else if (tui.task_action_on('command')) {
1352 server.send(['TASK:COMMAND', tui.inputEl.value]);
1353 tui.inputEl.value = "";
1355 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1356 if (tui.inputEl.value.length == 0) {
1357 tui.log_msg('@ aborted');
1359 server.send(['SET_MAP_CONTROL_PASSWORD',
1360 tui.tile_control_char, tui.inputEl.value]);
1361 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1363 tui.switch_mode('admin');
1364 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1365 explorer.set_portal(tui.inputEl.value);
1366 tui.switch_mode('edit');
1367 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1368 if (tui.inputEl.value.length == 0) {
1369 tui.inputEl.value = " ";
1371 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1373 tui.switch_mode('edit');
1374 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1375 explorer.annotate(tui.inputEl.value);
1376 tui.switch_mode('edit');
1377 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1378 if (tui.inputEl.value.length == 0) {
1379 tui.inputEl.value = " ";
1381 tui.password = tui.inputEl.value
1382 tui.switch_mode('edit');
1383 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1384 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1385 tui.switch_mode('play');
1386 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1387 if (tui.inputEl.value.length != 1) {
1388 tui.log_msg('@ entered non-single-char, therefore aborted');
1389 tui.switch_mode('admin');
1391 tui.tile_control_char = tui.inputEl.value[0];
1392 tui.switch_mode('control_pw_pw');
1394 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1395 if (tui.inputEl.value.length != 1) {
1396 tui.log_msg('@ entered non-single-char, therefore aborted');
1397 tui.switch_mode('admin');
1399 tui.tile_control_char = tui.inputEl.value[0];
1400 tui.switch_mode('control_tile_draw');
1402 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1403 if (tui.inputEl.value.length != 1) {
1404 tui.log_msg('@ entered non-single-char, therefore aborted');
1406 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1407 tui.log_msg('@ sent new protection character for thing');
1409 tui.switch_mode('admin');
1410 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1411 let tokens = parser.tokenize(tui.inputEl.value);
1412 if (tokens.length > 0 && tokens[0].length > 0) {
1413 if (tui.inputEl.value[0][0] == '/') {
1414 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1415 tui.switch_mode('play');
1416 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1417 tui.switch_mode('study');
1418 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1419 tui.switch_mode('edit');
1420 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1421 tui.switch_mode('admin_enter');
1422 } else if (tokens[0].slice(1) == 'nick') {
1423 if (tokens.length > 1) {
1424 server.send(['NICK', tokens[1]]);
1426 tui.log_msg('? need new name');
1429 tui.log_msg('? unknown command');
1432 server.send(['ALL', tui.inputEl.value]);
1434 } else if (tui.inputEl.valuelength > 0) {
1435 server.send(['ALL', tui.inputEl.value]);
1437 tui.inputEl.value = "";
1438 } else if (tui.mode.name == 'play') {
1439 if (tui.mode.mode_switch_on_key(event)) {
1441 } else if (event.key === tui.keys.take_thing && tui.task_action_on('take_thing')) {
1442 server.send(["TASK:PICK_UP"]);
1443 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1444 server.send(["TASK:DROP"]);
1445 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1446 server.send(["TASK:INTOXICATE"]);
1447 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1448 server.send(["TASK:DOOR"]);
1449 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1450 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1451 } else if (event.key === tui.keys.teleport) {
1454 } else if (tui.mode.name == 'study') {
1455 if (tui.mode.mode_switch_on_key(event)) {
1457 } else if (event.key in tui.movement_keys) {
1458 explorer.move(tui.movement_keys[event.key]);
1459 } else if (event.key == tui.keys.toggle_map_mode) {
1460 tui.toggle_map_mode();
1462 } else if (tui.mode.name == 'control_tile_draw') {
1463 if (tui.mode.mode_switch_on_key(event)) {
1465 } else if (event.key in tui.movement_keys) {
1466 explorer.move(tui.movement_keys[event.key]);
1467 } else if (event.key === tui.keys.toggle_tile_draw) {
1468 tui.toggle_tile_draw();
1470 } else if (tui.mode.name == 'admin') {
1471 if (tui.mode.mode_switch_on_key(event)) {
1473 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1474 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1476 } else if (tui.mode.name == 'edit') {
1477 if (tui.mode.mode_switch_on_key(event)) {
1479 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1480 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1481 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1482 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1483 } else if (event.key == tui.keys.toggle_map_mode) {
1484 tui.toggle_map_mode();
1490 rows_selector.addEventListener('input', function() {
1491 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1494 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1495 terminal.initialize();
1498 cols_selector.addEventListener('input', function() {
1499 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1502 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1503 terminal.initialize();
1504 tui.window_width = terminal.cols / 2,
1507 for (let key_selector of key_selectors) {
1508 key_selector.addEventListener('input', function() {
1509 window.localStorage.setItem(key_selector.id, key_selector.value);
1513 window.setInterval(function() {
1514 if (server.connected) {
1515 server.send(['PING']);
1517 server.reconnect_to(server.url);
1518 tui.log_msg('@ attempting reconnect …')
1521 window.setInterval(function() {
1523 let span_decoration = "none";
1524 if (document.activeElement == tui.inputEl) {
1525 val = "on (click outside terminal to change)";
1527 val = "off (click into terminal to change)";
1528 span_decoration = "line-through";
1530 document.getElementById("keyboard_control").textContent = val;
1531 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1532 span.style.textDecoration = span_decoration;
1535 document.getElementById("terminal").onclick = function() {
1536 tui.inputEl.focus();
1538 document.getElementById("help").onclick = function() {
1539 tui.show_help = true;
1542 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1543 const mode = switchEl.id.slice("switch_to_".length);
1544 switchEl.onclick = function() {
1545 tui.switch_mode(mode);
1549 document.getElementById("toggle_tile_draw").onclick = function() {
1550 tui.toggle_tile_draw();
1552 document.getElementById("toggle_map_mode").onclick = function() {
1553 tui.toggle_map_mode();
1556 document.getElementById("take_thing").onclick = function() {
1557 server.send(['TASK:PICK_UP']);
1559 document.getElementById("drop_thing").onclick = function() {
1560 server.send(['TASK:DROP']);
1562 document.getElementById("flatten").onclick = function() {
1563 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1565 document.getElementById("door").onclick = function() {
1566 server.send(['TASK:DOOR']);
1568 document.getElementById("consume").onclick = function() {
1569 server.send(['TASK:INTOXICATE']);
1571 document.getElementById("teleport").onclick = function() {
1574 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1575 let direction = move_button.id.split('_')[2].toUpperCase();
1576 move_button.onclick = function() {
1577 if (tui.mode.available_actions.includes("move")
1578 || tui.mode.available_actions.includes("move_explorer")) {
1579 server.send(['TASK:MOVE', direction]);
1581 explorer.move(direction);