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_design"></button>
79 <td><button id="switch_to_admin_enter"></button></td>
81 <button id="switch_to_control_pw_type"></button>
82 <button id="switch_to_control_tile_type"></button>
83 <button id="switch_to_admin_thing_protect"></button>
84 <button id="toggle_tile_draw"></button>
89 <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 />
91 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
92 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
93 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
94 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
95 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
96 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
97 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
98 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
99 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
100 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
101 <li>help: <input id="key_help" type="text" value="h" />
102 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
103 <li>teleport: <input id="key_teleport" type="text" value="p" />
104 <li>spin: <input id="key_spin" type="text" value="S" />
105 <li>dance: <input id="key_dance" type="text" value="T" />
106 <li>open/close: <input id="key_door" type="text" value="D" />
107 <li>consume: <input id="key_consume" type="text" value="C" />
108 <li>install: <input id="key_install" type="text" value="I" />
109 <li>(un-)wear: <input id="key_wear" type="text" value="W" />
110 <li><input id="key_switch_to_drop_thing" type="text" value="u" />
111 <li><input id="key_switch_to_enter_face" type="text" value="f" />
112 <li><input id="key_switch_to_enter_design" type="text" value="D" />
113 <li><input id="key_switch_to_take_thing" type="text" value="z" />
114 <li><input id="key_switch_to_chat" type="text" value="t" />
115 <li><input id="key_switch_to_play" type="text" value="p" />
116 <li><input id="key_switch_to_study" type="text" value="?" />
117 <li><input id="key_switch_to_edit" type="text" value="E" />
118 <li><input id="key_switch_to_write" type="text" value="m" />
119 <li><input id="key_switch_to_name_thing" type="text" value="N" />
120 <li><input id="key_switch_to_command_thing" type="text" value="O" />
121 <li><input id="key_switch_to_password" type="text" value="P" />
122 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
123 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
124 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
125 <li><input id="key_switch_to_admin_thing_protect" type="text" value="T" />
126 <li><input id="key_switch_to_annotate" type="text" value="M" />
127 <li><input id="key_switch_to_portal" type="text" value="T" />
128 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
129 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
134 let websocket_location = "wss://plomlompom.com/rogue_chat/";
135 //let websocket_location = "ws://localhost:8000/";
141 'long': 'This mode allows you to interact with the map in various ways.'
146 '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.'},
148 'short': 'world edit',
150 '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.'
153 'short': 'name thing',
155 'long': 'Give name to/change name of carried thing.'
160 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
164 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
165 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
169 'intro': 'Enter number of direction to which you want to drop thing.',
170 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
172 'admin_thing_protect': {
173 'short': 'change thing protection',
174 'intro': '@ enter thing protection character:',
175 'long': 'Change protection character for carried thing.'
178 'short': 'edit face',
179 'intro': '@ enter face line:',
180 '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.'
183 'short': 'edit design',
184 'intro': '@ enter design:',
185 'long': 'Enter design for carried thing as ASCII art.'
188 'short': 'edit tile',
190 '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.'
193 'short': 'change protection character password',
194 'intro': '@ enter protection character for which you want to change the password:',
195 '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.'
198 'short': 'change protection character password',
200 '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.'
202 'control_tile_type': {
203 'short': 'change tiles protection',
204 'intro': '@ enter protection character which you want to draw:',
205 '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.'
207 'control_tile_draw': {
208 'short': 'change tiles protection',
210 '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.'
213 'short': 'annotate tile',
215 '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.'
218 'short': 'edit portal',
220 '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.'
225 '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'
230 'long': 'Enter your player name.'
232 'waiting_for_server': {
233 'short': 'waiting for server response',
234 'intro': '@ waiting for server …',
235 'long': 'Waiting for a server response.'
238 'short': 'waiting for server response',
240 'long': 'Waiting for a server response.'
243 'short': 'set world edit password',
245 '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.'
248 'short': 'become admin',
249 'intro': '@ enter admin password:',
250 'long': 'This mode allows you to become admin if you know an admin password.'
255 'long': 'This mode allows you access to actions limited to administrators.'
258 let key_descriptions = {
260 'flatten': 'flatten surroundings',
261 'teleport': 'teleport',
262 'door': 'open/close',
263 'consume': 'consume',
264 'install': '(un-)install',
268 'toggle_map_mode': 'toggle map view',
269 'toggle_tile_draw': 'toggle protection character drawing',
270 'hex_move_upleft': 'up-left',
271 'hex_move_upright': 'up-right',
272 'hex_move_right': 'right',
273 'hex_move_left': 'left',
274 'hex_move_downleft': 'down-left',
275 'hex_move_downright': 'down-right',
276 'square_move_up': 'up',
277 'square_move_left': 'left',
278 'square_move_down': 'down',
279 'square_move_right': 'right',
281 for (const mode_name of Object.keys(mode_helps)) {
282 key_descriptions['switch_to_' + mode_name] = mode_helps[mode_name].short;
285 let rows_selector = document.getElementById("n_rows");
286 let cols_selector = document.getElementById("n_cols");
287 let key_selectors = document.querySelectorAll('[id^="key_"]');
289 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
290 const action = key_switch_selector.id.slice("key_switch_to_".length);
291 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
294 function restore_selector_value(selector) {
295 let stored_selection = window.localStorage.getItem(selector.id);
296 if (stored_selection) {
297 selector.value = stored_selection;
300 restore_selector_value(rows_selector);
301 restore_selector_value(cols_selector);
302 for (let key_selector of key_selectors) {
303 restore_selector_value(key_selector);
306 function escapeHTML(str) {
308 replace(/&/g, '&').
309 replace(/</g, '<').
310 replace(/>/g, '>').
311 replace(/'/g, ''').
312 replace(/"/g, '"');
316 initialize: function() {
317 this.rows = rows_selector.value;
318 this.cols = cols_selector.value;
319 this.pre_el = document.getElementById("terminal");
320 this.set_default_colors();
324 for (let y = 0, x = 0; y <= this.rows; x++) {
325 if (x == this.cols) {
328 this.content.push(line);
330 if (y == this.rows) {
337 apply_colors: function() {
338 this.pre_el.style.color = this.foreground;
339 this.pre_el.style.backgroundColor = this.background;
341 set_default_colors: function() {
342 this.foreground = 'white';
343 this.background = 'black';
346 set_random_colors: function() {
347 function rand(offset) {
348 return Math.floor(offset + Math.random() * 96).toString(16).padStart(2, '0');
350 this.foreground = '#' + rand(159) + rand(159) + rand(159);
351 this.background = '#' + rand(0) + rand(0) + rand(0);
354 blink_screen: function() {
355 this.pre_el.style.color = this.background;
356 this.pre_el.style.backgroundColor = this.foreground;
358 this.pre_el.style.color = this.foreground;
359 this.pre_el.style.backgroundColor = this.background;
362 refresh: function() {
363 let pre_content = '';
364 for (let y = 0; y < this.rows; y++) {
365 let line = this.content[y].join('');
367 if (y in tui.links) {
369 for (let span of tui.links[y]) {
370 chunks.push(escapeHTML(line.slice(start_x, span[0])));
371 chunks.push('<a target="_blank" href="');
372 chunks.push(escapeHTML(span[2]));
374 chunks.push(escapeHTML(line.slice(span[0], span[1])));
378 chunks.push(escapeHTML(line.slice(start_x)));
380 chunks = [escapeHTML(line)];
382 for (const chunk of chunks) {
383 pre_content += chunk;
387 this.pre_el.innerHTML = pre_content;
389 write: function(start_y, start_x, msg) {
390 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
391 this.content[start_y][x] = msg[i];
394 drawBox: function(start_y, start_x, height, width) {
395 let end_y = start_y + height;
396 let end_x = start_x + width;
397 for (let y = start_y, x = start_x; y < this.rows; x++) {
405 this.content[y][x] = ' ';
409 terminal.initialize();
412 tokenize: function(str) {
417 for (let i = 0; i < str.length; i++) {
423 } else if (c == '\\') {
425 } else if (c == '"') {
430 } else if (c == '"') {
432 } else if (c === ' ') {
433 if (token.length > 0) {
441 if (token.length > 0) {
446 parse_yx: function(position_string) {
447 let coordinate_strings = position_string.split(',')
448 let position = [0, 0];
449 position[0] = parseInt(coordinate_strings[0].slice(2));
450 position[1] = parseInt(coordinate_strings[1].slice(2));
462 init: function(url) {
464 this.websocket = new WebSocket(this.url);
465 this.websocket.onopen = function(event) {
466 game.thing_types = {};
468 tui.is_admin = false;
469 server.send(['TASKS']);
470 server.send(['TERRAINS']);
471 server.send(['THING_TYPES']);
472 tui.log_msg("@ server connected! :)");
473 tui.switch_mode('login');
475 this.websocket.onclose = function(event) {
476 tui.switch_mode('waiting_for_server');
477 tui.log_msg("@ server disconnected :(");
479 this.websocket.onmessage = this.handle_event;
481 reconnect_to: function(url) {
482 this.websocket.close();
485 send: function(tokens) {
486 this.websocket.send(unparser.untokenize(tokens));
488 handle_event: function(event) {
489 let tokens = parser.tokenize(event.data);
490 if (tokens[0] === 'TURN') {
491 game.turn_complete = false;
492 } else if (tokens[0] === 'OTHER_WIPE') {
493 game.portals_new = {};
494 explorer.annotations_new = {};
495 game.things_new = [];
496 } else if (tokens[0] === 'STATS') {
497 game.bladder_pressure_new = parseInt(tokens[1])
498 game.energy_new = parseInt(tokens[2])
499 } else if (tokens[0] === 'THING') {
500 let t = game.get_thing_temp(tokens[4], true);
501 t.position = parser.parse_yx(tokens[1]);
503 t.protection = tokens[3];
504 t.portable = parseInt(tokens[5]);
505 t.commandable = parseInt(tokens[6]);
506 } else if (tokens[0] === 'THING_NAME') {
507 let t = game.get_thing_temp(tokens[1]);
509 } else if (tokens[0] === 'THING_FACE') {
510 let t = game.get_thing_temp(tokens[1]);
512 } else if (tokens[0] === 'THING_HAT') {
513 let t = game.get_thing_temp(tokens[1]);
515 } else if (tokens[0] === 'THING_DESIGN') {
516 let t = game.get_thing_temp(tokens[1]);
517 t.design = [parser.parse_yx(tokens[2]), tokens[3]];
518 } else if (tokens[0] === 'THING_CHAR') {
519 let t = game.get_thing_temp(tokens[1]);
520 t.thing_char = tokens[2];
521 } else if (tokens[0] === 'TASKS') {
522 game.tasks = tokens[1].split(',');
523 tui.mode_write.legal = game.tasks.includes('WRITE');
524 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
525 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
526 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
527 } else if (tokens[0] === 'THING_TYPE') {
528 game.thing_types[tokens[1]] = tokens[2]
529 } else if (tokens[0] === 'THING_CARRYING') {
530 let t = game.get_thing_temp(tokens[1]);
531 t.carrying = game.get_thing_temp(tokens[2], false);
532 } else if (tokens[0] === 'THING_INSTALLED') {
533 let t = game.get_thing_temp(tokens[1]);
535 } else if (tokens[0] === 'TERRAIN') {
536 game.terrains[tokens[1]] = tokens[2]
537 } else if (tokens[0] === 'MAP') {
538 game.map_geometry_new = tokens[1];
539 game.map_size_new = parser.parse_yx(tokens[2]);
540 game.map_new = tokens[3]
541 } else if (tokens[0] === 'FOV') {
542 game.fov_new = tokens[1]
543 } else if (tokens[0] === 'MAP_CONTROL') {
544 game.map_control_new = tokens[1]
545 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
546 game.portals = game.portals_new;
547 game.map_geometry = game.map_geometry_new;
548 game.map_size = game.map_size_new;
549 game.map = game.map_new;
550 game.fov = game.fov_new;
552 game.map_control = game.map_control_new;
553 explorer.annotations = explorer.annotations_new;
554 explorer.info_cached = false;
555 game.things = game.things_new;
556 game.player = game.things[game.player_id];
557 game.players_hat_chars = game.players_hat_chars_new;
558 game.bladder_pressure = game.bladder_pressure_new
559 game.energy = game.energy_new
560 game.turn_complete = true;
561 if (tui.mode.name == 'post_login_wait') {
562 tui.switch_mode('play');
566 } else if (tokens[0] === 'CHAT') {
567 tui.log_msg('# ' + tokens[1], 1);
568 } else if (tokens[0] === 'CHATFACE') {
569 tui.draw_face = tokens[1];
571 } else if (tokens[0] === 'REPLY') {
572 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
573 } else if (tokens[0] === 'PLAYER_ID') {
574 game.player_id = parseInt(tokens[1]);
575 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
576 game.players_hat_chars_new = tokens[1];
577 } else if (tokens[0] === 'LOGIN_OK') {
578 this.send(['GET_GAMESTATE']);
579 tui.switch_mode('post_login_wait');
580 tui.log_msg('@ welcome!')
581 } else if (tokens[0] === 'DEFAULT_COLORS') {
582 terminal.set_default_colors();
583 } else if (tokens[0] === 'RANDOM_COLORS') {
584 terminal.set_random_colors();
585 } else if (tokens[0] === 'ADMIN_OK') {
587 tui.log_msg('@ you now have admin rights');
588 tui.switch_mode('admin');
589 } else if (tokens[0] === 'PORTAL') {
590 let position = parser.parse_yx(tokens[1]);
591 game.portals_new[position] = tokens[2];
592 } else if (tokens[0] === 'ANNOTATION') {
593 let position = parser.parse_yx(tokens[1]);
594 explorer.annotations_new[position] = tokens[2];
595 } else if (tokens[0] === 'UNHANDLED_INPUT') {
596 tui.log_msg('? unknown command');
597 } else if (tokens[0] === 'PLAY_ERROR') {
598 tui.log_msg('? ' + tokens[1]);
599 terminal.blink_screen();
600 } else if (tokens[0] === 'ARGUMENT_ERROR') {
601 tui.log_msg('? syntax error: ' + tokens[1]);
602 } else if (tokens[0] === 'GAME_ERROR') {
603 tui.log_msg('? game error: ' + tokens[1]);
604 } else if (tokens[0] === 'PONG') {
607 tui.log_msg('? unhandled input: ' + event.data);
613 quote: function(str) {
615 for (let i = 0; i < str.length; i++) {
617 if (['"', '\\'].includes(c)) {
623 return quoted.join('');
625 to_yx: function(yx_coordinate) {
626 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
628 untokenize: function(tokens) {
629 let quoted_tokens = [];
630 for (let token of tokens) {
631 quoted_tokens.push(this.quote(token));
633 return quoted_tokens.join(" ");
638 constructor(name, has_input_prompt=false, shows_info=false,
639 is_intro=false, is_single_char_entry=false) {
641 this.short_desc = mode_helps[name].short;
642 this.available_modes = [];
643 this.available_actions = [];
644 this.has_input_prompt = has_input_prompt;
645 this.shows_info= shows_info;
646 this.is_intro = is_intro;
647 this.help_intro = mode_helps[name].long;
648 this.intro_msg = mode_helps[name].intro;
649 this.is_single_char_entry = is_single_char_entry;
652 *iter_available_modes() {
653 for (let mode_name of this.available_modes) {
654 let mode = tui['mode_' + mode_name];
658 let key = tui.keys['switch_to_' + mode.name];
662 list_available_modes() {
664 if (this.available_modes.length > 0) {
665 msg += 'Other modes available from here:\n';
666 for (let [mode, key] of this.iter_available_modes()) {
667 msg += '[' + key + '] – ' + mode.short_desc + '\n';
672 mode_switch_on_key(key_event) {
673 for (let [mode, key] of this.iter_available_modes()) {
674 if (key_event.key == key) {
675 event.preventDefault();
676 tui.switch_mode(mode.name);
695 mode_waiting_for_server: new Mode('waiting_for_server',
697 mode_login: new Mode('login', true, false, true),
698 mode_post_login_wait: new Mode('post_login_wait'),
699 mode_chat: new Mode('chat', true),
700 mode_annotate: new Mode('annotate', true, true),
701 mode_play: new Mode('play'),
702 mode_study: new Mode('study', false, true),
703 mode_write: new Mode('write', false, false, false, true),
704 mode_edit: new Mode('edit'),
705 mode_control_pw_type: new Mode('control_pw_type', true),
706 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
707 mode_portal: new Mode('portal', true, true),
708 mode_password: new Mode('password', true),
709 mode_name_thing: new Mode('name_thing', true, true),
710 mode_command_thing: new Mode('command_thing', true),
711 mode_take_thing: new Mode('take_thing', true),
712 mode_drop_thing: new Mode('drop_thing', true),
713 mode_enter_face: new Mode('enter_face', true),
714 mode_enter_design: new Mode('enter_design', true),
715 mode_admin_enter: new Mode('admin_enter', true),
716 mode_admin: new Mode('admin'),
717 mode_control_pw_pw: new Mode('control_pw_pw', true),
718 mode_control_tile_type: new Mode('control_tile_type', true),
719 mode_control_tile_draw: new Mode('control_tile_draw'),
721 'flatten': 'FLATTEN_SURROUNDINGS',
722 'take_thing': 'PICK_UP',
723 'drop_thing': 'DROP',
726 'install': 'INSTALL',
728 'command': 'COMMAND',
729 'consume': 'INTOXICATE',
740 this.reset_screen_size();
741 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
742 "command_thing", "take_thing", "drop_thing"]
743 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
744 "wear", "spin", "dance"];
745 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
746 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
747 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
748 "control_tile_type", "chat",
749 "study", "play", "edit"]
750 this.mode_admin.available_actions = ["move", "toggle_map_mode"];
751 this.mode_control_tile_draw.available_modes = ["admin_enter"]
752 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
753 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
754 "enter_design", "password", "chat", "study",
755 "play", "admin_enter", "enter_face"]
756 this.mode_edit.available_actions = ["move", "flatten", "install",
758 this.inputEl = document.getElementById("input");
759 this.switch_mode('waiting_for_server');
760 this.recalc_input_lines();
761 this.height_header = this.height_turn_line + this.height_mode_line;
764 reset_screen_size: function() {
765 this.left_window_width = Math.min(52, terminal.cols / 2);
766 this.right_window_width = terminal.cols - tui.left_window_width;
768 init_keys: function() {
769 document.getElementById("move_table").hidden = true;
771 for (let key_selector of key_selectors) {
772 this.keys[key_selector.id.slice(4)] = key_selector.value;
774 this.movement_keys = {};
775 let geometry_prefix = 'undefinedMapGeometry_';
776 if (game.map_geometry) {
777 geometry_prefix = game.map_geometry.toLowerCase() + '_';
779 for (const key_name of Object.keys(key_descriptions)) {
780 if (key_name.startsWith(geometry_prefix)) {
781 let direction = key_name.split('_')[2].toUpperCase();
782 let key = this.keys[key_name];
783 this.movement_keys[key] = direction;
786 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
787 if (move_button.id.startsWith('key_')) {
790 move_button.hidden = true;
792 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
793 document.getElementById("move_table").hidden = false;
794 move_button.hidden = false;
796 for (let el of document.getElementsByTagName("button")) {
797 let action_desc = key_descriptions[el.id];
798 let action_key = '[' + this.keys[el.id] + ']';
799 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
802 task_action_on: function(action) {
803 return game.tasks.includes(this.action_tasks[action]);
805 switch_mode: function(mode_name) {
807 function fail(msg, return_mode='play') {
808 tui.log_msg('? ' + msg);
809 terminal.blink_screen();
810 tui.switch_mode(return_mode);
813 if (this.mode && this.mode.name == 'control_tile_draw') {
814 tui.log_msg('@ finished tile protection drawing.')
816 this.draw_face = false;
817 this.tile_draw = false;
818 this.ascii_draw_stage = 0;
819 this.full_ascii_draw = '';
820 if (mode_name == 'command_thing' && (!game.player.carrying
821 || !game.player.carrying.commandable)) {
822 return fail('not carrying anything commandable');
823 } else if (mode_name == 'name_thing' && !game.player.carrying) {
824 return fail('not carrying anything to re-name', 'edit');
825 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
826 return fail('not carrying anything to protect')
827 } else if (mode_name == 'take_thing' && game.player.carrying) {
828 return fail('already carrying something');
829 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
830 return fail('not carrying anything droppable');
831 } else if (mode_name == 'enter_design' && (!game.player.carrying
832 || !game.player.carrying.design)) {
833 return fail('not carrying designable to edit', 'edit');
835 if (mode_name == 'admin_enter' && this.is_admin) {
838 this.mode = this['mode_' + mode_name];
839 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
840 this.map_mode = 'protections';
841 } else if (this.mode.name != "edit") {
842 this.map_mode = 'terrain + things';
844 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
845 explorer.position = game.player.position;
847 this.inputEl.value = "";
848 this.restore_input_values();
849 for (let el of document.getElementsByTagName("button")) {
852 document.getElementById("help").disabled = false;
853 for (const action of this.mode.available_actions) {
854 if (["move", "move_explorer"].includes(action)) {
855 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
856 move_key.disabled = false;
858 } else if (Object.keys(this.action_tasks).includes(action)) {
859 if (this.task_action_on(action)) {
860 document.getElementById(action).disabled = false;
863 document.getElementById(action).disabled = false;
866 for (const mode_name of this.mode.available_modes) {
867 document.getElementById('switch_to_' + mode_name).disabled = false;
869 if (this.mode.intro_msg.length > 0) {
870 this.log_msg(this.mode.intro_msg);
872 if (this.mode.name == 'login') {
873 if (this.login_name) {
874 server.send(['LOGIN', this.login_name]);
876 this.log_msg("? need login name");
878 } else if (this.mode.is_single_char_entry) {
879 this.show_help = true;
880 } else if (this.mode.name == 'take_thing') {
881 this.log_msg("Portable things in reach for pick-up:");
882 const y = game.player.position[0]
883 const x = game.player.position[1]
884 let directed_moves = {
885 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
887 if (game.map_geometry == 'Square') {
888 directed_moves['UP'] = [-1, 0];
889 directed_moves['DOWN'] = [1, 0];
890 } else if (game.map_geometry == 'Hex') {
892 directed_moves['UPLEFT'] = [-1, 0];
893 directed_moves['UPRIGHT'] = [-1, 1];
894 directed_moves['DOWNLEFT'] = [1, 0];
895 directed_moves['DOWNRIGHT'] = [1, 1];
897 directed_moves['UPLEFT'] = [-1, -1];
898 directed_moves['UPRIGHT'] = [-1, 0];
899 directed_moves['DOWNLEFT'] = [1, -1];
900 directed_moves['DOWNRIGHT'] = [1, 0];
903 let select_range = {};
904 for (const direction in directed_moves) {
905 const move = directed_moves[direction];
906 select_range[direction] = [y + move[0], x + move[1]];
908 this.selectables = [];
910 for (const direction in select_range) {
911 for (const t_id in game.things) {
912 const t = game.things[t_id];
913 const position = select_range[direction];
915 && t.position[0] == position[0]
916 && t.position[1] == position[1]) {
917 this.selectables.push(t_id);
918 directions.push(direction);
922 if (this.selectables.length == 0) {
923 this.log_msg('none');
924 terminal.blink_screen();
925 this.switch_mode('play');
928 for (let [i, t_id] of this.selectables.entries()) {
929 const t = game.things[t_id];
930 const direction = directions[i];
931 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
934 } else if (this.mode.name == 'drop_thing') {
935 this.log_msg('Direction to drop thing to:');
936 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
937 for (let [i, direction] of this.selectables.entries()) {
938 this.log_msg(i + ': ' + direction);
940 } else if (this.mode.name == 'enter_design') {
941 if (game.player.carrying.type_ == 'Hat') {
942 this.log_msg('@ The design you enter must be '
943 + game.player.carrying.design[0][0] + ' lines of max '
944 + game.player.carrying.design[0][1] + ' characters width each');
945 this.log_msg('@ Legal characters: ' + game.players_hat_chars);
946 this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
948 this.log_msg('@ Width of first line determines maximum width for remaining design')
949 this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
951 } else if (this.mode.name == 'command_thing') {
952 server.send(['TASK:COMMAND', 'HELP']);
953 } else if (this.mode.name == 'control_pw_pw') {
954 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
955 } else if (this.mode.name == 'control_tile_draw') {
956 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 + '].')
960 offset_links: function(offset, links) {
961 for (let y in links) {
962 let real_y = offset[0] + parseInt(y);
963 if (!this.links[real_y]) {
964 this.links[real_y] = [];
966 for (let link of links[y]) {
967 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
968 this.links[real_y].push(offset_link);
972 restore_input_values: function() {
973 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
974 let info = explorer.annotations[explorer.position];
975 if (info != "(none)") {
976 this.inputEl.value = info;
978 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
979 let portal = game.portals[explorer.position]
980 this.inputEl.value = portal;
981 } else if (this.mode.name == 'password') {
982 this.inputEl.value = this.password;
983 } else if (this.mode.name == 'name_thing') {
984 if (game.player.carrying && game.player.carrying.name_) {
985 this.inputEl.value = game.player.carrying.name_;
987 } else if (this.mode.name == 'admin_thing_protect') {
988 if (game.player.carrying && game.player.carrying.protection) {
989 this.inputEl.value = game.player.carrying.protection;
991 } else if (this.mode.name == 'enter_face') {
992 const start = this.ascii_draw_stage * 6;
993 const end = (this.ascii_draw_stage + 1) * 6;
994 this.inputEl.value = game.player.face.slice(start, end);
995 } else if (this.mode.name == 'enter_design') {
996 const width = game.player.carrying.design[0][1];
997 const start = this.ascii_draw_stage * width;
998 const end = (this.ascii_draw_stage + 1) * width;
999 this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1002 recalc_input_lines: function() {
1003 if (this.mode.has_input_prompt) {
1005 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1007 this.input_lines = [];
1009 this.height_input = this.input_lines.length;
1011 msg_into_lines_of_width: function(msg, width) {
1012 function push_inner_link(y, end_x) {
1013 if (!inner_links[y]) {
1014 inner_links[y] = [];
1016 inner_links[y].push([url_start_x, end_x, url]);
1020 const regexp = RegExp('https?://[^\\s]+', 'g');
1022 while ((match = regexp.exec(msg)) !== null) {
1023 const url = match[0];
1024 const url_start = match.index;
1025 const url_end = match.index + match[0].length;
1026 link_data[url_start] = url;
1027 url_ends.push(url_end);
1029 let url_start_x = 0;
1031 let inner_links = {};
1032 let in_link = false;
1035 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1036 if (x >= width || msg[i] == "\n") {
1038 push_inner_link(y, chunk.length);
1040 if (url_ends[0] == i) {
1048 if (msg[i] == "\n") {
1053 if (msg[i] != "\n") {
1056 if (i in link_data) {
1060 } else if (url_ends[0] == i) {
1062 push_inner_link(y, x);
1068 push_inner_link(lines.length - 1, chunk.length);
1070 return [lines, inner_links];
1072 log_msg: function(msg) {
1074 while (this.log.length > 100) {
1077 this.full_refresh();
1079 pick_selectable: function(task_name) {
1080 const i = parseInt(this.inputEl.value);
1081 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1082 tui.log_msg('? invalid index, aborted');
1084 server.send(['TASK:' + task_name, tui.selectables[i]]);
1086 this.inputEl.value = "";
1087 this.switch_mode('play');
1089 enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1090 if (with_size && this.ascii_draw_stage == 0) {
1091 width = this.inputEl.value.length;
1093 this.log_msg('? wrong input length, must be max 36; try again');
1096 if (width != game.player.carrying.design[0][1]) {
1097 game.player.carrying.design[1] = '';
1098 game.player.carrying.design[0][1] = width;
1100 } else if (this.inputEl.value.length > width) {
1101 this.log_msg('? wrong input length, must be max ' + width + '; try again');
1104 this.log_msg(' ' + this.inputEl.value);
1105 if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1106 height = this.ascii_draw_stage;
1109 height = this.ascii_draw_stage + 2;
1111 while (this.inputEl.value.length < width) {
1112 this.inputEl.value += ' ';
1114 this.full_ascii_draw += this.inputEl.value;
1117 game.player.carrying.design[0][0] = height;
1119 this.ascii_draw_stage += 1;
1120 if (this.ascii_draw_stage < height) {
1121 this.restore_input_values();
1123 if (with_pw && with_size) {
1124 server.send([command + '_SIZE',
1125 unparser.to_yx(game.player.carrying.design[0]),
1129 server.send([command, this.full_ascii_draw, this.password]);
1131 server.send([command, this.full_ascii_draw]);
1133 this.full_ascii_draw = '';
1134 this.ascii_draw_stage = 0;
1135 this.inputEl.value = '';
1136 this.switch_mode('edit');
1139 draw_map: function() {
1140 if (!game.turn_complete && this.map_lines.length == 0) {
1143 if (game.turn_complete) {
1144 let map_lines_split = [];
1146 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1147 if (j == game.map_size[1]) {
1148 map_lines_split.push(line);
1152 if (this.map_mode == 'protections') {
1153 line.push(game.map_control[i] + ' ');
1155 line.push(game.map[i] + ' ');
1158 map_lines_split.push(line);
1159 if (this.map_mode == 'terrain + annotations') {
1160 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1161 const yx = coordinate.split(',')
1162 map_lines_split[yx[0]][yx[1]] = 'A ';
1164 } else if (this.map_mode == 'terrain + things') {
1165 for (const p in game.portals) {
1166 let coordinate = p.split(',')
1167 let original = map_lines_split[coordinate[0]][coordinate[1]];
1168 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1170 let used_positions = [];
1171 function draw_thing(t, used_positions) {
1172 let symbol = game.thing_types[t.type_];
1173 let meta_char = ' ';
1175 meta_char = t.thing_char;
1177 if (used_positions.includes(t.position.toString())) {
1183 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1184 used_positions.push(t.position.toString());
1186 for (const thing_id in game.things) {
1187 let t = game.things[thing_id];
1188 if (t.type_ != 'Player') {
1189 draw_thing(t, used_positions);
1192 for (const thing_id in game.things) {
1193 let t = game.things[thing_id];
1194 if (t.type_ == 'Player') {
1195 draw_thing(t, used_positions);
1199 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1200 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1201 } else if (tui.map_mode != 'terrain + things') {
1202 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1205 if (game.map_geometry == 'Square') {
1206 for (let line_split of map_lines_split) {
1207 this.map_lines.push(line_split.join(''));
1209 } else if (game.map_geometry == 'Hex') {
1211 for (let line_split of map_lines_split) {
1212 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1220 let window_center = [terminal.rows / 2, this.left_window_width / 2];
1221 let center_position = [game.player.position[0], game.player.position[1]];
1222 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1223 center_position = [explorer.position[0], explorer.position[1]];
1225 center_position[1] = center_position[1] * 2;
1226 this.offset = [center_position[0] - window_center[0],
1227 center_position[1] - window_center[1]]
1228 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1229 this.offset[1] += 1;
1232 let term_y = Math.max(0, -this.offset[0]);
1233 let term_x = Math.max(0, -this.offset[1]);
1234 let map_y = Math.max(0, this.offset[0]);
1235 let map_x = Math.max(0, this.offset[1]);
1236 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1237 let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1238 terminal.write(term_y, term_x, to_draw);
1241 draw_names: function() {
1243 for (const thing_id in game.things) {
1244 let t = game.things[thing_id];
1245 if (t.type_ == 'Player') {
1249 function compare(a, b) {
1250 if (a.name_.length > b.name_.length) {
1252 } else if (a.name_.length < b.name_.length) {
1258 players.sort(compare);
1259 const shrink_offset = Math.max(0, (terminal.rows - tui.left_window_width / 2) / 2);
1261 for (const player of players) {
1262 let name = player.name_;
1263 const offset_y = y - shrink_offset;
1264 const max_len = Math.max(4, (tui.left_window_width / 2) - (offset_y * 2) - 8);
1265 if (name.length > max_len) {
1266 name = name.slice(0, max_len) + '…';
1268 terminal.write(y, 0, '@' + player.thing_char + ':' + name);
1270 if (y >= terminal.rows) {
1275 draw_face_popup: function() {
1276 const t = game.things[this.draw_face];
1277 if (!t || !t.face) {
1278 this.draw_face = false;
1281 const start_x = tui.left_window_width - 10;
1282 function draw_body_part(body_part, end_y) {
1283 terminal.write(end_y - 3, start_x, '----------');
1284 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1285 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1286 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1289 draw_body_part(t.face, terminal.rows - 3);
1292 draw_body_part(t.hat, terminal.rows - 6);
1294 terminal.write(terminal.rows - 2, start_x, '----------');
1296 if (name.length > 6) {
1297 name = name.slice(0, 6) + '…';
1299 terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1301 draw_mode_line: function() {
1302 let help = 'hit [' + this.keys.help + '] for help';
1303 if (this.mode.has_input_prompt) {
1304 help = 'enter /help for help';
1306 terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1308 draw_stats_line: function(n) {
1309 terminal.write(0, this.left_window_width,
1310 'ENERGY: ' + game.energy +
1311 ' BLADDER: ' + game.bladder_pressure);
1313 draw_history: function() {
1314 let log_display_lines = [];
1316 let y_offset_in_log = 0;
1317 for (let line of this.log) {
1318 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1319 this.right_window_width)
1320 log_display_lines = log_display_lines.concat(new_lines);
1321 for (const y in link_data) {
1322 const rel_y = y_offset_in_log + parseInt(y);
1323 log_links[rel_y] = [];
1324 for (let link of link_data[y]) {
1325 log_links[rel_y].push(link);
1328 y_offset_in_log += new_lines.length;
1330 let i = log_display_lines.length - 1;
1331 for (let y = terminal.rows - 1 - this.height_input;
1332 y >= this.height_header && i >= 0;
1334 terminal.write(y, this.left_window_width, log_display_lines[i]);
1336 for (const key of Object.keys(log_links)) {
1337 if (parseInt(key) <= i) {
1338 delete log_links[key];
1341 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1342 this.left_window_width];
1343 this.offset_links(offset, log_links);
1345 draw_info: function() {
1346 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1347 let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1348 let offset = [this.height_header, this.left_window_width];
1349 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1350 terminal.write(y, offset[1], lines[i]);
1352 this.offset_links(offset, link_data);
1354 draw_input: function() {
1355 if (this.mode.has_input_prompt) {
1356 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1357 terminal.write(y, this.left_window_width, this.input_lines[i]);
1361 draw_help: function() {
1362 let movement_keys_desc = '';
1363 if (!this.mode.is_intro) {
1364 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1366 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1367 if (this.mode.available_actions.length > 0) {
1368 content += "Available actions:\n";
1369 for (let action of this.mode.available_actions) {
1370 if (Object.keys(this.action_tasks).includes(action)) {
1371 if (!this.task_action_on(action)) {
1375 if (action == 'move_explorer') {
1378 if (action == 'move') {
1379 content += "[" + movement_keys_desc + "] – move\n"
1381 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1386 content += this.mode.list_available_modes();
1390 if (!this.mode.has_input_prompt) {
1391 start_x = this.left_window_width;
1392 this.draw_links = false;
1393 terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1394 [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1397 terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1398 [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1400 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1401 terminal.write(y, start_x, lines[i]);
1404 toggle_tile_draw: function() {
1405 if (tui.tile_draw) {
1406 tui.tile_draw = false;
1408 tui.tile_draw = true;
1411 toggle_map_mode: function() {
1412 if (tui.map_mode == 'terrain only') {
1413 tui.map_mode = 'terrain + annotations';
1414 } else if (tui.map_mode == 'terrain + annotations') {
1415 tui.map_mode = 'terrain + things';
1416 } else if (tui.map_mode == 'terrain + things') {
1417 tui.map_mode = 'protections';
1418 } else if (tui.map_mode == 'protections') {
1419 tui.map_mode = 'terrain only';
1422 full_refresh: function() {
1423 this.draw_links = true;
1425 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1426 this.recalc_input_lines();
1427 if (this.mode.is_intro) {
1428 this.draw_history();
1432 this.draw_stats_line();
1433 this.draw_mode_line();
1434 if (this.mode.shows_info) {
1437 this.draw_history();
1441 if (this.show_help) {
1444 if (['chat', 'play'].includes(this.mode.name)) {
1446 if (this.draw_face) {
1447 this.draw_face_popup();
1450 if (!this.draw_links) {
1460 this.player_id = -1;
1463 this.things_new = {};
1468 this.map_control = "";
1469 this.map_control_new = "";
1470 this.map_size = [0,0];
1471 this.map_size_new = [0,0];
1473 this.portals_new = {};
1474 this.players_hat_chars = "";
1475 this.bladder_pressure = 0;
1476 this.bladder_pressure_new = 0;
1478 get_thing_temp: function(id_, create_if_not_found=false) {
1479 if (id_ in game.things_new) {
1480 return game.things_new[id_];
1481 } else if (create_if_not_found) {
1482 let t = new Thing([0,0]);
1483 game.things_new[id_] = t;
1487 get_thing: function(id_, create_if_not_found=false) {
1488 if (id_ in game.things) {
1489 return game.things[id_];
1492 move: function(start_position, direction) {
1493 let target = [start_position[0], start_position[1]];
1494 if (direction == 'LEFT') {
1496 } else if (direction == 'RIGHT') {
1498 } else if (game.map_geometry == 'Square') {
1499 if (direction == 'UP') {
1501 } else if (direction == 'DOWN') {
1504 } else if (game.map_geometry == 'Hex') {
1505 let start_indented = start_position[0] % 2;
1506 if (direction == 'UPLEFT') {
1508 if (!start_indented) {
1511 } else if (direction == 'UPRIGHT') {
1513 if (start_indented) {
1516 } else if (direction == 'DOWNLEFT') {
1518 if (!start_indented) {
1521 } else if (direction == 'DOWNRIGHT') {
1523 if (start_indented) {
1528 if (target[0] < 0 || target[1] < 0 ||
1529 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1534 teleport: function() {
1535 if (game.player.position in this.portals) {
1536 server.reconnect_to(this.portals[game.player.position]);
1538 terminal.blink_screen();
1539 tui.log_msg('? not standing on portal')
1547 server.init(websocket_location);
1552 annotations_new: {},
1554 move: function(direction) {
1555 let target = game.move(this.position, direction);
1557 this.position = target
1558 this.info_cached = false;
1559 if (tui.tile_draw) {
1560 this.send_tile_control_command();
1563 terminal.blink_screen();
1566 get_info: function() {
1567 if (this.info_cached) {
1568 return this.info_cached;
1570 let info_to_cache = '';
1571 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1572 if (game.fov[position_i] != '.') {
1573 info_to_cache += 'outside field of view';
1575 for (let t_id in game.things) {
1576 let t = game.things[t_id];
1577 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1578 info_to_cache += this.get_thing_info(t, true);
1581 let terrain_char = game.map[position_i]
1582 let terrain_desc = '?'
1583 if (game.terrains[terrain_char]) {
1584 terrain_desc = game.terrains[terrain_char];
1586 info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1587 let protection = game.map_control[position_i];
1588 if (protection != '.') {
1589 info_to_cache += '/protection:' + protection;
1591 info_to_cache += ')\n';
1592 if (this.position in game.portals) {
1593 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1595 if (this.position in this.annotations) {
1596 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1599 this.info_cached = info_to_cache;
1600 return this.info_cached;
1602 get_thing_info: function(t, detailed=false) {
1607 info += game.thing_types[t.type_];
1609 info += t.thing_char;
1612 info += ": " + t.name_;
1614 info += ' (' + t.type_;
1616 info += "/installed";
1618 if (t.type_ == 'Bottle') {
1619 if (t.thing_char == '_') {
1621 } else if (t.thing_char == '~') {
1626 const protection = t.protection;
1627 if (protection != '.') {
1628 info += '/protection:' + protection;
1631 if (t.hat || t.face) {
1632 info += '----------\n';
1635 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1636 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1637 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1640 info += '| ' + t.face.slice(0, 6) + ' |\n';
1641 info += '| ' + t.face.slice(6, 12) + ' |\n';
1642 info += '| ' + t.face.slice(12, 18) + ' |\n';
1643 info += '----------\n';
1646 const line_length = t.design[0][1];
1647 info += '-'.repeat(line_length + 4) + '\n';
1649 if (line_length > 0) {
1650 const regexp = RegExp('.{1,' + line_length + '}', 'g');
1651 lines = t.design[1].match(regexp);
1653 for (const line of lines) {
1654 info += '| ' + line + ' |\n';
1656 info += '-'.repeat(line_length + 4) + '\n';
1663 annotate: function(msg) {
1664 if (msg.length == 0) {
1665 msg = " "; // triggers annotation deletion
1667 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1669 set_portal: function(msg) {
1670 if (msg.length == 0) {
1671 msg = " "; // triggers portal deletion
1673 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1675 send_tile_control_command: function() {
1676 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1680 tui.inputEl.addEventListener('input', (event) => {
1681 if (tui.mode.has_input_prompt) {
1682 let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1683 if (tui.inputEl.value.length > max_length) {
1684 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1686 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1687 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1688 tui.switch_mode('edit');
1692 document.onclick = function() {
1693 if (!tui.mode.is_single_char_entry) {
1694 tui.show_help = false;
1697 tui.inputEl.addEventListener('keydown', (event) => {
1698 tui.show_help = false;
1699 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1700 event.preventDefault();
1702 if ((!tui.mode.is_intro && event.key == 'Escape')
1703 || (tui.mode.has_input_prompt && event.key == 'Enter'
1704 && tui.inputEl.value.length == 0
1705 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1706 'admin_enter'].includes(tui.mode.name))) {
1707 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1708 tui.log_msg('@ aborted');
1710 tui.switch_mode('play');
1711 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1712 tui.show_help = true;
1713 tui.inputEl.value = "";
1714 tui.restore_input_values();
1715 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1716 && !tui.mode.is_single_char_entry) {
1717 tui.show_help = true;
1718 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1719 tui.login_name = tui.inputEl.value;
1720 server.send(['LOGIN', tui.inputEl.value]);
1721 tui.inputEl.value = "";
1722 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1723 tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1724 } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1725 if (game.player.carrying.type_ == 'Hat') {
1726 tui.enter_ascii_art('THING_DESIGN',
1727 game.player.carrying.design[0][0],
1728 game.player.carrying.design[0][1], true);
1730 tui.enter_ascii_art('THING_DESIGN',
1731 game.player.carrying.design[0][0],
1732 game.player.carrying.design[0][1], true, true);
1734 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1735 server.send(['TASK:COMMAND', tui.inputEl.value]);
1736 tui.inputEl.value = "";
1737 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1738 tui.pick_selectable('PICK_UP');
1739 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1740 tui.pick_selectable('DROP');
1741 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1742 if (tui.inputEl.value.length == 0) {
1743 tui.log_msg('@ aborted');
1745 server.send(['SET_MAP_CONTROL_PASSWORD',
1746 tui.tile_control_char, tui.inputEl.value]);
1747 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1749 tui.switch_mode('admin');
1750 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1751 explorer.set_portal(tui.inputEl.value);
1752 tui.switch_mode('edit');
1753 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1754 if (tui.inputEl.value.length == 0) {
1755 tui.inputEl.value = " ";
1757 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1758 tui.switch_mode('edit');
1759 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1760 explorer.annotate(tui.inputEl.value);
1761 tui.switch_mode('edit');
1762 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1763 if (tui.inputEl.value.length == 0) {
1764 tui.inputEl.value = " ";
1766 tui.password = tui.inputEl.value
1767 tui.switch_mode('edit');
1768 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1769 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1770 tui.switch_mode('play');
1771 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1772 if (tui.inputEl.value.length != 1) {
1773 tui.log_msg('@ entered non-single-char, therefore aborted');
1774 tui.switch_mode('admin');
1776 tui.tile_control_char = tui.inputEl.value[0];
1777 tui.switch_mode('control_pw_pw');
1779 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1780 if (tui.inputEl.value.length != 1) {
1781 tui.log_msg('@ entered non-single-char, therefore aborted');
1782 tui.switch_mode('admin');
1784 tui.tile_control_char = tui.inputEl.value[0];
1785 tui.switch_mode('control_tile_draw');
1787 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1788 if (tui.inputEl.value.length != 1) {
1789 tui.log_msg('@ entered non-single-char, therefore aborted');
1791 server.send(['THING_PROTECTION', tui.inputEl.value])
1792 tui.log_msg('@ sent new protection character for thing');
1794 tui.switch_mode('admin');
1795 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1796 let tokens = parser.tokenize(tui.inputEl.value);
1797 if (tokens.length > 0 && tokens[0].length > 0) {
1798 if (tui.inputEl.value[0][0] == '/') {
1799 if (tokens[0].slice(1) == 'nick') {
1800 if (tokens.length > 1) {
1801 server.send(['NICK', tokens[1]]);
1803 tui.log_msg('? need new name');
1806 tui.log_msg('? unknown command');
1809 server.send(['ALL', tui.inputEl.value]);
1811 } else if (tui.inputEl.valuelength > 0) {
1812 server.send(['ALL', tui.inputEl.value]);
1814 tui.inputEl.value = "";
1815 } else if (tui.mode.name == 'play') {
1816 if (tui.mode.mode_switch_on_key(event)) {
1818 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1819 server.send(["TASK:INTOXICATE"]);
1820 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1821 server.send(["TASK:DOOR"]);
1822 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1823 server.send(["TASK:WEAR"]);
1824 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1825 server.send(["TASK:SPIN"]);
1826 } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1827 server.send(["TASK:DANCE"]);
1828 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1829 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1830 } else if (event.key === tui.keys.teleport) {
1833 } else if (tui.mode.name == 'study') {
1834 if (tui.mode.mode_switch_on_key(event)) {
1836 } else if (event.key in tui.movement_keys) {
1837 explorer.move(tui.movement_keys[event.key]);
1838 } else if (event.key == tui.keys.toggle_map_mode) {
1839 tui.toggle_map_mode();
1841 } else if (tui.mode.name == 'control_tile_draw') {
1842 if (tui.mode.mode_switch_on_key(event)) {
1844 } else if (event.key in tui.movement_keys) {
1845 explorer.move(tui.movement_keys[event.key]);
1846 } else if (event.key === tui.keys.toggle_tile_draw) {
1847 tui.toggle_tile_draw();
1849 } else if (tui.mode.name == 'admin') {
1850 if (tui.mode.mode_switch_on_key(event)) {
1852 } else if (event.key == tui.keys.toggle_map_mode) {
1853 tui.toggle_map_mode();
1854 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1855 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1857 } else if (tui.mode.name == 'edit') {
1858 if (tui.mode.mode_switch_on_key(event)) {
1860 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1861 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1862 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1863 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1864 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1865 server.send(["TASK:INSTALL", tui.password]);
1866 } else if (event.key == tui.keys.toggle_map_mode) {
1867 tui.toggle_map_mode();
1873 rows_selector.addEventListener('input', function() {
1874 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1877 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1878 terminal.initialize();
1881 cols_selector.addEventListener('input', function() {
1882 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1885 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1886 terminal.initialize();
1887 tui.reset_screen_size();
1890 for (let key_selector of key_selectors) {
1891 key_selector.addEventListener('input', function() {
1892 window.localStorage.setItem(key_selector.id, key_selector.value);
1896 window.setInterval(function() {
1897 if (server.websocket.readyState == 1) {
1898 server.send(['PING']);
1899 } else if (server.websocket.readyState != 0) {
1900 server.reconnect_to(server.url);
1901 tui.log_msg('@ attempting reconnect …')
1904 window.setInterval(function() {
1905 if (document.activeElement.tagName.toLowerCase() != 'input') {
1906 const scroll_x = window.scrollX;
1907 const scroll_y = window.scrollY;
1908 tui.inputEl.focus();
1909 window.scrollTo(scroll_x, scroll_y);
1912 document.getElementById("help").onclick = function() {
1913 tui.show_help = true;
1916 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1917 const mode = switchEl.id.slice("switch_to_".length);
1918 switchEl.onclick = function() {
1919 tui.switch_mode(mode);
1923 document.getElementById("toggle_tile_draw").onclick = function() {
1924 tui.toggle_tile_draw();
1926 document.getElementById("toggle_map_mode").onclick = function() {
1927 tui.toggle_map_mode();
1930 document.getElementById("flatten").onclick = function() {
1931 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1933 document.getElementById("door").onclick = function() {
1934 server.send(['TASK:DOOR']);
1936 document.getElementById("consume").onclick = function() {
1937 server.send(['TASK:INTOXICATE']);
1939 document.getElementById("install").onclick = function() {
1940 server.send(['TASK:INSTALL', tui.password]);
1942 document.getElementById("wear").onclick = function() {
1943 server.send(['TASK:WEAR']);
1945 document.getElementById("spin").onclick = function() {
1946 server.send(['TASK:SPIN']);
1948 document.getElementById("dance").onclick = function() {
1949 server.send(['TASK:DANCE']);
1951 document.getElementById("teleport").onclick = function() {
1954 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1955 if (move_button.id.startsWith('key_')) { // not a move button
1958 let direction = move_button.id.split('_')[2].toUpperCase();
1961 if (tui.mode.available_actions.includes("move")) {
1962 server.send(['TASK:MOVE', direction]);
1963 } else if (tui.mode.available_actions.includes("move_explorer")) {
1964 explorer.move(direction);
1968 move_button.onmousedown = function() {
1970 move_repeat = window.setInterval(move, 100);
1972 move_button.onmouseup = function() {
1973 window.clearInterval(move_repeat);