Clean up code

This commit is contained in:
Idrees Hassan
2025-10-25 23:46:50 -04:00
parent 7f9c388853
commit b9304fe11b
4 changed files with 485 additions and 353 deletions

278
birb.js
View File

@@ -1,6 +1,5 @@
// @ts-check // @ts-check
// @ts-ignore
const SHARED_CONFIG = { const SHARED_CONFIG = {
birbCssScale: 1, birbCssScale: 1,
uiCssScale: 1, uiCssScale: 1,
@@ -9,7 +8,6 @@ const SHARED_CONFIG = {
hopDistance: 45, hopDistance: 45,
}; };
const DESKTOP_CONFIG = { const DESKTOP_CONFIG = {
flySpeed: 0.25 flySpeed: 0.25
}; };
@@ -28,17 +26,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale;
const UI_CSS_SCALE = CONFIG.uiCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale;
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
// Time in milliseconds until the user is considered AFK
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const SPRITE_HEIGHT = 32;
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
birbMode: false birbMode: false
@@ -48,6 +35,23 @@ const DEFAULT_SETTINGS = {
* @typedef {typeof DEFAULT_SETTINGS} Settings * @typedef {typeof DEFAULT_SETTINGS} Settings
*/ */
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
* @property {string} site
* @property {string} content
* @property {number} top
* @property {number} left
*/
/**
* @typedef {Object} BirbSaveData
* @property {string[]} unlockedSpecies
* @property {string} currentSpecies
* @property {Partial<Settings>} settings
* @property {SavedStickyNote[]} [stickyNotes]
*/
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -380,6 +384,7 @@ const Directions = {
}; };
const SPRITE_WIDTH = 32; const SPRITE_WIDTH = 32;
const SPRITE_HEIGHT = 32;
const DECORATIONS_SPRITE_WIDTH = 48; const DECORATIONS_SPRITE_WIDTH = 48;
const FEATHER_SPRITE_WIDTH = 32; const FEATHER_SPRITE_WIDTH = 32;
@@ -387,6 +392,34 @@ const SPRITE_SHEET = "__SPRITE_SHEET__";
const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__"; const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__";
const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__";
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
/** Speed at which the feather falls per tick */
const FEATHER_FALL_SPEED = 1;
/** Time in milliseconds until the user is considered AFK */
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
// Per-frame chances
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
/** Multiplier after petting that increases the feather drop chance */
const PET_FEATHER_BOOST = 2;
/** How long the pet boost lasts in milliseconds */
const PET_BOOST_DURATION = 1000 * 60 * 5;
const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80;
/** Time between checking whether the URL has changed */
const URL_CHECK_INTERVAL = 500;
/** Time after petting before the menu can be opened */
const PET_MENU_COOLDOWN = 1000;
/** /**
* Load the sprite sheet and return the pixel-map template * Load the sprite sheet and return the pixel-map template
* @param {string} dataUri * @param {string} dataUri
@@ -445,7 +478,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
log("Loading sprite sheets..."); log("Loading sprite sheets...");
// @ts-ignore // @ts-expect-error
Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => { Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => {
const SPRITE_SHEET = birbPixels; const SPRITE_SHEET = birbPixels;
const DECORATIONS_SPRITE_SHEET = decorationPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels;
@@ -631,7 +664,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
// @ts-ignore // @ts-expect-error
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const States = { const States = {
@@ -672,7 +705,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @returns {boolean} Whether the script is running in a userscript extension context * @returns {boolean} Whether the script is running in a userscript extension context
*/ */
function isUserScript() { function isUserScript() {
// @ts-ignore // @ts-expect-error
return typeof GM_getValue === "function"; return typeof GM_getValue === "function";
} }
@@ -685,9 +718,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function load() { function load() {
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
let saveData = {}; let saveData = {};
if (isUserScript()) { if (isUserScript()) {
log("Loading save data from UserScript storage"); log("Loading save data from UserScript storage");
// @ts-ignore // @ts-expect-error
saveData = GM_getValue("birbSaveData", {}) ?? {}; saveData = GM_getValue("birbSaveData", {}) ?? {};
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, loading save data from localStorage"); log("Test environment detected, loading save data from localStorage");
@@ -695,14 +729,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} else { } else {
log("Not a UserScript"); log("Not a UserScript");
} }
debug("Loaded data: " + JSON.stringify(saveData)); debug("Loaded data: " + JSON.stringify(saveData));
if (!saveData.settings) { if (!saveData.settings) {
log("No user settings found in save data, starting fresh"); log("No user settings found in save data, starting fresh");
} }
userSettings = saveData.settings ?? {}; userSettings = saveData.settings ?? {};
unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD];
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
stickyNotes = []; stickyNotes = [];
if (saveData.stickyNotes) { if (saveData.stickyNotes) {
for (let note of saveData.stickyNotes) { for (let note of saveData.stickyNotes) {
if (note.id) { if (note.id) {
@@ -710,17 +748,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
} }
log(stickyNotes.length + " sticky notes loaded"); log(stickyNotes.length + " sticky notes loaded");
switchSpecies(currentSpecies); switchSpecies(currentSpecies);
} }
function save() { function save() {
/** @type {Record<string, any>} */ /** @type {BirbSaveData} */
let saveData = { const saveData = {
unlockedSpecies: unlockedSpecies, unlockedSpecies,
currentSpecies: currentSpecies, currentSpecies,
settings: userSettings settings: userSettings
}; };
if (stickyNotes.length > 0) { if (stickyNotes.length > 0) {
saveData.stickyNotes = stickyNotes.map(note => ({ saveData.stickyNotes = stickyNotes.map(note => ({
id: note.id, id: note.id,
@@ -730,9 +770,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
left: note.left left: note.left
})); }));
} }
if (isUserScript()) { if (isUserScript()) {
log("Saving data to UserScript storage"); log("Saving data to UserScript storage");
// @ts-ignore // @ts-expect-error
GM_setValue("birbSaveData", saveData); GM_setValue("birbSaveData", saveData);
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, saving data to localStorage"); log("Test environment detected, saving data to localStorage");
@@ -745,7 +786,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function resetSaveData() { function resetSaveData() {
if (isUserScript()) { if (isUserScript()) {
log("Resetting save data in UserScript storage"); log("Resetting save data in UserScript storage");
// @ts-ignore // @ts-expect-error
GM_deleteValue("birbSaveData"); GM_deleteValue("birbSaveData");
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, resetting save data in localStorage"); log("Test environment detected, resetting save data in localStorage");
@@ -871,17 +912,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return false; return false;
} }
/** @type {Record<string, string>} */ const stickyNoteParams = parseUrlParams(stickyNoteUrl);
const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { const currentParams = parseUrlParams(currentUrl);
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
/** @type {Record<string, string>} */
const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
debug("Comparing params: ", stickyNoteParams, currentParams); debug("Comparing params: ", stickyNoteParams, currentParams);
@@ -943,7 +975,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
}); });
onClick(canvas, () => { onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < 1000)) { if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu // Currently being pet, don't open menu
return; return;
} }
@@ -980,9 +1012,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
lastUrl = currentUrl; lastUrl = currentUrl;
drawStickyNotes(); drawStickyNotes();
} }
}, 500); }, URL_CHECK_INTERVAL);
setInterval(update, 1000 / 60); setInterval(update, UPDATE_INTERVAL);
} }
function drawStickyNotes() { function drawStickyNotes() {
@@ -1007,7 +1039,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop(); hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) { } else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something // Idle for a while, do something
@@ -1015,7 +1047,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
// Fly to an element // Fly to an element
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
@@ -1026,9 +1058,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
setState(States.IDLE); setState(States.IDLE);
} }
} }
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 1 every 2 hours (ticks * seconds * minutes * hours)
// Double the chance of a feather if recently pet // Double the chance of a feather if recently pet
let petMod = Date.now() - lastPetTimestamp < 1000 * 60 * 5 ? 2 : 1; const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) { if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0; lastPetTimestamp = 0;
activateFeather(); activateFeather();
@@ -1099,6 +1131,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return element; return element;
} }
/**
* Create a window element with header and content
* @param {string} id
* @param {string} title
* @param {string} contentHtml
* @param {() => void} [onClose]
* @returns {HTMLElement}
*/
function createWindow(id, title, contentHtml, onClose) {
const window = makeElement("birb-window", undefined, id);
window.innerHTML = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
${contentHtml}
</div>
`;
document.body.appendChild(window);
makeDraggable(window.querySelector(".birb-window-header"));
const closeButton = window.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
}
return window;
}
function insertDecoration() { function insertDecoration() {
// Create a canvas element for the decoration // Create a canvas element for the decoration
const decorationCanvas = document.createElement("canvas"); const decorationCanvas = document.createElement("canvas");
@@ -1178,21 +1246,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function updateFeather() { function updateFeather() {
const feather = document.querySelector("#birb-feather"); const feather = document.querySelector("#birb-feather");
const featherGravity = 1;
if (!feather || !(feather instanceof HTMLElement)) { if (!feather || !(feather instanceof HTMLElement)) {
return; return;
} }
const y = parseInt(feather.style.top || "0") + featherGravity; const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) { if (y < window.innerHeight - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
} }
} }
// insertDecoration();
// insertFieldGuide();
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element
*/ */
@@ -1209,28 +1272,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
if (document.querySelector("#" + FIELD_GUIDE_ID)) { if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return; return;
} }
let html = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<div class="birb-message-content">
${message}
</div>
</div>`
const modal = makeElement("birb-window");
modal.style.width = "270px";
modal.innerHTML = html;
document.body.appendChild(modal);
makeDraggable(modal.querySelector(".birb-window-header"));
const closeButton = modal.querySelector(".birb-window-close"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -1647,12 +1696,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const elements = document.querySelectorAll("img, video"); const elements = document.querySelectorAll("img, video");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
const rect = img.getBoundingClientRect(); const rect = img.getBoundingClientRect();
return rect.left >= 0 && rect.top >= 80 && rect.right <= window.innerWidth && rect.top <= window.innerHeight; return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
}); });
const MIN_WIDTH = 100;
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-ignore // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) { if (largeElements.length === 0) {
return; return;
} }
@@ -1669,12 +1717,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -1782,49 +1826,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
}).catch((e) => { }).catch((e) => {
error("Error while loading sprite sheets: ", e); error("Error while loading sprite sheets: ", e);
}); });
/**
* @param {number} start
* @param {number} end
* @param {number} amount
* @returns {number}
*/
function linearLerp(start, end, amount) {
return start + (end - start) * amount;
}
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

