13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table id="move_table" style="float: left">
25 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
26 <td style="text-align: center"><button id="square_move_up"></button></td>
27 <td><button id="hex_move_upright"></button></td>
30 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
35 <td><button id="hex_move_downleft"></button></td>
36 <td style="text-align: center"><button id="square_move_down"></button></td>
37 <td><button id="hex_move_downright"></button></td>
42 <td><button id="help"></button></td>
45 <td><button id="switch_to_chat"></button><br /></td>
48 <td><button id="switch_to_study"></button></td>
49 <td><button id="toggle_map_mode"></button>
52 <td><button id="switch_to_play"></button></td>
54 <button id="switch_to_take_thing"></button>
55 <button id="drop_thing"></button>
56 <button id="door"></button>
57 <button id="consume"></button>
58 <button id="switch_to_command_thing"></button>
59 <button id="teleport"></button>
60 <button id="install"></button>
64 <td><button id="switch_to_edit"></button></td>
66 <button id="switch_to_write"></button>
67 <button id="flatten"></button>
68 <button id="switch_to_annotate"></button>
69 <button id="switch_to_portal"></button>
70 <button id="switch_to_name_thing"></button>
71 <button id="switch_to_password"></button>
75 <td><button id="switch_to_admin_enter"></button></td>
77 <button id="switch_to_control_pw_type"></button>
78 <button id="switch_to_control_tile_type"></button>
79 <button id="switch_to_admin_thing_protect"></button>
80 <button id="toggle_tile_draw"></button>
85 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
87 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
88 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
89 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
90 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
91 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
92 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
93 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
94 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
95 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
96 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
97 <li>help: <input id="key_help" type="text" value="h" />
98 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
99 <li>teleport: <input id="key_teleport" type="text" value="p" />
100 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
101 <li>open/close: <input id="key_door" type="text" value="D" />
102 <li>consume: <input id="key_consume" type="text" value="C" />
103 <li>install: <input id="key_install" type="text" value="I" />
104 <li><input id="key_switch_to_take_thing" type="text" value="z" />
105 <li><input id="key_switch_to_chat" type="text" value="t" />
106 <li><input id="key_switch_to_play" type="text" value="p" />
107 <li><input id="key_switch_to_study" type="text" value="?" />
108 <li><input id="key_switch_to_edit" type="text" value="E" />
109 <li><input id="key_switch_to_write" type="text" value="m" />
110 <li><input id="key_switch_to_name_thing" type="text" value="N" />
111 <li><input id="key_switch_to_command_thing" type="text" value="O" />
112 <li><input id="key_switch_to_password" type="text" value="P" />
113 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
114 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
115 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
116 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
117 <li><input id="key_switch_to_annotate" type="text" value="M" />
118 <li><input id="key_switch_to_portal" type="text" value="T" />
119 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
120 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
125 let websocket_location = "wss://plomlompom.com/rogue_chat/";
126 //let websocket_location = "ws://localhost:8001/";
132 'long': 'This mode allows you to interact with the map in various ways.'
137 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
139 'short': 'world edit',
141 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
144 'short': 'name thing',
146 'long': 'Give name to/change name of thing here.'
149 'short': 'command thing',
151 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
154 'short': 'take thing',
155 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
156 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
158 'admin_thing_protect': {
159 'short': 'change thing protection',
160 'intro': '@ enter thing protection character:',
161 'long': 'Change protection character for thing here.'
164 'short': 'change terrain',
166 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
169 'short': 'change protection character password',
170 'intro': '@ enter protection character for which you want to change the password:',
171 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
174 'short': 'change protection character password',
176 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
178 'control_tile_type': {
179 'short': 'change tiles protection',
180 'intro': '@ enter protection character which you want to draw:',
181 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
183 'control_tile_draw': {
184 'short': 'change tiles protection',
186 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
189 'short': 'annotate tile',
191 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
194 'short': 'edit portal',
196 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
201 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
206 'long': 'Enter your player name.'
208 'waiting_for_server': {
209 'short': 'waiting for server response',
210 'intro': '@ waiting for server …',
211 'long': 'Waiting for a server response.'
214 'short': 'waiting for server response',
216 'long': 'Waiting for a server response.'
219 'short': 'set world edit password',
221 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
224 'short': 'become admin',
225 'intro': '@ enter admin password:',
226 'long': 'This mode allows you to become admin if you know an admin password.'
231 'long': 'This mode allows you access to actions limited to administrators.'
234 let key_descriptions = {
236 'flatten': 'flatten surroundings',
237 'teleport': 'teleport',
238 'drop_thing': 'drop thing',
239 'door': 'open/close',
240 'consume': 'consume',
241 'install': 'install',
242 'toggle_map_mode': 'toggle map view',
243 'toggle_tile_draw': 'toggle protection character drawing',
244 'hex_move_upleft': 'up-left',
245 'hex_move_upright': 'up-right',
246 'hex_move_right': 'right',
247 'hex_move_left': 'left',
248 'hex_move_downleft': 'down-left',
249 'hex_move_downright': 'down-right',
250 'square_move_up': 'up',
251 'square_move_left': 'left',
252 'square_move_down': 'down',
253 'square_move_right': 'right',
255 for (const mode_name of Object.keys(mode_helps)) {
256 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
259 let rows_selector = document.getElementById("n_rows");
260 let cols_selector = document.getElementById("n_cols");
261 let key_selectors = document.querySelectorAll('[id^="key_"]');
263 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
264 const action = key_switch_selector.id.slice("key_switch_to_".length);
265 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
268 function restore_selector_value(selector) {
269 let stored_selection = window.localStorage.getItem(selector.id);
270 if (stored_selection) {
271 selector.value = stored_selection;
274 restore_selector_value(rows_selector);
275 restore_selector_value(cols_selector);
276 for (let key_selector of key_selectors) {
277 restore_selector_value(key_selector);
280 function escapeHTML(str) {
282 replace(/&/g, '&').
283 replace(/</g, '<').
284 replace(/>/g, '>').
285 replace(/'/g, ''').
286 replace(/"/g, '"');
290 initialize: function() {
291 this.rows = rows_selector.value;
292 this.cols = cols_selector.value;
293 this.pre_el = document.getElementById("terminal");
294 this.set_default_colors();
298 for (let y = 0, x = 0; y <= this.rows; x++) {
299 if (x == this.cols) {
302 this.content.push(line);
304 if (y == this.rows) {
311 apply_colors: function() {
312 this.pre_el.style.color = this.foreground;
313 this.pre_el.style.backgroundColor = this.background;
315 set_default_colors: function() {
316 this.foreground = 'white';
317 this.background = 'black';
320 set_random_colors: function() {
321 function rand(offset) {
322 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
324 this.foreground = '#' + rand(159) + rand(159) + rand(159);
325 this.background = '#' + rand(0) + rand(0) + rand(0);
328 blink_screen: function() {
329 this.pre_el.style.color = this.background;
330 this.pre_el.style.backgroundColor = this.foreground;
332 this.pre_el.style.color = this.foreground;
333 this.pre_el.style.backgroundColor = this.background;
336 refresh: function() {
337 let pre_content = '';
338 for (let y = 0; y < this.rows; y++) {
339 let line = this.content[y].join('');
341 if (y in tui.links) {
343 for (let span of tui.links[y]) {
344 chunks.push(escapeHTML(line.slice(start_x, span[0])));
345 chunks.push('<a target="_blank" href="');
346 chunks.push(escapeHTML(span[2]));
348 chunks.push(escapeHTML(line.slice(span[0], span[1])));
352 chunks.push(escapeHTML(line.slice(start_x)));
354 chunks = [escapeHTML(line)];
356 for (const chunk of chunks) {
357 pre_content += chunk;
361 this.pre_el.innerHTML = pre_content;
363 write: function(start_y, start_x, msg) {
364 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
365 this.content[start_y][x] = msg[i];
368 drawBox: function(start_y, start_x, height, width) {
369 let end_y = start_y + height;
370 let end_x = start_x + width;
371 for (let y = start_y, x = start_x; y < this.rows; x++) {
379 this.content[y][x] = ' ';
383 terminal.initialize();
386 tokenize: function(str) {
391 for (let i = 0; i < str.length; i++) {
397 } else if (c == '\\') {
399 } else if (c == '"') {
404 } else if (c == '"') {
406 } else if (c === ' ') {
407 if (token.length > 0) {
415 if (token.length > 0) {
420 parse_yx: function(position_string) {
421 let coordinate_strings = position_string.split(',')
422 let position = [0, 0];
423 position[0] = parseInt(coordinate_strings[0].slice(2));
424 position[1] = parseInt(coordinate_strings[1].slice(2));
436 init: function(url) {
438 this.websocket = new WebSocket(this.url);
439 this.websocket.onopen = function(event) {
440 server.connected = true;
441 game.thing_types = {};
443 server.send(['TASKS']);
444 server.send(['TERRAINS']);
445 server.send(['THING_TYPES']);
446 tui.log_msg("@ server connected! :)");
447 tui.switch_mode('login');
449 this.websocket.onclose = function(event) {
450 server.connected = false;
451 tui.switch_mode('waiting_for_server');
452 tui.log_msg("@ server disconnected :(");
454 this.websocket.onmessage = this.handle_event;
456 reconnect_to: function(url) {
457 this.websocket.close();
460 send: function(tokens) {
461 this.websocket.send(unparser.untokenize(tokens));
463 handle_event: function(event) {
464 let tokens = parser.tokenize(event.data);
465 if (tokens[0] === 'TURN') {
466 game.turn_complete = false;
467 explorer.empty_annotations();
471 game.turn = parseInt(tokens[1]);
472 } else if (tokens[0] === 'THING') {
473 let t = game.get_thing(tokens[4], true);
474 t.position = parser.parse_yx(tokens[1]);
476 t.protection = tokens[3];
477 t.portable = parseInt(tokens[5]);
478 } else if (tokens[0] === 'THING_NAME') {
479 let t = game.get_thing(tokens[1], false);
483 } else if (tokens[0] === 'THING_CHAR') {
484 let t = game.get_thing(tokens[1], false);
486 t.thing_char = tokens[2];
488 } else if (tokens[0] === 'TASKS') {
489 game.tasks = tokens[1].split(',');
490 tui.mode_write.legal = game.tasks.includes('WRITE');
491 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
492 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
493 } else if (tokens[0] === 'THING_TYPE') {
494 game.thing_types[tokens[1]] = tokens[2]
495 } else if (tokens[0] === 'THING_CARRYING') {
496 let t = game.get_thing(tokens[1], false);
500 } else if (tokens[0] === 'TERRAIN') {
501 game.terrains[tokens[1]] = tokens[2]
502 } else if (tokens[0] === 'MAP') {
503 game.map_geometry = tokens[1];
505 game.map_size = parser.parse_yx(tokens[2]);
507 } else if (tokens[0] === 'FOV') {
509 } else if (tokens[0] === 'MAP_CONTROL') {
510 game.map_control = tokens[1]
511 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
512 game.turn_complete = true;
513 if (tui.mode.name == 'post_login_wait') {
514 tui.switch_mode('play');
516 explorer.info_cached = false;
518 } else if (tokens[0] === 'CHAT') {
519 tui.log_msg('# ' + tokens[1], 1);
520 } else if (tokens[0] === 'REPLY') {
521 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
522 } else if (tokens[0] === 'PLAYER_ID') {
523 game.player_id = parseInt(tokens[1]);
524 } else if (tokens[0] === 'LOGIN_OK') {
525 this.send(['GET_GAMESTATE']);
526 tui.switch_mode('post_login_wait');
527 } else if (tokens[0] === 'DEFAULT_COLORS') {
528 terminal.set_default_colors();
529 } else if (tokens[0] === 'RANDOM_COLORS') {
530 terminal.set_random_colors();
531 } else if (tokens[0] === 'ADMIN_OK') {
533 tui.log_msg('@ you now have admin rights');
534 tui.switch_mode('admin');
535 } else if (tokens[0] === 'PORTAL') {
536 let position = parser.parse_yx(tokens[1]);
537 game.portals[position] = tokens[2];
538 } else if (tokens[0] === 'ANNOTATION') {
539 let position = parser.parse_yx(tokens[1]);
540 explorer.update_annotations(position, tokens[2]);
542 } else if (tokens[0] === 'UNHANDLED_INPUT') {
543 tui.log_msg('? unknown command');
544 } else if (tokens[0] === 'PLAY_ERROR') {
545 tui.log_msg('? ' + tokens[1]);
546 terminal.blink_screen();
547 } else if (tokens[0] === 'ARGUMENT_ERROR') {
548 tui.log_msg('? syntax error: ' + tokens[1]);
549 } else if (tokens[0] === 'GAME_ERROR') {
550 tui.log_msg('? game error: ' + tokens[1]);
551 } else if (tokens[0] === 'PONG') {
554 tui.log_msg('? unhandled input: ' + event.data);
560 quote: function(str) {
562 for (let i = 0; i < str.length; i++) {
564 if (['"', '\\'].includes(c)) {
570 return quoted.join('');
572 to_yx: function(yx_coordinate) {
573 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
575 untokenize: function(tokens) {
576 let quoted_tokens = [];
577 for (let token of tokens) {
578 quoted_tokens.push(this.quote(token));
580 return quoted_tokens.join(" ");
585 constructor(name, has_input_prompt=false, shows_info=false,
586 is_intro=false, is_single_char_entry=false) {
588 this.short_desc = mode_helps[name].short;
589 this.available_modes = [];
590 this.available_actions = [];
591 this.has_input_prompt = has_input_prompt;
592 this.shows_info= shows_info;
593 this.is_intro = is_intro;
594 this.help_intro = mode_helps[name].long;
595 this.intro_msg = mode_helps[name].intro;
596 this.is_single_char_entry = is_single_char_entry;
599 *iter_available_modes() {
600 for (let mode_name of this.available_modes) {
601 let mode = tui['mode_' + mode_name];
605 let key = tui.keys['switch_to_' + mode.name];
609 list_available_modes() {
611 if (this.available_modes.length > 0) {
612 msg += 'Other modes available from here:\n';
613 for (let [mode, key] of this.iter_available_modes()) {
614 msg += '[' + key + '] – ' + mode.short_desc + '\n';
619 mode_switch_on_key(key_event) {
620 for (let [mode, key] of this.iter_available_modes()) {
621 if (key_event.key == key) {
622 event.preventDefault();
623 tui.switch_mode(mode.name);
635 window_width: terminal.cols / 2,
643 mode_waiting_for_server: new Mode('waiting_for_server',
645 mode_login: new Mode('login', true, false, true),
646 mode_post_login_wait: new Mode('post_login_wait'),
647 mode_chat: new Mode('chat', true),
648 mode_annotate: new Mode('annotate', true, true),
649 mode_play: new Mode('play'),
650 mode_study: new Mode('study', false, true),
651 mode_write: new Mode('write', false, false, false, true),
652 mode_edit: new Mode('edit'),
653 mode_control_pw_type: new Mode('control_pw_type', true),
654 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
655 mode_portal: new Mode('portal', true, true),
656 mode_password: new Mode('password', true),
657 mode_name_thing: new Mode('name_thing', true, true),
658 mode_command_thing: new Mode('command_thing', true),
659 mode_take_thing: new Mode('take_thing', true),
660 mode_admin_enter: new Mode('admin_enter', true),
661 mode_admin: new Mode('admin'),
662 mode_control_pw_pw: new Mode('control_pw_pw', true),
663 mode_control_tile_type: new Mode('control_tile_type', true),
664 mode_control_tile_draw: new Mode('control_tile_draw'),
666 'flatten': 'FLATTEN_SURROUNDINGS',
667 'take_thing': 'PICK_UP',
668 'drop_thing': 'DROP',
671 'install': 'INSTALL',
672 'command': 'COMMAND',
673 'consume': 'INTOXICATE',
679 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
680 "command_thing", "take_thing"]
681 this.mode_play.available_actions = ["move", "drop_thing",
682 "teleport", "door", "consume", "install"];
683 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
684 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
685 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
686 "control_tile_type", "chat",
687 "study", "play", "edit"]
688 this.mode_admin.available_actions = ["move"];
689 this.mode_control_tile_draw.available_modes = ["admin_enter"]
690 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
691 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
692 "password", "chat", "study", "play",
694 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
695 this.inputEl = document.getElementById("input");
696 this.inputEl.focus();
697 this.switch_mode('waiting_for_server');
698 this.recalc_input_lines();
699 this.height_header = this.height_turn_line + this.height_mode_line;
702 init_keys: function() {
703 document.getElementById("move_table").hidden = true;
705 for (let key_selector of key_selectors) {
706 this.keys[key_selector.id.slice(4)] = key_selector.value;
708 this.movement_keys = {};
709 let geometry_prefix = 'undefinedMapGeometry_';
710 if (game.map_geometry) {
711 geometry_prefix = game.map_geometry.toLowerCase() + '_';
713 for (const key_name of Object.keys(key_descriptions)) {
714 if (key_name.startsWith(geometry_prefix)) {
715 let direction = key_name.split('_')[2].toUpperCase();
716 let key = this.keys[key_name];
717 this.movement_keys[key] = direction;
720 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
721 if (move_button.id.startsWith('key_')) {
724 move_button.hidden = true;
726 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
727 document.getElementById("move_table").hidden = false;
728 move_button.hidden = false;
730 for (let el of document.getElementsByTagName("button")) {
731 let action_desc = key_descriptions[el.id];
732 let action_key = '[' + this.keys[el.id] + ']';
733 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
736 task_action_on: function(action) {
737 return game.tasks.includes(this.action_tasks[action]);
739 switch_mode: function(mode_name) {
740 if (this.mode && this.mode.name == 'control_tile_draw') {
741 tui.log_msg('@ finished tile protection drawing.')
743 this.tile_draw = false;
744 if (mode_name == 'admin_enter' && this.is_admin) {
746 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
747 let player_position = game.things[game.player_id].position;
749 for (let t_id in game.things) {
750 if (t_id == game.player_id) {
753 let t = game.things[t_id];
754 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
760 terminal.blink_screen();
761 this.log_msg('? not standing over thing');
764 this.selected_thing_id = thing_id;
767 this.mode = this['mode_' + mode_name];
768 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
769 this.map_mode = 'protections';
770 } else if (this.mode.name != "edit") {
771 this.map_mode = 'terrain + things';
773 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
774 this.inputEl.focus();
776 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
777 explorer.position = game.things[game.player_id].position;
779 this.inputEl.value = "";
780 this.restore_input_values();
781 for (let el of document.getElementsByTagName("button")) {
784 document.getElementById("help").disabled = false;
785 for (const action of this.mode.available_actions) {
786 if (["move", "move_explorer"].includes(action)) {
787 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
788 move_key.disabled = false;
790 } else if (Object.keys(this.action_tasks).includes(action)) {
791 if (this.task_action_on(action)) {
792 document.getElementById(action).disabled = false;
795 document.getElementById(action).disabled = false;
798 for (const mode_name of this.mode.available_modes) {
799 document.getElementById('switch_to_' + mode_name).disabled = false;
801 if (this.mode.intro_msg.length > 0) {
802 this.log_msg(this.mode.intro_msg);
804 if (this.mode.name == 'login') {
805 if (this.login_name) {
806 server.send(['LOGIN', this.login_name]);
808 this.log_msg("? need login name");
810 } else if (this.mode.is_single_char_entry) {
811 this.show_help = true;
812 } else if (this.mode.name == 'take_thing') {
813 this.log_msg("Portable things in reach for pick-up:");
814 const player = game.things[game.player_id];
815 const y = player.position[0]
816 const x = player.position[1]
817 let select_range = [y.toString() + ':' + x.toString(),
818 (y + 0).toString() + ':' + (x - 1).toString(),
819 (y + 0).toString() + ':' + (x + 1).toString(),
820 (y - 1).toString() + ':' + (x).toString(),
821 (y + 1).toString() + ':' + (x).toString()];
822 if (game.map_geometry == 'Hex') {
824 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
825 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
827 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
828 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
831 this.selectables = [];
832 for (const t_id in game.things) {
833 const t = game.things[t_id];
834 if (select_range.includes(t.position[0].toString()
835 + ':' + t.position[1].toString())
837 this.selectables.push([t_id, t]);
840 if (this.selectables.length == 0) {
843 for (let [i, t] of this.selectables.entries()) {
844 this.log_msg(i + ': ' + explorer.get_thing_info(t[1]));
847 } else if (this.mode.name == 'command_thing') {
848 server.send(['TASK:COMMAND', 'HELP']);
849 } else if (this.mode.name == 'control_pw_pw') {
850 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
851 } else if (this.mode.name == 'control_tile_draw') {
852 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 + '].')
856 offset_links: function(offset, links) {
857 for (let y in links) {
858 let real_y = offset[0] + parseInt(y);
859 if (!this.links[real_y]) {
860 this.links[real_y] = [];
862 for (let link of links[y]) {
863 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
864 this.links[real_y].push(offset_link);
868 restore_input_values: function() {
869 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
870 let info = explorer.annotations[explorer.position];
871 if (info != "(none)") {
872 this.inputEl.value = info;
874 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
875 let portal = game.portals[explorer.position]
876 this.inputEl.value = portal;
877 } else if (this.mode.name == 'password') {
878 this.inputEl.value = this.password;
879 } else if (this.mode.name == 'name_thing') {
880 let t = game.get_thing(this.selected_thing_id);
882 this.inputEl.value = t.name_;
884 } else if (this.mode.name == 'admin_thing_protect') {
885 let t = game.get_thing(this.selected_thing_id);
886 if (t && t.protection) {
887 this.inputEl.value = t.protection;
891 recalc_input_lines: function() {
892 if (this.mode.has_input_prompt) {
894 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
896 this.input_lines = [];
898 this.height_input = this.input_lines.length;
900 msg_into_lines_of_width: function(msg, width) {
901 function push_inner_link(y, end_x) {
902 if (!inner_links[y]) {
905 inner_links[y].push([url_start_x, end_x, url]);
907 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
910 for (const match of matches) {
911 const url = match[0];
912 const url_start = match.index;
913 const url_end = match.index + match[0].length;
914 link_data[url_start] = url;
915 url_ends.push(url_end);
919 let inner_links = {};
923 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
924 if (x >= width || msg[i] == "\n") {
926 push_inner_link(y, chunk.length);
928 if (url_ends[0] == i) {
936 if (msg[i] == "\n") {
941 if (msg[i] != "\n") {
944 if (i in link_data) {
948 } else if (url_ends[0] == i) {
950 push_inner_link(y, x);
956 push_inner_link(lines.length - 1, chunk.length);
958 return [lines, inner_links];
960 log_msg: function(msg) {
962 while (this.log.length > 100) {
967 draw_map: function() {
968 if (!game.turn_complete && this.map_lines.length == 0) {
971 if (game.turn_complete) {
972 let map_lines_split = [];
974 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
975 if (j == game.map_size[1]) {
976 map_lines_split.push(line);
980 if (this.map_mode == 'protections') {
981 line.push(game.map_control[i] + ' ');
983 line.push(game.map[i] + ' ');
986 map_lines_split.push(line);
987 if (this.map_mode == 'terrain + annotations') {
988 for (const coordinate of explorer.info_hints) {
989 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
991 } else if (this.map_mode == 'terrain + things') {
992 for (const p in game.portals) {
993 let coordinate = p.split(',')
994 let original = map_lines_split[coordinate[0]][coordinate[1]];
995 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
997 let used_positions = [];
998 function draw_thing(t, used_positions) {
999 let symbol = game.thing_types[t.type_];
1000 let meta_char = ' ';
1002 meta_char = t.thing_char;
1004 if (used_positions.includes(t.position.toString())) {
1010 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1011 used_positions.push(t.position.toString());
1013 for (const thing_id in game.things) {
1014 let t = game.things[thing_id];
1015 if (t.type_ != 'Player') {
1016 draw_thing(t, used_positions);
1019 for (const thing_id in game.things) {
1020 let t = game.things[thing_id];
1021 if (t.type_ == 'Player') {
1022 draw_thing(t, used_positions);
1026 let player = game.things[game.player_id];
1027 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1028 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1029 } else if (tui.map_mode != 'terrain + things') {
1030 map_lines_split[player.position[0]][player.position[1]] = '??';
1033 if (game.map_geometry == 'Square') {
1034 for (let line_split of map_lines_split) {
1035 this.map_lines.push(line_split.join(''));
1037 } else if (game.map_geometry == 'Hex') {
1039 for (let line_split of map_lines_split) {
1040 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1048 let window_center = [terminal.rows / 2, this.window_width / 2];
1049 let center_position = [player.position[0], player.position[1]];
1050 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1051 center_position = [explorer.position[0], explorer.position[1]];
1053 center_position[1] = center_position[1] * 2;
1054 this.offset = [center_position[0] - window_center[0],
1055 center_position[1] - window_center[1]]
1056 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1057 this.offset[1] += 1;
1060 let term_y = Math.max(0, -this.offset[0]);
1061 let term_x = Math.max(0, -this.offset[1]);
1062 let map_y = Math.max(0, this.offset[0]);
1063 let map_x = Math.max(0, this.offset[1]);
1064 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
1065 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1066 terminal.write(term_y, term_x, to_draw);
1069 draw_mode_line: function() {
1070 let help = 'hit [' + this.keys.help + '] for help';
1071 if (this.mode.has_input_prompt) {
1072 help = 'enter /help for help';
1074 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1076 draw_turn_line: function(n) {
1077 if (game.turn_complete) {
1078 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1081 draw_history: function() {
1082 let log_display_lines = [];
1084 let y_offset_in_log = 0;
1085 for (let line of this.log) {
1086 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1088 log_display_lines = log_display_lines.concat(new_lines);
1089 for (const y in link_data) {
1090 const rel_y = y_offset_in_log + parseInt(y);
1091 log_links[rel_y] = [];
1092 for (let link of link_data[y]) {
1093 log_links[rel_y].push(link);
1096 y_offset_in_log += new_lines.length;
1098 let i = log_display_lines.length - 1;
1099 for (let y = terminal.rows - 1 - this.height_input;
1100 y >= this.height_header && i >= 0;
1102 terminal.write(y, this.window_width, log_display_lines[i]);
1104 for (const key of Object.keys(log_links)) {
1105 if (parseInt(key) <= i) {
1106 delete log_links[key];
1109 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1111 this.offset_links(offset, log_links);
1113 draw_info: function() {
1114 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1115 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1116 let offset = [this.height_header, this.window_width];
1117 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1118 terminal.write(y, offset[1], lines[i]);
1120 this.offset_links(offset, link_data);
1122 draw_input: function() {
1123 if (this.mode.has_input_prompt) {
1124 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1125 terminal.write(y, this.window_width, this.input_lines[i]);
1129 draw_help: function() {
1130 let movement_keys_desc = '';
1131 if (!this.mode.is_intro) {
1132 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1134 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1135 if (this.mode.available_actions.length > 0) {
1136 content += "Available actions:\n";
1137 for (let action of this.mode.available_actions) {
1138 if (Object.keys(this.action_tasks).includes(action)) {
1139 if (!this.task_action_on(action)) {
1143 if (action == 'move_explorer') {
1146 if (action == 'move') {
1147 content += "[" + movement_keys_desc + "] – move\n"
1149 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1154 content += this.mode.list_available_modes();
1156 if (!this.mode.has_input_prompt) {
1157 start_x = this.window_width
1159 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1160 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1161 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1162 terminal.write(y, start_x, lines[i]);
1165 toggle_tile_draw: function() {
1166 if (tui.tile_draw) {
1167 tui.tile_draw = false;
1169 tui.tile_draw = true;
1172 toggle_map_mode: function() {
1173 if (tui.map_mode == 'terrain only') {
1174 tui.map_mode = 'terrain + annotations';
1175 } else if (tui.map_mode == 'terrain + annotations') {
1176 tui.map_mode = 'terrain + things';
1177 } else if (tui.map_mode == 'terrain + things') {
1178 tui.map_mode = 'protections';
1179 } else if (tui.map_mode == 'protections') {
1180 tui.map_mode = 'terrain only';
1183 full_refresh: function() {
1185 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1186 this.recalc_input_lines();
1187 if (this.mode.is_intro) {
1188 this.draw_history();
1192 this.draw_turn_line();
1193 this.draw_mode_line();
1194 if (this.mode.shows_info) {
1197 this.draw_history();
1201 if (this.show_help) {
1213 this.map_control = "";
1214 this.map_size = [0,0];
1215 this.player_id = -1;
1219 get_thing: function(id_, create_if_not_found=false) {
1220 if (id_ in game.things) {
1221 return game.things[id_];
1222 } else if (create_if_not_found) {
1223 let t = new Thing([0,0]);
1224 game.things[id_] = t;
1228 move: function(start_position, direction) {
1229 let target = [start_position[0], start_position[1]];
1230 if (direction == 'LEFT') {
1232 } else if (direction == 'RIGHT') {
1234 } else if (game.map_geometry == 'Square') {
1235 if (direction == 'UP') {
1237 } else if (direction == 'DOWN') {
1240 } else if (game.map_geometry == 'Hex') {
1241 let start_indented = start_position[0] % 2;
1242 if (direction == 'UPLEFT') {
1244 if (!start_indented) {
1247 } else if (direction == 'UPRIGHT') {
1249 if (start_indented) {
1252 } else if (direction == 'DOWNLEFT') {
1254 if (!start_indented) {
1257 } else if (direction == 'DOWNRIGHT') {
1259 if (start_indented) {
1264 if (target[0] < 0 || target[1] < 0 ||
1265 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1270 teleport: function() {
1271 let player = this.get_thing(game.player_id);
1272 if (player.position in this.portals) {
1273 server.reconnect_to(this.portals[player.position]);
1275 terminal.blink_screen();
1276 tui.log_msg('? not standing on portal')
1284 server.init(websocket_location);
1290 move: function(direction) {
1291 let target = game.move(this.position, direction);
1293 this.position = target
1294 this.info_cached = false;
1295 if (tui.tile_draw) {
1296 this.send_tile_control_command();
1299 terminal.blink_screen();
1302 update_annotations: function(yx, str) {
1303 this.annotations[yx] = str;
1304 if (tui.mode.name == 'study') {
1308 empty_annotations: function() {
1309 this.annotations = {};
1310 if (tui.mode.name == 'study') {
1314 get_info: function() {
1315 if (this.info_cached) {
1316 return this.info_cached;
1318 let info_to_cache = '';
1319 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1320 if (game.fov[position_i] != '.') {
1321 info_to_cache += 'outside field of view';
1323 let terrain_char = game.map[position_i]
1324 let terrain_desc = '?'
1325 if (game.terrains[terrain_char]) {
1326 terrain_desc = game.terrains[terrain_char];
1328 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1329 let protection = game.map_control[position_i];
1330 if (protection == '.') {
1331 protection = 'unprotected';
1333 info_to_cache += 'PROTECTION: ' + protection + '\n';
1334 for (let t_id in game.things) {
1335 let t = game.things[t_id];
1336 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1337 info_to_cache += "THING: " + this.get_thing_info(t);
1338 let protection = t.protection;
1339 if (protection == '.') {
1340 protection = 'none';
1342 info_to_cache += " / protection: " + protection + "\n";
1345 if (this.position in game.portals) {
1346 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1348 if (this.position in this.annotations) {
1349 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1352 this.info_cached = info_to_cache;
1353 return this.info_cached;
1355 get_thing_info: function(t) {
1356 const symbol = game.thing_types[t.type_];
1357 let info = t.type_ + " / " + symbol;
1359 info += t.thing_char;
1362 info += " (" + t.name_ + ")";
1366 annotate: function(msg) {
1367 if (msg.length == 0) {
1368 msg = " "; // triggers annotation deletion
1370 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1372 set_portal: function(msg) {
1373 if (msg.length == 0) {
1374 msg = " "; // triggers portal deletion
1376 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1378 send_tile_control_command: function() {
1379 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1383 tui.inputEl.addEventListener('input', (event) => {
1384 if (tui.mode.has_input_prompt) {
1385 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1386 if (tui.inputEl.value.length > max_length) {
1387 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1389 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1390 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1391 tui.switch_mode('edit');
1395 document.onclick = function() {
1396 tui.show_help = false;
1398 tui.inputEl.addEventListener('keydown', (event) => {
1399 tui.show_help = false;
1400 if (event.key == 'Enter') {
1401 event.preventDefault();
1403 if (tui.mode.has_input_prompt && event.key == 'Enter'
1404 && tui.inputEl.value.length == 0
1405 && ['chat', 'command_thing', 'take_thing',
1406 'admin_enter'].includes(tui.mode.name)) {
1407 if (tui.mode.name != 'chat') {
1408 tui.log_msg('@ aborted');
1410 tui.switch_mode('play');
1411 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1412 tui.show_help = true;
1413 tui.inputEl.value = "";
1414 tui.restore_input_values();
1415 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1416 && !tui.mode.is_single_char_entry) {
1417 tui.show_help = true;
1418 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1419 tui.login_name = tui.inputEl.value;
1420 server.send(['LOGIN', tui.inputEl.value]);
1421 tui.inputEl.value = "";
1422 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1423 if (tui.task_action_on('command')) {
1424 server.send(['TASK:COMMAND', tui.inputEl.value]);
1425 tui.inputEl.value = "";
1427 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1428 const i = parseInt(tui.inputEl.value);
1429 if (isNaN(i) || i < 0 || i >= tui.selectables.length) {
1430 tui.log_msg('? invalid index, aborted');
1432 server.send(['TASK:PICK_UP', tui.selectables[i][0]]);
1434 tui.inputEl.value = "";
1435 tui.switch_mode('play');
1436 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1437 if (tui.inputEl.value.length == 0) {
1438 tui.log_msg('@ aborted');
1440 server.send(['SET_MAP_CONTROL_PASSWORD',
1441 tui.tile_control_char, tui.inputEl.value]);
1442 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1444 tui.switch_mode('admin');
1445 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1446 explorer.set_portal(tui.inputEl.value);
1447 tui.switch_mode('edit');
1448 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1449 if (tui.inputEl.value.length == 0) {
1450 tui.inputEl.value = " ";
1452 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1454 tui.switch_mode('edit');
1455 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1456 explorer.annotate(tui.inputEl.value);
1457 tui.switch_mode('edit');
1458 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1459 if (tui.inputEl.value.length == 0) {
1460 tui.inputEl.value = " ";
1462 tui.password = tui.inputEl.value
1463 tui.switch_mode('edit');
1464 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1465 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1466 tui.switch_mode('play');
1467 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1468 if (tui.inputEl.value.length != 1) {
1469 tui.log_msg('@ entered non-single-char, therefore aborted');
1470 tui.switch_mode('admin');
1472 tui.tile_control_char = tui.inputEl.value[0];
1473 tui.switch_mode('control_pw_pw');
1475 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1476 if (tui.inputEl.value.length != 1) {
1477 tui.log_msg('@ entered non-single-char, therefore aborted');
1478 tui.switch_mode('admin');
1480 tui.tile_control_char = tui.inputEl.value[0];
1481 tui.switch_mode('control_tile_draw');
1483 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1484 if (tui.inputEl.value.length != 1) {
1485 tui.log_msg('@ entered non-single-char, therefore aborted');
1487 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1488 tui.log_msg('@ sent new protection character for thing');
1490 tui.switch_mode('admin');
1491 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1492 let tokens = parser.tokenize(tui.inputEl.value);
1493 if (tokens.length > 0 && tokens[0].length > 0) {
1494 if (tui.inputEl.value[0][0] == '/') {
1495 if (tokens[0].slice(1) == 'nick') {
1496 if (tokens.length > 1) {
1497 server.send(['NICK', tokens[1]]);
1499 tui.log_msg('? need new name');
1502 tui.log_msg('? unknown command');
1505 server.send(['ALL', tui.inputEl.value]);
1507 } else if (tui.inputEl.valuelength > 0) {
1508 server.send(['ALL', tui.inputEl.value]);
1510 tui.inputEl.value = "";
1511 } else if (tui.mode.name == 'play') {
1512 if (tui.mode.mode_switch_on_key(event)) {
1514 } else if (event.key === tui.keys.drop_thing && tui.task_action_on('drop_thing')) {
1515 server.send(["TASK:DROP"]);
1516 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1517 server.send(["TASK:INTOXICATE"]);
1518 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1519 server.send(["TASK:DOOR"]);
1520 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1521 server.send(["TASK:INSTALL"]);
1522 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1523 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1524 } else if (event.key === tui.keys.teleport) {
1527 } else if (tui.mode.name == 'study') {
1528 if (tui.mode.mode_switch_on_key(event)) {
1530 } else if (event.key in tui.movement_keys) {
1531 explorer.move(tui.movement_keys[event.key]);
1532 } else if (event.key == tui.keys.toggle_map_mode) {
1533 tui.toggle_map_mode();
1535 } else if (tui.mode.name == 'control_tile_draw') {
1536 if (tui.mode.mode_switch_on_key(event)) {
1538 } else if (event.key in tui.movement_keys) {
1539 explorer.move(tui.movement_keys[event.key]);
1540 } else if (event.key === tui.keys.toggle_tile_draw) {
1541 tui.toggle_tile_draw();
1543 } else if (tui.mode.name == 'admin') {
1544 if (tui.mode.mode_switch_on_key(event)) {
1546 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1547 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1549 } else if (tui.mode.name == 'edit') {
1550 if (tui.mode.mode_switch_on_key(event)) {
1552 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1553 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1554 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1555 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1556 } else if (event.key == tui.keys.toggle_map_mode) {
1557 tui.toggle_map_mode();
1563 rows_selector.addEventListener('input', function() {
1564 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1567 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1568 terminal.initialize();
1571 cols_selector.addEventListener('input', function() {
1572 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1575 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1576 terminal.initialize();
1577 tui.window_width = terminal.cols / 2,
1580 for (let key_selector of key_selectors) {
1581 key_selector.addEventListener('input', function() {
1582 window.localStorage.setItem(key_selector.id, key_selector.value);
1586 window.setInterval(function() {
1587 if (server.connected) {
1588 server.send(['PING']);
1590 server.reconnect_to(server.url);
1591 tui.log_msg('@ attempting reconnect …')
1594 window.setInterval(function() {
1596 let span_decoration = "none";
1597 if (document.activeElement == tui.inputEl) {
1598 val = "on (click outside terminal to change)";
1600 val = "off (click into terminal to change)";
1601 span_decoration = "line-through";
1603 document.getElementById("keyboard_control").textContent = val;
1604 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1605 span.style.textDecoration = span_decoration;
1608 document.getElementById("terminal").onclick = function() {
1609 tui.inputEl.focus();
1611 document.getElementById("help").onclick = function() {
1612 tui.show_help = true;
1615 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1616 const mode = switchEl.id.slice("switch_to_".length);
1617 switchEl.onclick = function() {
1618 tui.switch_mode(mode);
1622 document.getElementById("toggle_tile_draw").onclick = function() {
1623 tui.toggle_tile_draw();
1625 document.getElementById("toggle_map_mode").onclick = function() {
1626 tui.toggle_map_mode();
1629 document.getElementById("drop_thing").onclick = function() {
1630 server.send(['TASK:DROP']);
1632 document.getElementById("flatten").onclick = function() {
1633 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1635 document.getElementById("door").onclick = function() {
1636 server.send(['TASK:DOOR']);
1638 document.getElementById("consume").onclick = function() {
1639 server.send(['TASK:INTOXICATE']);
1641 document.getElementById("install").onclick = function() {
1642 server.send(['TASK:INSTALL']);
1644 document.getElementById("teleport").onclick = function() {
1647 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1648 let direction = move_button.id.split('_')[2].toUpperCase();
1649 move_button.onclick = function() {
1650 if (tui.mode.available_actions.includes("move")
1651 || tui.mode.available_actions.includes("move_explorer")) {
1652 server.send(['TASK:MOVE', direction]);
1654 explorer.move(direction);