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 server.send(['TASKS']);
469 server.send(['TERRAINS']);
470 server.send(['THING_TYPES']);
471 tui.log_msg("@ server connected! :)");
472 tui.switch_mode('login');
474 this.websocket.onclose = function(event) {
475 tui.switch_mode('waiting_for_server');
476 tui.log_msg("@ server disconnected :(");
478 this.websocket.onmessage = this.handle_event;
480 reconnect_to: function(url) {
481 this.websocket.close();
484 send: function(tokens) {
485 this.websocket.send(unparser.untokenize(tokens));
487 handle_event: function(event) {
488 let tokens = parser.tokenize(event.data);
489 if (tokens[0] === 'TURN') {
490 game.turn_complete = false;
491 } else if (tokens[0] === 'OTHER_WIPE') {
492 game.portals_new = {};
493 explorer.annotations_new = {};
494 game.things_new = [];
495 } else if (tokens[0] === 'STATS') {
496 game.bladder_pressure_new = parseInt(tokens[1])
497 game.energy_new = parseInt(tokens[2])
498 } else if (tokens[0] === 'THING') {
499 let t = game.get_thing_temp(tokens[4], true);
500 t.position = parser.parse_yx(tokens[1]);
502 t.protection = tokens[3];
503 t.portable = parseInt(tokens[5]);
504 t.commandable = parseInt(tokens[6]);
505 } else if (tokens[0] === 'THING_NAME') {
506 let t = game.get_thing_temp(tokens[1]);
508 } else if (tokens[0] === 'THING_FACE') {
509 let t = game.get_thing_temp(tokens[1]);
511 } else if (tokens[0] === 'THING_HAT') {
512 let t = game.get_thing_temp(tokens[1]);
514 } else if (tokens[0] === 'THING_DESIGN') {
515 let t = game.get_thing_temp(tokens[1]);
516 t.design = [parser.parse_yx(tokens[2]), tokens[3]];
517 } else if (tokens[0] === 'THING_CHAR') {
518 let t = game.get_thing_temp(tokens[1]);
519 t.thing_char = tokens[2];
520 } else if (tokens[0] === 'TASKS') {
521 game.tasks = tokens[1].split(',');
522 tui.mode_write.legal = game.tasks.includes('WRITE');
523 tui.mode_command_thing.legal = game.tasks.includes('WRITE');
524 tui.mode_take_thing.legal = game.tasks.includes('PICK_UP');
525 tui.mode_drop_thing.legal = game.tasks.includes('DROP');
526 } else if (tokens[0] === 'THING_TYPE') {
527 game.thing_types[tokens[1]] = tokens[2]
528 } else if (tokens[0] === 'THING_CARRYING') {
529 let t = game.get_thing_temp(tokens[1]);
530 t.carrying = game.get_thing_temp(tokens[2], false);
531 } else if (tokens[0] === 'THING_INSTALLED') {
532 let t = game.get_thing_temp(tokens[1]);
534 } else if (tokens[0] === 'TERRAIN') {
535 game.terrains[tokens[1]] = tokens[2]
536 } else if (tokens[0] === 'MAP') {
537 game.map_geometry_new = tokens[1];
538 game.map_size_new = parser.parse_yx(tokens[2]);
539 game.map_new = tokens[3]
540 } else if (tokens[0] === 'FOV') {
541 game.fov_new = tokens[1]
542 } else if (tokens[0] === 'MAP_CONTROL') {
543 game.map_control_new = tokens[1]
544 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
545 game.portals = game.portals_new;
546 game.map_geometry = game.map_geometry_new;
547 game.map_size = game.map_size_new;
548 game.map = game.map_new;
549 game.fov = game.fov_new;
551 game.map_control = game.map_control_new;
552 explorer.annotations = explorer.annotations_new;
553 explorer.info_cached = false;
554 game.things = game.things_new;
555 game.player = game.things[game.player_id];
556 game.players_hat_chars = game.players_hat_chars_new;
557 game.bladder_pressure = game.bladder_pressure_new
558 game.energy = game.energy_new
559 game.turn_complete = true;
560 if (tui.mode.name == 'post_login_wait') {
561 tui.switch_mode('play');
565 } else if (tokens[0] === 'CHAT') {
566 tui.log_msg('# ' + tokens[1], 1);
567 } else if (tokens[0] === 'CHATFACE') {
568 tui.draw_face = tokens[1];
570 } else if (tokens[0] === 'REPLY') {
571 tui.log_msg('#MUSICPLAYER: ' + tokens[1], 1);
572 } else if (tokens[0] === 'PLAYER_ID') {
573 game.player_id = parseInt(tokens[1]);
574 } else if (tokens[0] === 'PLAYERS_HAT_CHARS') {
575 game.players_hat_chars_new = tokens[1];
576 } else if (tokens[0] === 'LOGIN_OK') {
577 this.send(['GET_GAMESTATE']);
578 tui.switch_mode('post_login_wait');
579 tui.log_msg('@ welcome!')
580 tui.log_msg('@ hint: see top of terminal for how to get help.')
581 tui.log_msg('@ hint: enter study mode to understand your environment.')
582 } else if (tokens[0] === 'DEFAULT_COLORS') {
583 terminal.set_default_colors();
584 } else if (tokens[0] === 'RANDOM_COLORS') {
585 terminal.set_random_colors();
586 } else if (tokens[0] === 'ADMIN_OK') {
588 tui.log_msg('@ you now have admin rights');
589 tui.switch_mode('admin');
590 } else if (tokens[0] === 'PORTAL') {
591 let position = parser.parse_yx(tokens[1]);
592 game.portals_new[position] = tokens[2];
593 } else if (tokens[0] === 'ANNOTATION') {
594 let position = parser.parse_yx(tokens[1]);
595 explorer.annotations_new[position] = tokens[2];
596 } else if (tokens[0] === 'UNHANDLED_INPUT') {
597 tui.log_msg('? unknown command');
598 } else if (tokens[0] === 'PLAY_ERROR') {
599 tui.log_msg('? ' + tokens[1]);
600 terminal.blink_screen();
601 } else if (tokens[0] === 'ARGUMENT_ERROR') {
602 tui.log_msg('? syntax error: ' + tokens[1]);
603 } else if (tokens[0] === 'GAME_ERROR') {
604 tui.log_msg('? game error: ' + tokens[1]);
605 } else if (tokens[0] === 'PONG') {
608 tui.log_msg('? unhandled input: ' + event.data);
614 quote: function(str) {
616 for (let i = 0; i < str.length; i++) {
618 if (['"', '\\'].includes(c)) {
624 return quoted.join('');
626 to_yx: function(yx_coordinate) {
627 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
629 untokenize: function(tokens) {
630 let quoted_tokens = [];
631 for (let token of tokens) {
632 quoted_tokens.push(this.quote(token));
634 return quoted_tokens.join(" ");
639 constructor(name, has_input_prompt=false, shows_info=false,
640 is_intro=false, is_single_char_entry=false) {
642 this.short_desc = mode_helps[name].short;
643 this.available_modes = [];
644 this.available_actions = [];
645 this.has_input_prompt = has_input_prompt;
646 this.shows_info= shows_info;
647 this.is_intro = is_intro;
648 this.help_intro = mode_helps[name].long;
649 this.intro_msg = mode_helps[name].intro;
650 this.is_single_char_entry = is_single_char_entry;
653 *iter_available_modes() {
654 for (let mode_name of this.available_modes) {
655 let mode = tui['mode_' + mode_name];
659 let key = tui.keys['switch_to_' + mode.name];
663 list_available_modes() {
665 if (this.available_modes.length > 0) {
666 msg += 'Other modes available from here:\n';
667 for (let [mode, key] of this.iter_available_modes()) {
668 msg += '[' + key + '] – ' + mode.short_desc + '\n';
673 mode_switch_on_key(key_event) {
674 for (let [mode, key] of this.iter_available_modes()) {
675 if (key_event.key == key) {
676 event.preventDefault();
677 tui.switch_mode(mode.name);
696 mode_waiting_for_server: new Mode('waiting_for_server',
698 mode_login: new Mode('login', true, false, true),
699 mode_post_login_wait: new Mode('post_login_wait'),
700 mode_chat: new Mode('chat', true),
701 mode_annotate: new Mode('annotate', true, true),
702 mode_play: new Mode('play'),
703 mode_study: new Mode('study', false, true),
704 mode_write: new Mode('write', false, false, false, true),
705 mode_edit: new Mode('edit'),
706 mode_control_pw_type: new Mode('control_pw_type', true),
707 mode_admin_thing_protect: new Mode('admin_thing_protect', true),
708 mode_portal: new Mode('portal', true, true),
709 mode_password: new Mode('password', true),
710 mode_name_thing: new Mode('name_thing', true, true),
711 mode_command_thing: new Mode('command_thing', true),
712 mode_take_thing: new Mode('take_thing', true),
713 mode_drop_thing: new Mode('drop_thing', true),
714 mode_enter_face: new Mode('enter_face', true),
715 mode_enter_design: new Mode('enter_design', true),
716 mode_admin_enter: new Mode('admin_enter', true),
717 mode_admin: new Mode('admin'),
718 mode_control_pw_pw: new Mode('control_pw_pw', true),
719 mode_control_tile_type: new Mode('control_tile_type', true),
720 mode_control_tile_draw: new Mode('control_tile_draw'),
722 'flatten': 'FLATTEN_SURROUNDINGS',
723 'take_thing': 'PICK_UP',
724 'drop_thing': 'DROP',
727 'install': 'INSTALL',
729 'command': 'COMMAND',
730 'consume': 'INTOXICATE',
741 this.reset_screen_size();
742 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
743 "command_thing", "take_thing", "drop_thing"]
744 this.mode_play.available_actions = ["move", "teleport", "door", "consume",
745 "wear", "spin", "dance"];
746 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
747 this.mode_study.available_actions = ["toggle_map_mode", "move_explorer"];
748 this.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
749 "control_tile_type", "chat",
750 "study", "play", "edit"]
751 this.mode_admin.available_actions = ["move", "toggle_map_mode"];
752 this.mode_control_tile_draw.available_modes = ["admin_enter"]
753 this.mode_control_tile_draw.available_actions = ["toggle_tile_draw"];
754 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
755 "enter_design", "password", "chat", "study",
756 "play", "admin_enter", "enter_face"]
757 this.mode_edit.available_actions = ["move", "flatten", "install",
759 this.inputEl = document.getElementById("input");
760 this.switch_mode('waiting_for_server');
761 this.recalc_input_lines();
762 this.height_header = this.height_turn_line + this.height_mode_line;
765 reset_screen_size: function() {
766 this.left_window_width = Math.min(52, terminal.cols / 2);
767 this.right_window_width = terminal.cols - tui.left_window_width;
769 init_keys: function() {
770 document.getElementById("move_table").hidden = true;
772 for (let key_selector of key_selectors) {
773 this.keys[key_selector.id.slice(4)] = key_selector.value;
775 this.movement_keys = {};
776 let geometry_prefix = 'undefinedMapGeometry_';
777 if (game.map_geometry) {
778 geometry_prefix = game.map_geometry.toLowerCase() + '_';
780 for (const key_name of Object.keys(key_descriptions)) {
781 if (key_name.startsWith(geometry_prefix)) {
782 let direction = key_name.split('_')[2].toUpperCase();
783 let key = this.keys[key_name];
784 this.movement_keys[key] = direction;
787 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
788 if (move_button.id.startsWith('key_')) {
791 move_button.hidden = true;
793 for (const move_button of document.querySelectorAll('[id^="' + geometry_prefix + 'move_"]')) {
794 document.getElementById("move_table").hidden = false;
795 move_button.hidden = false;
797 for (let el of document.getElementsByTagName("button")) {
798 let action_desc = key_descriptions[el.id];
799 let action_key = '[' + this.keys[el.id] + ']';
800 el.innerHTML = escapeHTML(action_desc) + '<br /><span class="keyboard_controlled">' + escapeHTML(action_key) + '</span>';
803 task_action_on: function(action) {
804 return game.tasks.includes(this.action_tasks[action]);
806 switch_mode: function(mode_name) {
808 function fail(msg, return_mode='play') {
809 tui.log_msg('? ' + msg);
810 terminal.blink_screen();
811 tui.switch_mode(return_mode);
814 if (this.mode && this.mode.name == 'control_tile_draw') {
815 tui.log_msg('@ finished tile protection drawing.')
817 this.draw_face = false;
818 this.tile_draw = false;
819 if (mode_name == 'command_thing' && (!game.player.carrying
820 || !game.player.carrying.commandable)) {
821 return fail('not carrying anything commandable');
822 } else if (mode_name == 'name_thing' && !game.player.carrying) {
823 return fail('not carrying anything to re-name', 'edit');
824 } else if (mode_name == 'admin_thing_protect' && !game.player.carrying) {
825 return fail('not carrying anything to protect')
826 } else if (mode_name == 'take_thing' && game.player.carrying) {
827 return fail('already carrying something');
828 } else if (mode_name == 'drop_thing' && !game.player.carrying) {
829 return fail('not carrying anything droppable');
830 } else if (mode_name == 'enter_design' && (!game.player.carrying
831 || !game.player.carrying.design)) {
832 return fail('not carrying designable to edit', 'edit');
834 if (mode_name == 'admin_enter' && this.is_admin) {
837 this.mode = this['mode_' + mode_name];
838 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
839 this.map_mode = 'protections';
840 } else if (this.mode.name != "edit") {
841 this.map_mode = 'terrain + things';
843 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
844 explorer.position = game.player.position;
846 this.inputEl.value = "";
847 this.restore_input_values();
848 for (let el of document.getElementsByTagName("button")) {
851 document.getElementById("help").disabled = false;
852 for (const action of this.mode.available_actions) {
853 if (["move", "move_explorer"].includes(action)) {
854 for (const move_key of document.querySelectorAll('[id*="_move_"]')) {
855 move_key.disabled = false;
857 } else if (Object.keys(this.action_tasks).includes(action)) {
858 if (this.task_action_on(action)) {
859 document.getElementById(action).disabled = false;
862 document.getElementById(action).disabled = false;
865 for (const mode_name of this.mode.available_modes) {
866 document.getElementById('switch_to_' + mode_name).disabled = false;
868 if (this.mode.intro_msg.length > 0) {
869 this.log_msg(this.mode.intro_msg);
871 if (this.mode.name == 'login') {
872 if (this.login_name) {
873 server.send(['LOGIN', this.login_name]);
875 this.log_msg("? need login name");
877 } else if (this.mode.is_single_char_entry) {
878 this.show_help = true;
879 } else if (this.mode.name == 'take_thing') {
880 this.log_msg("Portable things in reach for pick-up:");
881 const y = game.player.position[0]
882 const x = game.player.position[1]
883 let directed_moves = {
884 'HERE': [0, 0], 'LEFT': [0, -1], 'RIGHT': [0, 1]
886 if (game.map_geometry == 'Square') {
887 directed_moves['UP'] = [-1, 0];
888 directed_moves['DOWN'] = [1, 0];
889 } else if (game.map_geometry == 'Hex') {
891 directed_moves['UPLEFT'] = [-1, 0];
892 directed_moves['UPRIGHT'] = [-1, 1];
893 directed_moves['DOWNLEFT'] = [1, 0];
894 directed_moves['DOWNRIGHT'] = [1, 1];
896 directed_moves['UPLEFT'] = [-1, -1];
897 directed_moves['UPRIGHT'] = [-1, 0];
898 directed_moves['DOWNLEFT'] = [1, -1];
899 directed_moves['DOWNRIGHT'] = [1, 0];
902 let select_range = {};
903 for (const direction in directed_moves) {
904 const move = directed_moves[direction];
905 select_range[direction] = [y + move[0], x + move[1]];
907 this.selectables = [];
909 for (const direction in select_range) {
910 for (const t_id in game.things) {
911 const t = game.things[t_id];
912 const position = select_range[direction];
914 && t.position[0] == position[0]
915 && t.position[1] == position[1]) {
916 this.selectables.push(t_id);
917 directions.push(direction);
921 if (this.selectables.length == 0) {
922 this.log_msg('none');
923 terminal.blink_screen();
924 this.switch_mode('play');
927 for (let [i, t_id] of this.selectables.entries()) {
928 const t = game.things[t_id];
929 const direction = directions[i];
930 this.log_msg(i + ' ' + direction + ': ' + explorer.get_thing_info(t));
933 } else if (this.mode.name == 'drop_thing') {
934 this.log_msg('Direction to drop thing to:');
935 this.selectables = ['HERE'].concat(Object.values(this.movement_keys));
936 for (let [i, direction] of this.selectables.entries()) {
937 this.log_msg(i + ': ' + direction);
939 } else if (this.mode.name == 'enter_design') {
940 if (game.player.carrying.type_ == 'Hat') {
941 this.log_msg('@ The design you enter must be '
942 + game.player.carrying.design[0][0] + ' lines of max '
943 + game.player.carrying.design[0][1] + ' characters width each');
944 this.log_msg('@ Legal characters: ' + game.players_hat_chars);
945 this.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)');
947 this.log_msg('@ Width of first line determines maximum width for remaining design')
948 this.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
950 } else if (this.mode.name == 'command_thing') {
951 server.send(['TASK:COMMAND', 'HELP']);
952 } else if (this.mode.name == 'control_pw_pw') {
953 this.log_msg('@ enter protection password for "' + this.tile_control_char + '":');
954 } else if (this.mode.name == 'control_tile_draw') {
955 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 + '].')
959 offset_links: function(offset, links) {
960 for (let y in links) {
961 let real_y = offset[0] + parseInt(y);
962 if (!this.links[real_y]) {
963 this.links[real_y] = [];
965 for (let link of links[y]) {
966 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
967 this.links[real_y].push(offset_link);
971 restore_input_values: function() {
972 if (this.mode.name == 'annotate' && explorer.position in explorer.annotations) {
973 let info = explorer.annotations[explorer.position];
974 if (info != "(none)") {
975 this.inputEl.value = info;
977 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
978 let portal = game.portals[explorer.position]
979 this.inputEl.value = portal;
980 } else if (this.mode.name == 'password') {
981 this.inputEl.value = this.password;
982 } else if (this.mode.name == 'name_thing') {
983 if (game.player.carrying && game.player.carrying.name_) {
984 this.inputEl.value = game.player.carrying.name_;
986 } else if (this.mode.name == 'admin_thing_protect') {
987 if (game.player.carrying && game.player.carrying.protection) {
988 this.inputEl.value = game.player.carrying.protection;
990 } else if (this.mode.name == 'enter_face') {
991 const start = this.ascii_draw_stage * 6;
992 const end = (this.ascii_draw_stage + 1) * 6;
993 this.inputEl.value = game.player.face.slice(start, end);
994 } else if (this.mode.name == 'enter_design') {
995 const width = game.player.carrying.design[0][1];
996 const start = this.ascii_draw_stage * width;
997 const end = (this.ascii_draw_stage + 1) * width;
998 this.inputEl.value = game.player.carrying.design[1].slice(start, end);
1001 recalc_input_lines: function() {
1002 if (this.mode.has_input_prompt) {
1004 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value + '█', this.right_window_width);
1006 this.input_lines = [];
1008 this.height_input = this.input_lines.length;
1010 msg_into_lines_of_width: function(msg, width) {
1011 function push_inner_link(y, end_x) {
1012 if (!inner_links[y]) {
1013 inner_links[y] = [];
1015 inner_links[y].push([url_start_x, end_x, url]);
1019 const regexp = RegExp('https?://[^\\s]+', 'g');
1021 while ((match = regexp.exec(msg)) !== null) {
1022 const url = match[0];
1023 const url_start = match.index;
1024 const url_end = match.index + match[0].length;
1025 link_data[url_start] = url;
1026 url_ends.push(url_end);
1028 let url_start_x = 0;
1030 let inner_links = {};
1031 let in_link = false;
1034 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
1035 if (x >= width || msg[i] == "\n") {
1037 push_inner_link(y, chunk.length);
1039 if (url_ends[0] == i) {
1047 if (msg[i] == "\n") {
1052 if (msg[i] != "\n") {
1055 if (i in link_data) {
1059 } else if (url_ends[0] == i) {
1061 push_inner_link(y, x);
1067 push_inner_link(lines.length - 1, chunk.length);
1069 return [lines, inner_links];
1071 log_msg: function(msg) {
1073 while (this.log.length > 100) {
1076 this.full_refresh();
1078 pick_selectable: function(task_name) {
1079 const i = parseInt(this.inputEl.value);
1080 if (isNaN(this.inputEl.value) || i < 0 || i >= this.selectables.length) {
1081 tui.log_msg('? invalid index, aborted');
1083 server.send(['TASK:' + task_name, tui.selectables[i]]);
1085 this.inputEl.value = "";
1086 this.switch_mode('play');
1088 enter_ascii_art: function(command, height, width, with_pw=false, with_size=false) {
1089 if (with_size && this.ascii_draw_stage == 0) {
1090 width = this.inputEl.value.length;
1092 this.log_msg('? wrong input length, must be max 36; try again');
1095 if (width != game.player.carrying.design[0][1]) {
1096 game.player.carrying.design[1] = '';
1097 game.player.carrying.design[0][1] = width;
1099 } else if (this.inputEl.value.length > width) {
1100 this.log_msg('? wrong input length, must be max ' + width + '; try again');
1103 this.log_msg(' ' + this.inputEl.value);
1104 if (with_size && ['', ' '].includes(this.inputEl.value) && this.ascii_draw_stage > 0) {
1105 height = this.ascii_draw_stage;
1108 height = this.ascii_draw_stage + 2;
1110 while (this.inputEl.value.length < width) {
1111 this.inputEl.value += ' ';
1113 this.full_ascii_draw += this.inputEl.value;
1116 game.player.carrying.design[0][0] = height;
1118 this.ascii_draw_stage += 1;
1119 if (this.ascii_draw_stage < height) {
1120 this.restore_input_values();
1122 if (with_pw && with_size) {
1123 server.send([command + '_SIZE',
1124 unparser.to_yx(game.player.carrying.design[0]),
1128 server.send([command, this.full_ascii_draw, this.password]);
1130 server.send([command, this.full_ascii_draw]);
1132 this.full_ascii_draw = '';
1133 this.ascii_draw_stage = 0;
1134 this.inputEl.value = '';
1135 this.switch_mode('edit');
1138 draw_map: function() {
1139 if (!game.turn_complete && this.map_lines.length == 0) {
1142 if (game.turn_complete) {
1143 let map_lines_split = [];
1145 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
1146 if (j == game.map_size[1]) {
1147 map_lines_split.push(line);
1151 if (this.map_mode == 'protections') {
1152 line.push(game.map_control[i] + ' ');
1154 line.push(game.map[i] + ' ');
1157 map_lines_split.push(line);
1158 if (this.map_mode == 'terrain + annotations') {
1159 for (const [coordinate, _] of Object.entries(explorer.annotations)) {
1160 const yx = coordinate.split(',')
1161 map_lines_split[yx[0]][yx[1]] = 'A ';
1163 } else if (this.map_mode == 'terrain + things') {
1164 for (const p in game.portals) {
1165 let coordinate = p.split(',')
1166 let original = map_lines_split[coordinate[0]][coordinate[1]];
1167 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
1169 let used_positions = [];
1170 function draw_thing(t, used_positions) {
1171 let symbol = game.thing_types[t.type_];
1172 let meta_char = ' ';
1174 meta_char = t.thing_char;
1176 if (used_positions.includes(t.position.toString())) {
1182 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
1183 used_positions.push(t.position.toString());
1185 for (const thing_id in game.things) {
1186 let t = game.things[thing_id];
1187 if (t.type_ != 'Player') {
1188 draw_thing(t, used_positions);
1191 for (const thing_id in game.things) {
1192 let t = game.things[thing_id];
1193 if (t.type_ == 'Player') {
1194 draw_thing(t, used_positions);
1198 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1199 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
1200 } else if (tui.map_mode != 'terrain + things') {
1201 map_lines_split[game.player.position[0]][game.player.position[1]] = '??';
1204 if (game.map_geometry == 'Square') {
1205 for (let line_split of map_lines_split) {
1206 this.map_lines.push(line_split.join(''));
1208 } else if (game.map_geometry == 'Hex') {
1210 for (let line_split of map_lines_split) {
1211 this.map_lines.push(' '.repeat(indent) + line_split.join(''));
1219 let window_center = [terminal.rows / 2, this.left_window_width / 2];
1220 let center_position = [game.player.position[0], game.player.position[1]];
1221 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
1222 center_position = [explorer.position[0], explorer.position[1]];
1224 center_position[1] = center_position[1] * 2;
1225 this.offset = [center_position[0] - window_center[0],
1226 center_position[1] - window_center[1]]
1227 if (game.map_geometry == 'Hex' && this.offset[0] % 2) {
1228 this.offset[1] += 1;
1231 let term_y = Math.max(0, -this.offset[0]);
1232 let term_x = Math.max(0, -this.offset[1]);
1233 let map_y = Math.max(0, this.offset[0]);
1234 let map_x = Math.max(0, this.offset[1]);
1235 for (; term_y < terminal.rows && map_y < this.map_lines.length; term_y++, map_y++) {
1236 let to_draw = this.map_lines[map_y].slice(map_x, this.left_window_width + this.offset[1]);
1237 terminal.write(term_y, term_x, to_draw);
1240 draw_face_popup: function() {
1241 const t = game.things[this.draw_face];
1242 if (!t || !t.face) {
1243 this.draw_face = false;
1246 const start_x = tui.left_window_width - 10;
1247 function draw_body_part(body_part, end_y) {
1248 terminal.write(end_y - 3, start_x, '----------');
1249 terminal.write(end_y - 2, start_x, '| ' + body_part.slice(0, 6) + ' |');
1250 terminal.write(end_y - 1, start_x, '| ' + body_part.slice(6, 12) + ' |');
1251 terminal.write(end_y, start_x, '| ' + body_part.slice(12, 18) + ' |');
1254 draw_body_part(t.face, terminal.rows - 3);
1257 draw_body_part(t.hat, terminal.rows - 6);
1259 terminal.write(terminal.rows - 2, start_x, '----------');
1261 if (name.length > 6) {
1262 name = name.slice(0, 6) + '…';
1264 terminal.write(terminal.rows - 1, start_x, '@' + t.thing_char + ':' + name);
1266 draw_mode_line: function() {
1267 let help = 'hit [' + this.keys.help + '] for help';
1268 if (this.mode.has_input_prompt) {
1269 help = 'enter /help for help';
1271 terminal.write(1, this.left_window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
1273 draw_stats_line: function(n) {
1274 terminal.write(0, this.left_window_width,
1275 'ENERGY: ' + game.energy +
1276 ' BLADDER: ' + game.bladder_pressure);
1278 draw_history: function() {
1279 let log_display_lines = [];
1281 let y_offset_in_log = 0;
1282 for (let line of this.log) {
1283 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
1284 this.right_window_width)
1285 log_display_lines = log_display_lines.concat(new_lines);
1286 for (const y in link_data) {
1287 const rel_y = y_offset_in_log + parseInt(y);
1288 log_links[rel_y] = [];
1289 for (let link of link_data[y]) {
1290 log_links[rel_y].push(link);
1293 y_offset_in_log += new_lines.length;
1295 let i = log_display_lines.length - 1;
1296 for (let y = terminal.rows - 1 - this.height_input;
1297 y >= this.height_header && i >= 0;
1299 terminal.write(y, this.left_window_width, log_display_lines[i]);
1301 for (const key of Object.keys(log_links)) {
1302 if (parseInt(key) <= i) {
1303 delete log_links[key];
1306 let offset = [terminal.rows - this.height_input - log_display_lines.length,
1307 this.left_window_width];
1308 this.offset_links(offset, log_links);
1310 draw_info: function() {
1311 const info = "MAP VIEW: " + tui.map_mode + "\n" + explorer.get_info();
1312 let [lines, link_data] = this.msg_into_lines_of_width(info, this.right_window_width);
1313 let offset = [this.height_header, this.left_window_width];
1314 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1315 terminal.write(y, offset[1], lines[i]);
1317 this.offset_links(offset, link_data);
1319 draw_input: function() {
1320 if (this.mode.has_input_prompt) {
1321 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
1322 terminal.write(y, this.left_window_width, this.input_lines[i]);
1326 draw_help: function() {
1327 let movement_keys_desc = '';
1328 if (!this.mode.is_intro) {
1329 movement_keys_desc = Object.keys(this.movement_keys).join(',');
1331 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
1332 if (this.mode.available_actions.length > 0) {
1333 content += "Available actions:\n";
1334 for (let action of this.mode.available_actions) {
1335 if (Object.keys(this.action_tasks).includes(action)) {
1336 if (!this.task_action_on(action)) {
1340 if (action == 'move_explorer') {
1343 if (action == 'move') {
1344 content += "[" + movement_keys_desc + "] – move\n"
1346 content += "[" + this.keys[action] + "] – " + key_descriptions[action] + "\n";
1351 content += this.mode.list_available_modes();
1355 if (!this.mode.has_input_prompt) {
1356 start_x = this.left_window_width;
1357 this.draw_links = false;
1358 terminal.drawBox(0, start_x, terminal.rows, this.right_window_width);
1359 [lines, _] = this.msg_into_lines_of_width(content, this.right_window_width);
1362 terminal.drawBox(0, start_x, terminal.rows, this.left_window_width);
1363 [lines, _] = this.msg_into_lines_of_width(content, this.left_window_width);
1365 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1366 terminal.write(y, start_x, lines[i]);
1369 toggle_tile_draw: function() {
1370 if (tui.tile_draw) {
1371 tui.tile_draw = false;
1373 tui.tile_draw = true;
1376 toggle_map_mode: function() {
1377 if (tui.map_mode == 'terrain only') {
1378 tui.map_mode = 'terrain + annotations';
1379 } else if (tui.map_mode == 'terrain + annotations') {
1380 tui.map_mode = 'terrain + things';
1381 } else if (tui.map_mode == 'terrain + things') {
1382 tui.map_mode = 'protections';
1383 } else if (tui.map_mode == 'protections') {
1384 tui.map_mode = 'terrain only';
1387 full_refresh: function() {
1388 this.draw_links = true;
1390 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1391 this.recalc_input_lines();
1392 if (this.mode.is_intro) {
1393 this.draw_history();
1397 this.draw_stats_line();
1398 this.draw_mode_line();
1399 if (this.mode.shows_info) {
1402 this.draw_history();
1406 if (this.show_help) {
1409 if (this.draw_face && ['chat', 'play'].includes(this.mode.name)) {
1410 this.draw_face_popup();
1412 if (!this.draw_links) {
1422 this.player_id = -1;
1425 this.things_new = {};
1430 this.map_control = "";
1431 this.map_control_new = "";
1432 this.map_size = [0,0];
1433 this.map_size_new = [0,0];
1435 this.portals_new = {};
1436 this.players_hat_chars = "";
1437 this.bladder_pressure = 0;
1438 this.bladder_pressure_new = 0;
1440 get_thing_temp: function(id_, create_if_not_found=false) {
1441 if (id_ in game.things_new) {
1442 return game.things_new[id_];
1443 } else if (create_if_not_found) {
1444 let t = new Thing([0,0]);
1445 game.things_new[id_] = t;
1449 get_thing: function(id_, create_if_not_found=false) {
1450 if (id_ in game.things) {
1451 return game.things[id_];
1454 move: function(start_position, direction) {
1455 let target = [start_position[0], start_position[1]];
1456 if (direction == 'LEFT') {
1458 } else if (direction == 'RIGHT') {
1460 } else if (game.map_geometry == 'Square') {
1461 if (direction == 'UP') {
1463 } else if (direction == 'DOWN') {
1466 } else if (game.map_geometry == 'Hex') {
1467 let start_indented = start_position[0] % 2;
1468 if (direction == 'UPLEFT') {
1470 if (!start_indented) {
1473 } else if (direction == 'UPRIGHT') {
1475 if (start_indented) {
1478 } else if (direction == 'DOWNLEFT') {
1480 if (!start_indented) {
1483 } else if (direction == 'DOWNRIGHT') {
1485 if (start_indented) {
1490 if (target[0] < 0 || target[1] < 0 ||
1491 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1496 teleport: function() {
1497 if (game.player.position in this.portals) {
1498 server.reconnect_to(this.portals[game.player.position]);
1500 terminal.blink_screen();
1501 tui.log_msg('? not standing on portal')
1509 server.init(websocket_location);
1514 annotations_new: {},
1516 move: function(direction) {
1517 let target = game.move(this.position, direction);
1519 this.position = target
1520 this.info_cached = false;
1521 if (tui.tile_draw) {
1522 this.send_tile_control_command();
1525 terminal.blink_screen();
1528 get_info: function() {
1529 if (this.info_cached) {
1530 return this.info_cached;
1532 let info_to_cache = '';
1533 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1534 if (game.fov[position_i] != '.') {
1535 info_to_cache += 'outside field of view';
1537 for (let t_id in game.things) {
1538 let t = game.things[t_id];
1539 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1540 info_to_cache += this.get_thing_info(t, true);
1543 let terrain_char = game.map[position_i]
1544 let terrain_desc = '?'
1545 if (game.terrains[terrain_char]) {
1546 terrain_desc = game.terrains[terrain_char];
1548 info_to_cache += 'TERRAIN: "' + terrain_char + '" (' + terrain_desc;
1549 let protection = game.map_control[position_i];
1550 if (protection != '.') {
1551 info_to_cache += '/protection:' + protection;
1553 info_to_cache += ')\n';
1554 if (this.position in game.portals) {
1555 info_to_cache += "PORTAL: " + game.portals[this.position] + "\n";
1557 if (this.position in this.annotations) {
1558 info_to_cache += "ANNOTATION: " + this.annotations[this.position];
1561 this.info_cached = info_to_cache;
1562 return this.info_cached;
1564 get_thing_info: function(t, detailed=false) {
1569 info += game.thing_types[t.type_];
1571 info += t.thing_char;
1574 info += ": " + t.name_;
1576 info += ' (' + t.type_;
1578 info += "/installed";
1580 if (t.type_ == 'Bottle') {
1581 if (t.thing_char == '_') {
1583 } else if (t.thing_char == '~') {
1588 const protection = t.protection;
1589 if (protection != '.') {
1590 info += '/protection:' + protection;
1593 if (t.hat || t.face) {
1594 info += '----------\n';
1597 info += '| ' + t.hat.slice(0, 6) + ' |\n';
1598 info += '| ' + t.hat.slice(6, 12) + ' |\n';
1599 info += '| ' + t.hat.slice(12, 18) + ' |\n';
1602 info += '| ' + t.face.slice(0, 6) + ' |\n';
1603 info += '| ' + t.face.slice(6, 12) + ' |\n';
1604 info += '| ' + t.face.slice(12, 18) + ' |\n';
1605 info += '----------\n';
1608 const line_length = t.design[0][1];
1609 info += '-'.repeat(line_length + 4) + '\n';
1611 if (line_length > 0) {
1612 const regexp = RegExp('.{1,' + line_length + '}', 'g');
1613 lines = t.design[1].match(regexp);
1615 for (const line of lines) {
1616 info += '| ' + line + ' |\n';
1618 info += '-'.repeat(line_length + 4) + '\n';
1625 annotate: function(msg) {
1626 if (msg.length == 0) {
1627 msg = " "; // triggers annotation deletion
1629 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1631 set_portal: function(msg) {
1632 if (msg.length == 0) {
1633 msg = " "; // triggers portal deletion
1635 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1637 send_tile_control_command: function() {
1638 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1642 tui.inputEl.addEventListener('input', (event) => {
1643 if (tui.mode.has_input_prompt) {
1644 let max_length = tui.right_window_width * terminal.rows - tui.input_prompt.length;
1645 if (tui.inputEl.value.length > max_length) {
1646 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1648 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1649 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1650 tui.switch_mode('edit');
1654 document.onclick = function() {
1655 if (!tui.mode.is_single_char_entry) {
1656 tui.show_help = false;
1659 tui.inputEl.addEventListener('keydown', (event) => {
1660 tui.show_help = false;
1661 if (['Enter', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
1662 event.preventDefault();
1664 if ((!tui.mode.is_intro && event.key == 'Escape')
1665 || (tui.mode.has_input_prompt && event.key == 'Enter'
1666 && tui.inputEl.value.length == 0
1667 && ['chat', 'command_thing', 'take_thing', 'drop_thing',
1668 'admin_enter'].includes(tui.mode.name))) {
1669 if (!['chat', 'play', 'study', 'edit'].includes(tui.mode.name)) {
1670 tui.log_msg('@ aborted');
1672 tui.switch_mode('play');
1673 } else if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1674 tui.show_help = true;
1675 tui.inputEl.value = "";
1676 tui.restore_input_values();
1677 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1678 && !tui.mode.is_single_char_entry) {
1679 tui.show_help = true;
1680 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1681 tui.login_name = tui.inputEl.value;
1682 server.send(['LOGIN', tui.inputEl.value]);
1683 tui.inputEl.value = "";
1684 } else if (tui.mode.name == 'enter_face' && event.key == 'Enter') {
1685 tui.enter_ascii_art('PLAYER_FACE', 3, 6);
1686 } else if (tui.mode.name == 'enter_design' && event.key == 'Enter') {
1687 if (game.player.carrying.type_ == 'Hat') {
1688 tui.enter_ascii_art('THING_DESIGN',
1689 game.player.carrying.design[0][0],
1690 game.player.carrying.design[0][1], true);
1692 tui.enter_ascii_art('THING_DESIGN',
1693 game.player.carrying.design[0][0],
1694 game.player.carrying.design[0][1], true, true);
1696 } else if (tui.mode.name == 'command_thing' && event.key == 'Enter') {
1697 server.send(['TASK:COMMAND', tui.inputEl.value]);
1698 tui.inputEl.value = "";
1699 } else if (tui.mode.name == 'take_thing' && event.key == 'Enter') {
1700 tui.pick_selectable('PICK_UP');
1701 } else if (tui.mode.name == 'drop_thing' && event.key == 'Enter') {
1702 tui.pick_selectable('DROP');
1703 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1704 if (tui.inputEl.value.length == 0) {
1705 tui.log_msg('@ aborted');
1707 server.send(['SET_MAP_CONTROL_PASSWORD',
1708 tui.tile_control_char, tui.inputEl.value]);
1709 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1711 tui.switch_mode('admin');
1712 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1713 explorer.set_portal(tui.inputEl.value);
1714 tui.switch_mode('edit');
1715 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1716 if (tui.inputEl.value.length == 0) {
1717 tui.inputEl.value = " ";
1719 server.send(["THING_NAME", tui.inputEl.value, tui.password]);
1720 tui.switch_mode('edit');
1721 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1722 explorer.annotate(tui.inputEl.value);
1723 tui.switch_mode('edit');
1724 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1725 if (tui.inputEl.value.length == 0) {
1726 tui.inputEl.value = " ";
1728 tui.password = tui.inputEl.value
1729 tui.switch_mode('edit');
1730 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1731 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1732 tui.switch_mode('play');
1733 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1734 if (tui.inputEl.value.length != 1) {
1735 tui.log_msg('@ entered non-single-char, therefore aborted');
1736 tui.switch_mode('admin');
1738 tui.tile_control_char = tui.inputEl.value[0];
1739 tui.switch_mode('control_pw_pw');
1741 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1742 if (tui.inputEl.value.length != 1) {
1743 tui.log_msg('@ entered non-single-char, therefore aborted');
1744 tui.switch_mode('admin');
1746 tui.tile_control_char = tui.inputEl.value[0];
1747 tui.switch_mode('control_tile_draw');
1749 } else if (tui.mode.name == 'admin_thing_protect' && event.key == 'Enter') {
1750 if (tui.inputEl.value.length != 1) {
1751 tui.log_msg('@ entered non-single-char, therefore aborted');
1753 server.send(['THING_PROTECTION', tui.inputEl.value])
1754 tui.log_msg('@ sent new protection character for thing');
1756 tui.switch_mode('admin');
1757 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1758 let tokens = parser.tokenize(tui.inputEl.value);
1759 if (tokens.length > 0 && tokens[0].length > 0) {
1760 if (tui.inputEl.value[0][0] == '/') {
1761 if (tokens[0].slice(1) == 'nick') {
1762 if (tokens.length > 1) {
1763 server.send(['NICK', tokens[1]]);
1765 tui.log_msg('? need new name');
1768 tui.log_msg('? unknown command');
1771 server.send(['ALL', tui.inputEl.value]);
1773 } else if (tui.inputEl.valuelength > 0) {
1774 server.send(['ALL', tui.inputEl.value]);
1776 tui.inputEl.value = "";
1777 } else if (tui.mode.name == 'play') {
1778 if (tui.mode.mode_switch_on_key(event)) {
1780 } else if (event.key === tui.keys.consume && tui.task_action_on('consume')) {
1781 server.send(["TASK:INTOXICATE"]);
1782 } else if (event.key === tui.keys.door && tui.task_action_on('door')) {
1783 server.send(["TASK:DOOR"]);
1784 } else if (event.key === tui.keys.wear && tui.task_action_on('wear')) {
1785 server.send(["TASK:WEAR"]);
1786 } else if (event.key === tui.keys.spin && tui.task_action_on('spin')) {
1787 server.send(["TASK:SPIN"]);
1788 } else if (event.key === tui.keys.dance && tui.task_action_on('dance')) {
1789 server.send(["TASK:DANCE"]);
1790 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1791 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1792 } else if (event.key === tui.keys.teleport) {
1795 } else if (tui.mode.name == 'study') {
1796 if (tui.mode.mode_switch_on_key(event)) {
1798 } else if (event.key in tui.movement_keys) {
1799 explorer.move(tui.movement_keys[event.key]);
1800 } else if (event.key == tui.keys.toggle_map_mode) {
1801 tui.toggle_map_mode();
1803 } else if (tui.mode.name == 'control_tile_draw') {
1804 if (tui.mode.mode_switch_on_key(event)) {
1806 } else if (event.key in tui.movement_keys) {
1807 explorer.move(tui.movement_keys[event.key]);
1808 } else if (event.key === tui.keys.toggle_tile_draw) {
1809 tui.toggle_tile_draw();
1811 } else if (tui.mode.name == 'admin') {
1812 if (tui.mode.mode_switch_on_key(event)) {
1814 } else if (event.key == tui.keys.toggle_map_mode) {
1815 tui.toggle_map_mode();
1816 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1817 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1819 } else if (tui.mode.name == 'edit') {
1820 if (tui.mode.mode_switch_on_key(event)) {
1822 } else if (event.key in tui.movement_keys && tui.task_action_on('move')) {
1823 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1824 } else if (event.key === tui.keys.flatten && tui.task_action_on('flatten')) {
1825 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1826 } else if (event.key === tui.keys.install && tui.task_action_on('install')) {
1827 server.send(["TASK:INSTALL", tui.password]);
1828 } else if (event.key == tui.keys.toggle_map_mode) {
1829 tui.toggle_map_mode();
1835 rows_selector.addEventListener('input', function() {
1836 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1839 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1840 terminal.initialize();
1843 cols_selector.addEventListener('input', function() {
1844 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1847 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1848 terminal.initialize();
1849 tui.reset_screen_size();
1852 for (let key_selector of key_selectors) {
1853 key_selector.addEventListener('input', function() {
1854 window.localStorage.setItem(key_selector.id, key_selector.value);
1858 window.setInterval(function() {
1859 if (server.websocket.readyState == 1) {
1860 server.send(['PING']);
1861 } else if (server.websocket.readyState != 0) {
1862 server.reconnect_to(server.url);
1863 tui.log_msg('@ attempting reconnect …')
1866 window.setInterval(function() {
1867 if (document.activeElement.tagName.toLowerCase() != 'input') {
1868 const scroll_x = window.scrollX;
1869 const scroll_y = window.scrollY;
1870 tui.inputEl.focus();
1871 window.scrollTo(scroll_x, scroll_y);
1874 document.getElementById("help").onclick = function() {
1875 tui.show_help = true;
1878 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1879 const mode = switchEl.id.slice("switch_to_".length);
1880 switchEl.onclick = function() {
1881 tui.switch_mode(mode);
1885 document.getElementById("toggle_tile_draw").onclick = function() {
1886 tui.toggle_tile_draw();
1888 document.getElementById("toggle_map_mode").onclick = function() {
1889 tui.toggle_map_mode();
1892 document.getElementById("flatten").onclick = function() {
1893 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1895 document.getElementById("door").onclick = function() {
1896 server.send(['TASK:DOOR']);
1898 document.getElementById("consume").onclick = function() {
1899 server.send(['TASK:INTOXICATE']);
1901 document.getElementById("install").onclick = function() {
1902 server.send(['TASK:INSTALL', tui.password]);
1904 document.getElementById("wear").onclick = function() {
1905 server.send(['TASK:WEAR']);
1907 document.getElementById("spin").onclick = function() {
1908 server.send(['TASK:SPIN']);
1910 document.getElementById("dance").onclick = function() {
1911 server.send(['TASK:DANCE']);
1913 document.getElementById("teleport").onclick = function() {
1916 for (const move_button of document.querySelectorAll('[id*="_move_"]')) {
1917 if (move_button.id.startsWith('key_')) { // not a move button
1920 let direction = move_button.id.split('_')[2].toUpperCase();
1923 if (tui.mode.available_actions.includes("move")) {
1924 server.send(['TASK:MOVE', direction]);
1925 } else if (tui.mode.available_actions.includes("move_explorer")) {
1926 explorer.move(direction);
1930 move_button.onmousedown = function() {
1932 move_repeat = window.setInterval(move, 100);
1934 move_button.onmouseup = function() {
1935 window.clearInterval(move_repeat);