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 tui.log_msg('@ hint: see top of terminal for how to get help.')
582 tui.log_msg('@ hint: enter study mode to understand your environment.')
583 } else if (tokens[0] === 'DEFAULT_COLORS') {
584 terminal.set_default_colors();
585 } else if (tokens[0] === 'RANDOM_COLORS') {
586 terminal.set_random_colors();
587 } else if (tokens[0] === 'ADMIN_OK') {
589 tui.log_msg('@ you now have admin rights');
590 tui.switch_mode('admin');
591 } else if (tokens[0] === 'PORTAL') {
592 let position = parser.parse_yx(tokens[1]);
593 game.portals_new[position] = tokens[2];
594 } else if (tokens[0] === 'ANNOTATION') {
595 let position = parser.parse_yx(tokens[1]);
596 explorer.annotations_new[position] = tokens[2];
597 } else if (tokens[0] === 'UNHANDLED_INPUT') {
598 tui.log_msg('? unknown command');
599 } else if (tokens[0] === 'PLAY_ERROR') {
600 tui.log_msg('? ' + tokens[1]);
601 terminal.blink_screen();
602 } else if (tokens[0] === 'ARGUMENT_ERROR') {
603 tui.log_msg('? syntax error: ' + tokens[1]);
604 } else if (tokens[0] === 'GAME_ERROR') {
605 tui.log_msg('? game error: ' + tokens[1]);
606 } else if (tokens[0] === 'PONG') {
609 tui.log_msg('? unhandled input: ' + event.data);
615 quote: function(str) {
617 for (let i = 0; i < str.length; i++) {
619 if (['"', '\\'].includes(c)) {
625 return quoted.join('');
627 to_yx: function(yx_coordinate) {
628 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
630 untokenize: function(tokens) {
631 let quoted_tokens = [];
632 for (let token of tokens) {
633 quoted_tokens.push(this.quote(token));
635 return quoted_tokens.join(" ");
640 constructor(name, has_input_prompt=false, shows_info=false,
641 is_intro=false, is_single_char_entry=false) {
643 this.short_desc = mode_helps[name].short;
644 this.available_modes = [];
645 this.available_actions = [];
646 this.has_input_prompt = has_input_prompt;
647 this.shows_info= shows_info;
648 this.is_intro = is_intro;
649 this.help_intro = mode_helps[name].long;
650 this.intro_msg = mode_helps[name].intro;
651 this.is_single_char_entry = is_single_char_entry;
654 *iter_available_modes() {
655 for (let mode_name of this.available_modes) {
656 let mode = tui['mode_' + mode_name];
660 let key = tui.keys['switch_to_' + mode.name];
664 list_available_modes() {
666 if (this.available_modes.length > 0) {
667 msg += 'Other modes available from here:\n';
668 for (let [mode, key] of this.iter_available_modes()) {
669 msg += '[' + key + '] – ' + mode.short_desc + '\n';
674 mode_switch_on_key(key_event) {
675 for (let [mode, key] of this.iter_available_modes()) {
676 if (key_event.key == key) {
677 event.preventDefault();
678 tui.switch_mode(mode.name);
697 mode_waiting_for_server: new Mode('waiting_for_server',
699 mode_login: new Mode('login', true, false, true),
700 mode_post_login_wait: new Mode('post_login_wait'),
701 mode_chat: new Mode('chat', true),
702 mode_annotate: new Mode('annotate', true, true),
703 mode_play: new Mode('play'),
704 mode_study: new Mode('study', false, true),
705 mode_write: new Mode('write', false, false, false, true),
706 mode_edit: new Mode('edit'),
707 mode_control_pw_type: new Mode('control_pw_type', true),
708 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
709 mode_portal: new Mode('portal', true, true),
710 mode_password: new Mode('password', true),
711 mode_name_thing: new Mode('name_thing', true, true),
712 mode_command_thing: new Mode('command_thing', true),
713 mode_take_thing: new Mode('take_thing', true),
714 mode_drop_thing: new Mode('drop_thing', true),
715 mode_enter_face: new Mode('enter_face', true),
716 mode_enter_design: new Mode('enter_design', true),
717 mode_admin_enter: new Mode('admin_enter', true),
718 mode_admin: new Mode('admin'),
719 mode_control_pw_pw: new Mode('control_pw_pw', true),
720 mode_control_tile_type: new Mode('control_tile_type', true),
721 mode_control_tile_draw: new Mode('control_tile_draw'),
723 'flatten': 'FLATTEN_SURROUNDINGS',
724 'take_thing': 'PICK_UP',
725 'drop_thing': 'DROP',
728 'install': 'INSTALL',
730 'command': 'COMMAND',
731 'consume': 'INTOXICATE',
742 this.reset_screen_size();
743 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
744 "command_thing", "take_thing", "drop_thing"]
745 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
746 "wear", "spin", "dance"];
747 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
748 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
749 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
750 "control_tile_type", "chat",
751 "study", "play", "edit"]
752 this.mode_admin.available_actions = ["move", "toggle_map_mode"];
753 this.mode_control_tile_draw.available_modes = ["admin_enter"]
754 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
755 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
756 "enter_design", "password", "chat", "study",
757 "play", "admin_enter", "enter_face"]
758 this.mode_edit.available_actions = ["move", "flatten", "install",
760 this.inputEl = document.getElementById("input");
761 this.switch_mode('waiting_for_server');
762 this.recalc_input_lines();
763 this.height_header = this.height_turn_line + this.height_mode_line;
766 reset_screen_size: function() {
767 this.left_window_width = Math.min(52, terminal.cols / 2);
768 this.right_window_width = terminal.cols - tui.left_window_width;
770 init_keys: function() {
771 document.getElementById("move_table").hidden = true;
773 for (let key_selector of key_selectors) {
774 this.keys[key_selector.id.slice(4)] = key_selector.value;
776 this.movement_keys = {};
777 let geometry_prefix = 'undefinedMapGeometry_';
778 if (game.map_geometry) {
779 geometry_prefix = game.map_geometry.toLowerCase() + '_';
781 for (const key_name of Object.keys(key_descriptions)) {
782 if (key_name.startsWith(geometry_prefix)) {
783 let direction = key_name.split('_')[2].toUpperCase();
784 let key = this.keys[key_name];
785 this.movement_keys[key] = direction;
788 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
789 if (move_button.id.startsWith('key_')) {
792 move_button.hidden = true;
794 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
795 document.getElementById("move_table").hidden = false;
796 move_button.hidden = false;
798 for (let el of document.getElementsByTagName("button")) {
799 let action_desc = key_descriptions[el.id];
800 let action_key = '[' + this.keys[el.id] + ']';
801 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
804 task_action_on: function(action) {
805 return game.tasks.includes(this.action_tasks[action]);
807 switch_mode: function(mode_name) {
809 function fail(msg, return_mode='play') {
810 tui.log_msg('? ' + msg);
811 terminal.blink_screen();
812 tui.switch_mode(return_mode);
815 if (this.mode && this.mode.name == 'control_tile_draw') {
816 tui.log_msg('@ finished tile protection drawing.')
818 this.draw_face = false;
819 this.tile_draw = false;
820 this.ascii_draw_stage = 0;
821 this.full_ascii_draw = '';
822 if (mode_name == 'command_thing' && (!game.player.carrying
823 || !game.player.carrying.commandable)) {
824 return fail('not carrying anything commandable');
825 } else if (mode_name == 'name_thing' && !game.player.carrying) {
826 return fail('not carrying anything to re-name', 'edit');
827 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
828 return fail('not carrying anything to protect')
829 } else if (mode_name == 'take_thing' && game.player.carrying) {
830 return fail('already carrying something');
831 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
832 return fail('not carrying anything droppable');
833 } else if (mode_name == 'enter_design' && (!game.player.carrying
834 || !game.player.carrying.design)) {
835 return fail('not carrying designable to edit', 'edit');
837 if (mode_name == 'admin_enter' && this.is_admin) {
840 this.mode = this['mode_' + mode_name];
841 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
842 this.map_mode = 'protections';
843 } else if (this.mode.name != "edit") {
844 this.map_mode = 'terrain + things';
846 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
847 explorer.position = game.player.position;
849 this.inputEl.value = "";
850 this.restore_input_values();
851 for (let el of document.getElementsByTagName("button")) {
854 document.getElementById("help").disabled = false;
855 for (const action of this.mode.available_actions) {
856 if (["move", "move_explorer"].includes(action)) {
857 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
858 move_key.disabled = false;
860 } else if (Object.keys(this.action_tasks).includes(action)) {
861 if (this.task_action_on(action)) {
862 document.getElementById(action).disabled = false;
865 document.getElementById(action).disabled = false;
868 for (const mode_name of this.mode.available_modes) {
869 document.getElementById('switch_to_' + mode_name).disabled = false;
871 if (this.mode.intro_msg.length > 0) {
872 this.log_msg(this.mode.intro_msg);
874 if (this.mode.name == 'login') {
875 if (this.login_name) {
876 server.send(['LOGIN', this.login_name]);
878 this.log_msg("? need login name");
880 } else if (this.mode.is_single_char_entry) {
881 this.show_help = true;
882 } else if (this.mode.name == 'take_thing') {
883 this.log_msg("Portable things in reach for pick-up:");
884 const y = game.player.position[0]
885 const x = game.player.position[1]
886 let directed_moves = {
887 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
889 if (game.map_geometry == 'Square') {
890 directed_moves['UP'] = [-1, 0];
891 directed_moves['DOWN'] = [1, 0];
892 } else if (game.map_geometry == 'Hex') {
894 directed_moves['UPLEFT'] = [-1, 0];
895 directed_moves['UPRIGHT'] = [-1, 1];
896 directed_moves['DOWNLEFT'] = [1, 0];
897 directed_moves['DOWNRIGHT'] = [1, 1];
899 directed_moves['UPLEFT'] = [-1, -1];
900 directed_moves['UPRIGHT'] = [-1, 0];
901 directed_moves['DOWNLEFT'] = [1, -1];
902 directed_moves['DOWNRIGHT'] = [1, 0];
905 let select_range = {};
906 for (const direction in directed_moves) {
907 const move = directed_moves[direction];
908 select_range[direction] = [y + move[0], x + move[1]];
910 this.selectables = [];
912 for (const direction in select_range) {
913 for (const t_id in game.things) {
914 const t = game.things[t_id];
915 const position = select_range[direction];
917 && t.position[0] == position[0]
918 && t.position[1] == position[1]) {
919 this.selectables.push(t_id);
920 directions.push(direction);
924 if (this.selectables.length == 0) {
925 this.log_msg('none');
926 terminal.blink_screen();
927 this.switch_mode('play');
930 for (let [i, t_id] of this.selectables.entries()) {
931 const t = game.things[t_id];
932 const direction = directions[i];
933 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
936 } else if (this.mode.name == 'drop_thing') {
937 this.log_msg('Direction to drop thing to:');
938 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
939 for (let [i, direction] of this.selectables.entries()) {
940 this.log_msg(i + ': ' + direction);
942 } else if (this.mode.name == 'enter_design') {
943 if (game.player.carrying.type_ == 'Hat') {
944 this.log_msg('@ The design you enter must be '
945 + game.player.carrying.design[0][0] + ' lines of max '
946 + game.player.carrying.design[0][1] + ' characters width each');
947 this.log_msg('@ Legal characters: ' + game.players_hat_chars);
948 this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
950 this.log_msg('@ Width of first line determines maximum width for remaining design')
951 this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
953 } else if (this.mode.name == 'command_thing') {
954 server.send(['TASK:COMMAND', 'HELP']);
955 } else if (this.mode.name == 'control_pw_pw') {
956 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
957 } else if (this.mode.name == 'control_tile_draw') {
958 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 + '].')
962 offset_links: function(offset, links) {
963 for (let y in links) {
964 let real_y = offset[0] + parseInt(y);
965 if (!this.links[real_y]) {
966 this.links[real_y] = [];
968 for (let link of links[y]) {
969 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
970 this.links[real_y].push(offset_link);
974 restore_input_values: function() {
975 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
976 let info = explorer.annotations[explorer.position];
977 if (info != "(none)") {
978 this.inputEl.value = info;
980 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
981 let portal = game.portals[explorer.position]
982 this.inputEl.value = portal;
983 } else if (this.mode.name == 'password') {
984 this.inputEl.value = this.password;
985 } else if (this.mode.name == 'name_thing') {
986 if (game.player.carrying && game.player.carrying.name_) {
987 this.inputEl.value = game.player.carrying.name_;
989 } else if (this.mode.name == 'admin_thing_protect') {
990 if (game.player.carrying && game.player.carrying.protection) {
991 this.inputEl.value = game.player.carrying.protection;
993 } else if (this.mode.name == 'enter_face') {
994 const start = this.ascii_draw_stage * 6;
995 const end = (this.ascii_draw_stage + 1) * 6;
996 this.inputEl.value = game.player.face.slice(start, end);
997 } else if (this.mode.name == 'enter_design') {
998 const width = game.player.carrying.design[0][1];
999 const start = this.ascii_draw_stage * width;
1000 const end = (this.ascii_draw_stage + 1) * width;
1001 this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1004 recalc_input_lines: function() {
1005 if (this.mode.has_input_prompt) {
1007 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1009 this.input_lines = [];
1011 this.height_input = this.input_lines.length;
1013 msg_into_lines_of_width: function(msg, width) {
1014 function push_inner_link(y, end_x) {
1015 if (!inner_links[y]) {
1016 inner_links[y] = [];
1018 inner_links[y].push([url_start_x, end_x, url]);
1022 const regexp = RegExp('https?://[^\\s]+', 'g');
1024 while ((match = regexp.exec(msg)) !== null) {
1025 const url = match[0];
1026 const url_start = match.index;
1027 const url_end = match.index + match[0].length;
1028 link_data[url_start] = url;
1029 url_ends.push(url_end);
1031 let url_start_x = 0;
1033 let inner_links = {};
1034 let in_link = false;
1037 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1038 if (x >= width || msg[i] == "\n") {
1040 push_inner_link(y, chunk.length);
1042 if (url_ends[0] == i) {
1050 if (msg[i] == "\n") {
1055 if (msg[i] != "\n") {
1058 if (i in link_data) {
1062 } else if (url_ends[0] == i) {
1064 push_inner_link(y, x);
1070 push_inner_link(lines.length - 1, chunk.length);
1072 return [lines, inner_links];
1074 log_msg: function(msg) {
1076 while (this.log.length > 100) {
1079 this.full_refresh();
1081 pick_selectable: function(task_name) {
1082 const i = parseInt(this.inputEl.value);
1083 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1084 tui.log_msg('? invalid index, aborted');
1086 server.send(['TASK:' + task_name, tui.selectables[i]]);
1088 this.inputEl.value = "";
1089 this.switch_mode('play');
1091 enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1092 if (with_size && this.ascii_draw_stage == 0) {
1093 width = this.inputEl.value.length;
1095 this.log_msg('? wrong input length, must be max 36; try again');
1098 if (width != game.player.carrying.design[0][1]) {
1099 game.player.carrying.design[1] = '';
1100 game.player.carrying.design[0][1] = width;
1102 } else if (this.inputEl.value.length > width) {
1103 this.log_msg('? wrong input length, must be max ' + width + '; try again');
1106 this.log_msg(' ' + this.inputEl.value);
1107 if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1108 height = this.ascii_draw_stage;
1111 height = this.ascii_draw_stage + 2;
1113 while (this.inputEl.value.length < width) {
1114 this.inputEl.value += ' ';
1116 this.full_ascii_draw += this.inputEl.value;
1119 game.player.carrying.design[0][0] = height;
1121 this.ascii_draw_stage += 1;
1122 if (this.ascii_draw_stage < height) {
1123 this.restore_input_values();
1125 if (with_pw && with_size) {
1126 server.send([command + '_SIZE',
1127 unparser.to_yx(game.player.carrying.design[0]),
1131 server.send([command, this.full_ascii_draw, this.password]);
1133 server.send([command, this.full_ascii_draw]);
1135 this.full_ascii_draw = '';
1136 this.ascii_draw_stage = 0;
1137 this.inputEl.value = '';
1138 this.switch_mode('edit');
1141 draw_map: function() {
1142 if (!game.turn_complete && this.map_lines.length == 0) {
1145 if (game.turn_complete) {
1146 let map_lines_split = [];
1148 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1149 if (j == game.map_size[1]) {
1150 map_lines_split.push(line);
1154 if (this.map_mode == 'protections') {
1155 line.push(game.map_control[i] + ' ');
1157 line.push(game.map[i] + ' ');
1160 map_lines_split.push(line);
1161 if (this.map_mode == 'terrain + annotations') {
1162 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1163 const yx = coordinate.split(',')
1164 map_lines_split[yx[0]][yx[1]] = 'A ';
1166 } else if (this.map_mode == 'terrain + things') {
1167 for (const p in game.portals) {
1168 let coordinate = p.split(',')
1169 let original = map_lines_split[coordinate[0]][coordinate[1]];
1170 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1172 let used_positions = [];
1173 function draw_thing(t, used_positions) {
1174 let symbol = game.thing_types[t.type_];
1175 let meta_char = ' ';
1177 meta_char = t.thing_char;
1179 if (used_positions.includes(t.position.toString())) {
1185 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1186 used_positions.push(t.position.toString());
1188 for (const thing_id in game.things) {
1189 let t = game.things[thing_id];
1190 if (t.type_ != 'Player') {
1191 draw_thing(t, used_positions);
1194 for (const thing_id in game.things) {
1195 let t = game.things[thing_id];
1196 if (t.type_ == 'Player') {
1197 draw_thing(t, used_positions);
1201 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1202 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1203 } else if (tui.map_mode != 'terrain + things') {
1204 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1207 if (game.map_geometry == 'Square') {
1208 for (let line_split of map_lines_split) {
1209 this.map_lines.push(line_split.join(''));
1211 } else if (game.map_geometry == 'Hex') {
1213 for (let line_split of map_lines_split) {
1214 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1222 let window_center = [terminal.rows / 2, this.left_window_width / 2];
1223 let center_position = [game.player.position[0], game.player.position[1]];
1224 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1225 center_position = [explorer.position[0], explorer.position[1]];
1227 center_position[1] = center_position[1] * 2;
1228 this.offset = [center_position[0] - window_center[0],
1229 center_position[1] - window_center[1]]
1230 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1231 this.offset[1] += 1;
1234 let term_y = Math.max(0, -this.offset[0]);
1235 let term_x = Math.max(0, -this.offset[1]);
1236 let map_y = Math.max(0, this.offset[0]);
1237 let map_x = Math.max(0, this.offset[1]);
1238 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1239 let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1240 terminal.write(term_y, term_x, to_draw);
1243 draw_face_popup: function() {
1244 const t = game.things[this.draw_face];
1245 if (!t || !t.face) {
1246 this.draw_face = false;
1249 const start_x = tui.left_window_width - 10;
1250 function draw_body_part(body_part, end_y) {
1251 terminal.write(end_y - 3, start_x, '----------');
1252 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1253 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1254 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1257 draw_body_part(t.face, terminal.rows - 3);
1260 draw_body_part(t.hat, terminal.rows - 6);
1262 terminal.write(terminal.rows - 2, start_x, '----------');
1264 if (name.length > 6) {
1265 name = name.slice(0, 6) + '…';
1267 terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1269 draw_mode_line: function() {
1270 let help = 'hit [' + this.keys.help + '] for help';
1271 if (this.mode.has_input_prompt) {
1272 help = 'enter /help for help';
1274 terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1276 draw_stats_line: function(n) {
1277 terminal.write(0, this.left_window_width,
1278 'ENERGY: ' + game.energy +
1279 ' BLADDER: ' + game.bladder_pressure);
1281 draw_history: function() {
1282 let log_display_lines = [];
1284 let y_offset_in_log = 0;
1285 for (let line of this.log) {
1286 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1287 this.right_window_width)
1288 log_display_lines = log_display_lines.concat(new_lines);
1289 for (const y in link_data) {
1290 const rel_y = y_offset_in_log + parseInt(y);
1291 log_links[rel_y] = [];
1292 for (let link of link_data[y]) {
1293 log_links[rel_y].push(link);
1296 y_offset_in_log += new_lines.length;
1298 let i = log_display_lines.length - 1;
1299 for (let y = terminal.rows - 1 - this.height_input;
1300 y >= this.height_header && i >= 0;
1302 terminal.write(y, this.left_window_width, log_display_lines[i]);
1304 for (const key of Object.keys(log_links)) {
1305 if (parseInt(key) <= i) {
1306 delete log_links[key];
1309 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1310 this.left_window_width];
1311 this.offset_links(offset, log_links);
1313 draw_info: function() {
1314 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1315 let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1316 let offset = [this.height_header, this.left_window_width];
1317 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1318 terminal.write(y, offset[1], lines[i]);
1320 this.offset_links(offset, link_data);
1322 draw_input: function() {
1323 if (this.mode.has_input_prompt) {
1324 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1325 terminal.write(y, this.left_window_width, this.input_lines[i]);
1329 draw_help: function() {
1330 let movement_keys_desc = '';
1331 if (!this.mode.is_intro) {
1332 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1334 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1335 if (this.mode.available_actions.length > 0) {
1336 content += "Available actions:\n";
1337 for (let action of this.mode.available_actions) {
1338 if (Object.keys(this.action_tasks).includes(action)) {
1339 if (!this.task_action_on(action)) {
1343 if (action == 'move_explorer') {
1346 if (action == 'move') {
1347 content += "[" + movement_keys_desc + "] – move\n"
1349 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1354 content += this.mode.list_available_modes();
1358 if (!this.mode.has_input_prompt) {
1359 start_x = this.left_window_width;
1360 this.draw_links = false;
1361 terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1362 [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1365 terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1366 [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1368 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1369 terminal.write(y, start_x, lines[i]);
1372 toggle_tile_draw: function() {
1373 if (tui.tile_draw) {
1374 tui.tile_draw = false;
1376 tui.tile_draw = true;
1379 toggle_map_mode: function() {
1380 if (tui.map_mode == 'terrain only') {
1381 tui.map_mode = 'terrain + annotations';
1382 } else if (tui.map_mode == 'terrain + annotations') {
1383 tui.map_mode = 'terrain + things';
1384 } else if (tui.map_mode == 'terrain + things') {
1385 tui.map_mode = 'protections';
1386 } else if (tui.map_mode == 'protections') {
1387 tui.map_mode = 'terrain only';
1390 full_refresh: function() {
1391 this.draw_links = true;
1393 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1394 this.recalc_input_lines();
1395 if (this.mode.is_intro) {
1396 this.draw_history();
1400 this.draw_stats_line();
1401 this.draw_mode_line();
1402 if (this.mode.shows_info) {
1405 this.draw_history();
1409 if (this.show_help) {
1412 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1413 this.draw_face_popup();
1415 if (!this.draw_links) {
1425 this.player_id = -1;
1428 this.things_new = {};
1433 this.map_control = "";
1434 this.map_control_new = "";
1435 this.map_size = [0,0];
1436 this.map_size_new = [0,0];
1438 this.portals_new = {};
1439 this.players_hat_chars = "";
1440 this.bladder_pressure = 0;
1441 this.bladder_pressure_new = 0;
1443 get_thing_temp: function(id_, create_if_not_found=false) {
1444 if (id_ in game.things_new) {
1445 return game.things_new[id_];
1446 } else if (create_if_not_found) {
1447 let t = new Thing([0,0]);
1448 game.things_new[id_] = t;
1452 get_thing: function(id_, create_if_not_found=false) {
1453 if (id_ in game.things) {
1454 return game.things[id_];
1457 move: function(start_position, direction) {
1458 let target = [start_position[0], start_position[1]];
1459 if (direction == 'LEFT') {
1461 } else if (direction == 'RIGHT') {
1463 } else if (game.map_geometry == 'Square') {
1464 if (direction == 'UP') {
1466 } else if (direction == 'DOWN') {
1469 } else if (game.map_geometry == 'Hex') {
1470 let start_indented = start_position[0] % 2;
1471 if (direction == 'UPLEFT') {
1473 if (!start_indented) {
1476 } else if (direction == 'UPRIGHT') {
1478 if (start_indented) {
1481 } else if (direction == 'DOWNLEFT') {
1483 if (!start_indented) {
1486 } else if (direction == 'DOWNRIGHT') {
1488 if (start_indented) {
1493 if (target[0] < 0 || target[1] < 0 ||
1494 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1499 teleport: function() {
1500 if (game.player.position in this.portals) {
1501 server.reconnect_to(this.portals[game.player.position]);
1503 terminal.blink_screen();
1504 tui.log_msg('? not standing on portal')
1512 server.init(websocket_location);
1517 annotations_new: {},
1519 move: function(direction) {
1520 let target = game.move(this.position, direction);
1522 this.position = target
1523 this.info_cached = false;
1524 if (tui.tile_draw) {
1525 this.send_tile_control_command();
1528 terminal.blink_screen();
1531 get_info: function() {
1532 if (this.info_cached) {
1533 return this.info_cached;
1535 let info_to_cache = '';
1536 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1537 if (game.fov[position_i] != '.') {
1538 info_to_cache += 'outside field of view';
1540 for (let t_id in game.things) {
1541 let t = game.things[t_id];
1542 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1543 info_to_cache += this.get_thing_info(t, true);
1546 let terrain_char = game.map[position_i]
1547 let terrain_desc = '?'
1548 if (game.terrains[terrain_char]) {
1549 terrain_desc = game.terrains[terrain_char];
1551 info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1552 let protection = game.map_control[position_i];
1553 if (protection != '.') {
1554 info_to_cache += '/protection:' + protection;
1556 info_to_cache += ')\n';
1557 if (this.position in game.portals) {
1558 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1560 if (this.position in this.annotations) {
1561 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1564 this.info_cached = info_to_cache;
1565 return this.info_cached;
1567 get_thing_info: function(t, detailed=false) {
1572 info += game.thing_types[t.type_];
1574 info += t.thing_char;
1577 info += ": " + t.name_;
1579 info += ' (' + t.type_;
1581 info += "/installed";
1583 if (t.type_ == 'Bottle') {
1584 if (t.thing_char == '_') {
1586 } else if (t.thing_char == '~') {
1591 const protection = t.protection;
1592 if (protection != '.') {
1593 info += '/protection:' + protection;
1596 if (t.hat || t.face) {
1597 info += '----------\n';
1600 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1601 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1602 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1605 info += '| ' + t.face.slice(0, 6) + ' |\n';
1606 info += '| ' + t.face.slice(6, 12) + ' |\n';
1607 info += '| ' + t.face.slice(12, 18) + ' |\n';
1608 info += '----------\n';
1611 const line_length = t.design[0][1];
1612 info += '-'.repeat(line_length + 4) + '\n';
1614 if (line_length > 0) {
1615 const regexp = RegExp('.{1,' + line_length + '}', 'g');
1616 lines = t.design[1].match(regexp);
1618 for (const line of lines) {
1619 info += '| ' + line + ' |\n';
1621 info += '-'.repeat(line_length + 4) + '\n';
1628 annotate: function(msg) {
1629 if (msg.length == 0) {
1630 msg = " "; // triggers annotation deletion
1632 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1634 set_portal: function(msg) {
1635 if (msg.length == 0) {
1636 msg = " "; // triggers portal deletion
1638 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1640 send_tile_control_command: function() {
1641 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1645 tui.inputEl.addEventListener('input', (event) => {
1646 if (tui.mode.has_input_prompt) {
1647 let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1648 if (tui.inputEl.value.length > max_length) {
1649 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1651 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1652 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1653 tui.switch_mode('edit');
1657 document.onclick = function() {
1658 if (!tui.mode.is_single_char_entry) {
1659 tui.show_help = false;
1662 tui.inputEl.addEventListener('keydown', (event) => {
1663 tui.show_help = false;
1664 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1665 event.preventDefault();
1667 if ((!tui.mode.is_intro && event.key == 'Escape')
1668 || (tui.mode.has_input_prompt && event.key == 'Enter'
1669 && tui.inputEl.value.length == 0
1670 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1671 'admin_enter'].includes(tui.mode.name))) {
1672 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1673 tui.log_msg('@ aborted');
1675 tui.switch_mode('play');
1676 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1677 tui.show_help = true;
1678 tui.inputEl.value = "";
1679 tui.restore_input_values();
1680 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1681 && !tui.mode.is_single_char_entry) {
1682 tui.show_help = true;
1683 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1684 tui.login_name = tui.inputEl.value;
1685 server.send(['LOGIN', tui.inputEl.value]);
1686 tui.inputEl.value = "";
1687 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1688 tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1689 } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1690 if (game.player.carrying.type_ == 'Hat') {
1691 tui.enter_ascii_art('THING_DESIGN',
1692 game.player.carrying.design[0][0],
1693 game.player.carrying.design[0][1], true);
1695 tui.enter_ascii_art('THING_DESIGN',
1696 game.player.carrying.design[0][0],
1697 game.player.carrying.design[0][1], true, true);
1699 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1700 server.send(['TASK:COMMAND', tui.inputEl.value]);
1701 tui.inputEl.value = "";
1702 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1703 tui.pick_selectable('PICK_UP');
1704 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1705 tui.pick_selectable('DROP');
1706 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1707 if (tui.inputEl.value.length == 0) {
1708 tui.log_msg('@ aborted');
1710 server.send(['SET_MAP_CONTROL_PASSWORD',
1711 tui.tile_control_char, tui.inputEl.value]);
1712 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1714 tui.switch_mode('admin');
1715 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1716 explorer.set_portal(tui.inputEl.value);
1717 tui.switch_mode('edit');
1718 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1719 if (tui.inputEl.value.length == 0) {
1720 tui.inputEl.value = " ";
1722 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1723 tui.switch_mode('edit');
1724 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1725 explorer.annotate(tui.inputEl.value);
1726 tui.switch_mode('edit');
1727 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1728 if (tui.inputEl.value.length == 0) {
1729 tui.inputEl.value = " ";
1731 tui.password = tui.inputEl.value
1732 tui.switch_mode('edit');
1733 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1734 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1735 tui.switch_mode('play');
1736 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1737 if (tui.inputEl.value.length != 1) {
1738 tui.log_msg('@ entered non-single-char, therefore aborted');
1739 tui.switch_mode('admin');
1741 tui.tile_control_char = tui.inputEl.value[0];
1742 tui.switch_mode('control_pw_pw');
1744 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1745 if (tui.inputEl.value.length != 1) {
1746 tui.log_msg('@ entered non-single-char, therefore aborted');
1747 tui.switch_mode('admin');
1749 tui.tile_control_char = tui.inputEl.value[0];
1750 tui.switch_mode('control_tile_draw');
1752 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1753 if (tui.inputEl.value.length != 1) {
1754 tui.log_msg('@ entered non-single-char, therefore aborted');
1756 server.send(['THING_PROTECTION', tui.inputEl.value])
1757 tui.log_msg('@ sent new protection character for thing');
1759 tui.switch_mode('admin');
1760 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1761 let tokens = parser.tokenize(tui.inputEl.value);
1762 if (tokens.length > 0 && tokens[0].length > 0) {
1763 if (tui.inputEl.value[0][0] == '/') {
1764 if (tokens[0].slice(1) == 'nick') {
1765 if (tokens.length > 1) {
1766 server.send(['NICK', tokens[1]]);
1768 tui.log_msg('? need new name');
1771 tui.log_msg('? unknown command');
1774 server.send(['ALL', tui.inputEl.value]);
1776 } else if (tui.inputEl.valuelength > 0) {
1777 server.send(['ALL', tui.inputEl.value]);
1779 tui.inputEl.value = "";
1780 } else if (tui.mode.name == 'play') {
1781 if (tui.mode.mode_switch_on_key(event)) {
1783 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1784 server.send(["TASK:INTOXICATE"]);
1785 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1786 server.send(["TASK:DOOR"]);
1787 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1788 server.send(["TASK:WEAR"]);
1789 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1790 server.send(["TASK:SPIN"]);
1791 } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1792 server.send(["TASK:DANCE"]);
1793 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1794 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1795 } else if (event.key === tui.keys.teleport) {
1798 } else if (tui.mode.name == 'study') {
1799 if (tui.mode.mode_switch_on_key(event)) {
1801 } else if (event.key in tui.movement_keys) {
1802 explorer.move(tui.movement_keys[event.key]);
1803 } else if (event.key == tui.keys.toggle_map_mode) {
1804 tui.toggle_map_mode();
1806 } else if (tui.mode.name == 'control_tile_draw') {
1807 if (tui.mode.mode_switch_on_key(event)) {
1809 } else if (event.key in tui.movement_keys) {
1810 explorer.move(tui.movement_keys[event.key]);
1811 } else if (event.key === tui.keys.toggle_tile_draw) {
1812 tui.toggle_tile_draw();
1814 } else if (tui.mode.name == 'admin') {
1815 if (tui.mode.mode_switch_on_key(event)) {
1817 } else if (event.key == tui.keys.toggle_map_mode) {
1818 tui.toggle_map_mode();
1819 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1820 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1822 } else if (tui.mode.name == 'edit') {
1823 if (tui.mode.mode_switch_on_key(event)) {
1825 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1826 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1827 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1828 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1829 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1830 server.send(["TASK:INSTALL", tui.password]);
1831 } else if (event.key == tui.keys.toggle_map_mode) {
1832 tui.toggle_map_mode();
1838 rows_selector.addEventListener('input', function() {
1839 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1842 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1843 terminal.initialize();
1846 cols_selector.addEventListener('input', function() {
1847 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1850 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1851 terminal.initialize();
1852 tui.reset_screen_size();
1855 for (let key_selector of key_selectors) {
1856 key_selector.addEventListener('input', function() {
1857 window.localStorage.setItem(key_selector.id, key_selector.value);
1861 window.setInterval(function() {
1862 if (server.websocket.readyState == 1) {
1863 server.send(['PING']);
1864 } else if (server.websocket.readyState != 0) {
1865 server.reconnect_to(server.url);
1866 tui.log_msg('@ attempting reconnect …')
1869 window.setInterval(function() {
1870 if (document.activeElement.tagName.toLowerCase() != 'input') {
1871 const scroll_x = window.scrollX;
1872 const scroll_y = window.scrollY;
1873 tui.inputEl.focus();
1874 window.scrollTo(scroll_x, scroll_y);
1877 document.getElementById("help").onclick = function() {
1878 tui.show_help = true;
1881 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1882 const mode = switchEl.id.slice("switch_to_".length);
1883 switchEl.onclick = function() {
1884 tui.switch_mode(mode);
1888 document.getElementById("toggle_tile_draw").onclick = function() {
1889 tui.toggle_tile_draw();
1891 document.getElementById("toggle_map_mode").onclick = function() {
1892 tui.toggle_map_mode();
1895 document.getElementById("flatten").onclick = function() {
1896 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1898 document.getElementById("door").onclick = function() {
1899 server.send(['TASK:DOOR']);
1901 document.getElementById("consume").onclick = function() {
1902 server.send(['TASK:INTOXICATE']);
1904 document.getElementById("install").onclick = function() {
1905 server.send(['TASK:INSTALL', tui.password]);
1907 document.getElementById("wear").onclick = function() {
1908 server.send(['TASK:WEAR']);
1910 document.getElementById("spin").onclick = function() {
1911 server.send(['TASK:SPIN']);
1913 document.getElementById("dance").onclick = function() {
1914 server.send(['TASK:DANCE']);
1916 document.getElementById("teleport").onclick = function() {
1919 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1920 if (move_button.id.startsWith('key_')) { // not a move button
1923 let direction = move_button.id.split('_')[2].toUpperCase();
1926 if (tui.mode.available_actions.includes("move")) {
1927 server.send(['TASK:MOVE', direction]);
1928 } else if (tui.mode.available_actions.includes("move_explorer")) {
1929 explorer.move(direction);
1933 move_button.onmousedown = function() {
1935 move_repeat = window.setInterval(move, 100);
1937 move_button.onmouseup = function() {
1938 window.clearInterval(move_repeat);