From b9304fe11b8dea7935f5f6bd5f426a32c682d229 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sat, 25 Oct 2025 23:46:50 -0400 Subject: [PATCH] Clean up code --- birb.js | 278 ++++++++++++++++++++++++++------------------- dist/birb.js | 278 ++++++++++++++++++++++++++------------------- dist/birb.user.js | 280 +++++++++++++++++++++++++++------------------- manifest.json | 2 +- 4 files changed, 485 insertions(+), 353 deletions(-) diff --git a/birb.js b/birb.js index 4f1e789..795f1cb 100644 --- a/birb.js +++ b/birb.js @@ -1,6 +1,5 @@ // @ts-check -// @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, uiCssScale: 1, @@ -9,7 +8,6 @@ const SHARED_CONFIG = { hopDistance: 45, }; - const DESKTOP_CONFIG = { flySpeed: 0.25 }; @@ -28,17 +26,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; 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 = { birbMode: false @@ -48,6 +35,23 @@ const DEFAULT_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 + * @property {SavedStickyNote[]} [stickyNotes] + */ + /** @type {Partial} */ let userSettings = {}; @@ -380,6 +384,7 @@ const Directions = { }; const SPRITE_WIDTH = 32; +const SPRITE_HEIGHT = 32; const DECORATIONS_SPRITE_WIDTH = 48; const FEATHER_SPRITE_WIDTH = 32; @@ -387,6 +392,34 @@ const SPRITE_SHEET = "__SPRITE_SHEET__"; const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_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 * @param {string} dataUri @@ -445,7 +478,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { 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]) => { const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; @@ -631,7 +664,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-ignore + // @ts-expect-error const ctx = canvas.getContext("2d"); 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 */ function isUserScript() { - // @ts-ignore + // @ts-expect-error return typeof GM_getValue === "function"; } @@ -685,9 +718,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function load() { /** @type {Record} */ let saveData = {}; + if (isUserScript()) { log("Loading save data from UserScript storage"); - // @ts-ignore + // @ts-expect-error saveData = GM_getValue("birbSaveData", {}) ?? {}; } else if (isTestEnvironment()) { log("Test environment detected, loading save data from localStorage"); @@ -695,14 +729,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } else { log("Not a UserScript"); } + debug("Loaded data: " + JSON.stringify(saveData)); + if (!saveData.settings) { log("No user settings found in save data, starting fresh"); } + userSettings = saveData.settings ?? {}; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; stickyNotes = []; + if (saveData.stickyNotes) { for (let note of saveData.stickyNotes) { if (note.id) { @@ -710,17 +748,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } } + log(stickyNotes.length + " sticky notes loaded"); switchSpecies(currentSpecies); } function save() { - /** @type {Record} */ - let saveData = { - unlockedSpecies: unlockedSpecies, - currentSpecies: currentSpecies, + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, settings: userSettings }; + if (stickyNotes.length > 0) { saveData.stickyNotes = stickyNotes.map(note => ({ id: note.id, @@ -730,9 +770,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI left: note.left })); } + if (isUserScript()) { log("Saving data to UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_setValue("birbSaveData", saveData); } else if (isTestEnvironment()) { log("Test environment detected, saving data to localStorage"); @@ -745,7 +786,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function resetSaveData() { if (isUserScript()) { log("Resetting save data in UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_deleteValue("birbSaveData"); } else if (isTestEnvironment()) { log("Test environment detected, resetting save data in localStorage"); @@ -871,17 +912,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return false; } - /** @type {Record} */ - const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - - /** @type {Record} */ - const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); debug("Comparing params: ", stickyNoteParams, currentParams); @@ -943,7 +975,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI }); 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 return; } @@ -980,9 +1012,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI lastUrl = currentUrl; drawStickyNotes(); } - }, 500); + }, URL_CHECK_INTERVAL); - setInterval(update, 1000 / 60); + setInterval(update, UPDATE_INTERVAL); } function drawStickyNotes() { @@ -1007,7 +1039,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -1015,7 +1047,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Fly to an element focusOnElement(); 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 focusOnElement(); lastActionTimestamp = Date.now(); @@ -1026,9 +1058,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 - 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) { lastPetTimestamp = 0; activateFeather(); @@ -1099,6 +1131,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 = ` +
+
${title}
+
x
+
+
+ ${contentHtml} +
+ `; + + 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() { // Create a canvas element for the decoration const decorationCanvas = document.createElement("canvas"); @@ -1178,21 +1246,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function updateFeather() { const feather = document.querySelector("#birb-feather"); - const featherGravity = 1; if (!feather || !(feather instanceof HTMLElement)) { 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`; if (y < window.innerHeight - feather.offsetHeight) { feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; } } - - // insertDecoration(); - // insertFieldGuide(); - /** * @param {HTMLElement} element */ @@ -1209,28 +1272,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; } - let html = ` -
-
${title}
-
x
-
-
-
- ${message} -
-
` - 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"); - if (closeButton) { - makeClosable(() => { - modal.remove(); - }, closeButton); - } + const modal = createWindow("birb-modal", title, ` +
+ ${message} +
+ `); + + modal.style.width = "270px"; centerElement(modal); } @@ -1647,12 +1696,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const elements = document.querySelectorAll("img, video"); const inWindow = Array.from(elements).filter((img) => { 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[]} */ - // @ts-ignore - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); if (largeElements.length === 0) { return; } @@ -1669,12 +1717,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; return; } - const rect = focusedElement.getBoundingClientRect(); - focusedBounds = { - left: rect.left, - right: rect.right, - top: rect.top - }; + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; } function getCanvasWidth() { @@ -1782,49 +1826,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } 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} + */ + 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) => { 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 */ diff --git a/dist/birb.js b/dist/birb.js index c5cb424..d525656 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,6 +1,5 @@ // @ts-check -// @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, uiCssScale: 1, @@ -9,7 +8,6 @@ const SHARED_CONFIG = { hopDistance: 45, }; - const DESKTOP_CONFIG = { flySpeed: 0.25 }; @@ -28,17 +26,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; 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 = { birbMode: false @@ -48,6 +35,23 @@ const DEFAULT_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 + * @property {SavedStickyNote[]} [stickyNotes] + */ + /** @type {Partial} */ let userSettings = {}; @@ -723,6 +727,7 @@ const Directions = { }; const SPRITE_WIDTH = 32; +const SPRITE_HEIGHT = 32; const DECORATIONS_SPRITE_WIDTH = 48; 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 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 * @param {string} dataUri @@ -788,7 +821,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { 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]) => { const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; @@ -974,7 +1007,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-ignore + // @ts-expect-error const ctx = canvas.getContext("2d"); 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 */ function isUserScript() { - // @ts-ignore + // @ts-expect-error return typeof GM_getValue === "function"; } @@ -1028,9 +1061,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function load() { /** @type {Record} */ let saveData = {}; + if (isUserScript()) { log("Loading save data from UserScript storage"); - // @ts-ignore + // @ts-expect-error saveData = GM_getValue("birbSaveData", {}) ?? {}; } else if (isTestEnvironment()) { log("Test environment detected, loading save data from localStorage"); @@ -1038,14 +1072,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } else { log("Not a UserScript"); } + debug("Loaded data: " + JSON.stringify(saveData)); + if (!saveData.settings) { log("No user settings found in save data, starting fresh"); } + userSettings = saveData.settings ?? {}; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; stickyNotes = []; + if (saveData.stickyNotes) { for (let note of saveData.stickyNotes) { if (note.id) { @@ -1053,17 +1091,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } } + log(stickyNotes.length + " sticky notes loaded"); switchSpecies(currentSpecies); } function save() { - /** @type {Record} */ - let saveData = { - unlockedSpecies: unlockedSpecies, - currentSpecies: currentSpecies, + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, settings: userSettings }; + if (stickyNotes.length > 0) { saveData.stickyNotes = stickyNotes.map(note => ({ id: note.id, @@ -1073,9 +1113,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI left: note.left })); } + if (isUserScript()) { log("Saving data to UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_setValue("birbSaveData", saveData); } else if (isTestEnvironment()) { log("Test environment detected, saving data to localStorage"); @@ -1088,7 +1129,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function resetSaveData() { if (isUserScript()) { log("Resetting save data in UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_deleteValue("birbSaveData"); } else if (isTestEnvironment()) { log("Test environment detected, resetting save data in localStorage"); @@ -1214,17 +1255,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return false; } - /** @type {Record} */ - const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - - /** @type {Record} */ - const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); debug("Comparing params: ", stickyNoteParams, currentParams); @@ -1286,7 +1318,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI }); 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 return; } @@ -1323,9 +1355,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI lastUrl = currentUrl; drawStickyNotes(); } - }, 500); + }, URL_CHECK_INTERVAL); - setInterval(update, 1000 / 60); + setInterval(update, UPDATE_INTERVAL); } function drawStickyNotes() { @@ -1350,7 +1382,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -1358,7 +1390,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Fly to an element focusOnElement(); 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 focusOnElement(); lastActionTimestamp = Date.now(); @@ -1369,9 +1401,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 - 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) { lastPetTimestamp = 0; activateFeather(); @@ -1442,6 +1474,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 = ` +
+
${title}
+
x
+
+
+ ${contentHtml} +
+ `; + + 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() { // Create a canvas element for the decoration const decorationCanvas = document.createElement("canvas"); @@ -1521,21 +1589,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function updateFeather() { const feather = document.querySelector("#birb-feather"); - const featherGravity = 1; if (!feather || !(feather instanceof HTMLElement)) { 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`; if (y < window.innerHeight - feather.offsetHeight) { feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; } } - - // insertDecoration(); - // insertFieldGuide(); - /** * @param {HTMLElement} element */ @@ -1552,28 +1615,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; } - let html = ` -
-
${title}
-
x
-
-
-
- ${message} -
-
` - 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"); - if (closeButton) { - makeClosable(() => { - modal.remove(); - }, closeButton); - } + const modal = createWindow("birb-modal", title, ` +
+ ${message} +
+ `); + + modal.style.width = "270px"; centerElement(modal); } @@ -1990,12 +2039,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const elements = document.querySelectorAll("img, video"); const inWindow = Array.from(elements).filter((img) => { 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[]} */ - // @ts-ignore - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); if (largeElements.length === 0) { return; } @@ -2012,12 +2060,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; return; } - const rect = focusedElement.getBoundingClientRect(); - focusedBounds = { - left: rect.left, - right: rect.right, - top: rect.top - }; + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; } function getCanvasWidth() { @@ -2125,49 +2169,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } 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} + */ + 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) => { 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 */ diff --git a/dist/birb.user.js b/dist/birb.user.js index 9ce4980..ab361a5 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.25.15 +// @version 2025.10.25.117 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -14,7 +14,6 @@ // @ts-check -// @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, uiCssScale: 1, @@ -23,7 +22,6 @@ const SHARED_CONFIG = { hopDistance: 45, }; - const DESKTOP_CONFIG = { flySpeed: 0.25 }; @@ -42,17 +40,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; 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 = { birbMode: false @@ -62,6 +49,23 @@ const DEFAULT_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 + * @property {SavedStickyNote[]} [stickyNotes] + */ + /** @type {Partial} */ let userSettings = {}; @@ -737,6 +741,7 @@ const Directions = { }; const SPRITE_WIDTH = 32; +const SPRITE_HEIGHT = 32; const DECORATIONS_SPRITE_WIDTH = 48; 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 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 * @param {string} dataUri @@ -802,7 +835,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { 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]) => { const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; @@ -988,7 +1021,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-ignore + // @ts-expect-error const ctx = canvas.getContext("2d"); 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 */ function isUserScript() { - // @ts-ignore + // @ts-expect-error return typeof GM_getValue === "function"; } @@ -1042,9 +1075,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function load() { /** @type {Record} */ let saveData = {}; + if (isUserScript()) { log("Loading save data from UserScript storage"); - // @ts-ignore + // @ts-expect-error saveData = GM_getValue("birbSaveData", {}) ?? {}; } else if (isTestEnvironment()) { log("Test environment detected, loading save data from localStorage"); @@ -1052,14 +1086,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } else { log("Not a UserScript"); } + debug("Loaded data: " + JSON.stringify(saveData)); + if (!saveData.settings) { log("No user settings found in save data, starting fresh"); } + userSettings = saveData.settings ?? {}; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; stickyNotes = []; + if (saveData.stickyNotes) { for (let note of saveData.stickyNotes) { if (note.id) { @@ -1067,17 +1105,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } } + log(stickyNotes.length + " sticky notes loaded"); switchSpecies(currentSpecies); } function save() { - /** @type {Record} */ - let saveData = { - unlockedSpecies: unlockedSpecies, - currentSpecies: currentSpecies, + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, settings: userSettings }; + if (stickyNotes.length > 0) { saveData.stickyNotes = stickyNotes.map(note => ({ id: note.id, @@ -1087,9 +1127,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI left: note.left })); } + if (isUserScript()) { log("Saving data to UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_setValue("birbSaveData", saveData); } else if (isTestEnvironment()) { log("Test environment detected, saving data to localStorage"); @@ -1102,7 +1143,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function resetSaveData() { if (isUserScript()) { log("Resetting save data in UserScript storage"); - // @ts-ignore + // @ts-expect-error GM_deleteValue("birbSaveData"); } else if (isTestEnvironment()) { log("Test environment detected, resetting save data in localStorage"); @@ -1228,17 +1269,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return false; } - /** @type {Record} */ - const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - - /** @type {Record} */ - const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); debug("Comparing params: ", stickyNoteParams, currentParams); @@ -1300,7 +1332,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI }); 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 return; } @@ -1337,9 +1369,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI lastUrl = currentUrl; drawStickyNotes(); } - }, 500); + }, URL_CHECK_INTERVAL); - setInterval(update, 1000 / 60); + setInterval(update, UPDATE_INTERVAL); } function drawStickyNotes() { @@ -1364,7 +1396,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -1372,7 +1404,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Fly to an element focusOnElement(); 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 focusOnElement(); lastActionTimestamp = Date.now(); @@ -1383,9 +1415,9 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 - 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) { lastPetTimestamp = 0; activateFeather(); @@ -1456,6 +1488,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI 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 = ` +
+
${title}
+
x
+
+
+ ${contentHtml} +
+ `; + + 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() { // Create a canvas element for the decoration const decorationCanvas = document.createElement("canvas"); @@ -1535,21 +1603,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI function updateFeather() { const feather = document.querySelector("#birb-feather"); - const featherGravity = 1; if (!feather || !(feather instanceof HTMLElement)) { 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`; if (y < window.innerHeight - feather.offsetHeight) { feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; } } - - // insertDecoration(); - // insertFieldGuide(); - /** * @param {HTMLElement} element */ @@ -1566,28 +1629,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; } - let html = ` -
-
${title}
-
x
-
-
-
- ${message} -
-
` - 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"); - if (closeButton) { - makeClosable(() => { - modal.remove(); - }, closeButton); - } + const modal = createWindow("birb-modal", title, ` +
+ ${message} +
+ `); + + modal.style.width = "270px"; centerElement(modal); } @@ -2004,12 +2053,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI const elements = document.querySelectorAll("img, video"); const inWindow = Array.from(elements).filter((img) => { 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[]} */ - // @ts-ignore - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); if (largeElements.length === 0) { return; } @@ -2026,12 +2074,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; return; } - const rect = focusedElement.getBoundingClientRect(); - focusedBounds = { - left: rect.left, - right: rect.right, - top: rect.top - }; + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; } function getCanvasWidth() { @@ -2139,49 +2183,49 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } 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} + */ + 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) => { 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 */ diff --git a/manifest.json b/manifest.json index b611f46..37f1cf4 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "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", "content_scripts": [ {