278
dist/birb.js vendored
View File

@@ -1,6 +1,5 @@
// @ts-check // @ts-check
// @ts-ignore
const SHARED_CONFIG = { const SHARED_CONFIG = {
birbCssScale: 1, birbCssScale: 1,
uiCssScale: 1, uiCssScale: 1,
@@ -9,7 +8,6 @@ const SHARED_CONFIG = {
hopDistance: 45, hopDistance: 45,
}; };
const DESKTOP_CONFIG = { const DESKTOP_CONFIG = {
flySpeed: 0.25 flySpeed: 0.25
}; };
@@ -28,17 +26,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale;
const UI_CSS_SCALE = CONFIG.uiCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale;
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
// Time in milliseconds until the user is considered AFK
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const SPRITE_HEIGHT = 32;
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
birbMode: false birbMode: false
@@ -48,6 +35,23 @@ const DEFAULT_SETTINGS = {
* @typedef {typeof DEFAULT_SETTINGS} Settings * @typedef {typeof DEFAULT_SETTINGS} Settings
*/ */
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
* @property {string} site
* @property {string} content
* @property {number} top
* @property {number} left
*/
/**
* @typedef {Object} BirbSaveData
* @property {string[]} unlockedSpecies
* @property {string} currentSpecies
* @property {Partial<Settings>} settings
* @property {SavedStickyNote[]} [stickyNotes]
*/
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -723,6 +727,7 @@ const Directions = {
}; };
const SPRITE_WIDTH = 32; const SPRITE_WIDTH = 32;
const SPRITE_HEIGHT = 32;
const DECORATIONS_SPRITE_WIDTH = 48; const DECORATIONS_SPRITE_WIDTH = 48;
const FEATHER_SPRITE_WIDTH = 32; const FEATHER_SPRITE_WIDTH = 32;
@@ -730,6 +735,34 @@ const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYA
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg=="; const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
/** Speed at which the feather falls per tick */
const FEATHER_FALL_SPEED = 1;
/** Time in milliseconds until the user is considered AFK */
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
// Per-frame chances
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
/** Multiplier after petting that increases the feather drop chance */
const PET_FEATHER_BOOST = 2;
/** How long the pet boost lasts in milliseconds */
const PET_BOOST_DURATION = 1000 * 60 * 5;
const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80;
/** Time between checking whether the URL has changed */
const URL_CHECK_INTERVAL = 500;
/** Time after petting before the menu can be opened */
const PET_MENU_COOLDOWN = 1000;
/** /**
* Load the sprite sheet and return the pixel-map template * Load the sprite sheet and return the pixel-map template
* @param {string} dataUri * @param {string} dataUri
@@ -788,7 +821,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
log("Loading sprite sheets..."); log("Loading sprite sheets...");
// @ts-ignore // @ts-expect-error
Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => { Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => {
const SPRITE_SHEET = birbPixels; const SPRITE_SHEET = birbPixels;
const DECORATIONS_SPRITE_SHEET = decorationPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels;
@@ -974,7 +1007,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
// @ts-ignore // @ts-expect-error
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const States = { const States = {
@@ -1015,7 +1048,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @returns {boolean} Whether the script is running in a userscript extension context * @returns {boolean} Whether the script is running in a userscript extension context
*/ */
function isUserScript() { function isUserScript() {
// @ts-ignore // @ts-expect-error
return typeof GM_getValue === "function"; return typeof GM_getValue === "function";
} }
@@ -1028,9 +1061,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function load() { function load() {
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
let saveData = {}; let saveData = {};
if (isUserScript()) { if (isUserScript()) {
log("Loading save data from UserScript storage"); log("Loading save data from UserScript storage");
// @ts-ignore // @ts-expect-error
saveData = GM_getValue("birbSaveData", {}) ?? {}; saveData = GM_getValue("birbSaveData", {}) ?? {};
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, loading save data from localStorage"); log("Test environment detected, loading save data from localStorage");
@@ -1038,14 +1072,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} else { } else {
log("Not a UserScript"); log("Not a UserScript");
} }
debug("Loaded data: " + JSON.stringify(saveData)); debug("Loaded data: " + JSON.stringify(saveData));
if (!saveData.settings) { if (!saveData.settings) {
log("No user settings found in save data, starting fresh"); log("No user settings found in save data, starting fresh");
} }
userSettings = saveData.settings ?? {}; userSettings = saveData.settings ?? {};
unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD];
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
stickyNotes = []; stickyNotes = [];
if (saveData.stickyNotes) { if (saveData.stickyNotes) {
for (let note of saveData.stickyNotes) { for (let note of saveData.stickyNotes) {
if (note.id) { if (note.id) {
@@ -1053,17 +1091,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
} }
log(stickyNotes.length + " sticky notes loaded"); log(stickyNotes.length + " sticky notes loaded");
switchSpecies(currentSpecies); switchSpecies(currentSpecies);
} }
function save() { function save() {
/** @type {Record<string, any>} */ /** @type {BirbSaveData} */
let saveData = { const saveData = {
unlockedSpecies: unlockedSpecies, unlockedSpecies,
currentSpecies: currentSpecies, currentSpecies,
settings: userSettings settings: userSettings
}; };
if (stickyNotes.length > 0) { if (stickyNotes.length > 0) {
saveData.stickyNotes = stickyNotes.map(note => ({ saveData.stickyNotes = stickyNotes.map(note => ({
id: note.id, id: note.id,
@@ -1073,9 +1113,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
left: note.left left: note.left
})); }));
} }
if (isUserScript()) { if (isUserScript()) {
log("Saving data to UserScript storage"); log("Saving data to UserScript storage");
// @ts-ignore // @ts-expect-error
GM_setValue("birbSaveData", saveData); GM_setValue("birbSaveData", saveData);
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, saving data to localStorage"); log("Test environment detected, saving data to localStorage");
@@ -1088,7 +1129,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function resetSaveData() { function resetSaveData() {
if (isUserScript()) { if (isUserScript()) {
log("Resetting save data in UserScript storage"); log("Resetting save data in UserScript storage");
// @ts-ignore // @ts-expect-error
GM_deleteValue("birbSaveData"); GM_deleteValue("birbSaveData");
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, resetting save data in localStorage"); log("Test environment detected, resetting save data in localStorage");
@@ -1214,17 +1255,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return false; return false;
} }
/** @type {Record<string, string>} */ const stickyNoteParams = parseUrlParams(stickyNoteUrl);
const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { const currentParams = parseUrlParams(currentUrl);
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
/** @type {Record<string, string>} */
const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
debug("Comparing params: ", stickyNoteParams, currentParams); debug("Comparing params: ", stickyNoteParams, currentParams);
@@ -1286,7 +1318,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
}); });
onClick(canvas, () => { onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < 1000)) { if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu // Currently being pet, don't open menu
return; return;
} }
@@ -1323,9 +1355,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
lastUrl = currentUrl; lastUrl = currentUrl;
drawStickyNotes(); drawStickyNotes();
} }
}, 500); }, URL_CHECK_INTERVAL);
setInterval(update, 1000 / 60); setInterval(update, UPDATE_INTERVAL);
} }
function drawStickyNotes() { function drawStickyNotes() {
@@ -1350,7 +1382,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop(); hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) { } else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something // Idle for a while, do something
@@ -1358,7 +1390,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
// Fly to an element // Fly to an element
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
@@ -1369,9 +1401,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
setState(States.IDLE); setState(States.IDLE);
} }
} }
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 1 every 2 hours (ticks * seconds * minutes * hours)
// Double the chance of a feather if recently pet // Double the chance of a feather if recently pet
let petMod = Date.now() - lastPetTimestamp < 1000 * 60 * 5 ? 2 : 1; const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) { if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0; lastPetTimestamp = 0;
activateFeather(); activateFeather();
@@ -1442,6 +1474,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return element; return element;
} }
/**
* Create a window element with header and content
* @param {string} id
* @param {string} title
* @param {string} contentHtml
* @param {() => void} [onClose]
* @returns {HTMLElement}
*/
function createWindow(id, title, contentHtml, onClose) {
const window = makeElement("birb-window", undefined, id);
window.innerHTML = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
${contentHtml}
</div>
`;
document.body.appendChild(window);
makeDraggable(window.querySelector(".birb-window-header"));
const closeButton = window.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
}
return window;
}
function insertDecoration() { function insertDecoration() {
// Create a canvas element for the decoration // Create a canvas element for the decoration
const decorationCanvas = document.createElement("canvas"); const decorationCanvas = document.createElement("canvas");
@@ -1521,21 +1589,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function updateFeather() { function updateFeather() {
const feather = document.querySelector("#birb-feather"); const feather = document.querySelector("#birb-feather");
const featherGravity = 1;
if (!feather || !(feather instanceof HTMLElement)) { if (!feather || !(feather instanceof HTMLElement)) {
return; return;
} }
const y = parseInt(feather.style.top || "0") + featherGravity; const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) { if (y < window.innerHeight - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
} }
} }
// insertDecoration();
// insertFieldGuide();
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element
*/ */
@@ -1552,28 +1615,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
if (document.querySelector("#" + FIELD_GUIDE_ID)) { if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return; return;
} }
let html = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<div class="birb-message-content">
${message}
</div>
</div>`
const modal = makeElement("birb-window");
modal.style.width = "270px";
modal.innerHTML = html;
document.body.appendChild(modal);
makeDraggable(modal.querySelector(".birb-window-header"));
const closeButton = modal.querySelector(".birb-window-close"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -1990,12 +2039,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const elements = document.querySelectorAll("img, video"); const elements = document.querySelectorAll("img, video");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
const rect = img.getBoundingClientRect(); const rect = img.getBoundingClientRect();
return rect.left >= 0 && rect.top >= 80 && rect.right <= window.innerWidth && rect.top <= window.innerHeight; return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
}); });
const MIN_WIDTH = 100;
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-ignore // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) { if (largeElements.length === 0) {
return; return;
} }
@@ -2012,12 +2060,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -2125,49 +2169,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
}).catch((e) => { }).catch((e) => {
error("Error while loading sprite sheets: ", e); error("Error while loading sprite sheets: ", e);
}); });
/**
* @param {number} start
* @param {number} end
* @param {number} amount
* @returns {number}
*/
function linearLerp(start, end, amount) {
return start + (end - start) * amount;
}
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

