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 hard-to-remember keybindings</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="switch_to_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="wear"></button>
61 <button id="spin"></button>
65 <td><button id="switch_to_edit"></button></td>
67 <button id="switch_to_write"></button>
68 <button id="flatten"></button>
69 <button id="install"></button>
70 <button id="switch_to_annotate"></button>
71 <button id="switch_to_portal"></button>
72 <button id="switch_to_name_thing"></button>
73 <button id="switch_to_password"></button>
74 <button id="switch_to_enter_face"></button>
78 <td><button id="switch_to_admin_enter"></button></td>
80 <button id="switch_to_control_pw_type"></button>
81 <button id="switch_to_control_tile_type"></button>
82 <button id="switch_to_admin_thing_protect"></button>
83 <button id="toggle_tile_draw"></button>
88 <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 />
90 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
91 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
92 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
93 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
94 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
95 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
96 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
97 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
98 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
99 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
100 <li>help: <input id="key_help" type="text" value="h" />
101 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
102 <li>teleport: <input id="key_teleport" type="text" value="p" />
103 <li>spin: <input id="key_spin" type="text" value="S" />
104 <li>open/close: <input id="key_door" type="text" value="D" />
105 <li>consume: <input id="key_consume" type="text" value="C" />
106 <li>install: <input id="key_install" type="text" value="I" />
107 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
108 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
109 <li><input id="key_switch_to_enter_face" type="text" value="f" />
110 <li><input id="key_switch_to_take_thing" type="text" value="z" />
111 <li><input id="key_switch_to_chat" type="text" value="t" />
112 <li><input id="key_switch_to_play" type="text" value="p" />
113 <li><input id="key_switch_to_study" type="text" value="?" />
114 <li><input id="key_switch_to_edit" type="text" value="E" />
115 <li><input id="key_switch_to_write" type="text" value="m" />
116 <li><input id="key_switch_to_name_thing" type="text" value="N" />
117 <li><input id="key_switch_to_command_thing" type="text" value="O" />
118 <li><input id="key_switch_to_password" type="text" value="P" />
119 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
120 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
121 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
122 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
123 <li><input id="key_switch_to_annotate" type="text" value="M" />
124 <li><input id="key_switch_to_portal" type="text" value="T" />
125 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
126 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
131 let websocket_location = "wss://plomlompom.com/rogue_chat/";
132 //let websocket_location = "ws://localhost:8000/";
138 'long': 'This mode allows you to interact with the map in various ways.'
143 '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.'},
145 'short': 'world edit',
147 '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.'
150 'short': 'name thing',
152 'long': 'Give name to/change name of thing here.'
155 'short': 'command thing',
157 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
160 'short': 'take thing',
161 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
162 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
165 'short': 'drop thing',
166 'intro': 'Enter number of direction to which you want to drop thing.',
167 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
169 'admin_thing_protect': {
170 'short': 'change thing protection',
171 'intro': '@ enter thing protection character:',
172 'long': 'Change protection character for thing here.'
175 'short': 'enter your face',
176 'intro': '@ enter face line (enter nothing to abort):',
177 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
180 'short': 'change terrain',
182 '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.'
185 'short': 'change protection character password',
186 'intro': '@ enter protection character for which you want to change the password:',
187 '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.'
190 'short': 'change protection character password',
192 '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.'
194 'control_tile_type': {
195 'short': 'change tiles protection',
196 'intro': '@ enter protection character which you want to draw:',
197 '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.'
199 'control_tile_draw': {
200 'short': 'change tiles protection',
202 '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.'
205 'short': 'annotate tile',
207 '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.'
210 'short': 'edit portal',
212 '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.'
217 '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'
222 'long': 'Enter your player name.'
224 'waiting_for_server': {
225 'short': 'waiting for server response',
226 'intro': '@ waiting for server …',
227 'long': 'Waiting for a server response.'
230 'short': 'waiting for server response',
232 'long': 'Waiting for a server response.'
235 'short': 'set world edit password',
237 '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.'
240 'short': 'become admin',
241 'intro': '@ enter admin password:',
242 'long': 'This mode allows you to become admin if you know an admin password.'
247 'long': 'This mode allows you access to actions limited to administrators.'
250 let key_descriptions = {
252 'flatten': 'flatten surroundings',
253 'teleport': 'teleport',
254 'door': 'open/close',
255 'consume': 'consume',
256 'install': '(un-)install',
259 'toggle_map_mode': 'toggle map view',
260 'toggle_tile_draw': 'toggle protection character drawing',
261 'hex_move_upleft': 'up-left',
262 'hex_move_upright': 'up-right',
263 'hex_move_right': 'right',
264 'hex_move_left': 'left',
265 'hex_move_downleft': 'down-left',
266 'hex_move_downright': 'down-right',
267 'square_move_up': 'up',
268 'square_move_left': 'left',
269 'square_move_down': 'down',
270 'square_move_right': 'right',
272 for (const mode_name of Object.keys(mode_helps)) {
273 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
276 let rows_selector = document.getElementById("n_rows");
277 let cols_selector = document.getElementById("n_cols");
278 let key_selectors = document.querySelectorAll('[id^="key_"]');
280 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
281 const action = key_switch_selector.id.slice("key_switch_to_".length);
282 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
285 function restore_selector_value(selector) {
286 let stored_selection = window.localStorage.getItem(selector.id);
287 if (stored_selection) {
288 selector.value = stored_selection;
291 restore_selector_value(rows_selector);
292 restore_selector_value(cols_selector);
293 for (let key_selector of key_selectors) {
294 restore_selector_value(key_selector);
297 function escapeHTML(str) {
299 replace(/&/g, '&').
300 replace(/</g, '<').
301 replace(/>/g, '>').
302 replace(/'/g, ''').
303 replace(/"/g, '"');
307 initialize: function() {
308 this.rows = rows_selector.value;
309 this.cols = cols_selector.value;
310 this.pre_el = document.getElementById("terminal");
311 this.set_default_colors();
315 for (let y = 0, x = 0; y <= this.rows; x++) {
316 if (x == this.cols) {
319 this.content.push(line);
321 if (y == this.rows) {
328 apply_colors: function() {
329 this.pre_el.style.color = this.foreground;
330 this.pre_el.style.backgroundColor = this.background;
332 set_default_colors: function() {
333 this.foreground = 'white';
334 this.background = 'black';
337 set_random_colors: function() {
338 function rand(offset) {
339 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
341 this.foreground = '#' + rand(159) + rand(159) + rand(159);
342 this.background = '#' + rand(0) + rand(0) + rand(0);
345 blink_screen: function() {
346 this.pre_el.style.color = this.background;
347 this.pre_el.style.backgroundColor = this.foreground;
349 this.pre_el.style.color = this.foreground;
350 this.pre_el.style.backgroundColor = this.background;
353 refresh: function() {
354 let pre_content = '';
355 for (let y = 0; y < this.rows; y++) {
356 let line = this.content[y].join('');
358 if (y in tui.links) {
360 for (let span of tui.links[y]) {
361 chunks.push(escapeHTML(line.slice(start_x, span[0])));
362 chunks.push('<a target="_blank" href="');
363 chunks.push(escapeHTML(span[2]));
365 chunks.push(escapeHTML(line.slice(span[0], span[1])));
369 chunks.push(escapeHTML(line.slice(start_x)));
371 chunks = [escapeHTML(line)];
373 for (const chunk of chunks) {
374 pre_content += chunk;
378 this.pre_el.innerHTML = pre_content;
380 write: function(start_y, start_x, msg) {
381 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
382 this.content[start_y][x] = msg[i];
385 drawBox: function(start_y, start_x, height, width) {
386 let end_y = start_y + height;
387 let end_x = start_x + width;
388 for (let y = start_y, x = start_x; y < this.rows; x++) {
396 this.content[y][x] = ' ';
400 terminal.initialize();
403 tokenize: function(str) {
408 for (let i = 0; i < str.length; i++) {
414 } else if (c == '\\') {
416 } else if (c == '"') {
421 } else if (c == '"') {
423 } else if (c === ' ') {
424 if (token.length > 0) {
432 if (token.length > 0) {
437 parse_yx: function(position_string) {
438 let coordinate_strings = position_string.split(',')
439 let position = [0, 0];
440 position[0] = parseInt(coordinate_strings[0].slice(2));
441 position[1] = parseInt(coordinate_strings[1].slice(2));
453 init: function(url) {
455 this.websocket = new WebSocket(this.url);
456 this.websocket.onopen = function(event) {
457 game.thing_types = {};
459 server.send(['TASKS']);
460 server.send(['TERRAINS']);
461 server.send(['THING_TYPES']);
462 tui.log_msg("@ server connected! :)");
463 tui.switch_mode('login');
465 this.websocket.onclose = function(event) {
466 tui.switch_mode('waiting_for_server');
467 tui.log_msg("@ server disconnected :(");
469 this.websocket.onmessage = this.handle_event;
471 reconnect_to: function(url) {
472 this.websocket.close();
475 send: function(tokens) {
476 this.websocket.send(unparser.untokenize(tokens));
478 handle_event: function(event) {
479 let tokens = parser.tokenize(event.data);
480 if (tokens[0] === 'TURN') {
481 game.turn_complete = false;
482 explorer.empty_annotations();
486 game.turn = parseInt(tokens[1]);
487 } else if (tokens[0] === 'THING') {
488 let t = game.get_thing(tokens[4], true);
489 t.position = parser.parse_yx(tokens[1]);
491 t.protection = tokens[3];
492 t.portable = parseInt(tokens[5]);
493 t.commandable = parseInt(tokens[6]);
494 } else if (tokens[0] === 'THING_NAME') {
495 let t = game.get_thing(tokens[1], false);
497 } else if (tokens[0] === 'THING_FACE') {
498 let t = game.get_thing(tokens[1], false);
500 } else if (tokens[0] === 'THING_HAT') {
501 let t = game.get_thing(tokens[1], false);
503 } else if (tokens[0] === 'THING_CHAR') {
504 let t = game.get_thing(tokens[1], false);
505 t.thing_char = tokens[2];
506 } else if (tokens[0] === 'TASKS') {
507 game.tasks = tokens[1].split(',');
508 tui.mode_write.legal = game.tasks.includes('WRITE');
509 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
510 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
511 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
512 } else if (tokens[0] === 'THING_TYPE') {
513 game.thing_types[tokens[1]] = tokens[2]
514 } else if (tokens[0] === 'THING_CARRYING') {
515 let t = game.get_thing(tokens[1], false);
516 t.carrying = game.get_thing(tokens[2], false);
517 } else if (tokens[0] === 'THING_INSTALLED') {
518 let t = game.get_thing(tokens[1], false);
520 } else if (tokens[0] === 'TERRAIN') {
521 game.terrains[tokens[1]] = tokens[2]
522 } else if (tokens[0] === 'MAP') {
523 game.map_geometry = tokens[1];
525 game.map_size = parser.parse_yx(tokens[2]);
527 } else if (tokens[0] === 'FOV') {
529 } else if (tokens[0] === 'MAP_CONTROL') {
530 game.map_control = tokens[1]
531 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
532 game.turn_complete = true;
533 game.player = game.things[game.player_id];
534 explorer.info_cached = false;
535 if (tui.mode.name == 'post_login_wait') {
536 tui.switch_mode('play');
540 } else if (tokens[0] === 'CHAT') {
541 tui.log_msg('# ' + tokens[1], 1);
542 } else if (tokens[0] === 'CHATFACE') {
543 tui.draw_face = tokens[1];
544 } else if (tokens[0] === 'REPLY') {
545 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
546 } else if (tokens[0] === 'PLAYER_ID') {
547 game.player_id = parseInt(tokens[1]);
548 } else if (tokens[0] === 'LOGIN_OK') {
549 this.send(['GET_GAMESTATE']);
550 tui.switch_mode('post_login_wait');
551 } else if (tokens[0] === 'DEFAULT_COLORS') {
552 terminal.set_default_colors();
553 } else if (tokens[0] === 'RANDOM_COLORS') {
554 terminal.set_random_colors();
555 } else if (tokens[0] === 'ADMIN_OK') {
557 tui.log_msg('@ you now have admin rights');
558 tui.switch_mode('admin');
559 } else if (tokens[0] === 'PORTAL') {
560 let position = parser.parse_yx(tokens[1]);
561 game.portals[position] = tokens[2];
562 } else if (tokens[0] === 'ANNOTATION') {
563 let position = parser.parse_yx(tokens[1]);
564 explorer.update_annotations(position, tokens[2]);
566 } else if (tokens[0] === 'UNHANDLED_INPUT') {
567 tui.log_msg('? unknown command');
568 } else if (tokens[0] === 'PLAY_ERROR') {
569 tui.log_msg('? ' + tokens[1]);
570 terminal.blink_screen();
571 } else if (tokens[0] === 'ARGUMENT_ERROR') {
572 tui.log_msg('? syntax error: ' + tokens[1]);
573 } else if (tokens[0] === 'GAME_ERROR') {
574 tui.log_msg('? game error: ' + tokens[1]);
575 } else if (tokens[0] === 'PONG') {
578 tui.log_msg('? unhandled input: ' + event.data);
584 quote: function(str) {
586 for (let i = 0; i < str.length; i++) {
588 if (['"', '\\'].includes(c)) {
594 return quoted.join('');
596 to_yx: function(yx_coordinate) {
597 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
599 untokenize: function(tokens) {
600 let quoted_tokens = [];
601 for (let token of tokens) {
602 quoted_tokens.push(this.quote(token));
604 return quoted_tokens.join(" ");
609 constructor(name, has_input_prompt=false, shows_info=false,
610 is_intro=false, is_single_char_entry=false) {
612 this.short_desc = mode_helps[name].short;
613 this.available_modes = [];
614 this.available_actions = [];
615 this.has_input_prompt = has_input_prompt;
616 this.shows_info= shows_info;
617 this.is_intro = is_intro;
618 this.help_intro = mode_helps[name].long;
619 this.intro_msg = mode_helps[name].intro;
620 this.is_single_char_entry = is_single_char_entry;
623 *iter_available_modes() {
624 for (let mode_name of this.available_modes) {
625 let mode = tui['mode_' + mode_name];
629 let key = tui.keys['switch_to_' + mode.name];
633 list_available_modes() {
635 if (this.available_modes.length > 0) {
636 msg += 'Other modes available from here:\n';
637 for (let [mode, key] of this.iter_available_modes()) {
638 msg += '[' + key + '] – ' + mode.short_desc + '\n';
643 mode_switch_on_key(key_event) {
644 for (let [mode, key] of this.iter_available_modes()) {
645 if (key_event.key == key) {
646 event.preventDefault();
647 tui.switch_mode(mode.name);
659 window_width: terminal.cols / 2,
667 mode_waiting_for_server: new Mode('waiting_for_server',
669 mode_login: new Mode('login', true, false, true),
670 mode_post_login_wait: new Mode('post_login_wait'),
671 mode_chat: new Mode('chat', true),
672 mode_annotate: new Mode('annotate', true, true),
673 mode_play: new Mode('play'),
674 mode_study: new Mode('study', false, true),
675 mode_write: new Mode('write', false, false, false, true),
676 mode_edit: new Mode('edit'),
677 mode_control_pw_type: new Mode('control_pw_type', true),
678 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
679 mode_portal: new Mode('portal', true, true),
680 mode_password: new Mode('password', true),
681 mode_name_thing: new Mode('name_thing', true, true),
682 mode_command_thing: new Mode('command_thing', true),
683 mode_take_thing: new Mode('take_thing', true),
684 mode_drop_thing: new Mode('drop_thing', true),
685 mode_enter_face: new Mode('enter_face', true),
686 mode_admin_enter: new Mode('admin_enter', true),
687 mode_admin: new Mode('admin'),
688 mode_control_pw_pw: new Mode('control_pw_pw', true),
689 mode_control_tile_type: new Mode('control_tile_type', true),
690 mode_control_tile_draw: new Mode('control_tile_draw'),
692 'flatten': 'FLATTEN_SURROUNDINGS',
693 'take_thing': 'PICK_UP',
694 'drop_thing': 'DROP',
697 'install': 'INSTALL',
699 'command': 'COMMAND',
700 'consume': 'INTOXICATE',
708 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
709 "command_thing", "take_thing", "drop_thing"]
710 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
712 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
713 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
714 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
715 "control_tile_type", "chat",
716 "study", "play", "edit"]
717 this.mode_admin.available_actions = ["move"];
718 this.mode_control_tile_draw.available_modes = ["admin_enter"]
719 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
720 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
721 "password", "chat", "study", "play",
722 "admin_enter", "enter_face"]
723 this.mode_edit.available_actions = ["move", "flatten", "install",
725 this.inputEl = document.getElementById("input");
726 this.inputEl.focus();
727 this.switch_mode('waiting_for_server');
728 this.recalc_input_lines();
729 this.height_header = this.height_turn_line + this.height_mode_line;
732 init_keys: function() {
733 document.getElementById("move_table").hidden = true;
735 for (let key_selector of key_selectors) {
736 this.keys[key_selector.id.slice(4)] = key_selector.value;
738 this.movement_keys = {};
739 let geometry_prefix = 'undefinedMapGeometry_';
740 if (game.map_geometry) {
741 geometry_prefix = game.map_geometry.toLowerCase() + '_';
743 for (const key_name of Object.keys(key_descriptions)) {
744 if (key_name.startsWith(geometry_prefix)) {
745 let direction = key_name.split('_')[2].toUpperCase();
746 let key = this.keys[key_name];
747 this.movement_keys[key] = direction;
750 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
751 if (move_button.id.startsWith('key_')) {
754 move_button.hidden = true;
756 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
757 document.getElementById("move_table").hidden = false;
758 move_button.hidden = false;
760 for (let el of document.getElementsByTagName("button")) {
761 let action_desc = key_descriptions[el.id];
762 let action_key = '[' + this.keys[el.id] + ']';
763 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
766 task_action_on: function(action) {
767 return game.tasks.includes(this.action_tasks[action]);
769 switch_mode: function(mode_name) {
771 function fail(msg, return_mode) {
772 tui.log_msg('? ' + msg);
773 terminal.blink_screen();
774 this.switch_mode(return_mode);
777 if (this.mode && this.mode.name == 'control_tile_draw') {
778 tui.log_msg('@ finished tile protection drawing.')
780 this.tile_draw = false;
781 if (mode_name == 'command_thing' && (!game.player.carrying
782 || !game.player.carrying.commandable)) {
783 return fail('not carrying anything commandable', 'play');
785 if (mode_name == 'take_thing' && game.player.carrying) {
786 return fail('already carrying something', 'play');
788 if (mode_name == 'drop_thing' && !game.player.carrying) {
789 return fail('not carrying anything droppable', 'play');
791 if (mode_name == 'admin_enter' && this.is_admin) {
793 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
795 for (let t_id in game.things) {
796 if (t_id == game.player_id) {
799 let t = game.things[t_id];
800 if (game.player.position[0] == t.position[0]
801 && game.player.position[1] == t.position[1]) {
807 return fail('not standing over thing', 'fail');
809 this.selected_thing_id = thing_id;
812 this.mode = this['mode_' + mode_name];
813 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
814 this.map_mode = 'protections';
815 } else if (this.mode.name != "edit") {
816 this.map_mode = 'terrain + things';
818 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
819 this.inputEl.focus();
821 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
822 explorer.position = game.player.position;
824 this.inputEl.value = "";
825 this.restore_input_values();
826 for (let el of document.getElementsByTagName("button")) {
829 document.getElementById("help").disabled = false;
830 for (const action of this.mode.available_actions) {
831 if (["move", "move_explorer"].includes(action)) {
832 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
833 move_key.disabled = false;
835 } else if (Object.keys(this.action_tasks).includes(action)) {
836 if (this.task_action_on(action)) {
837 document.getElementById(action).disabled = false;
840 document.getElementById(action).disabled = false;
843 for (const mode_name of this.mode.available_modes) {
844 document.getElementById('switch_to_' + mode_name).disabled = false;
846 if (this.mode.intro_msg.length > 0) {
847 this.log_msg(this.mode.intro_msg);
849 if (this.mode.name == 'login') {
850 if (this.login_name) {
851 server.send(['LOGIN', this.login_name]);
853 this.log_msg("? need login name");
855 } else if (this.mode.is_single_char_entry) {
856 this.show_help = true;
857 } else if (this.mode.name == 'take_thing') {
858 this.log_msg("Portable things in reach for pick-up:");
859 const y = game.player.position[0]
860 const x = game.player.position[1]
861 let select_range = [y.toString() + ':' + x.toString(),
862 (y + 0).toString() + ':' + (x - 1).toString(),
863 (y + 0).toString() + ':' + (x + 1).toString(),
864 (y - 1).toString() + ':' + (x).toString(),
865 (y + 1).toString() + ':' + (x).toString()];
866 if (game.map_geometry == 'Hex') {
868 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
869 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
871 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
872 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
875 this.selectables = [];
876 for (const t_id in game.things) {
877 const t = game.things[t_id];
878 if (select_range.includes(t.position[0].toString()
879 + ':' + t.position[1].toString())
881 this.selectables.push(t_id);
884 if (this.selectables.length == 0) {
885 this.log_msg('none');
886 terminal.blink_screen();
887 this.switch_mode('play');
890 for (let [i, t_id] of this.selectables.entries()) {
891 const t = game.things[t_id];
892 this.log_msg(i + ': ' + explorer.get_thing_info(t));
895 } else if (this.mode.name == 'drop_thing') {
896 this.log_msg('Direction to drop thing to:');
897 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
898 for (let [i, direction] of this.selectables.entries()) {
899 this.log_msg(i + ': ' + direction);
901 } else if (this.mode.name == 'command_thing') {
902 server.send(['TASK:COMMAND', 'HELP']);
903 } else if (this.mode.name == 'control_pw_pw') {
904 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
905 } else if (this.mode.name == 'control_tile_draw') {
906 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 + '].')
910 offset_links: function(offset, links) {
911 for (let y in links) {
912 let real_y = offset[0] + parseInt(y);
913 if (!this.links[real_y]) {
914 this.links[real_y] = [];
916 for (let link of links[y]) {
917 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
918 this.links[real_y].push(offset_link);
922 restore_input_values: function() {
923 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
924 let info = explorer.annotations[explorer.position];
925 if (info != "(none)") {
926 this.inputEl.value = info;
928 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
929 let portal = game.portals[explorer.position]
930 this.inputEl.value = portal;
931 } else if (this.mode.name == 'password') {
932 this.inputEl.value = this.password;
933 } else if (this.mode.name == 'name_thing') {
934 let t = game.get_thing(this.selected_thing_id);
936 this.inputEl.value = t.name_;
938 } else if (this.mode.name == 'admin_thing_protect') {
939 let t = game.get_thing(this.selected_thing_id);
940 if (t && t.protection) {
941 this.inputEl.value = t.protection;
945 recalc_input_lines: function() {
946 if (this.mode.has_input_prompt) {
948 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
950 this.input_lines = [];
952 this.height_input = this.input_lines.length;
954 msg_into_lines_of_width: function(msg, width) {
955 function push_inner_link(y, end_x) {
956 if (!inner_links[y]) {
959 inner_links[y].push([url_start_x, end_x, url]);
961 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
964 for (const match of matches) {
965 const url = match[0];
966 const url_start = match.index;
967 const url_end = match.index + match[0].length;
968 link_data[url_start] = url;
969 url_ends.push(url_end);
973 let inner_links = {};
977 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
978 if (x >= width || msg[i] == "\n") {
980 push_inner_link(y, chunk.length);
982 if (url_ends[0] == i) {
990 if (msg[i] == "\n") {
995 if (msg[i] != "\n") {
998 if (i in link_data) {
1002 } else if (url_ends[0] == i) {
1004 push_inner_link(y, x);
1010 push_inner_link(lines.length - 1, chunk.length);
1012 return [lines, inner_links];
1014 log_msg: function(msg) {
1016 while (this.log.length > 100) {
1019 this.full_refresh();
1021 pick_selectable: function(task_name) {
1022 const i = parseInt(this.inputEl.value);
1023 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1024 tui.log_msg('? invalid index, aborted');
1026 server.send(['TASK:' + task_name, tui.selectables[i]]);
1028 this.inputEl.value = "";
1029 this.switch_mode('play');
1031 draw_map: function() {
1032 if (!game.turn_complete && this.map_lines.length == 0) {
1035 if (game.turn_complete) {
1036 let map_lines_split = [];
1038 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1039 if (j == game.map_size[1]) {
1040 map_lines_split.push(line);
1044 if (this.map_mode == 'protections') {
1045 line.push(game.map_control[i] + ' ');
1047 line.push(game.map[i] + ' ');
1050 map_lines_split.push(line);
1051 if (this.map_mode == 'terrain + annotations') {
1052 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1053 const yx = coordinate.split(',')
1054 map_lines_split[yx[0]][yx[1]] = 'A ';
1056 } else if (this.map_mode == 'terrain + things') {
1057 for (const p in game.portals) {
1058 let coordinate = p.split(',')
1059 let original = map_lines_split[coordinate[0]][coordinate[1]];
1060 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1062 let used_positions = [];
1063 function draw_thing(t, used_positions) {
1064 let symbol = game.thing_types[t.type_];
1065 let meta_char = ' ';
1067 meta_char = t.thing_char;
1069 if (used_positions.includes(t.position.toString())) {
1075 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1076 used_positions.push(t.position.toString());
1078 for (const thing_id in game.things) {
1079 let t = game.things[thing_id];
1080 if (t.type_ != 'Player') {
1081 draw_thing(t, used_positions);
1084 for (const thing_id in game.things) {
1085 let t = game.things[thing_id];
1086 if (t.type_ == 'Player') {
1087 draw_thing(t, used_positions);
1091 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1092 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1093 } else if (tui.map_mode != 'terrain + things') {
1094 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1097 if (game.map_geometry == 'Square') {
1098 for (let line_split of map_lines_split) {
1099 this.map_lines.push(line_split.join(''));
1101 } else if (game.map_geometry == 'Hex') {
1103 for (let line_split of map_lines_split) {
1104 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1112 let window_center = [terminal.rows / 2, this.window_width / 2];
1113 let center_position = [game.player.position[0], game.player.position[1]];
1114 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1115 center_position = [explorer.position[0], explorer.position[1]];
1117 center_position[1] = center_position[1] * 2;
1118 this.offset = [center_position[0] - window_center[0],
1119 center_position[1] - window_center[1]]
1120 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1121 this.offset[1] += 1;
1124 let term_y = Math.max(0, -this.offset[0]);
1125 let term_x = Math.max(0, -this.offset[1]);
1126 let map_y = Math.max(0, this.offset[0]);
1127 let map_x = Math.max(0, this.offset[1]);
1128 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1129 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1130 terminal.write(term_y, term_x, to_draw);
1133 draw_face_popup: function() {
1134 const t = game.things[this.draw_face];
1136 this.draw_face = false;
1139 function draw_body_part(body_part, end_y) {
1140 const start_x = tui.window_width - 10;
1141 terminal.write(end_y - 4, start_x, '+--------+');
1142 terminal.write(end_y - 3, start_x, '| |');
1143 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1144 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1145 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1148 draw_body_part(t.face, terminal.rows - 1);
1151 draw_body_part(t.hat, terminal.rows - 4);
1154 draw_mode_line: function() {
1155 let help = 'hit [' + this.keys.help + '] for help';
1156 if (this.mode.has_input_prompt) {
1157 help = 'enter /help for help';
1159 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1161 draw_turn_line: function(n) {
1162 if (game.turn_complete) {
1163 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1166 draw_history: function() {
1167 let log_display_lines = [];
1169 let y_offset_in_log = 0;
1170 for (let line of this.log) {
1171 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1173 log_display_lines = log_display_lines.concat(new_lines);
1174 for (const y in link_data) {
1175 const rel_y = y_offset_in_log + parseInt(y);
1176 log_links[rel_y] = [];
1177 for (let link of link_data[y]) {
1178 log_links[rel_y].push(link);
1181 y_offset_in_log += new_lines.length;
1183 let i = log_display_lines.length - 1;
1184 for (let y = terminal.rows - 1 - this.height_input;
1185 y >= this.height_header && i >= 0;
1187 terminal.write(y, this.window_width, log_display_lines[i]);
1189 for (const key of Object.keys(log_links)) {
1190 if (parseInt(key) <= i) {
1191 delete log_links[key];
1194 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1196 this.offset_links(offset, log_links);
1198 draw_info: function() {
1199 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1200 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1201 let offset = [this.height_header, this.window_width];
1202 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1203 terminal.write(y, offset[1], lines[i]);
1205 this.offset_links(offset, link_data);
1207 draw_input: function() {
1208 if (this.mode.has_input_prompt) {
1209 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1210 terminal.write(y, this.window_width, this.input_lines[i]);
1214 draw_help: function() {
1215 let movement_keys_desc = '';
1216 if (!this.mode.is_intro) {
1217 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1219 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1220 if (this.mode.available_actions.length > 0) {
1221 content += "Available actions:\n";
1222 for (let action of this.mode.available_actions) {
1223 if (Object.keys(this.action_tasks).includes(action)) {
1224 if (!this.task_action_on(action)) {
1228 if (action == 'move_explorer') {
1231 if (action == 'move') {
1232 content += "[" + movement_keys_desc + "] – move\n"
1234 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1239 content += this.mode.list_available_modes();
1241 if (!this.mode.has_input_prompt) {
1242 start_x = this.window_width;
1243 this.draw_links = false;
1245 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1246 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1247 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1248 terminal.write(y, start_x, lines[i]);
1251 toggle_tile_draw: function() {
1252 if (tui.tile_draw) {
1253 tui.tile_draw = false;
1255 tui.tile_draw = true;
1258 toggle_map_mode: function() {
1259 if (tui.map_mode == 'terrain only') {
1260 tui.map_mode = 'terrain + annotations';
1261 } else if (tui.map_mode == 'terrain + annotations') {
1262 tui.map_mode = 'terrain + things';
1263 } else if (tui.map_mode == 'terrain + things') {
1264 tui.map_mode = 'protections';
1265 } else if (tui.map_mode == 'protections') {
1266 tui.map_mode = 'terrain only';
1269 full_refresh: function() {
1270 this.draw_links = true;
1272 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1273 this.recalc_input_lines();
1274 if (this.mode.is_intro) {
1275 this.draw_history();
1279 this.draw_turn_line();
1280 this.draw_mode_line();
1281 if (this.mode.shows_info) {
1284 this.draw_history();
1288 if (this.show_help) {
1291 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1292 this.draw_face_popup();
1294 if (!this.draw_links) {
1306 this.map_control = "";
1307 this.map_size = [0,0];
1308 this.player_id = -1;
1312 get_thing: function(id_, create_if_not_found=false) {
1313 if (id_ in game.things) {
1314 return game.things[id_];
1315 } else if (create_if_not_found) {
1316 let t = new Thing([0,0]);
1317 game.things[id_] = t;
1321 move: function(start_position, direction) {
1322 let target = [start_position[0], start_position[1]];
1323 if (direction == 'LEFT') {
1325 } else if (direction == 'RIGHT') {
1327 } else if (game.map_geometry == 'Square') {
1328 if (direction == 'UP') {
1330 } else if (direction == 'DOWN') {
1333 } else if (game.map_geometry == 'Hex') {
1334 let start_indented = start_position[0] % 2;
1335 if (direction == 'UPLEFT') {
1337 if (!start_indented) {
1340 } else if (direction == 'UPRIGHT') {
1342 if (start_indented) {
1345 } else if (direction == 'DOWNLEFT') {
1347 if (!start_indented) {
1350 } else if (direction == 'DOWNRIGHT') {
1352 if (start_indented) {
1357 if (target[0] < 0 || target[1] < 0 ||
1358 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1363 teleport: function() {
1364 if (game.player.position in this.portals) {
1365 server.reconnect_to(this.portals[game.player.position]);
1367 terminal.blink_screen();
1368 tui.log_msg('? not standing on portal')
1376 server.init(websocket_location);
1382 move: function(direction) {
1383 let target = game.move(this.position, direction);
1385 this.position = target
1386 this.info_cached = false;
1387 if (tui.tile_draw) {
1388 this.send_tile_control_command();
1391 terminal.blink_screen();
1394 update_annotations: function(yx, str) {
1395 this.annotations[yx] = str;
1396 if (tui.mode.name == 'study') {
1400 empty_annotations: function() {
1401 this.annotations = {};
1402 if (tui.mode.name == 'study') {
1406 get_info: function() {
1407 if (this.info_cached) {
1408 return this.info_cached;
1410 let info_to_cache = '';
1411 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1412 if (game.fov[position_i] != '.') {
1413 info_to_cache += 'outside field of view';
1415 for (let t_id in game.things) {
1416 let t = game.things[t_id];
1417 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1418 info_to_cache += "THING: " + this.get_thing_info(t);
1419 let protection = t.protection;
1420 if (protection == '.') {
1421 protection = 'none';
1423 info_to_cache += " / protection: " + protection + "\n";
1425 info_to_cache += t.hat.slice(0, 6) + '\n';
1426 info_to_cache += t.hat.slice(6, 12) + '\n';
1427 info_to_cache += t.hat.slice(12, 18) + '\n';
1430 info_to_cache += t.face.slice(0, 6) + '\n';
1431 info_to_cache += t.face.slice(6, 12) + '\n';
1432 info_to_cache += t.face.slice(12, 18) + '\n';
1436 let terrain_char = game.map[position_i]
1437 let terrain_desc = '?'
1438 if (game.terrains[terrain_char]) {
1439 terrain_desc = game.terrains[terrain_char];
1441 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1442 let protection = game.map_control[position_i];
1443 if (protection == '.') {
1444 protection = 'unprotected';
1446 info_to_cache += 'PROTECTION: ' + protection + '\n';
1447 if (this.position in game.portals) {
1448 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1450 if (this.position in this.annotations) {
1451 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1454 this.info_cached = info_to_cache;
1455 return this.info_cached;
1457 get_thing_info: function(t) {
1458 const symbol = game.thing_types[t.type_];
1459 let info = t.type_ + " / " + symbol;
1461 info += t.thing_char;
1464 info += " (" + t.name_ + ")";
1467 info += " / installed";
1471 annotate: function(msg) {
1472 if (msg.length == 0) {
1473 msg = " "; // triggers annotation deletion
1475 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1477 set_portal: function(msg) {
1478 if (msg.length == 0) {
1479 msg = " "; // triggers portal deletion
1481 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1483 send_tile_control_command: function() {
1484 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1488 tui.inputEl.addEventListener('input', (event) => {
1489 if (tui.mode.has_input_prompt) {
1490 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1491 if (tui.inputEl.value.length > max_length) {
1492 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1494 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1495 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1496 tui.switch_mode('edit');
1500 document.onclick = function() {
1501 tui.show_help = false;
1503 tui.inputEl.addEventListener('keydown', (event) => {
1504 tui.show_help = false;
1505 tui.draw_face = false;
1506 if (event.key == 'Enter') {
1507 event.preventDefault();
1509 if ((!tui.mode.is_intro && event.key == 'Escape')
1510 || (tui.mode.has_input_prompt && event.key == 'Enter'
1511 && tui.inputEl.value.length == 0
1512 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1513 'admin_enter'].includes(tui.mode.name))) {
1514 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1515 tui.log_msg('@ aborted');
1517 tui.switch_mode('play');
1518 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1519 tui.show_help = true;
1520 tui.inputEl.value = "";
1521 tui.restore_input_values();
1522 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1523 && !tui.mode.is_single_char_entry) {
1524 tui.show_help = true;
1525 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1526 tui.login_name = tui.inputEl.value;
1527 server.send(['LOGIN', tui.inputEl.value]);
1528 tui.inputEl.value = "";
1529 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1530 if (tui.inputEl.value.length != 18) {
1531 tui.log_msg('? wrong input length, aborting');
1533 server.send(['PLAYER_FACE', tui.inputEl.value]);
1535 tui.inputEl.value = "";
1536 tui.switch_mode('edit');
1537 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1538 server.send(['TASK:COMMAND', tui.inputEl.value]);
1539 tui.inputEl.value = "";
1540 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1541 tui.pick_selectable('PICK_UP');
1542 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1543 tui.pick_selectable('DROP');
1544 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1545 if (tui.inputEl.value.length == 0) {
1546 tui.log_msg('@ aborted');
1548 server.send(['SET_MAP_CONTROL_PASSWORD',
1549 tui.tile_control_char, tui.inputEl.value]);
1550 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1552 tui.switch_mode('admin');
1553 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1554 explorer.set_portal(tui.inputEl.value);
1555 tui.switch_mode('edit');
1556 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1557 if (tui.inputEl.value.length == 0) {
1558 tui.inputEl.value = " ";
1560 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1562 tui.switch_mode('edit');
1563 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1564 explorer.annotate(tui.inputEl.value);
1565 tui.switch_mode('edit');
1566 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1567 if (tui.inputEl.value.length == 0) {
1568 tui.inputEl.value = " ";
1570 tui.password = tui.inputEl.value
1571 tui.switch_mode('edit');
1572 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1573 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1574 tui.switch_mode('play');
1575 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1576 if (tui.inputEl.value.length != 1) {
1577 tui.log_msg('@ entered non-single-char, therefore aborted');
1578 tui.switch_mode('admin');
1580 tui.tile_control_char = tui.inputEl.value[0];
1581 tui.switch_mode('control_pw_pw');
1583 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1584 if (tui.inputEl.value.length != 1) {
1585 tui.log_msg('@ entered non-single-char, therefore aborted');
1586 tui.switch_mode('admin');
1588 tui.tile_control_char = tui.inputEl.value[0];
1589 tui.switch_mode('control_tile_draw');
1591 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1592 if (tui.inputEl.value.length != 1) {
1593 tui.log_msg('@ entered non-single-char, therefore aborted');
1595 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1596 tui.log_msg('@ sent new protection character for thing');
1598 tui.switch_mode('admin');
1599 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1600 let tokens = parser.tokenize(tui.inputEl.value);
1601 if (tokens.length > 0 && tokens[0].length > 0) {
1602 if (tui.inputEl.value[0][0] == '/') {
1603 if (tokens[0].slice(1) == 'nick') {
1604 if (tokens.length > 1) {
1605 server.send(['NICK', tokens[1]]);
1607 tui.log_msg('? need new name');
1610 tui.log_msg('? unknown command');
1613 server.send(['ALL', tui.inputEl.value]);
1615 } else if (tui.inputEl.valuelength > 0) {
1616 server.send(['ALL', tui.inputEl.value]);
1618 tui.inputEl.value = "";
1619 } else if (tui.mode.name == 'play') {
1620 if (tui.mode.mode_switch_on_key(event)) {
1622 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1623 server.send(["TASK:INTOXICATE"]);
1624 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1625 server.send(["TASK:DOOR"]);
1626 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1627 server.send(["TASK:WEAR"]);
1628 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1629 server.send(["TASK:SPIN"]);
1630 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1631 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1632 } else if (event.key === tui.keys.teleport) {
1635 } else if (tui.mode.name == 'study') {
1636 if (tui.mode.mode_switch_on_key(event)) {
1638 } else if (event.key in tui.movement_keys) {
1639 explorer.move(tui.movement_keys[event.key]);
1640 } else if (event.key == tui.keys.toggle_map_mode) {
1641 tui.toggle_map_mode();
1643 } else if (tui.mode.name == 'control_tile_draw') {
1644 if (tui.mode.mode_switch_on_key(event)) {
1646 } else if (event.key in tui.movement_keys) {
1647 explorer.move(tui.movement_keys[event.key]);
1648 } else if (event.key === tui.keys.toggle_tile_draw) {
1649 tui.toggle_tile_draw();
1651 } else if (tui.mode.name == 'admin') {
1652 if (tui.mode.mode_switch_on_key(event)) {
1654 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1655 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1657 } else if (tui.mode.name == 'edit') {
1658 if (tui.mode.mode_switch_on_key(event)) {
1660 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1661 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1662 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1663 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1664 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1665 server.send(["TASK:INSTALL", tui.password]);
1666 } else if (event.key == tui.keys.toggle_map_mode) {
1667 tui.toggle_map_mode();
1673 rows_selector.addEventListener('input', function() {
1674 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1677 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1678 terminal.initialize();
1681 cols_selector.addEventListener('input', function() {
1682 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1685 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1686 terminal.initialize();
1687 tui.window_width = terminal.cols / 2,
1690 for (let key_selector of key_selectors) {
1691 key_selector.addEventListener('input', function() {
1692 window.localStorage.setItem(key_selector.id, key_selector.value);
1696 window.setInterval(function() {
1697 if (server.websocket.readyState == 1) {
1698 server.send(['PING']);
1699 } else if (server.websocket.readyState != 0) {
1700 server.reconnect_to(server.url);
1701 tui.log_msg('@ attempting reconnect …')
1704 window.setInterval(function() {
1706 let span_decoration = "none";
1707 if (document.activeElement == tui.inputEl) {
1708 val = "on (click outside terminal to change)";
1710 val = "off (click into terminal to change)";
1711 span_decoration = "line-through";
1713 document.getElementById("keyboard_control").textContent = val;
1714 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1715 span.style.textDecoration = span_decoration;
1718 document.getElementById("terminal").onclick = function() {
1719 tui.inputEl.focus();
1721 document.getElementById("help").onclick = function() {
1722 tui.show_help = true;
1725 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1726 const mode = switchEl.id.slice("switch_to_".length);
1727 switchEl.onclick = function() {
1728 tui.switch_mode(mode);
1732 document.getElementById("toggle_tile_draw").onclick = function() {
1733 tui.toggle_tile_draw();
1735 document.getElementById("toggle_map_mode").onclick = function() {
1736 tui.toggle_map_mode();
1739 document.getElementById("flatten").onclick = function() {
1740 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1742 document.getElementById("door").onclick = function() {
1743 server.send(['TASK:DOOR']);
1745 document.getElementById("consume").onclick = function() {
1746 server.send(['TASK:INTOXICATE']);
1748 document.getElementById("install").onclick = function() {
1749 server.send(['TASK:INSTALL']);
1751 document.getElementById("wear").onclick = function() {
1752 server.send(['TASK:WEAR']);
1754 document.getElementById("spin").onclick = function() {
1755 server.send(['TASK:SPIN']);
1757 document.getElementById("teleport").onclick = function() {
1760 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1761 if (move_button.id.startsWith('key_')) { // not a move button
1764 let direction = move_button.id.split('_')[2].toUpperCase();
1765 move_button.onclick = function() {
1766 if (tui.mode.available_actions.includes("move")) {
1767 server.send(['TASK:MOVE', direction]);
1768 } else if (tui.mode.available_actions.includes("move_explorer")) {
1769 explorer.move(direction);