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 <div style="position: relative; display: inline-block;">
18 <pre id="terminal"></pre>
19 <textarea id="input" style="position: absolute; left: 0; height: 100%; width: 100%; opacity: 0; z-index: -1;"></textarea>
21 <h3>button controls for hard-to-remember keybindings</h3>
22 <table id="move_table" style="float: left">
24 <td style="text-align: right"><button id="hex_move_upleft"></button></td>
25 <td style="text-align: center"><button id="square_move_up"></button></td>
26 <td><button id="hex_move_upright"></button></td>
29 <td style="text-align: right;"><button id="square_move_left"></button><button id="hex_move_left">left</button></td>
30 <td stlye="text-align: center;">move</td>
31 <td><button id="square_move_right"></button><button id="hex_move_right"></button></td>
34 <td><button id="hex_move_downleft"></button></td>
35 <td style="text-align: center"><button id="square_move_down"></button></td>
36 <td><button id="hex_move_downright"></button></td>
41 <td><button id="help"></button></td>
44 <td><button id="switch_to_chat"></button><br /></td>
47 <td><button id="switch_to_study"></button></td>
48 <td><button id="toggle_map_mode"></button>
51 <td><button id="switch_to_play"></button></td>
53 <button id="switch_to_take_thing"></button>
54 <button id="switch_to_drop_thing"></button>
55 <button id="door"></button>
56 <button id="consume"></button>
57 <button id="dance"></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>
75 <button id="switch_to_enter_hat"></button>
76 <button id="switch_to_enter_design"></button>
80 <td><button id="switch_to_admin_enter"></button></td>
82 <button id="switch_to_control_pw_type"></button>
83 <button id="switch_to_control_tile_type"></button>
84 <button id="switch_to_admin_thing_protect"></button>
85 <button id="toggle_tile_draw"></button>
90 <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 />
92 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
93 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
94 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
95 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
96 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
97 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
98 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
99 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
100 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
101 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
102 <li>help: <input id="key_help" type="text" value="h" />
103 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
104 <li>teleport: <input id="key_teleport" type="text" value="p" />
105 <li>spin: <input id="key_spin" type="text" value="S" />
106 <li>dance: <input id="key_dance" type="text" value="T" />
107 <li>open/close: <input id="key_door" type="text" value="D" />
108 <li>consume: <input id="key_consume" type="text" value="C" />
109 <li>install: <input id="key_install" type="text" value="I" />
110 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
111 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
112 <li><input id="key_switch_to_enter_face" type="text" value="f" />
113 <li><input id="key_switch_to_enter_hat" type="text" value="H" />
114 <li><input id="key_switch_to_enter_design" type="text" value="D" />
115 <li><input id="key_switch_to_take_thing" type="text" value="z" />
116 <li><input id="key_switch_to_chat" type="text" value="t" />
117 <li><input id="key_switch_to_play" type="text" value="p" />
118 <li><input id="key_switch_to_study" type="text" value="?" />
119 <li><input id="key_switch_to_edit" type="text" value="E" />
120 <li><input id="key_switch_to_write" type="text" value="m" />
121 <li><input id="key_switch_to_name_thing" type="text" value="N" />
122 <li><input id="key_switch_to_command_thing" type="text" value="O" />
123 <li><input id="key_switch_to_password" type="text" value="P" />
124 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
125 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
126 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
127 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
128 <li><input id="key_switch_to_annotate" type="text" value="M" />
129 <li><input id="key_switch_to_portal" type="text" value="T" />
130 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
131 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
136 let websocket_location = "wss://plomlompom.com/rogue_chat/";
137 //let websocket_location = "ws://localhost:8000/";
143 'long': 'This mode allows you to interact with the map in various ways.'
148 '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.'},
150 'short': 'world edit',
152 '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.'
155 'short': 'name thing',
157 'long': 'Give name to/change name of carried thing.'
162 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
166 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
167 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
171 'intro': 'Enter number of direction to which you want to drop thing.',
172 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
174 'admin_thing_protect': {
175 'short': 'change thing protection',
176 'intro': '@ enter thing protection character:',
177 'long': 'Change protection character for carried thing.'
180 'short': 'edit face',
181 'intro': '@ enter face line:',
182 '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. Eat cookies to extend the ASCII characters available for drawing.'
185 'short': 'edit design',
186 'intro': '@ enter design:',
187 'long': 'Enter design for carried thing as ASCII art.'
191 'intro': '@ enter hat line:',
192 'long': 'Draw your hat 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. Eat cookies to extend the ASCII characters available for drawing.'
195 'short': 'edit tile',
197 '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.'
200 'short': 'change protection character password',
201 'intro': '@ enter protection character for which you want to change the password:',
202 '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.'
205 'short': 'change protection character password',
207 '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.'
209 'control_tile_type': {
210 'short': 'change tiles protection',
211 'intro': '@ enter protection character which you want to draw:',
212 '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.'
214 'control_tile_draw': {
215 'short': 'change tiles protection',
217 '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.'
220 'short': 'annotate tile',
222 '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.'
225 'short': 'edit portal',
227 '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.'
232 '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'
237 'long': 'Enter your player name.'
239 'waiting_for_server': {
240 'short': 'waiting for server response',
241 'intro': '@ waiting for server …',
242 'long': 'Waiting for a server response.'
245 'short': 'waiting for server response',
247 'long': 'Waiting for a server response.'
250 'short': 'set world edit password',
252 '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.'
255 'short': 'become admin',
256 'intro': '@ enter admin password:',
257 'long': 'This mode allows you to become admin if you know an admin password.'
262 'long': 'This mode allows you access to actions limited to administrators.'
265 let key_descriptions = {
267 'flatten': 'flatten surroundings',
268 'teleport': 'teleport',
269 'door': 'open/close',
270 'consume': 'consume',
271 'install': '(un-)install',
275 'toggle_map_mode': 'toggle map view',
276 'toggle_tile_draw': 'toggle protection character drawing',
277 'hex_move_upleft': 'up-left',
278 'hex_move_upright': 'up-right',
279 'hex_move_right': 'right',
280 'hex_move_left': 'left',
281 'hex_move_downleft': 'down-left',
282 'hex_move_downright': 'down-right',
283 'square_move_up': 'up',
284 'square_move_left': 'left',
285 'square_move_down': 'down',
286 'square_move_right': 'right',
288 for (const mode_name of Object.keys(mode_helps)) {
289 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
292 let rows_selector = document.getElementById("n_rows");
293 let cols_selector = document.getElementById("n_cols");
294 let key_selectors = document.querySelectorAll('[id^="key_"]');
296 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
297 const action = key_switch_selector.id.slice("key_switch_to_".length);
298 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
301 function restore_selector_value(selector) {
302 let stored_selection = window.localStorage.getItem(selector.id);
303 if (stored_selection) {
304 selector.value = stored_selection;
307 restore_selector_value(rows_selector);
308 restore_selector_value(cols_selector);
309 for (let key_selector of key_selectors) {
310 restore_selector_value(key_selector);
313 function escapeHTML(str) {
315 replace(/&/g, '&').
316 replace(/</g, '<').
317 replace(/>/g, '>').
318 replace(/'/g, ''').
319 replace(/"/g, '"');
323 initialize: function() {
324 this.rows = rows_selector.value;
325 this.cols = cols_selector.value;
326 this.pre_el = document.getElementById("terminal");
327 this.set_default_colors();
331 for (let y = 0, x = 0; y <= this.rows; x++) {
332 if (x == this.cols) {
335 this.content.push(line);
337 if (y == this.rows) {
344 apply_colors: function() {
345 this.pre_el.style.color = this.foreground;
346 this.pre_el.style.backgroundColor = this.background;
348 set_default_colors: function() {
349 this.foreground = 'white';
350 this.background = 'black';
353 set_random_colors: function() {
354 function rand(offset) {
355 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
357 this.foreground = '#' + rand(159) + rand(159) + rand(159);
358 this.background = '#' + rand(0) + rand(0) + rand(0);
361 blink_screen: function() {
362 this.pre_el.style.color = this.background;
363 this.pre_el.style.backgroundColor = this.foreground;
365 this.pre_el.style.color = this.foreground;
366 this.pre_el.style.backgroundColor = this.background;
369 refresh: function() {
370 let pre_content = '';
371 for (let y = 0; y < this.rows; y++) {
372 let line = this.content[y].join('');
374 if (y in tui.links) {
376 for (let span of tui.links[y]) {
377 chunks.push(escapeHTML(line.slice(start_x, span[0])));
378 chunks.push('<a target="_blank" href="');
379 chunks.push(escapeHTML(span[2]));
381 chunks.push(escapeHTML(line.slice(span[0], span[1])));
385 chunks.push(escapeHTML(line.slice(start_x)));
387 chunks = [escapeHTML(line)];
389 for (const chunk of chunks) {
390 pre_content += chunk;
394 this.pre_el.innerHTML = pre_content;
396 write: function(start_y, start_x, msg) {
397 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
398 this.content[start_y][x] = msg[i];
401 drawBox: function(start_y, start_x, height, width) {
402 let end_y = start_y + height;
403 let end_x = start_x + width;
404 for (let y = start_y, x = start_x; y < this.rows; x++) {
412 this.content[y][x] = ' ';
416 terminal.initialize();
419 tokenize: function(str) {
424 for (let i = 0; i < str.length; i++) {
430 } else if (c == '\\') {
432 } else if (c == '"') {
437 } else if (c == '"') {
439 } else if (c === ' ') {
440 if (token.length > 0) {
448 if (token.length > 0) {
453 parse_yx: function(position_string) {
454 let coordinate_strings = position_string.split(',')
455 let position = [0, 0];
456 position[0] = parseInt(coordinate_strings[0].slice(2));
457 position[1] = parseInt(coordinate_strings[1].slice(2));
469 init: function(url) {
471 this.websocket = new WebSocket(this.url);
472 this.websocket.onopen = function(event) {
473 game.thing_types = {};
475 server.send(['TASKS']);
476 server.send(['TERRAINS']);
477 server.send(['THING_TYPES']);
478 tui.log_msg("@ server connected! :)");
479 tui.switch_mode('login');
481 this.websocket.onclose = function(event) {
482 tui.switch_mode('waiting_for_server');
483 tui.log_msg("@ server disconnected :(");
485 this.websocket.onmessage = this.handle_event;
487 reconnect_to: function(url) {
488 this.websocket.close();
491 send: function(tokens) {
492 this.websocket.send(unparser.untokenize(tokens));
494 handle_event: function(event) {
495 let tokens = parser.tokenize(event.data);
496 if (tokens[0] === 'TURN') {
497 game.turn_complete = false;
498 } else if (tokens[0] === 'OTHER_WIPE') {
499 game.portals_new = {};
500 explorer.annotations_new = {};
501 game.things_new = [];
502 } else if (tokens[0] === 'STATS') {
503 game.bladder_pressure_new = parseInt(tokens[1])
504 game.energy_new = parseInt(tokens[2])
505 } else if (tokens[0] === 'THING') {
506 let t = game.get_thing_temp(tokens[4], true);
507 t.position = parser.parse_yx(tokens[1]);
509 t.protection = tokens[3];
510 t.portable = parseInt(tokens[5]);
511 t.commandable = parseInt(tokens[6]);
512 } else if (tokens[0] === 'THING_NAME') {
513 let t = game.get_thing_temp(tokens[1]);
515 } else if (tokens[0] === 'THING_FACE') {
516 let t = game.get_thing_temp(tokens[1]);
518 } else if (tokens[0] === 'THING_HAT') {
519 let t = game.get_thing_temp(tokens[1]);
521 } else if (tokens[0] === 'THING_DESIGN') {
522 let t = game.get_thing_temp(tokens[1]);
523 t.design = [parser.parse_yx(tokens[2]), tokens[3]];
524 } else if (tokens[0] === 'THING_CHAR') {
525 let t = game.get_thing_temp(tokens[1]);
526 t.thing_char = tokens[2];
527 } else if (tokens[0] === 'TASKS') {
528 game.tasks = tokens[1].split(',');
529 tui.mode_write.legal = game.tasks.includes('WRITE');
530 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
531 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
532 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
533 } else if (tokens[0] === 'THING_TYPE') {
534 game.thing_types[tokens[1]] = tokens[2]
535 } else if (tokens[0] === 'THING_CARRYING') {
536 let t = game.get_thing_temp(tokens[1]);
537 t.carrying = game.get_thing_temp(tokens[2], false);
538 } else if (tokens[0] === 'THING_INSTALLED') {
539 let t = game.get_thing_temp(tokens[1]);
541 } else if (tokens[0] === 'TERRAIN') {
542 game.terrains[tokens[1]] = tokens[2]
543 } else if (tokens[0] === 'MAP') {
544 game.map_geometry_new = tokens[1];
545 game.map_size_new = parser.parse_yx(tokens[2]);
546 game.map_new = tokens[3]
547 } else if (tokens[0] === 'FOV') {
548 game.fov_new = tokens[1]
549 } else if (tokens[0] === 'MAP_CONTROL') {
550 game.map_control_new = tokens[1]
551 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
552 game.portals = game.portals_new;
553 game.map_geometry = game.map_geometry_new;
554 game.map_size = game.map_size_new;
555 game.map = game.map_new;
556 game.fov = game.fov_new;
558 game.map_control = game.map_control_new;
559 explorer.annotations = explorer.annotations_new;
560 explorer.info_cached = false;
561 game.things = game.things_new;
562 game.player = game.things[game.player_id];
563 game.players_hat_chars = game.players_hat_chars_new;
564 game.bladder_pressure = game.bladder_pressure_new
565 game.energy = game.energy_new
566 game.turn_complete = true;
567 if (tui.mode.name == 'post_login_wait') {
568 tui.switch_mode('play');
572 } else if (tokens[0] === 'CHAT') {
573 tui.log_msg('# ' + tokens[1], 1);
574 } else if (tokens[0] === 'CHATFACE') {
575 tui.draw_face = tokens[1];
577 } else if (tokens[0] === 'REPLY') {
578 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
579 } else if (tokens[0] === 'PLAYER_ID') {
580 game.player_id = parseInt(tokens[1]);
581 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
582 game.players_hat_chars_new = tokens[1];
583 } else if (tokens[0] === 'LOGIN_OK') {
584 this.send(['GET_GAMESTATE']);
585 tui.switch_mode('post_login_wait');
586 tui.log_msg('@ welcome!')
587 tui.log_msg('@ hint: see top of terminal for how to get help.')
588 tui.log_msg('@ hint: enter study mode to understand your environment.')
589 } else if (tokens[0] === 'DEFAULT_COLORS') {
590 terminal.set_default_colors();
591 } else if (tokens[0] === 'RANDOM_COLORS') {
592 terminal.set_random_colors();
593 } else if (tokens[0] === 'ADMIN_OK') {
595 tui.log_msg('@ you now have admin rights');
596 tui.switch_mode('admin');
597 } else if (tokens[0] === 'PORTAL') {
598 let position = parser.parse_yx(tokens[1]);
599 game.portals_new[position] = tokens[2];
600 } else if (tokens[0] === 'ANNOTATION') {
601 let position = parser.parse_yx(tokens[1]);
602 explorer.annotations_new[position] = tokens[2];
603 } else if (tokens[0] === 'UNHANDLED_INPUT') {
604 tui.log_msg('? unknown command');
605 } else if (tokens[0] === 'PLAY_ERROR') {
606 tui.log_msg('? ' + tokens[1]);
607 terminal.blink_screen();
608 } else if (tokens[0] === 'ARGUMENT_ERROR') {
609 tui.log_msg('? syntax error: ' + tokens[1]);
610 } else if (tokens[0] === 'GAME_ERROR') {
611 tui.log_msg('? game error: ' + tokens[1]);
612 } else if (tokens[0] === 'PONG') {
615 tui.log_msg('? unhandled input: ' + event.data);
621 quote: function(str) {
623 for (let i = 0; i < str.length; i++) {
625 if (['"', '\\'].includes(c)) {
631 return quoted.join('');
633 to_yx: function(yx_coordinate) {
634 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
636 untokenize: function(tokens) {
637 let quoted_tokens = [];
638 for (let token of tokens) {
639 quoted_tokens.push(this.quote(token));
641 return quoted_tokens.join(" ");
646 constructor(name, has_input_prompt=false, shows_info=false,
647 is_intro=false, is_single_char_entry=false) {
649 this.short_desc = mode_helps[name].short;
650 this.available_modes = [];
651 this.available_actions = [];
652 this.has_input_prompt = has_input_prompt;
653 this.shows_info= shows_info;
654 this.is_intro = is_intro;
655 this.help_intro = mode_helps[name].long;
656 this.intro_msg = mode_helps[name].intro;
657 this.is_single_char_entry = is_single_char_entry;
660 *iter_available_modes() {
661 for (let mode_name of this.available_modes) {
662 let mode = tui['mode_' + mode_name];
666 let key = tui.keys['switch_to_' + mode.name];
670 list_available_modes() {
672 if (this.available_modes.length > 0) {
673 msg += 'Other modes available from here:\n';
674 for (let [mode, key] of this.iter_available_modes()) {
675 msg += '[' + key + '] – ' + mode.short_desc + '\n';
680 mode_switch_on_key(key_event) {
681 for (let [mode, key] of this.iter_available_modes()) {
682 if (key_event.key == key) {
683 event.preventDefault();
684 tui.switch_mode(mode.name);
696 window_width: terminal.cols / 2,
704 mode_waiting_for_server: new Mode('waiting_for_server',
706 mode_login: new Mode('login', true, false, true),
707 mode_post_login_wait: new Mode('post_login_wait'),
708 mode_chat: new Mode('chat', true),
709 mode_annotate: new Mode('annotate', true, true),
710 mode_play: new Mode('play'),
711 mode_study: new Mode('study', false, true),
712 mode_write: new Mode('write', false, false, false, true),
713 mode_edit: new Mode('edit'),
714 mode_control_pw_type: new Mode('control_pw_type', true),
715 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
716 mode_portal: new Mode('portal', true, true),
717 mode_password: new Mode('password', true),
718 mode_name_thing: new Mode('name_thing', true, true),
719 mode_command_thing: new Mode('command_thing', true),
720 mode_take_thing: new Mode('take_thing', true),
721 mode_drop_thing: new Mode('drop_thing', true),
722 mode_enter_face: new Mode('enter_face', true),
723 mode_enter_hat: new Mode('enter_hat', true),
724 mode_enter_design: new Mode('enter_design', true),
725 mode_admin_enter: new Mode('admin_enter', true),
726 mode_admin: new Mode('admin'),
727 mode_control_pw_pw: new Mode('control_pw_pw', true),
728 mode_control_tile_type: new Mode('control_tile_type', true),
729 mode_control_tile_draw: new Mode('control_tile_draw'),
731 'flatten': 'FLATTEN_SURROUNDINGS',
732 'take_thing': 'PICK_UP',
733 'drop_thing': 'DROP',
736 'install': 'INSTALL',
738 'command': 'COMMAND',
739 'consume': 'INTOXICATE',
750 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
751 "command_thing", "take_thing", "drop_thing"]
752 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
753 "wear", "spin", "dance"];
754 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
755 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
756 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
757 "control_tile_type", "chat",
758 "study", "play", "edit"]
759 this.mode_admin.available_actions = ["move", "toggle_map_mode"];
760 this.mode_control_tile_draw.available_modes = ["admin_enter"]
761 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
762 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
763 "enter_design", "password", "chat", "study",
764 "play", "admin_enter", "enter_face",
766 this.mode_edit.available_actions = ["move", "flatten", "install",
768 this.inputEl = document.getElementById("input");
769 this.switch_mode('waiting_for_server');
770 this.recalc_input_lines();
771 this.height_header = this.height_turn_line + this.height_mode_line;
774 init_keys: function() {
775 document.getElementById("move_table").hidden = true;
777 for (let key_selector of key_selectors) {
778 this.keys[key_selector.id.slice(4)] = key_selector.value;
780 this.movement_keys = {};
781 let geometry_prefix = 'undefinedMapGeometry_';
782 if (game.map_geometry) {
783 geometry_prefix = game.map_geometry.toLowerCase() + '_';
785 for (const key_name of Object.keys(key_descriptions)) {
786 if (key_name.startsWith(geometry_prefix)) {
787 let direction = key_name.split('_')[2].toUpperCase();
788 let key = this.keys[key_name];
789 this.movement_keys[key] = direction;
792 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
793 if (move_button.id.startsWith('key_')) {
796 move_button.hidden = true;
798 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
799 document.getElementById("move_table").hidden = false;
800 move_button.hidden = false;
802 for (let el of document.getElementsByTagName("button")) {
803 let action_desc = key_descriptions[el.id];
804 let action_key = '[' + this.keys[el.id] + ']';
805 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
808 task_action_on: function(action) {
809 return game.tasks.includes(this.action_tasks[action]);
811 switch_mode: function(mode_name) {
813 function fail(msg, return_mode='play') {
814 tui.log_msg('? ' + msg);
815 terminal.blink_screen();
816 tui.switch_mode(return_mode);
819 if (this.mode && this.mode.name == 'control_tile_draw') {
820 tui.log_msg('@ finished tile protection drawing.')
822 this.draw_face = false;
823 this.tile_draw = false;
824 if (mode_name == 'command_thing' && (!game.player.carrying
825 || !game.player.carrying.commandable)) {
826 return fail('not carrying anything commandable');
827 } else if (mode_name == 'name_thing' && !game.player.carrying) {
828 return fail('not carrying anything to re-name', 'edit');
829 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
830 return fail('not carrying anything to protect')
831 } else if (mode_name == 'take_thing' && game.player.carrying) {
832 return fail('already carrying something');
833 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
834 return fail('not carrying anything droppable');
835 } else if (mode_name == 'enter_hat' && !game.player.hat) {
836 return fail('not wearing hat to edit', 'edit');
837 } else if (mode_name == 'enter_design' && (!game.player.carrying
838 || !game.player.carrying.design)) {
839 return fail('not carrying designable to edit', 'edit');
841 if (mode_name == 'admin_enter' && this.is_admin) {
844 this.mode = this['mode_' + mode_name];
845 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
846 this.map_mode = 'protections';
847 } else if (this.mode.name != "edit") {
848 this.map_mode = 'terrain + things';
850 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
851 explorer.position = game.player.position;
853 this.inputEl.value = "";
854 this.restore_input_values();
855 for (let el of document.getElementsByTagName("button")) {
858 document.getElementById("help").disabled = false;
859 for (const action of this.mode.available_actions) {
860 if (["move", "move_explorer"].includes(action)) {
861 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
862 move_key.disabled = false;
864 } else if (Object.keys(this.action_tasks).includes(action)) {
865 if (this.task_action_on(action)) {
866 document.getElementById(action).disabled = false;
869 document.getElementById(action).disabled = false;
872 for (const mode_name of this.mode.available_modes) {
873 document.getElementById('switch_to_' + mode_name).disabled = false;
875 if (this.mode.intro_msg.length > 0) {
876 this.log_msg(this.mode.intro_msg);
878 if (this.mode.name == 'login') {
879 if (this.login_name) {
880 server.send(['LOGIN', this.login_name]);
882 this.log_msg("? need login name");
884 } else if (this.mode.is_single_char_entry) {
885 this.show_help = true;
886 } else if (this.mode.name == 'take_thing') {
887 this.log_msg("Portable things in reach for pick-up:");
888 const y = game.player.position[0]
889 const x = game.player.position[1]
890 let directed_moves = {
891 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
893 if (game.map_geometry == 'Square') {
894 directed_moves['UP'] = [-1, 0];
895 directed_moves['DOWN'] = [1, 0];
896 } else if (game.map_geometry == 'Hex') {
898 directed_moves['UPLEFT'] = [-1, 0];
899 directed_moves['UPRIGHT'] = [-1, 1];
900 directed_moves['DOWNLEFT'] = [1, 0];
901 directed_moves['DOWNRIGHT'] = [1, 1];
903 directed_moves['UPLEFT'] = [-1, -1];
904 directed_moves['UPRIGHT'] = [-1, 0];
905 directed_moves['DOWNLEFT'] = [1, -1];
906 directed_moves['DOWNRIGHT'] = [1, 0];
909 let select_range = {};
910 for (const direction in directed_moves) {
911 const move = directed_moves[direction];
912 select_range[direction] = [y + move[0], x + move[1]];
914 this.selectables = [];
916 for (const direction in select_range) {
917 for (const t_id in game.things) {
918 const t = game.things[t_id];
919 const position = select_range[direction];
921 && t.position[0] == position[0]
922 && t.position[1] == position[1]) {
923 this.selectables.push(t_id);
924 directions.push(direction);
928 if (this.selectables.length == 0) {
929 this.log_msg('none');
930 terminal.blink_screen();
931 this.switch_mode('play');
934 for (let [i, t_id] of this.selectables.entries()) {
935 const t = game.things[t_id];
936 const direction = directions[i];
937 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
940 } else if (this.mode.name == 'drop_thing') {
941 this.log_msg('Direction to drop thing to:');
942 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
943 for (let [i, direction] of this.selectables.entries()) {
944 this.log_msg(i + ': ' + direction);
946 } else if (this.mode.name == 'enter_hat') {
947 this.log_msg('legal characters: ' + game.players_hat_chars);
948 } else if (this.mode.name == 'command_thing') {
949 server.send(['TASK:COMMAND', 'HELP']);
950 } else if (this.mode.name == 'control_pw_pw') {
951 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
952 } else if (this.mode.name == 'control_tile_draw') {
953 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 + '].')
957 offset_links: function(offset, links) {
958 for (let y in links) {
959 let real_y = offset[0] + parseInt(y);
960 if (!this.links[real_y]) {
961 this.links[real_y] = [];
963 for (let link of links[y]) {
964 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
965 this.links[real_y].push(offset_link);
969 restore_input_values: function() {
970 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
971 let info = explorer.annotations[explorer.position];
972 if (info != "(none)") {
973 this.inputEl.value = info;
975 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
976 let portal = game.portals[explorer.position]
977 this.inputEl.value = portal;
978 } else if (this.mode.name == 'password') {
979 this.inputEl.value = this.password;
980 } else if (this.mode.name == 'name_thing') {
981 if (game.player.carrying && game.player.carrying.name_) {
982 this.inputEl.value = game.player.carrying.name_;
984 } else if (this.mode.name == 'admin_thing_protect') {
985 if (game.player.carrying && game.player.carrying.protection) {
986 this.inputEl.value = game.player.carrying.protection;
988 } else if (['enter_face', 'enter_hat'].includes(this.mode.name)) {
989 const start = this.ascii_draw_stage * 6;
990 const end = (this.ascii_draw_stage + 1) * 6;
991 if (this.mode.name == 'enter_face') {
992 this.inputEl.value = game.player.face.slice(start, end);
993 } else if (this.mode.name == 'enter_hat') {
994 this.inputEl.value = game.player.hat.slice(start, end);
996 } else if (this.mode.name == 'enter_design') {
997 const width = game.player.carrying.design[0][1];
998 const start = this.ascii_draw_stage * width;
999 const end = (this.ascii_draw_stage + 1) * width;
1000 this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1003 recalc_input_lines: function() {
1004 if (this.mode.has_input_prompt) {
1006 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.window_width);
1008 this.input_lines = [];
1010 this.height_input = this.input_lines.length;
1012 msg_into_lines_of_width: function(msg, width) {
1013 function push_inner_link(y, end_x) {
1014 if (!inner_links[y]) {
1015 inner_links[y] = [];
1017 inner_links[y].push([url_start_x, end_x, url]);
1021 const regexp = RegExp('https?://[^\\s]+', 'g');
1023 while ((match = regexp.exec(msg)) !== null) {
1024 const url = match[0];
1025 const url_start = match.index;
1026 const url_end = match.index + match[0].length;
1027 link_data[url_start] = url;
1028 url_ends.push(url_end);
1030 let url_start_x = 0;
1032 let inner_links = {};
1033 let in_link = false;
1036 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1037 if (x >= width || msg[i] == "\n") {
1039 push_inner_link(y, chunk.length);
1041 if (url_ends[0] == i) {
1049 if (msg[i] == "\n") {
1054 if (msg[i] != "\n") {
1057 if (i in link_data) {
1061 } else if (url_ends[0] == i) {
1063 push_inner_link(y, x);
1069 push_inner_link(lines.length - 1, chunk.length);
1071 return [lines, inner_links];
1073 log_msg: function(msg) {
1075 while (this.log.length > 100) {
1078 this.full_refresh();
1080 pick_selectable: function(task_name) {
1081 const i = parseInt(this.inputEl.value);
1082 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1083 tui.log_msg('? invalid index, aborted');
1085 server.send(['TASK:' + task_name, tui.selectables[i]]);
1087 this.inputEl.value = "";
1088 this.switch_mode('play');
1090 enter_ascii_art: function(command, height, width, with_pw=false) {
1091 if (this.inputEl.value.length > width) {
1092 this.log_msg('? wrong input length, must be max ' + width + '; try again');
1094 } else if (this.inputEl.value.length < width) {
1095 while (this.inputEl.value.length < width) {
1096 this.inputEl.value += ' ';
1099 this.log_msg(' ' + this.inputEl.value);
1100 this.full_ascii_draw += this.inputEl.value;
1101 this.ascii_draw_stage += 1;
1102 if (this.ascii_draw_stage < height) {
1103 this.restore_input_values();
1106 server.send([command, this.full_ascii_draw, this.password]);
1108 server.send([command, this.full_ascii_draw]);
1110 this.full_ascii_draw = '';
1111 this.ascii_draw_stage = 0;
1112 this.inputEl.value = '';
1113 this.switch_mode('edit');
1116 draw_map: function() {
1117 if (!game.turn_complete && this.map_lines.length == 0) {
1120 if (game.turn_complete) {
1121 let map_lines_split = [];
1123 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1124 if (j == game.map_size[1]) {
1125 map_lines_split.push(line);
1129 if (this.map_mode == 'protections') {
1130 line.push(game.map_control[i] + ' ');
1132 line.push(game.map[i] + ' ');
1135 map_lines_split.push(line);
1136 if (this.map_mode == 'terrain + annotations') {
1137 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1138 const yx = coordinate.split(',')
1139 map_lines_split[yx[0]][yx[1]] = 'A ';
1141 } else if (this.map_mode == 'terrain + things') {
1142 for (const p in game.portals) {
1143 let coordinate = p.split(',')
1144 let original = map_lines_split[coordinate[0]][coordinate[1]];
1145 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1147 let used_positions = [];
1148 function draw_thing(t, used_positions) {
1149 let symbol = game.thing_types[t.type_];
1150 let meta_char = ' ';
1152 meta_char = t.thing_char;
1154 if (used_positions.includes(t.position.toString())) {
1160 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1161 used_positions.push(t.position.toString());
1163 for (const thing_id in game.things) {
1164 let t = game.things[thing_id];
1165 if (t.type_ != 'Player') {
1166 draw_thing(t, used_positions);
1169 for (const thing_id in game.things) {
1170 let t = game.things[thing_id];
1171 if (t.type_ == 'Player') {
1172 draw_thing(t, used_positions);
1176 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1177 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1178 } else if (tui.map_mode != 'terrain + things') {
1179 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1182 if (game.map_geometry == 'Square') {
1183 for (let line_split of map_lines_split) {
1184 this.map_lines.push(line_split.join(''));
1186 } else if (game.map_geometry == 'Hex') {
1188 for (let line_split of map_lines_split) {
1189 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1197 let window_center = [terminal.rows / 2, this.window_width / 2];
1198 let center_position = [game.player.position[0], game.player.position[1]];
1199 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1200 center_position = [explorer.position[0], explorer.position[1]];
1202 center_position[1] = center_position[1] * 2;
1203 this.offset = [center_position[0] - window_center[0],
1204 center_position[1] - window_center[1]]
1205 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1206 this.offset[1] += 1;
1209 let term_y = Math.max(0, -this.offset[0]);
1210 let term_x = Math.max(0, -this.offset[1]);
1211 let map_y = Math.max(0, this.offset[0]);
1212 let map_x = Math.max(0, this.offset[1]);
1213 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1214 let to_draw = this.map_lines[map_y].slice(map_x, this.window_width + this.offset[1]);
1215 terminal.write(term_y, term_x, to_draw);
1218 draw_face_popup: function() {
1219 const t = game.things[this.draw_face];
1220 if (!t || !t.face) {
1221 this.draw_face = false;
1224 const start_x = tui.window_width - 10;
1227 t_char = t.thing_char;
1229 function draw_body_part(body_part, end_y) {
1230 terminal.write(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ');
1231 terminal.write(end_y - 3, start_x, '| |');
1232 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1233 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1234 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1237 draw_body_part(t.face, terminal.rows - 2);
1240 draw_body_part(t.hat, terminal.rows - 5);
1242 terminal.write(terminal.rows - 1, start_x, '| |');
1244 draw_mode_line: function() {
1245 let help = 'hit [' + this.keys.help + '] for help';
1246 if (this.mode.has_input_prompt) {
1247 help = 'enter /help for help';
1249 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1251 draw_stats_line: function(n) {
1252 terminal.write(1, this.window_width,
1253 'ENERGY: ' + game.energy +
1254 ' BLADDER: ' + game.bladder_pressure);
1256 draw_history: function() {
1257 let log_display_lines = [];
1259 let y_offset_in_log = 0;
1260 for (let line of this.log) {
1261 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1263 log_display_lines = log_display_lines.concat(new_lines);
1264 for (const y in link_data) {
1265 const rel_y = y_offset_in_log + parseInt(y);
1266 log_links[rel_y] = [];
1267 for (let link of link_data[y]) {
1268 log_links[rel_y].push(link);
1271 y_offset_in_log += new_lines.length;
1273 let i = log_display_lines.length - 1;
1274 for (let y = terminal.rows - 1 - this.height_input;
1275 y >= this.height_header && i >= 0;
1277 terminal.write(y, this.window_width, log_display_lines[i]);
1279 for (const key of Object.keys(log_links)) {
1280 if (parseInt(key) <= i) {
1281 delete log_links[key];
1284 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1286 this.offset_links(offset, log_links);
1288 draw_info: function() {
1289 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1290 let [lines, link_data] = this.msg_into_lines_of_width(info, this.window_width);
1291 let offset = [this.height_header, this.window_width];
1292 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1293 terminal.write(y, offset[1], lines[i]);
1295 this.offset_links(offset, link_data);
1297 draw_input: function() {
1298 if (this.mode.has_input_prompt) {
1299 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1300 terminal.write(y, this.window_width, this.input_lines[i]);
1304 draw_help: function() {
1305 let movement_keys_desc = '';
1306 if (!this.mode.is_intro) {
1307 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1309 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1310 if (this.mode.available_actions.length > 0) {
1311 content += "Available actions:\n";
1312 for (let action of this.mode.available_actions) {
1313 if (Object.keys(this.action_tasks).includes(action)) {
1314 if (!this.task_action_on(action)) {
1318 if (action == 'move_explorer') {
1321 if (action == 'move') {
1322 content += "[" + movement_keys_desc + "] – move\n"
1324 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1329 content += this.mode.list_available_modes();
1331 if (!this.mode.has_input_prompt) {
1332 start_x = this.window_width;
1333 this.draw_links = false;
1335 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1336 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1337 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1338 terminal.write(y, start_x, lines[i]);
1341 toggle_tile_draw: function() {
1342 if (tui.tile_draw) {
1343 tui.tile_draw = false;
1345 tui.tile_draw = true;
1348 toggle_map_mode: function() {
1349 if (tui.map_mode == 'terrain only') {
1350 tui.map_mode = 'terrain + annotations';
1351 } else if (tui.map_mode == 'terrain + annotations') {
1352 tui.map_mode = 'terrain + things';
1353 } else if (tui.map_mode == 'terrain + things') {
1354 tui.map_mode = 'protections';
1355 } else if (tui.map_mode == 'protections') {
1356 tui.map_mode = 'terrain only';
1359 full_refresh: function() {
1360 this.draw_links = true;
1362 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1363 this.recalc_input_lines();
1364 if (this.mode.is_intro) {
1365 this.draw_history();
1369 this.draw_stats_line();
1370 this.draw_mode_line();
1371 if (this.mode.shows_info) {
1374 this.draw_history();
1378 if (this.show_help) {
1381 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1382 this.draw_face_popup();
1384 if (!this.draw_links) {
1394 this.player_id = -1;
1397 this.things_new = {};
1402 this.map_control = "";
1403 this.map_control_new = "";
1404 this.map_size = [0,0];
1405 this.map_size_new = [0,0];
1407 this.portals_new = {};
1408 this.players_hat_chars = "";
1409 this.bladder_pressure = 0;
1410 this.bladder_pressure_new = 0;
1412 get_thing_temp: function(id_, create_if_not_found=false) {
1413 if (id_ in game.things_new) {
1414 return game.things_new[id_];
1415 } else if (create_if_not_found) {
1416 let t = new Thing([0,0]);
1417 game.things_new[id_] = t;
1421 get_thing: function(id_, create_if_not_found=false) {
1422 if (id_ in game.things) {
1423 return game.things[id_];
1426 move: function(start_position, direction) {
1427 let target = [start_position[0], start_position[1]];
1428 if (direction == 'LEFT') {
1430 } else if (direction == 'RIGHT') {
1432 } else if (game.map_geometry == 'Square') {
1433 if (direction == 'UP') {
1435 } else if (direction == 'DOWN') {
1438 } else if (game.map_geometry == 'Hex') {
1439 let start_indented = start_position[0] % 2;
1440 if (direction == 'UPLEFT') {
1442 if (!start_indented) {
1445 } else if (direction == 'UPRIGHT') {
1447 if (start_indented) {
1450 } else if (direction == 'DOWNLEFT') {
1452 if (!start_indented) {
1455 } else if (direction == 'DOWNRIGHT') {
1457 if (start_indented) {
1462 if (target[0] < 0 || target[1] < 0 ||
1463 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1468 teleport: function() {
1469 if (game.player.position in this.portals) {
1470 server.reconnect_to(this.portals[game.player.position]);
1472 terminal.blink_screen();
1473 tui.log_msg('? not standing on portal')
1481 server.init(websocket_location);
1486 annotations_new: {},
1488 move: function(direction) {
1489 let target = game.move(this.position, direction);
1491 this.position = target
1492 this.info_cached = false;
1493 if (tui.tile_draw) {
1494 this.send_tile_control_command();
1497 terminal.blink_screen();
1500 get_info: function() {
1501 if (this.info_cached) {
1502 return this.info_cached;
1504 let info_to_cache = '';
1505 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1506 if (game.fov[position_i] != '.') {
1507 info_to_cache += 'outside field of view';
1509 for (let t_id in game.things) {
1510 let t = game.things[t_id];
1511 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1512 info_to_cache += "THING: " + this.get_thing_info(t);
1513 let protection = t.protection;
1514 if (protection == '.') {
1515 protection = 'none';
1517 info_to_cache += " / protection: " + protection + "\n";
1519 info_to_cache += t.hat.slice(0, 6) + '\n';
1520 info_to_cache += t.hat.slice(6, 12) + '\n';
1521 info_to_cache += t.hat.slice(12, 18) + '\n';
1524 info_to_cache += t.face.slice(0, 6) + '\n';
1525 info_to_cache += t.face.slice(6, 12) + '\n';
1526 info_to_cache += t.face.slice(12, 18) + '\n';
1529 const line_length = t.design[0][1];
1530 if (t.type_ == 'Sign') {
1531 info_to_cache += '-'.repeat(line_length + 4) + '\n';
1533 const regexp = RegExp('.{1,' + line_length + '}', 'g');
1534 const lines = t.design[1].match(regexp);
1536 for (const line of lines) {
1537 info_to_cache += '| ' + line + ' |\n';
1539 if (t.type_ == 'Sign') {
1540 info_to_cache += '-'.repeat(line_length + 4) + '\n';
1545 let terrain_char = game.map[position_i]
1546 let terrain_desc = '?'
1547 if (game.terrains[terrain_char]) {
1548 terrain_desc = game.terrains[terrain_char];
1550 info_to_cache += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1551 let protection = game.map_control[position_i];
1552 if (protection == '.') {
1553 protection = 'unprotected';
1555 info_to_cache += 'PROTECTION: ' + protection + '\n';
1556 if (this.position in game.portals) {
1557 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1559 if (this.position in this.annotations) {
1560 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1563 this.info_cached = info_to_cache;
1564 return this.info_cached;
1566 get_thing_info: function(t) {
1567 const symbol = game.thing_types[t.type_];
1568 let info = t.type_ + " / " + symbol;
1570 info += t.thing_char;
1573 info += " (" + t.name_ + ")";
1576 info += " / installed";
1580 annotate: function(msg) {
1581 if (msg.length == 0) {
1582 msg = " "; // triggers annotation deletion
1584 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1586 set_portal: function(msg) {
1587 if (msg.length == 0) {
1588 msg = " "; // triggers portal deletion
1590 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1592 send_tile_control_command: function() {
1593 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1597 tui.inputEl.addEventListener('input', (event) => {
1598 if (tui.mode.has_input_prompt) {
1599 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1600 if (tui.inputEl.value.length > max_length) {
1601 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1603 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1604 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1605 tui.switch_mode('edit');
1609 document.onclick = function() {
1610 if (!tui.mode.is_single_char_entry) {
1611 tui.show_help = false;
1614 tui.inputEl.addEventListener('keydown', (event) => {
1615 tui.show_help = false;
1616 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1617 event.preventDefault();
1619 if ((!tui.mode.is_intro && event.key == 'Escape')
1620 || (tui.mode.has_input_prompt && event.key == 'Enter'
1621 && tui.inputEl.value.length == 0
1622 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1623 'admin_enter'].includes(tui.mode.name))) {
1624 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1625 tui.log_msg('@ aborted');
1627 tui.switch_mode('play');
1628 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1629 tui.show_help = true;
1630 tui.inputEl.value = "";
1631 tui.restore_input_values();
1632 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1633 && !tui.mode.is_single_char_entry) {
1634 tui.show_help = true;
1635 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1636 tui.login_name = tui.inputEl.value;
1637 server.send(['LOGIN', tui.inputEl.value]);
1638 tui.inputEl.value = "";
1639 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1640 tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1641 } else if (tui.mode.name == 'enter_hat' && event.key == 'Enter') {
1642 tui.enter_ascii_art('PLAYER_HAT', 3, 6);
1643 } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1644 tui.enter_ascii_art('THING_DESIGN',
1645 game.player.carrying.design[0][0],
1646 game.player.carrying.design[0][1], true);
1647 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1648 server.send(['TASK:COMMAND', tui.inputEl.value]);
1649 tui.inputEl.value = "";
1650 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1651 tui.pick_selectable('PICK_UP');
1652 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1653 tui.pick_selectable('DROP');
1654 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1655 if (tui.inputEl.value.length == 0) {
1656 tui.log_msg('@ aborted');
1658 server.send(['SET_MAP_CONTROL_PASSWORD',
1659 tui.tile_control_char, tui.inputEl.value]);
1660 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1662 tui.switch_mode('admin');
1663 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1664 explorer.set_portal(tui.inputEl.value);
1665 tui.switch_mode('edit');
1666 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1667 if (tui.inputEl.value.length == 0) {
1668 tui.inputEl.value = " ";
1670 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1671 tui.switch_mode('edit');
1672 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1673 explorer.annotate(tui.inputEl.value);
1674 tui.switch_mode('edit');
1675 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1676 if (tui.inputEl.value.length == 0) {
1677 tui.inputEl.value = " ";
1679 tui.password = tui.inputEl.value
1680 tui.switch_mode('edit');
1681 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1682 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1683 tui.switch_mode('play');
1684 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1685 if (tui.inputEl.value.length != 1) {
1686 tui.log_msg('@ entered non-single-char, therefore aborted');
1687 tui.switch_mode('admin');
1689 tui.tile_control_char = tui.inputEl.value[0];
1690 tui.switch_mode('control_pw_pw');
1692 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1693 if (tui.inputEl.value.length != 1) {
1694 tui.log_msg('@ entered non-single-char, therefore aborted');
1695 tui.switch_mode('admin');
1697 tui.tile_control_char = tui.inputEl.value[0];
1698 tui.switch_mode('control_tile_draw');
1700 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1701 if (tui.inputEl.value.length != 1) {
1702 tui.log_msg('@ entered non-single-char, therefore aborted');
1704 server.send(['THING_PROTECTION', tui.inputEl.value])
1705 tui.log_msg('@ sent new protection character for thing');
1707 tui.switch_mode('admin');
1708 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1709 let tokens = parser.tokenize(tui.inputEl.value);
1710 if (tokens.length > 0 && tokens[0].length > 0) {
1711 if (tui.inputEl.value[0][0] == '/') {
1712 if (tokens[0].slice(1) == 'nick') {
1713 if (tokens.length > 1) {
1714 server.send(['NICK', tokens[1]]);
1716 tui.log_msg('? need new name');
1719 tui.log_msg('? unknown command');
1722 server.send(['ALL', tui.inputEl.value]);
1724 } else if (tui.inputEl.valuelength > 0) {
1725 server.send(['ALL', tui.inputEl.value]);
1727 tui.inputEl.value = "";
1728 } else if (tui.mode.name == 'play') {
1729 if (tui.mode.mode_switch_on_key(event)) {
1731 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1732 server.send(["TASK:INTOXICATE"]);
1733 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1734 server.send(["TASK:DOOR"]);
1735 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1736 server.send(["TASK:WEAR"]);
1737 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1738 server.send(["TASK:SPIN"]);
1739 } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1740 server.send(["TASK:DANCE"]);
1741 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1742 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1743 } else if (event.key === tui.keys.teleport) {
1746 } else if (tui.mode.name == 'study') {
1747 if (tui.mode.mode_switch_on_key(event)) {
1749 } else if (event.key in tui.movement_keys) {
1750 explorer.move(tui.movement_keys[event.key]);
1751 } else if (event.key == tui.keys.toggle_map_mode) {
1752 tui.toggle_map_mode();
1754 } else if (tui.mode.name == 'control_tile_draw') {
1755 if (tui.mode.mode_switch_on_key(event)) {
1757 } else if (event.key in tui.movement_keys) {
1758 explorer.move(tui.movement_keys[event.key]);
1759 } else if (event.key === tui.keys.toggle_tile_draw) {
1760 tui.toggle_tile_draw();
1762 } else if (tui.mode.name == 'admin') {
1763 if (tui.mode.mode_switch_on_key(event)) {
1765 } else if (event.key == tui.keys.toggle_map_mode) {
1766 tui.toggle_map_mode();
1767 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1768 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1770 } else if (tui.mode.name == 'edit') {
1771 if (tui.mode.mode_switch_on_key(event)) {
1773 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1774 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1775 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1776 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1777 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1778 server.send(["TASK:INSTALL", tui.password]);
1779 } else if (event.key == tui.keys.toggle_map_mode) {
1780 tui.toggle_map_mode();
1786 rows_selector.addEventListener('input', function() {
1787 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1790 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1791 terminal.initialize();
1794 cols_selector.addEventListener('input', function() {
1795 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1798 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1799 terminal.initialize();
1800 tui.window_width = terminal.cols / 2,
1803 for (let key_selector of key_selectors) {
1804 key_selector.addEventListener('input', function() {
1805 window.localStorage.setItem(key_selector.id, key_selector.value);
1809 window.setInterval(function() {
1810 if (server.websocket.readyState == 1) {
1811 server.send(['PING']);
1812 } else if (server.websocket.readyState != 0) {
1813 server.reconnect_to(server.url);
1814 tui.log_msg('@ attempting reconnect …')
1817 window.setInterval(function() {
1818 if (document.activeElement.tagName.toLowerCase() != 'input') {
1819 const scroll_x = window.scrollX;
1820 const scroll_y = window.scrollY;
1821 tui.inputEl.focus();
1822 window.scrollTo(scroll_x, scroll_y);
1825 document.getElementById("help").onclick = function() {
1826 tui.show_help = true;
1829 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1830 const mode = switchEl.id.slice("switch_to_".length);
1831 switchEl.onclick = function() {
1832 tui.switch_mode(mode);
1836 document.getElementById("toggle_tile_draw").onclick = function() {
1837 tui.toggle_tile_draw();
1839 document.getElementById("toggle_map_mode").onclick = function() {
1840 tui.toggle_map_mode();
1843 document.getElementById("flatten").onclick = function() {
1844 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1846 document.getElementById("door").onclick = function() {
1847 server.send(['TASK:DOOR']);
1849 document.getElementById("consume").onclick = function() {
1850 server.send(['TASK:INTOXICATE']);
1852 document.getElementById("install").onclick = function() {
1853 server.send(['TASK:INSTALL', tui.password]);
1855 document.getElementById("wear").onclick = function() {
1856 server.send(['TASK:WEAR']);
1858 document.getElementById("spin").onclick = function() {
1859 server.send(['TASK:SPIN']);
1861 document.getElementById("dance").onclick = function() {
1862 server.send(['TASK:DANCE']);
1864 document.getElementById("teleport").onclick = function() {
1867 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1868 if (move_button.id.startsWith('key_')) { // not a move button
1871 let direction = move_button.id.split('_')[2].toUpperCase();
1874 if (tui.mode.available_actions.includes("move")) {
1875 server.send(['TASK:MOVE', direction]);
1876 } else if (tui.mode.available_actions.includes("move_explorer")) {
1877 explorer.move(direction);
1881 move_button.onmousedown = function() {
1883 move_repeat = window.setInterval(move, 100);
1885 move_button.onmouseup = function() {
1886 window.clearInterval(move_repeat);