280
dist/birb.user.js vendored
View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version 2025.10.25.15 // @version 2025.10.25.117
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
@@ -14,7 +14,6 @@
// @ts-check // @ts-check
// @ts-ignore
const SHARED_CONFIG = { const SHARED_CONFIG = {
birbCssScale: 1, birbCssScale: 1,
uiCssScale: 1, uiCssScale: 1,
@@ -23,7 +22,6 @@ const SHARED_CONFIG = {
hopDistance: 45, hopDistance: 45,
}; };
const DESKTOP_CONFIG = { const DESKTOP_CONFIG = {
flySpeed: 0.25 flySpeed: 0.25
}; };
@@ -42,17 +40,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale;
const UI_CSS_SCALE = CONFIG.uiCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale;
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
// Time in milliseconds until the user is considered AFK
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const SPRITE_HEIGHT = 32;
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
birbMode: false birbMode: false
@@ -62,6 +49,23 @@ const DEFAULT_SETTINGS = {
* @typedef {typeof DEFAULT_SETTINGS} Settings * @typedef {typeof DEFAULT_SETTINGS} Settings
*/ */
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
* @property {string} site
* @property {string} content
* @property {number} top
* @property {number} left
*/
/**
* @typedef {Object} BirbSaveData
* @property {string[]} unlockedSpecies
* @property {string} currentSpecies
* @property {Partial<Settings>} settings
* @property {SavedStickyNote[]} [stickyNotes]
*/
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -737,6 +741,7 @@ const Directions = {
}; };
const SPRITE_WIDTH = 32; const SPRITE_WIDTH = 32;
const SPRITE_HEIGHT = 32;
const DECORATIONS_SPRITE_WIDTH = 48; const DECORATIONS_SPRITE_WIDTH = 48;
const FEATHER_SPRITE_WIDTH = 32; const FEATHER_SPRITE_WIDTH = 32;
@@ -744,6 +749,34 @@ const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYA
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg=="; const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
/** Speed at which the feather falls per tick */
const FEATHER_FALL_SPEED = 1;
/** Time in milliseconds until the user is considered AFK */
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
// Per-frame chances
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
/** Multiplier after petting that increases the feather drop chance */
const PET_FEATHER_BOOST = 2;
/** How long the pet boost lasts in milliseconds */
const PET_BOOST_DURATION = 1000 * 60 * 5;
const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80;
/** Time between checking whether the URL has changed */
const URL_CHECK_INTERVAL = 500;
/** Time after petting before the menu can be opened */
const PET_MENU_COOLDOWN = 1000;
/** /**
* Load the sprite sheet and return the pixel-map template * Load the sprite sheet and return the pixel-map template
* @param {string} dataUri * @param {string} dataUri
@@ -802,7 +835,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
log("Loading sprite sheets..."); log("Loading sprite sheets...");
// @ts-ignore // @ts-expect-error
Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => { Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => {
const SPRITE_SHEET = birbPixels; const SPRITE_SHEET = birbPixels;
const DECORATIONS_SPRITE_SHEET = decorationPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels;
@@ -988,7 +1021,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
// @ts-ignore // @ts-expect-error
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const States = { const States = {
@@ -1029,7 +1062,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @returns {boolean} Whether the script is running in a userscript extension context * @returns {boolean} Whether the script is running in a userscript extension context
*/ */
function isUserScript() { function isUserScript() {
// @ts-ignore // @ts-expect-error
return typeof GM_getValue === "function"; return typeof GM_getValue === "function";
} }
@@ -1042,9 +1075,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function load() { function load() {
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
let saveData = {}; let saveData = {};
if (isUserScript()) { if (isUserScript()) {
log("Loading save data from UserScript storage"); log("Loading save data from UserScript storage");
// @ts-ignore // @ts-expect-error
saveData = GM_getValue("birbSaveData", {}) ?? {}; saveData = GM_getValue("birbSaveData", {}) ?? {};
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, loading save data from localStorage"); log("Test environment detected, loading save data from localStorage");
@@ -1052,14 +1086,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} else { } else {
log("Not a UserScript"); log("Not a UserScript");
} }
debug("Loaded data: " + JSON.stringify(saveData)); debug("Loaded data: " + JSON.stringify(saveData));
if (!saveData.settings) { if (!saveData.settings) {
log("No user settings found in save data, starting fresh"); log("No user settings found in save data, starting fresh");
} }
userSettings = saveData.settings ?? {}; userSettings = saveData.settings ?? {};
unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD];
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
stickyNotes = []; stickyNotes = [];
if (saveData.stickyNotes) { if (saveData.stickyNotes) {
for (let note of saveData.stickyNotes) { for (let note of saveData.stickyNotes) {
if (note.id) { if (note.id) {
@@ -1067,17 +1105,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
} }
log(stickyNotes.length + " sticky notes loaded"); log(stickyNotes.length + " sticky notes loaded");
switchSpecies(currentSpecies); switchSpecies(currentSpecies);
} }
function save() { function save() {
/** @type {Record<string, any>} */ /** @type {BirbSaveData} */
let saveData = { const saveData = {
unlockedSpecies: unlockedSpecies, unlockedSpecies,
currentSpecies: currentSpecies, currentSpecies,
settings: userSettings settings: userSettings
}; };
if (stickyNotes.length > 0) { if (stickyNotes.length > 0) {
saveData.stickyNotes = stickyNotes.map(note => ({ saveData.stickyNotes = stickyNotes.map(note => ({
id: note.id, id: note.id,
@@ -1087,9 +1127,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
left: note.left left: note.left
})); }));
} }
if (isUserScript()) { if (isUserScript()) {
log("Saving data to UserScript storage"); log("Saving data to UserScript storage");
// @ts-ignore // @ts-expect-error
GM_setValue("birbSaveData", saveData); GM_setValue("birbSaveData", saveData);
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, saving data to localStorage"); log("Test environment detected, saving data to localStorage");
@@ -1102,7 +1143,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function resetSaveData() { function resetSaveData() {
if (isUserScript()) { if (isUserScript()) {
log("Resetting save data in UserScript storage"); log("Resetting save data in UserScript storage");
// @ts-ignore // @ts-expect-error
GM_deleteValue("birbSaveData"); GM_deleteValue("birbSaveData");
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, resetting save data in localStorage"); log("Test environment detected, resetting save data in localStorage");
@@ -1228,17 +1269,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return false; return false;
} }
/** @type {Record<string, string>} */ const stickyNoteParams = parseUrlParams(stickyNoteUrl);
const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { const currentParams = parseUrlParams(currentUrl);
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
/** @type {Record<string, string>} */
const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
debug("Comparing params: ", stickyNoteParams, currentParams); debug("Comparing params: ", stickyNoteParams, currentParams);
@@ -1300,7 +1332,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
}); });
onClick(canvas, () => { onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < 1000)) { if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu // Currently being pet, don't open menu
return; return;
} }
@@ -1337,9 +1369,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
lastUrl = currentUrl; lastUrl = currentUrl;
drawStickyNotes(); drawStickyNotes();
} }
}, 500); }, URL_CHECK_INTERVAL);
setInterval(update, 1000 / 60); setInterval(update, UPDATE_INTERVAL);
} }
function drawStickyNotes() { function drawStickyNotes() {
@@ -1364,7 +1396,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop(); hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) { } else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something // Idle for a while, do something
@@ -1372,7 +1404,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
// Fly to an element // Fly to an element
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
@@ -1383,9 +1415,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
setState(States.IDLE); setState(States.IDLE);
} }
} }
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 1 every 2 hours (ticks * seconds * minutes * hours)
// Double the chance of a feather if recently pet // Double the chance of a feather if recently pet
let petMod = Date.now() - lastPetTimestamp < 1000 * 60 * 5 ? 2 : 1; const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) { if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0; lastPetTimestamp = 0;
activateFeather(); activateFeather();
@@ -1456,6 +1488,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return element; return element;
} }
/**
* Create a window element with header and content
* @param {string} id
* @param {string} title
* @param {string} contentHtml
* @param {() => void} [onClose]
* @returns {HTMLElement}
*/
function createWindow(id, title, contentHtml, onClose) {
const window = makeElement("birb-window", undefined, id);
window.innerHTML = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
${contentHtml}
</div>
`;
document.body.appendChild(window);
makeDraggable(window.querySelector(".birb-window-header"));
const closeButton = window.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
}
return window;
}
function insertDecoration() { function insertDecoration() {
// Create a canvas element for the decoration // Create a canvas element for the decoration
const decorationCanvas = document.createElement("canvas"); const decorationCanvas = document.createElement("canvas");
@@ -1535,21 +1603,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function updateFeather() { function updateFeather() {
const feather = document.querySelector("#birb-feather"); const feather = document.querySelector("#birb-feather");
const featherGravity = 1;
if (!feather || !(feather instanceof HTMLElement)) { if (!feather || !(feather instanceof HTMLElement)) {
return; return;
} }
const y = parseInt(feather.style.top || "0") + featherGravity; const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) { if (y < window.innerHeight - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
} }
} }
// insertDecoration();
// insertFieldGuide();
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element
*/ */
@@ -1566,28 +1629,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
if (document.querySelector("#" + FIELD_GUIDE_ID)) { if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return; return;
} }
let html = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<div class="birb-message-content">
${message}
</div>
</div>`
const modal = makeElement("birb-window");
modal.style.width = "270px";
modal.innerHTML = html;
document.body.appendChild(modal);
makeDraggable(modal.querySelector(".birb-window-header"));
const closeButton = modal.querySelector(".birb-window-close"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -2004,12 +2053,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const elements = document.querySelectorAll("img, video"); const elements = document.querySelectorAll("img, video");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
const rect = img.getBoundingClientRect(); const rect = img.getBoundingClientRect();
return rect.left >= 0 && rect.top >= 80 && rect.right <= window.innerWidth && rect.top <= window.innerHeight; return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
}); });
const MIN_WIDTH = 100;
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-ignore // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) { if (largeElements.length === 0) {
return; return;
} }
@@ -2026,12 +2074,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -2139,49 +2183,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
}).catch((e) => { }).catch((e) => {
error("Error while loading sprite sheets: ", e); error("Error while loading sprite sheets: ", e);
}); });
/**
* @param {number} start
* @param {number} end
* @param {number} amount
* @returns {number}
*/
function linearLerp(start, end, amount) {
return start + (end - start) * amount;
}
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Pocket Bird", "name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?", "description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.25.15", "version": "2025.10.25.117",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"content_scripts": [ "content_scripts": [
{ {