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="install"></button>
61 <button id="wear"></button>
62 <button id="spin"></button>
66 <td><button id="switch_to_edit"></button></td>
68 <button id="switch_to_write"></button>
69 <button id="flatten"></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 = t = 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 if (tui.mode.name == 'post_login_wait') {
534 tui.switch_mode('play');
536 explorer.info_cached = false;
538 } else if (tokens[0] === 'CHAT') {
539 tui.log_msg('# ' + tokens[1], 1);
540 } else if (tokens[0] === 'REPLY') {
541 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
542 } else if (tokens[0] === 'PLAYER_ID') {
543 game.player_id = parseInt(tokens[1]);
544 } else if (tokens[0] === 'LOGIN_OK') {
545 this.send(['GET_GAMESTATE']);
546 tui.switch_mode('post_login_wait');
547 } else if (tokens[0] === 'DEFAULT_COLORS') {
548 terminal.set_default_colors();
549 } else if (tokens[0] === 'RANDOM_COLORS') {
550 terminal.set_random_colors();
551 } else if (tokens[0] === 'ADMIN_OK') {
553 tui.log_msg('@ you now have admin rights');
554 tui.switch_mode('admin');
555 } else if (tokens[0] === 'PORTAL') {
556 let position = parser.parse_yx(tokens[1]);
557 game.portals[position] = tokens[2];
558 } else if (tokens[0] === 'ANNOTATION') {
559 let position = parser.parse_yx(tokens[1]);
560 explorer.update_annotations(position, tokens[2]);
562 } else if (tokens[0] === 'UNHANDLED_INPUT') {
563 tui.log_msg('? unknown command');
564 } else if (tokens[0] === 'PLAY_ERROR') {
565 tui.log_msg('? ' + tokens[1]);
566 terminal.blink_screen();
567 } else if (tokens[0] === 'ARGUMENT_ERROR') {
568 tui.log_msg('? syntax error: ' + tokens[1]);
569 } else if (tokens[0] === 'GAME_ERROR') {
570 tui.log_msg('? game error: ' + tokens[1]);
571 } else if (tokens[0] === 'PONG') {
574 tui.log_msg('? unhandled input: ' + event.data);
580 quote: function(str) {
582 for (let i = 0; i < str.length; i++) {
584 if (['"', '\\'].includes(c)) {
590 return quoted.join('');
592 to_yx: function(yx_coordinate) {
593 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
595 untokenize: function(tokens) {
596 let quoted_tokens = [];
597 for (let token of tokens) {
598 quoted_tokens.push(this.quote(token));
600 return quoted_tokens.join(" ");
605 constructor(name, has_input_prompt=false, shows_info=false,
606 is_intro=false, is_single_char_entry=false) {
608 this.short_desc = mode_helps[name].short;
609 this.available_modes = [];
610 this.available_actions = [];
611 this.has_input_prompt = has_input_prompt;
612 this.shows_info= shows_info;
613 this.is_intro = is_intro;
614 this.help_intro = mode_helps[name].long;
615 this.intro_msg = mode_helps[name].intro;
616 this.is_single_char_entry = is_single_char_entry;
619 *iter_available_modes() {
620 for (let mode_name of this.available_modes) {
621 let mode = tui['mode_' + mode_name];
625 let key = tui.keys['switch_to_' + mode.name];
629 list_available_modes() {
631 if (this.available_modes.length > 0) {
632 msg += 'Other modes available from here:\n';
633 for (let [mode, key] of this.iter_available_modes()) {
634 msg += '[' + key + '] – ' + mode.short_desc + '\n';
639 mode_switch_on_key(key_event) {
640 for (let [mode, key] of this.iter_available_modes()) {
641 if (key_event.key == key) {
642 event.preventDefault();
643 tui.switch_mode(mode.name);
655 window_width: terminal.cols / 2,
663 mode_waiting_for_server: new Mode('waiting_for_server',
665 mode_login: new Mode('login', true, false, true),
666 mode_post_login_wait: new Mode('post_login_wait'),
667 mode_chat: new Mode('chat', true),
668 mode_annotate: new Mode('annotate', true, true),
669 mode_play: new Mode('play'),
670 mode_study: new Mode('study', false, true),
671 mode_write: new Mode('write', false, false, false, true),
672 mode_edit: new Mode('edit'),
673 mode_control_pw_type: new Mode('control_pw_type', true),
674 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
675 mode_portal: new Mode('portal', true, true),
676 mode_password: new Mode('password', true),
677 mode_name_thing: new Mode('name_thing', true, true),
678 mode_command_thing: new Mode('command_thing', true),
679 mode_take_thing: new Mode('take_thing', true),
680 mode_drop_thing: new Mode('drop_thing', true),
681 mode_enter_face: new Mode('enter_face', true),
682 mode_admin_enter: new Mode('admin_enter', true),
683 mode_admin: new Mode('admin'),
684 mode_control_pw_pw: new Mode('control_pw_pw', true),
685 mode_control_tile_type: new Mode('control_tile_type', true),
686 mode_control_tile_draw: new Mode('control_tile_draw'),
688 'flatten': 'FLATTEN_SURROUNDINGS',
689 'take_thing': 'PICK_UP',
690 'drop_thing': 'DROP',
693 'install': 'INSTALL',
695 'command': 'COMMAND',
696 'consume': 'INTOXICATE',
703 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
704 "command_thing", "take_thing", "drop_thing"]
705 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
706 "install", "wear", "spin"];
707 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
708 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
709 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
710 "control_tile_type", "chat",
711 "study", "play", "edit"]
712 this.mode_admin.available_actions = ["move"];
713 this.mode_control_tile_draw.available_modes = ["admin_enter"]
714 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
715 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
716 "password", "chat", "study", "play",
717 "admin_enter", "enter_face"]
718 this.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
719 this.inputEl = document.getElementById("input");
720 this.inputEl.focus();
721 this.switch_mode('waiting_for_server');
722 this.recalc_input_lines();
723 this.height_header = this.height_turn_line + this.height_mode_line;
726 init_keys: function() {
727 document.getElementById("move_table").hidden = true;
729 for (let key_selector of key_selectors) {
730 this.keys[key_selector.id.slice(4)] = key_selector.value;
732 this.movement_keys = {};
733 let geometry_prefix = 'undefinedMapGeometry_';
734 if (game.map_geometry) {
735 geometry_prefix = game.map_geometry.toLowerCase() + '_';
737 for (const key_name of Object.keys(key_descriptions)) {
738 if (key_name.startsWith(geometry_prefix)) {
739 let direction = key_name.split('_')[2].toUpperCase();
740 let key = this.keys[key_name];
741 this.movement_keys[key] = direction;
744 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
745 if (move_button.id.startsWith('key_')) {
748 move_button.hidden = true;
750 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
751 document.getElementById("move_table").hidden = false;
752 move_button.hidden = false;
754 for (let el of document.getElementsByTagName("button")) {
755 let action_desc = key_descriptions[el.id];
756 let action_key = '[' + this.keys[el.id] + ']';
757 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
760 task_action_on: function(action) {
761 return game.tasks.includes(this.action_tasks[action]);
763 switch_mode: function(mode_name) {
764 if (this.mode && this.mode.name == 'control_tile_draw') {
765 tui.log_msg('@ finished tile protection drawing.')
767 this.tile_draw = false;
768 const player = game.things[game.player_id];
769 if (mode_name == 'command_thing' && (!player.carrying || !player.carrying.commandable)) {
770 this.log_msg('? not carrying anything commandable');
771 terminal.blink_screen();
772 this.switch_mode('play');
775 if (mode_name == 'drop_thing' && (!player.carrying)) {
776 this.log_msg('? not carrying anything droppable');
777 terminal.blink_screen();
778 this.switch_mode('play');
781 if (mode_name == 'admin_enter' && this.is_admin) {
783 } else if (['name_thing', 'admin_thing_protect'].includes(mode_name)) {
785 for (let t_id in game.things) {
786 if (t_id == game.player_id) {
789 let t = game.things[t_id];
790 if (player.position[0] == t.position[0]
791 && player.position[1] == t.position[1]) {
797 terminal.blink_screen();
798 this.log_msg('? not standing over thing');
801 this.selected_thing_id = thing_id;
804 this.mode = this['mode_' + mode_name];
805 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
806 this.map_mode = 'protections';
807 } else if (this.mode.name != "edit") {
808 this.map_mode = 'terrain + things';
810 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
811 this.inputEl.focus();
813 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
814 explorer.position = game.things[game.player_id].position;
816 this.inputEl.value = "";
817 this.restore_input_values();
818 for (let el of document.getElementsByTagName("button")) {
821 document.getElementById("help").disabled = false;
822 for (const action of this.mode.available_actions) {
823 if (["move", "move_explorer"].includes(action)) {
824 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
825 move_key.disabled = false;
827 } else if (Object.keys(this.action_tasks).includes(action)) {
828 if (this.task_action_on(action)) {
829 document.getElementById(action).disabled = false;
832 document.getElementById(action).disabled = false;
835 for (const mode_name of this.mode.available_modes) {
836 document.getElementById('switch_to_' + mode_name).disabled = false;
838 if (this.mode.intro_msg.length > 0) {
839 this.log_msg(this.mode.intro_msg);
841 if (this.mode.name == 'login') {
842 if (this.login_name) {
843 server.send(['LOGIN', this.login_name]);
845 this.log_msg("? need login name");
847 } else if (this.mode.is_single_char_entry) {
848 this.show_help = true;
849 } else if (this.mode.name == 'take_thing') {
850 this.log_msg("Portable things in reach for pick-up:");
851 const player = game.things[game.player_id];
852 const y = player.position[0]
853 const x = player.position[1]
854 let select_range = [y.toString() + ':' + x.toString(),
855 (y + 0).toString() + ':' + (x - 1).toString(),
856 (y + 0).toString() + ':' + (x + 1).toString(),
857 (y - 1).toString() + ':' + (x).toString(),
858 (y + 1).toString() + ':' + (x).toString()];
859 if (game.map_geometry == 'Hex') {
861 select_range.push((y - 1).toString() + ':' + (x + 1).toString());
862 select_range.push((y + 1).toString() + ':' + (x + 1).toString());
864 select_range.push((y - 1).toString() + ':' + (x - 1).toString());
865 select_range.push((y + 1).toString() + ':' + (x - 1).toString());
868 this.selectables = [];
869 for (const t_id in game.things) {
870 const t = game.things[t_id];
871 if (select_range.includes(t.position[0].toString()
872 + ':' + t.position[1].toString())
874 this.selectables.push(t_id);
877 if (this.selectables.length == 0) {
878 this.log_msg('none');
879 terminal.blink_screen();
880 this.switch_mode('play');
883 for (let [i, t_id] of this.selectables.entries()) {
884 const t = game.things[t_id];
885 this.log_msg(i + ': ' + explorer.get_thing_info(t));
888 } else if (this.mode.name == 'drop_thing') {
889 this.log_msg('Direction to drop thing to:');
890 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
891 for (let [i, direction] of this.selectables.entries()) {
892 this.log_msg(i + ': ' + direction);
894 } else if (this.mode.name == 'command_thing') {
895 server.send(['TASK:COMMAND', 'HELP']);
896 } else if (this.mode.name == 'control_pw_pw') {
897 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
898 } else if (this.mode.name == 'control_tile_draw') {
899 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 + '].')
903 offset_links: function(offset, links) {
904 for (let y in links) {
905 let real_y = offset[0] + parseInt(y);
906 if (!this.links[real_y]) {
907 this.links[real_y] = [];
909 for (let link of links[y]) {
910 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
911 this.links[real_y].push(offset_link);
915 restore_input_values: function() {
916 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
917 let info = explorer.annotations[explorer.position];
918 if (info != "(none)") {
919 this.inputEl.value = info;
921 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
922 let portal = game.portals[explorer.position]
923 this.inputEl.value = portal;
924 } else if (this.mode.name == 'password') {
925 this.inputEl.value = this.password;
926 } else if (this.mode.name == 'name_thing') {
927 let t = game.get_thing(this.selected_thing_id);
929 this.inputEl.value = t.name_;
931 } else if (this.mode.name == 'admin_thing_protect') {
932 let t = game.get_thing(this.selected_thing_id);
933 if (t && t.protection) {
934 this.inputEl.value = t.protection;
938 recalc_input_lines: function() {
939 if (this.mode.has_input_prompt) {
941 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
943 this.input_lines = [];
945 this.height_input = this.input_lines.length;
947 msg_into_lines_of_width: function(msg, width) {
948 function push_inner_link(y, end_x) {
949 if (!inner_links[y]) {
952 inner_links[y].push([url_start_x, end_x, url]);
954 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
957 for (const match of matches) {
958 const url = match[0];
959 const url_start = match.index;
960 const url_end = match.index + match[0].length;
961 link_data[url_start] = url;
962 url_ends.push(url_end);
966 let inner_links = {};
970 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
971 if (x >= width || msg[i] == "\n") {
973 push_inner_link(y, chunk.length);
975 if (url_ends[0] == i) {
983 if (msg[i] == "\n") {
988 if (msg[i] != "\n") {
991 if (i in link_data) {
995 } else if (url_ends[0] == i) {
997 push_inner_link(y, x);
1003 push_inner_link(lines.length - 1, chunk.length);
1005 return [lines, inner_links];
1007 log_msg: function(msg) {
1009 while (this.log.length > 100) {
1012 this.full_refresh();
1014 pick_selectable: function(task_name) {
1015 const i = parseInt(this.inputEl.value);
1016 if (isNaN(i) || i < 0 || i >= this.selectables.length) {
1017 tui.log_msg('? invalid index, aborted');
1019 server.send(['TASK:' + task_name, tui.selectables[i]]);
1021 this.inputEl.value = "";
1022 this.switch_mode('play');
1024 draw_map: function() {
1025 if (!game.turn_complete && this.map_lines.length == 0) {
1028 if (game.turn_complete) {
1029 let map_lines_split = [];
1031 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1032 if (j == game.map_size[1]) {
1033 map_lines_split.push(line);
1037 if (this.map_mode == 'protections') {
1038 line.push(game.map_control[i] + ' ');
1040 line.push(game.map[i] + ' ');
1043 map_lines_split.push(line);
1044 if (this.map_mode == 'terrain + annotations') {
1045 for (const coordinate of explorer.info_hints) {
1046 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
1048 } else if (this.map_mode == 'terrain + things') {
1049 for (const p in game.portals) {
1050 let coordinate = p.split(',')
1051 let original = map_lines_split[coordinate[0]][coordinate[1]];
1052 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1054 let used_positions = [];
1055 function draw_thing(t, used_positions) {
1056 let symbol = game.thing_types[t.type_];
1057 let meta_char = ' ';
1059 meta_char = t.thing_char;
1061 if (used_positions.includes(t.position.toString())) {
1067 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1068 used_positions.push(t.position.toString());
1070 for (const thing_id in game.things) {
1071 let t = game.things[thing_id];
1072 if (t.type_ != 'Player') {
1073 draw_thing(t, used_positions);
1076 for (const thing_id in game.things) {
1077 let t = game.things[thing_id];
1078 if (t.type_ == 'Player') {
1079 draw_thing(t, used_positions);
1083 let player = game.things[game.player_id];
1084 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1085 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1086 } else if (tui.map_mode != 'terrain + things') {
1087 map_lines_split[player.position[0]][player.position[1]] = '??';
1090 if (game.map_geometry == 'Square') {
1091 for (let line_split of map_lines_split) {
1092 this.map_lines.push(line_split.join(''));
1094 } else if (game.map_geometry == 'Hex') {
1096 for (let line_split of map_lines_split) {
1097 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1105 let window_center = [terminal.rows / 2, this.window_width / 2];
1106 let center_position = [player.position[0], player.position[1]];
1107 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1108 center_position = [explorer.position[0], explorer.position[1]];
1110 center_position[1] = center_position[1] * 2;
1111 this.offset = [center_position[0] - window_center[0],
1112 center_position[1] - window_center[1]]
1113 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1114 this.offset[1] += 1;
1117 let term_y = Math.max(0, -this.offset[0]);
1118 let term_x = Math.max(0, -this.offset[1]);
1119 let map_y = Math.max(0, this.offset[0]);
1120 let map_x = Math.max(0, this.offset[1]);
1121 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1122 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1123 terminal.write(term_y, term_x, to_draw);
1126 draw_mode_line: function() {
1127 let help = 'hit [' + this.keys.help + '] for help';
1128 if (this.mode.has_input_prompt) {
1129 help = 'enter /help for help';
1131 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1133 draw_turn_line: function(n) {
1134 if (game.turn_complete) {
1135 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
1138 draw_history: function() {
1139 let log_display_lines = [];
1141 let y_offset_in_log = 0;
1142 for (let line of this.log) {
1143 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1145 log_display_lines = log_display_lines.concat(new_lines);
1146 for (const y in link_data) {
1147 const rel_y = y_offset_in_log + parseInt(y);
1148 log_links[rel_y] = [];
1149 for (let link of link_data[y]) {
1150 log_links[rel_y].push(link);
1153 y_offset_in_log += new_lines.length;
1155 let i = log_display_lines.length - 1;
1156 for (let y = terminal.rows - 1 - this.height_input;
1157 y >= this.height_header && i >= 0;
1159 terminal.write(y, this.window_width, log_display_lines[i]);
1161 for (const key of Object.keys(log_links)) {
1162 if (parseInt(key) <= i) {
1163 delete log_links[key];
1166 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1168 this.offset_links(offset, log_links);
1170 draw_info: function() {
1171 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1172 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1173 let offset = [this.height_header, this.window_width];
1174 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1175 terminal.write(y, offset[1], lines[i]);
1177 this.offset_links(offset, link_data);
1179 draw_input: function() {
1180 if (this.mode.has_input_prompt) {
1181 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1182 terminal.write(y, this.window_width, this.input_lines[i]);
1186 draw_help: function() {
1187 let movement_keys_desc = '';
1188 if (!this.mode.is_intro) {
1189 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1191 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1192 if (this.mode.available_actions.length > 0) {
1193 content += "Available actions:\n";
1194 for (let action of this.mode.available_actions) {
1195 if (Object.keys(this.action_tasks).includes(action)) {
1196 if (!this.task_action_on(action)) {
1200 if (action == 'move_explorer') {
1203 if (action == 'move') {
1204 content += "[" + movement_keys_desc + "] – move\n"
1206 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1211 content += this.mode.list_available_modes();
1213 if (!this.mode.has_input_prompt) {
1214 start_x = this.window_width
1216 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1217 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1218 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1219 terminal.write(y, start_x, lines[i]);
1222 toggle_tile_draw: function() {
1223 if (tui.tile_draw) {
1224 tui.tile_draw = false;
1226 tui.tile_draw = true;
1229 toggle_map_mode: function() {
1230 if (tui.map_mode == 'terrain only') {
1231 tui.map_mode = 'terrain + annotations';
1232 } else if (tui.map_mode == 'terrain + annotations') {
1233 tui.map_mode = 'terrain + things';
1234 } else if (tui.map_mode == 'terrain + things') {
1235 tui.map_mode = 'protections';
1236 } else if (tui.map_mode == 'protections') {
1237 tui.map_mode = 'terrain only';
1240 full_refresh: function() {
1242 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1243 this.recalc_input_lines();
1244 if (this.mode.is_intro) {
1245 this.draw_history();
1249 this.draw_turn_line();
1250 this.draw_mode_line();
1251 if (this.mode.shows_info) {
1254 this.draw_history();
1258 if (this.show_help) {
1270 this.map_control = "";
1271 this.map_size = [0,0];
1272 this.player_id = -1;
1276 get_thing: function(id_, create_if_not_found=false) {
1277 if (id_ in game.things) {
1278 return game.things[id_];
1279 } else if (create_if_not_found) {
1280 let t = new Thing([0,0]);
1281 game.things[id_] = t;
1285 move: function(start_position, direction) {
1286 let target = [start_position[0], start_position[1]];
1287 if (direction == 'LEFT') {
1289 } else if (direction == 'RIGHT') {
1291 } else if (game.map_geometry == 'Square') {
1292 if (direction == 'UP') {
1294 } else if (direction == 'DOWN') {
1297 } else if (game.map_geometry == 'Hex') {
1298 let start_indented = start_position[0] % 2;
1299 if (direction == 'UPLEFT') {
1301 if (!start_indented) {
1304 } else if (direction == 'UPRIGHT') {
1306 if (start_indented) {
1309 } else if (direction == 'DOWNLEFT') {
1311 if (!start_indented) {
1314 } else if (direction == 'DOWNRIGHT') {
1316 if (start_indented) {
1321 if (target[0] < 0 || target[1] < 0 ||
1322 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1327 teleport: function() {
1328 let player = this.get_thing(game.player_id);
1329 if (player.position in this.portals) {
1330 server.reconnect_to(this.portals[player.position]);
1332 terminal.blink_screen();
1333 tui.log_msg('? not standing on portal')
1341 server.init(websocket_location);
1347 move: function(direction) {
1348 let target = game.move(this.position, direction);
1350 this.position = target
1351 this.info_cached = false;
1352 if (tui.tile_draw) {
1353 this.send_tile_control_command();
1356 terminal.blink_screen();
1359 update_annotations: function(yx, str) {
1360 this.annotations[yx] = str;
1361 if (tui.mode.name == 'study') {
1365 empty_annotations: function() {
1366 this.annotations = {};
1367 if (tui.mode.name == 'study') {
1371 get_info: function() {
1372 if (this.info_cached) {
1373 return this.info_cached;
1375 let info_to_cache = '';
1376 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1377 if (game.fov[position_i] != '.') {
1378 info_to_cache += 'outside field of view';
1380 for (let t_id in game.things) {
1381 let t = game.things[t_id];
1382 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1383 info_to_cache += "THING: " + this.get_thing_info(t);
1384 let protection = t.protection;
1385 if (protection == '.') {
1386 protection = 'none';
1388 info_to_cache += " / protection: " + protection + "\n";
1390 info_to_cache += t.hat.slice(0, 6) + '\n';
1391 info_to_cache += t.hat.slice(6, 12) + '\n';
1392 info_to_cache += t.hat.slice(12, 18) + '\n';
1395 info_to_cache += t.face.slice(0, 6) + '\n';
1396 info_to_cache += t.face.slice(6, 12) + '\n';
1397 info_to_cache += t.face.slice(12, 18) + '\n';
1401 let terrain_char = game.map[position_i]
1402 let terrain_desc = '?'
1403 if (game.terrains[terrain_char]) {
1404 terrain_desc = game.terrains[terrain_char];
1406 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1407 let protection = game.map_control[position_i];
1408 if (protection == '.') {
1409 protection = 'unprotected';
1411 info_to_cache += 'PROTECTION: ' + protection + '\n';
1412 if (this.position in game.portals) {
1413 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1415 if (this.position in this.annotations) {
1416 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1419 this.info_cached = info_to_cache;
1420 return this.info_cached;
1422 get_thing_info: function(t) {
1423 const symbol = game.thing_types[t.type_];
1424 let info = t.type_ + " / " + symbol;
1426 info += t.thing_char;
1429 info += " (" + t.name_ + ")";
1432 info += " / installed";
1436 annotate: function(msg) {
1437 if (msg.length == 0) {
1438 msg = " "; // triggers annotation deletion
1440 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1442 set_portal: function(msg) {
1443 if (msg.length == 0) {
1444 msg = " "; // triggers portal deletion
1446 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1448 send_tile_control_command: function() {
1449 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1453 tui.inputEl.addEventListener('input', (event) => {
1454 if (tui.mode.has_input_prompt) {
1455 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1456 if (tui.inputEl.value.length > max_length) {
1457 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1459 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1460 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1461 tui.switch_mode('edit');
1465 document.onclick = function() {
1466 tui.show_help = false;
1468 tui.inputEl.addEventListener('keydown', (event) => {
1469 tui.show_help = false;
1470 if (event.key == 'Enter') {
1471 event.preventDefault();
1473 if ((!tui.mode.is_intro && event.key == 'Escape')
1474 || (tui.mode.has_input_prompt && event.key == 'Enter'
1475 && tui.inputEl.value.length == 0
1476 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1477 'admin_enter'].includes(tui.mode.name))) {
1478 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1479 tui.log_msg('@ aborted');
1481 tui.switch_mode('play');
1482 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1483 tui.show_help = true;
1484 tui.inputEl.value = "";
1485 tui.restore_input_values();
1486 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1487 && !tui.mode.is_single_char_entry) {
1488 tui.show_help = true;
1489 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1490 tui.login_name = tui.inputEl.value;
1491 server.send(['LOGIN', tui.inputEl.value]);
1492 tui.inputEl.value = "";
1493 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1494 if (tui.inputEl.value.length != 18) {
1495 tui.log_msg('? wrong input length, aborting');
1497 server.send(['PLAYER_FACE', tui.inputEl.value]);
1499 tui.inputEl.value = "";
1500 tui.switch_mode('edit');
1501 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1502 server.send(['TASK:COMMAND', tui.inputEl.value]);
1503 tui.inputEl.value = "";
1504 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1505 tui.pick_selectable('PICK_UP');
1506 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1507 tui.pick_selectable('DROP');
1508 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1509 if (tui.inputEl.value.length == 0) {
1510 tui.log_msg('@ aborted');
1512 server.send(['SET_MAP_CONTROL_PASSWORD',
1513 tui.tile_control_char, tui.inputEl.value]);
1514 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1516 tui.switch_mode('admin');
1517 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1518 explorer.set_portal(tui.inputEl.value);
1519 tui.switch_mode('edit');
1520 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1521 if (tui.inputEl.value.length == 0) {
1522 tui.inputEl.value = " ";
1524 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value,
1526 tui.switch_mode('edit');
1527 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1528 explorer.annotate(tui.inputEl.value);
1529 tui.switch_mode('edit');
1530 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1531 if (tui.inputEl.value.length == 0) {
1532 tui.inputEl.value = " ";
1534 tui.password = tui.inputEl.value
1535 tui.switch_mode('edit');
1536 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1537 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1538 tui.switch_mode('play');
1539 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1540 if (tui.inputEl.value.length != 1) {
1541 tui.log_msg('@ entered non-single-char, therefore aborted');
1542 tui.switch_mode('admin');
1544 tui.tile_control_char = tui.inputEl.value[0];
1545 tui.switch_mode('control_pw_pw');
1547 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1548 if (tui.inputEl.value.length != 1) {
1549 tui.log_msg('@ entered non-single-char, therefore aborted');
1550 tui.switch_mode('admin');
1552 tui.tile_control_char = tui.inputEl.value[0];
1553 tui.switch_mode('control_tile_draw');
1555 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1556 if (tui.inputEl.value.length != 1) {
1557 tui.log_msg('@ entered non-single-char, therefore aborted');
1559 server.send(['THING_PROTECTION', tui.selected_thing_id, tui.inputEl.value])
1560 tui.log_msg('@ sent new protection character for thing');
1562 tui.switch_mode('admin');
1563 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1564 let tokens = parser.tokenize(tui.inputEl.value);
1565 if (tokens.length > 0 && tokens[0].length > 0) {
1566 if (tui.inputEl.value[0][0] == '/') {
1567 if (tokens[0].slice(1) == 'nick') {
1568 if (tokens.length > 1) {
1569 server.send(['NICK', tokens[1]]);
1571 tui.log_msg('? need new name');
1574 tui.log_msg('? unknown command');
1577 server.send(['ALL', tui.inputEl.value]);
1579 } else if (tui.inputEl.valuelength > 0) {
1580 server.send(['ALL', tui.inputEl.value]);
1582 tui.inputEl.value = "";
1583 } else if (tui.mode.name == 'play') {
1584 if (tui.mode.mode_switch_on_key(event)) {
1586 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1587 server.send(["TASK:INTOXICATE"]);
1588 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1589 server.send(["TASK:DOOR"]);
1590 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1591 server.send(["TASK:INSTALL"]);
1592 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1593 server.send(["TASK:WEAR"]);
1594 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1595 server.send(["TASK:SPIN"]);
1596 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1597 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1598 } else if (event.key === tui.keys.teleport) {
1601 } else if (tui.mode.name == 'study') {
1602 if (tui.mode.mode_switch_on_key(event)) {
1604 } else if (event.key in tui.movement_keys) {
1605 explorer.move(tui.movement_keys[event.key]);
1606 } else if (event.key == tui.keys.toggle_map_mode) {
1607 tui.toggle_map_mode();
1609 } else if (tui.mode.name == 'control_tile_draw') {
1610 if (tui.mode.mode_switch_on_key(event)) {
1612 } else if (event.key in tui.movement_keys) {
1613 explorer.move(tui.movement_keys[event.key]);
1614 } else if (event.key === tui.keys.toggle_tile_draw) {
1615 tui.toggle_tile_draw();
1617 } else if (tui.mode.name == 'admin') {
1618 if (tui.mode.mode_switch_on_key(event)) {
1620 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1621 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1623 } else if (tui.mode.name == 'edit') {
1624 if (tui.mode.mode_switch_on_key(event)) {
1626 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1627 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1628 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1629 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1630 } else if (event.key == tui.keys.toggle_map_mode) {
1631 tui.toggle_map_mode();
1637 rows_selector.addEventListener('input', function() {
1638 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1641 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1642 terminal.initialize();
1645 cols_selector.addEventListener('input', function() {
1646 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1649 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1650 terminal.initialize();
1651 tui.window_width = terminal.cols / 2,
1654 for (let key_selector of key_selectors) {
1655 key_selector.addEventListener('input', function() {
1656 window.localStorage.setItem(key_selector.id, key_selector.value);
1660 window.setInterval(function() {
1661 if (server.websocket.readyState == 1) {
1662 server.send(['PING']);
1664 server.reconnect_to(server.url);
1665 tui.log_msg('@ attempting reconnect …')
1668 window.setInterval(function() {
1670 let span_decoration = "none";
1671 if (document.activeElement == tui.inputEl) {
1672 val = "on (click outside terminal to change)";
1674 val = "off (click into terminal to change)";
1675 span_decoration = "line-through";
1677 document.getElementById("keyboard_control").textContent = val;
1678 for (const span of document.querySelectorAll('.keyboard_controlled')) {
1679 span.style.textDecoration = span_decoration;
1682 document.getElementById("terminal").onclick = function() {
1683 tui.inputEl.focus();
1685 document.getElementById("help").onclick = function() {
1686 tui.show_help = true;
1689 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1690 const mode = switchEl.id.slice("switch_to_".length);
1691 switchEl.onclick = function() {
1692 tui.switch_mode(mode);
1696 document.getElementById("toggle_tile_draw").onclick = function() {
1697 tui.toggle_tile_draw();
1699 document.getElementById("toggle_map_mode").onclick = function() {
1700 tui.toggle_map_mode();
1703 document.getElementById("flatten").onclick = function() {
1704 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1706 document.getElementById("door").onclick = function() {
1707 server.send(['TASK:DOOR']);
1709 document.getElementById("consume").onclick = function() {
1710 server.send(['TASK:INTOXICATE']);
1712 document.getElementById("install").onclick = function() {
1713 server.send(['TASK:INSTALL']);
1715 document.getElementById("wear").onclick = function() {
1716 server.send(['TASK:WEAR']);
1718 document.getElementById("spin").onclick = function() {
1719 server.send(['TASK:SPIN']);
1721 document.getElementById("teleport").onclick = function() {
1724 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1725 if (move_button.id.startsWith('key_')) { // not a move button
1728 let direction = move_button.id.split('_')[2].toUpperCase();
1729 move_button.onclick = function() {
1730 if (tui.mode.available_actions.includes("move")) {
1731 server.send(['TASK:MOVE', direction]);
1732 } else if (tui.mode.available_actions.includes("move_explorer")) {
1733 explorer.move(direction);