mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
@@ -1,8 +1,8 @@
|
|||||||
# Browser Bird (Work in Progress!)
|
# Pocket Bird (Work in Progress!)
|
||||||
|
|
||||||
This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below!
|
This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below!
|
||||||
|
|
||||||
1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser
|
1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser
|
||||||
2. Enable the Tampermonkey extension and give it the permissions requested
|
2. Enable the Tampermonkey extension and give it the permissions requested
|
||||||
3. Install my Browser Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js)
|
3. Install my Pocket Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js)
|
||||||
4. Now any websites you visit will have a little bird hopping around!
|
4. Now any websites you visit will have a little bird hopping around!
|
||||||
723
birb.js
723
birb.js
@@ -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,9 +8,8 @@ const SHARED_CONFIG = {
|
|||||||
hopDistance: 45,
|
hopDistance: 45,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DESKTOP_CONFIG = {
|
const DESKTOP_CONFIG = {
|
||||||
flySpeed: 0.2,
|
flySpeed: 0.25
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOBILE_CONFIG = {
|
const MOBILE_CONFIG = {
|
||||||
@@ -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
|
||||||
@@ -443,8 +476,14 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
log("Loading sprite sheets...");
|
||||||
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;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -597,8 +636,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
// new MenuItem("Decorations", insertDecoration),
|
|
||||||
new DebugMenuItem("Applications", () => switchMenuItems(otherItems), false),
|
|
||||||
new MenuItem("Sticky Note", newStickyNote),
|
new MenuItem("Sticky Note", newStickyNote),
|
||||||
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
||||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||||
@@ -627,39 +664,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const otherItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Video Games", () => switchMenuItems(gameItems), false),
|
|
||||||
new MenuItem("Utilities", () => switchMenuItems(utilityItems), false),
|
|
||||||
new MenuItem("Music Player", () => insertMusicPlayer(), false),
|
|
||||||
];
|
|
||||||
|
|
||||||
const gameItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pinball", () => insertPico8("Terra Nova Pinball", "terra_nova_pinball")),
|
|
||||||
new MenuItem("Pico Dino", () => insertPico8("Pico Dino", "picodino")),
|
|
||||||
new MenuItem("Woodworm", () => insertPico8("Woodworm", "woodworm")),
|
|
||||||
new MenuItem("Pico and Chill", () => insertPico8("Pico and Chill", "picochill")),
|
|
||||||
new MenuItem("Lani's Trek", () => insertPico8("Celeste 2 Lani's Trek", "celeste_classic_2")),
|
|
||||||
new MenuItem("Tetraminis", () => insertPico8("Tetraminis", "tetraminisdeffect")),
|
|
||||||
// new MenuItem("Pool", () => insertPico8("Pool", "mot_pool")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const utilityItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pomodoro Timer", () => insertPico8("Pomodoro", "pomodorotimerv1")),
|
|
||||||
new MenuItem("Ohm Calculator", () => insertPico8("Resistor Calculator", "picoohm")),
|
|
||||||
new MenuItem("Wobblepaint ", () => insertPico8("Wobblepaint", "wobblepaint")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
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 = {
|
||||||
@@ -700,20 +709,23 @@ 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTestEnvironment() {
|
function isTestEnvironment() {
|
||||||
return window.location.hostname === "127.0.0.1";
|
return window.location.hostname === "127.0.0.1"
|
||||||
|
|| window.location.hostname === "localhost"
|
||||||
|
|| window.location.hostname.startsWith("192.168.");
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -721,14 +733,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) {
|
||||||
@@ -736,17 +752,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,
|
||||||
@@ -756,9 +774,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");
|
||||||
@@ -771,7 +790,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");
|
||||||
@@ -797,6 +816,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return settings().birbMode ? "Birb" : "Bird";
|
return settings().birbMode ? "Birb" : "Bird";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (window !== window.top) {
|
||||||
|
// Skip installation if within an iframe
|
||||||
|
log("In iframe, skipping Birb script initialization");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("Sprite sheets loaded successfully, initializing bird...");
|
||||||
|
|
||||||
|
// Preload font
|
||||||
|
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
||||||
|
const fontLink = document.createElement("link");
|
||||||
|
fontLink.rel = "stylesheet";
|
||||||
|
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
||||||
|
document.head.appendChild(fontLink);
|
||||||
|
|
||||||
|
// Add stylesheet font-face
|
||||||
|
const fontFace = `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Monocraft';
|
||||||
|
src: url(${MONOCRAFT_SRC}) format('opentype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const fontStyle = document.createElement("style");
|
||||||
|
fontStyle.innerHTML = fontFace;
|
||||||
|
document.head.appendChild(fontStyle);
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
styleElement.innerHTML = STYLESHEET;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
canvas.id = "birb";
|
||||||
|
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
||||||
|
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(document, (e) => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
||||||
|
removeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(canvas, () => {
|
||||||
|
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
|
||||||
|
// Currently being pet, don't open menu
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("mouseover", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
petStack.push(Date.now());
|
||||||
|
if (petStack.length > 10) {
|
||||||
|
petStack.shift();
|
||||||
|
}
|
||||||
|
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
||||||
|
if (pets >= 3) {
|
||||||
|
pet();
|
||||||
|
// Clear the stack
|
||||||
|
petStack = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("touchmove", (e) => {
|
||||||
|
pet();
|
||||||
|
});
|
||||||
|
|
||||||
|
drawStickyNotes();
|
||||||
|
|
||||||
|
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
setInterval(() => {
|
||||||
|
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
if (currentUrl !== lastUrl) {
|
||||||
|
log("URL changed, updating sticky notes");
|
||||||
|
lastUrl = currentUrl;
|
||||||
|
drawStickyNotes();
|
||||||
|
}
|
||||||
|
}, URL_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
setInterval(update, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
ticks++;
|
||||||
|
|
||||||
|
// Hide bird if the browser is fullscreen
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
hideBirb();
|
||||||
|
// Won't be restored on fullscreen exit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
||||||
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
|
hop();
|
||||||
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
|
// Idle for a while, do something
|
||||||
|
if (focusedElement === null) {
|
||||||
|
// Fly to an element
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
|
||||||
|
// Fly to another element if idle for a longer while
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentState === States.HOP) {
|
||||||
|
if (updateParabolicPath(HOP_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double the chance of a feather if recently pet
|
||||||
|
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
|
||||||
|
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
||||||
|
lastPetTimestamp = 0;
|
||||||
|
activateFeather();
|
||||||
|
}
|
||||||
|
updateFeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFocusedElementBounds();
|
||||||
|
|
||||||
|
// Update the bird's position
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
birdY = getFocusedY();
|
||||||
|
} else if (currentState === States.FLYING) {
|
||||||
|
// Fly to target location (even if in the air)
|
||||||
|
if (updateParabolicPath(FLY_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTargetY = targetY;
|
||||||
|
targetY = getFocusedY();
|
||||||
|
// Adjust startY to account for scrolling
|
||||||
|
startY += targetY - oldTargetY;
|
||||||
|
if (targetY < 0 || targetY > window.innerHeight) {
|
||||||
|
// Fly to ground if the focused element moves out of bounds
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
||||||
|
setAnimation(Animations.STILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update HTML element position
|
||||||
|
setX(birdX);
|
||||||
|
setY(birdY);
|
||||||
|
}
|
||||||
|
|
||||||
function newStickyNote() {
|
function newStickyNote() {
|
||||||
const id = Date.now().toString();
|
const id = Date.now().toString();
|
||||||
const site = window.location.href;
|
const site = window.location.href;
|
||||||
@@ -897,17 +1088,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);
|
||||||
|
|
||||||
@@ -919,88 +1101,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (window !== window.top) {
|
|
||||||
// Skip installation if within an iframe
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload font
|
|
||||||
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
|
||||||
const fontLink = document.createElement("link");
|
|
||||||
fontLink.rel = "stylesheet";
|
|
||||||
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
|
||||||
document.head.appendChild(fontLink);
|
|
||||||
|
|
||||||
// Add stylesheet font-face
|
|
||||||
const fontFace = `
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Monocraft';
|
|
||||||
src: url(${MONOCRAFT_SRC}) format('opentype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const fontStyle = document.createElement("style");
|
|
||||||
fontStyle.innerHTML = fontFace;
|
|
||||||
document.head.appendChild(fontStyle);
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
styleElement.innerHTML = STYLESHEET;
|
|
||||||
document.head.appendChild(styleElement);
|
|
||||||
|
|
||||||
canvas.id = "birb";
|
|
||||||
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
|
||||||
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(document, (e) => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
|
||||||
removeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(canvas, () => {
|
|
||||||
insertMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
petStack.push(Date.now());
|
|
||||||
if (petStack.length > 10) {
|
|
||||||
petStack.shift();
|
|
||||||
}
|
|
||||||
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
|
||||||
if (pets >= 3) {
|
|
||||||
setAnimation(Animations.HEART);
|
|
||||||
// Clear the stack
|
|
||||||
petStack = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
drawStickyNotes();
|
|
||||||
|
|
||||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
setInterval(() => {
|
|
||||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
if (currentUrl !== lastUrl) {
|
|
||||||
log("URL changed, updating sticky notes");
|
|
||||||
lastUrl = currentUrl;
|
|
||||||
drawStickyNotes();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setInterval(update, 1000 / 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawStickyNotes() {
|
function drawStickyNotes() {
|
||||||
// Remove all existing sticky notes
|
// Remove all existing sticky notes
|
||||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||||
@@ -1013,89 +1113,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
|
||||||
ticks++;
|
|
||||||
|
|
||||||
// Hide bird if the browser is fullscreen
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
hideBirb();
|
|
||||||
// Won't be restored on fullscreen exit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
|
||||||
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
|
|
||||||
hop();
|
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
|
||||||
// Idle for a while, do something
|
|
||||||
if (focusedElement === null) {
|
|
||||||
// Fly to an element
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
} else if (Math.random() < 1 / (60 * 20)) {
|
|
||||||
// Fly to another element if idle for a longer while
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentState === States.HOP) {
|
|
||||||
if (updateParabolicPath(HOP_SPEED)) {
|
|
||||||
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;
|
|
||||||
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
|
||||||
lastPetTimestamp = 0;
|
|
||||||
activateFeather();
|
|
||||||
}
|
|
||||||
updateFeather();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFocusedElementBounds();
|
|
||||||
|
|
||||||
// Update the bird's position
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
birdY = getFocusedY();
|
|
||||||
} else if (currentState === States.FLYING) {
|
|
||||||
// Fly to target location (even if in the air)
|
|
||||||
if (updateParabolicPath(FLY_SPEED)) {
|
|
||||||
setState(States.IDLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldTargetY = targetY;
|
|
||||||
targetY = getFocusedY();
|
|
||||||
// Adjust startY to account for scrolling
|
|
||||||
startY += targetY - oldTargetY;
|
|
||||||
if (targetY < 0 || targetY > window.innerHeight) {
|
|
||||||
// Fly to ground if the focused element moves out of bounds
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
|
||||||
setAnimation(Animations.STILL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update HTML element position
|
|
||||||
setX(birdX);
|
|
||||||
setY(birdY);
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an HTML element with the specified parameters
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -1115,6 +1132,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");
|
||||||
@@ -1194,21 +1247,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
|
||||||
*/
|
*/
|
||||||
@@ -1225,28 +1273,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1355,60 +1389,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertPico8();
|
|
||||||
|
|
||||||
function isSafari() {
|
function isSafari() {
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} pid
|
|
||||||
*/
|
|
||||||
function insertPico8(name, pid) {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">${name}</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-pico-8-content">
|
|
||||||
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=${pid}" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
|
|
||||||
</div>`
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertMusicPlayer() {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">Music Player</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-music-player-content">
|
|
||||||
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/31FWVQBp3WQydWLNhO0ACi?utm_source=generator" width="250" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
|
|
||||||
</div>`;
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
*/
|
*/
|
||||||
@@ -1522,7 +1506,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
*/
|
*/
|
||||||
function onClick(element, action) {
|
function onClick(element, action) {
|
||||||
element.addEventListener("click", (e) => action(e));
|
element.addEventListener("click", (e) => action(e));
|
||||||
element.addEventListener("touchstart", (e) => action(e));
|
element.addEventListener("touchend", (e) => {
|
||||||
|
if (e instanceof TouchEvent === false) {
|
||||||
|
return;
|
||||||
|
} else if (element instanceof HTMLElement === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = e.changedTouches[0];
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
touch.clientX >= rect.left &&
|
||||||
|
touch.clientX <= rect.right &&
|
||||||
|
touch.clientY >= rect.top &&
|
||||||
|
touch.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
|
action(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1682,7 +1682,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
function getFullWindowHeight() {
|
function getFullWindowHeight() {
|
||||||
return document.documentElement.clientHeight;
|
return document.documentElement.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusOnGround() {
|
function focusOnGround() {
|
||||||
console.log("Focusing on ground");
|
console.log("Focusing on ground");
|
||||||
focusedElement = null;
|
focusedElement = null;
|
||||||
@@ -1697,12 +1697,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;
|
||||||
}
|
}
|
||||||
@@ -1719,12 +1718,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() {
|
||||||
@@ -1752,7 +1747,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pet() {
|
function pet() {
|
||||||
if (currentState === States.IDLE) {
|
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
|
||||||
setAnimation(Animations.HEART);
|
setAnimation(Animations.HEART);
|
||||||
lastPetTimestamp = Date.now();
|
lastPetTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
@@ -1832,47 +1827,53 @@ 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 };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the birb
|
||||||
|
init();
|
||||||
|
draw();
|
||||||
|
}).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
|
* @returns {boolean} Whether the user is on a mobile device
|
||||||
*/
|
*/
|
||||||
|
|||||||
35
build.js
35
build.js
@@ -20,11 +20,44 @@ const spriteSheets = [
|
|||||||
const STYLESHEET_PATH = "./stylesheet.css";
|
const STYLESHEET_PATH = "./stylesheet.css";
|
||||||
const STYLESHEET_KEY = "___STYLESHEET___";
|
const STYLESHEET_KEY = "___STYLESHEET___";
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
|
||||||
|
|
||||||
|
// Get current build number from manifest.json
|
||||||
|
let buildNumber = 0;
|
||||||
|
try {
|
||||||
|
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
|
||||||
|
if (manifest.version) {
|
||||||
|
if (manifest.version.startsWith(versionDate)) {
|
||||||
|
// Same day, increment build number
|
||||||
|
const parts = manifest.version.split('.');
|
||||||
|
if (parts.length === 4) {
|
||||||
|
buildNumber = parseInt(parts[3], 10) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not read version from manifest.json");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update manifest.json with new version
|
||||||
|
const version = `${versionDate}.${buildNumber}`;
|
||||||
|
try {
|
||||||
|
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
|
||||||
|
manifest.version = version;
|
||||||
|
writeFileSync('manifest.json', JSON.stringify(manifest, null, 4), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not update version in manifest.json");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const userScriptHeader =
|
const userScriptHeader =
|
||||||
`// ==UserScript==
|
`// ==UserScript==
|
||||||
// @name Pocket Bird
|
// @name Pocket Bird
|
||||||
// @namespace https://idreesinc.com
|
// @namespace https://idreesinc.com
|
||||||
// @version 2025-10-23-01
|
// @version ${version}
|
||||||
// @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
|
||||||
|
|||||||
733
dist/birb.js
vendored
733
dist/birb.js
vendored
@@ -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,9 +8,8 @@ const SHARED_CONFIG = {
|
|||||||
hopDistance: 45,
|
hopDistance: 45,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DESKTOP_CONFIG = {
|
const DESKTOP_CONFIG = {
|
||||||
flySpeed: 0.2,
|
flySpeed: 0.25
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOBILE_CONFIG = {
|
const MOBILE_CONFIG = {
|
||||||
@@ -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 = {};
|
||||||
|
|
||||||
@@ -294,8 +298,8 @@ const STYLESHEET = `:root {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-left: 15px;
|
padding-left: 10px;
|
||||||
padding-right: 15px;
|
padding-right: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,12 +346,12 @@ const STYLESHEET = `:root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.birb-field-guide-description {
|
.birb-field-guide-description {
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 20px);
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: rgb(124, 108, 75);
|
color: rgb(124, 108, 75);
|
||||||
@@ -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
|
||||||
@@ -786,8 +819,14 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
log("Loading sprite sheets...");
|
||||||
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;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -940,8 +979,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
// new MenuItem("Decorations", insertDecoration),
|
|
||||||
new DebugMenuItem("Applications", () => switchMenuItems(otherItems), false),
|
|
||||||
new MenuItem("Sticky Note", newStickyNote),
|
new MenuItem("Sticky Note", newStickyNote),
|
||||||
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
||||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||||
@@ -970,39 +1007,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const otherItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Video Games", () => switchMenuItems(gameItems), false),
|
|
||||||
new MenuItem("Utilities", () => switchMenuItems(utilityItems), false),
|
|
||||||
new MenuItem("Music Player", () => insertMusicPlayer(), false),
|
|
||||||
];
|
|
||||||
|
|
||||||
const gameItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pinball", () => insertPico8("Terra Nova Pinball", "terra_nova_pinball")),
|
|
||||||
new MenuItem("Pico Dino", () => insertPico8("Pico Dino", "picodino")),
|
|
||||||
new MenuItem("Woodworm", () => insertPico8("Woodworm", "woodworm")),
|
|
||||||
new MenuItem("Pico and Chill", () => insertPico8("Pico and Chill", "picochill")),
|
|
||||||
new MenuItem("Lani's Trek", () => insertPico8("Celeste 2 Lani's Trek", "celeste_classic_2")),
|
|
||||||
new MenuItem("Tetraminis", () => insertPico8("Tetraminis", "tetraminisdeffect")),
|
|
||||||
// new MenuItem("Pool", () => insertPico8("Pool", "mot_pool")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const utilityItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pomodoro Timer", () => insertPico8("Pomodoro", "pomodorotimerv1")),
|
|
||||||
new MenuItem("Ohm Calculator", () => insertPico8("Resistor Calculator", "picoohm")),
|
|
||||||
new MenuItem("Wobblepaint ", () => insertPico8("Wobblepaint", "wobblepaint")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
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 = {
|
||||||
@@ -1043,20 +1052,23 @@ 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTestEnvironment() {
|
function isTestEnvironment() {
|
||||||
return window.location.hostname === "127.0.0.1";
|
return window.location.hostname === "127.0.0.1"
|
||||||
|
|| window.location.hostname === "localhost"
|
||||||
|
|| window.location.hostname.startsWith("192.168.");
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -1064,14 +1076,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) {
|
||||||
@@ -1079,17 +1095,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,
|
||||||
@@ -1099,9 +1117,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");
|
||||||
@@ -1114,7 +1133,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");
|
||||||
@@ -1140,6 +1159,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return settings().birbMode ? "Birb" : "Bird";
|
return settings().birbMode ? "Birb" : "Bird";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (window !== window.top) {
|
||||||
|
// Skip installation if within an iframe
|
||||||
|
log("In iframe, skipping Birb script initialization");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("Sprite sheets loaded successfully, initializing bird...");
|
||||||
|
|
||||||
|
// Preload font
|
||||||
|
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
||||||
|
const fontLink = document.createElement("link");
|
||||||
|
fontLink.rel = "stylesheet";
|
||||||
|
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
||||||
|
document.head.appendChild(fontLink);
|
||||||
|
|
||||||
|
// Add stylesheet font-face
|
||||||
|
const fontFace = `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Monocraft';
|
||||||
|
src: url(${MONOCRAFT_SRC}) format('opentype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const fontStyle = document.createElement("style");
|
||||||
|
fontStyle.innerHTML = fontFace;
|
||||||
|
document.head.appendChild(fontStyle);
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
styleElement.innerHTML = STYLESHEET;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
canvas.id = "birb";
|
||||||
|
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
||||||
|
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(document, (e) => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
||||||
|
removeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(canvas, () => {
|
||||||
|
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
|
||||||
|
// Currently being pet, don't open menu
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("mouseover", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
petStack.push(Date.now());
|
||||||
|
if (petStack.length > 10) {
|
||||||
|
petStack.shift();
|
||||||
|
}
|
||||||
|
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
||||||
|
if (pets >= 3) {
|
||||||
|
pet();
|
||||||
|
// Clear the stack
|
||||||
|
petStack = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("touchmove", (e) => {
|
||||||
|
pet();
|
||||||
|
});
|
||||||
|
|
||||||
|
drawStickyNotes();
|
||||||
|
|
||||||
|
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
setInterval(() => {
|
||||||
|
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
if (currentUrl !== lastUrl) {
|
||||||
|
log("URL changed, updating sticky notes");
|
||||||
|
lastUrl = currentUrl;
|
||||||
|
drawStickyNotes();
|
||||||
|
}
|
||||||
|
}, URL_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
setInterval(update, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
ticks++;
|
||||||
|
|
||||||
|
// Hide bird if the browser is fullscreen
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
hideBirb();
|
||||||
|
// Won't be restored on fullscreen exit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
||||||
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
|
hop();
|
||||||
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
|
// Idle for a while, do something
|
||||||
|
if (focusedElement === null) {
|
||||||
|
// Fly to an element
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
|
||||||
|
// Fly to another element if idle for a longer while
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentState === States.HOP) {
|
||||||
|
if (updateParabolicPath(HOP_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double the chance of a feather if recently pet
|
||||||
|
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
|
||||||
|
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
||||||
|
lastPetTimestamp = 0;
|
||||||
|
activateFeather();
|
||||||
|
}
|
||||||
|
updateFeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFocusedElementBounds();
|
||||||
|
|
||||||
|
// Update the bird's position
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
birdY = getFocusedY();
|
||||||
|
} else if (currentState === States.FLYING) {
|
||||||
|
// Fly to target location (even if in the air)
|
||||||
|
if (updateParabolicPath(FLY_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTargetY = targetY;
|
||||||
|
targetY = getFocusedY();
|
||||||
|
// Adjust startY to account for scrolling
|
||||||
|
startY += targetY - oldTargetY;
|
||||||
|
if (targetY < 0 || targetY > window.innerHeight) {
|
||||||
|
// Fly to ground if the focused element moves out of bounds
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
||||||
|
setAnimation(Animations.STILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update HTML element position
|
||||||
|
setX(birdX);
|
||||||
|
setY(birdY);
|
||||||
|
}
|
||||||
|
|
||||||
function newStickyNote() {
|
function newStickyNote() {
|
||||||
const id = Date.now().toString();
|
const id = Date.now().toString();
|
||||||
const site = window.location.href;
|
const site = window.location.href;
|
||||||
@@ -1240,17 +1431,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);
|
||||||
|
|
||||||
@@ -1262,88 +1444,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (window !== window.top) {
|
|
||||||
// Skip installation if within an iframe
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload font
|
|
||||||
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
|
||||||
const fontLink = document.createElement("link");
|
|
||||||
fontLink.rel = "stylesheet";
|
|
||||||
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
|
||||||
document.head.appendChild(fontLink);
|
|
||||||
|
|
||||||
// Add stylesheet font-face
|
|
||||||
const fontFace = `
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Monocraft';
|
|
||||||
src: url(${MONOCRAFT_SRC}) format('opentype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const fontStyle = document.createElement("style");
|
|
||||||
fontStyle.innerHTML = fontFace;
|
|
||||||
document.head.appendChild(fontStyle);
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
styleElement.innerHTML = STYLESHEET;
|
|
||||||
document.head.appendChild(styleElement);
|
|
||||||
|
|
||||||
canvas.id = "birb";
|
|
||||||
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
|
||||||
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(document, (e) => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
|
||||||
removeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(canvas, () => {
|
|
||||||
insertMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
petStack.push(Date.now());
|
|
||||||
if (petStack.length > 10) {
|
|
||||||
petStack.shift();
|
|
||||||
}
|
|
||||||
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
|
||||||
if (pets >= 3) {
|
|
||||||
setAnimation(Animations.HEART);
|
|
||||||
// Clear the stack
|
|
||||||
petStack = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
drawStickyNotes();
|
|
||||||
|
|
||||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
setInterval(() => {
|
|
||||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
if (currentUrl !== lastUrl) {
|
|
||||||
log("URL changed, updating sticky notes");
|
|
||||||
lastUrl = currentUrl;
|
|
||||||
drawStickyNotes();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setInterval(update, 1000 / 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawStickyNotes() {
|
function drawStickyNotes() {
|
||||||
// Remove all existing sticky notes
|
// Remove all existing sticky notes
|
||||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||||
@@ -1356,89 +1456,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
|
||||||
ticks++;
|
|
||||||
|
|
||||||
// Hide bird if the browser is fullscreen
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
hideBirb();
|
|
||||||
// Won't be restored on fullscreen exit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
|
||||||
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
|
|
||||||
hop();
|
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
|
||||||
// Idle for a while, do something
|
|
||||||
if (focusedElement === null) {
|
|
||||||
// Fly to an element
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
} else if (Math.random() < 1 / (60 * 20)) {
|
|
||||||
// Fly to another element if idle for a longer while
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentState === States.HOP) {
|
|
||||||
if (updateParabolicPath(HOP_SPEED)) {
|
|
||||||
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;
|
|
||||||
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
|
||||||
lastPetTimestamp = 0;
|
|
||||||
activateFeather();
|
|
||||||
}
|
|
||||||
updateFeather();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFocusedElementBounds();
|
|
||||||
|
|
||||||
// Update the bird's position
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
birdY = getFocusedY();
|
|
||||||
} else if (currentState === States.FLYING) {
|
|
||||||
// Fly to target location (even if in the air)
|
|
||||||
if (updateParabolicPath(FLY_SPEED)) {
|
|
||||||
setState(States.IDLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldTargetY = targetY;
|
|
||||||
targetY = getFocusedY();
|
|
||||||
// Adjust startY to account for scrolling
|
|
||||||
startY += targetY - oldTargetY;
|
|
||||||
if (targetY < 0 || targetY > window.innerHeight) {
|
|
||||||
// Fly to ground if the focused element moves out of bounds
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
|
||||||
setAnimation(Animations.STILL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update HTML element position
|
|
||||||
setX(birdX);
|
|
||||||
setY(birdY);
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an HTML element with the specified parameters
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -1458,6 +1475,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");
|
||||||
@@ -1537,21 +1590,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
|
||||||
*/
|
*/
|
||||||
@@ -1568,28 +1616,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1698,60 +1732,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertPico8();
|
|
||||||
|
|
||||||
function isSafari() {
|
function isSafari() {
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} pid
|
|
||||||
*/
|
|
||||||
function insertPico8(name, pid) {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">${name}</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-pico-8-content">
|
|
||||||
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=${pid}" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
|
|
||||||
</div>`
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertMusicPlayer() {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">Music Player</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-music-player-content">
|
|
||||||
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/31FWVQBp3WQydWLNhO0ACi?utm_source=generator" width="250" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
|
|
||||||
</div>`;
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
*/
|
*/
|
||||||
@@ -1865,7 +1849,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
*/
|
*/
|
||||||
function onClick(element, action) {
|
function onClick(element, action) {
|
||||||
element.addEventListener("click", (e) => action(e));
|
element.addEventListener("click", (e) => action(e));
|
||||||
element.addEventListener("touchstart", (e) => action(e));
|
element.addEventListener("touchend", (e) => {
|
||||||
|
if (e instanceof TouchEvent === false) {
|
||||||
|
return;
|
||||||
|
} else if (element instanceof HTMLElement === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = e.changedTouches[0];
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
touch.clientX >= rect.left &&
|
||||||
|
touch.clientX <= rect.right &&
|
||||||
|
touch.clientY >= rect.top &&
|
||||||
|
touch.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
|
action(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2025,7 +2025,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
function getFullWindowHeight() {
|
function getFullWindowHeight() {
|
||||||
return document.documentElement.clientHeight;
|
return document.documentElement.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusOnGround() {
|
function focusOnGround() {
|
||||||
console.log("Focusing on ground");
|
console.log("Focusing on ground");
|
||||||
focusedElement = null;
|
focusedElement = null;
|
||||||
@@ -2040,12 +2040,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;
|
||||||
}
|
}
|
||||||
@@ -2062,12 +2061,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() {
|
||||||
@@ -2095,7 +2090,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pet() {
|
function pet() {
|
||||||
if (currentState === States.IDLE) {
|
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
|
||||||
setAnimation(Animations.HEART);
|
setAnimation(Animations.HEART);
|
||||||
lastPetTimestamp = Date.now();
|
lastPetTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
@@ -2175,47 +2170,53 @@ 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 };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the birb
|
||||||
|
init();
|
||||||
|
draw();
|
||||||
|
}).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
|
* @returns {boolean} Whether the user is on a mobile device
|
||||||
*/
|
*/
|
||||||
|
|||||||
735
dist/birb.user.js
vendored
735
dist/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Pocket Bird
|
// @name Pocket Bird
|
||||||
// @namespace https://idreesinc.com
|
// @namespace https://idreesinc.com
|
||||||
// @version 2025-10-23-01
|
// @version 2025.10.25.130
|
||||||
// @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,9 +22,8 @@ const SHARED_CONFIG = {
|
|||||||
hopDistance: 45,
|
hopDistance: 45,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DESKTOP_CONFIG = {
|
const DESKTOP_CONFIG = {
|
||||||
flySpeed: 0.2,
|
flySpeed: 0.25
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOBILE_CONFIG = {
|
const MOBILE_CONFIG = {
|
||||||
@@ -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 = {};
|
||||||
|
|
||||||
@@ -308,8 +312,8 @@ const STYLESHEET = `:root {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-left: 15px;
|
padding-left: 10px;
|
||||||
padding-right: 15px;
|
padding-right: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,12 +360,12 @@ const STYLESHEET = `:root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.birb-field-guide-description {
|
.birb-field-guide-description {
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 20px);
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: rgb(124, 108, 75);
|
color: rgb(124, 108, 75);
|
||||||
@@ -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
|
||||||
@@ -800,8 +833,14 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
log("Loading sprite sheets...");
|
||||||
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;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -954,8 +993,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
// new MenuItem("Decorations", insertDecoration),
|
|
||||||
new DebugMenuItem("Applications", () => switchMenuItems(otherItems), false),
|
|
||||||
new MenuItem("Sticky Note", newStickyNote),
|
new MenuItem("Sticky Note", newStickyNote),
|
||||||
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
|
||||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||||
@@ -984,39 +1021,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const otherItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Video Games", () => switchMenuItems(gameItems), false),
|
|
||||||
new MenuItem("Utilities", () => switchMenuItems(utilityItems), false),
|
|
||||||
new MenuItem("Music Player", () => insertMusicPlayer(), false),
|
|
||||||
];
|
|
||||||
|
|
||||||
const gameItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pinball", () => insertPico8("Terra Nova Pinball", "terra_nova_pinball")),
|
|
||||||
new MenuItem("Pico Dino", () => insertPico8("Pico Dino", "picodino")),
|
|
||||||
new MenuItem("Woodworm", () => insertPico8("Woodworm", "woodworm")),
|
|
||||||
new MenuItem("Pico and Chill", () => insertPico8("Pico and Chill", "picochill")),
|
|
||||||
new MenuItem("Lani's Trek", () => insertPico8("Celeste 2 Lani's Trek", "celeste_classic_2")),
|
|
||||||
new MenuItem("Tetraminis", () => insertPico8("Tetraminis", "tetraminisdeffect")),
|
|
||||||
// new MenuItem("Pool", () => insertPico8("Pool", "mot_pool")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const utilityItems = [
|
|
||||||
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
|
|
||||||
new Separator(),
|
|
||||||
new MenuItem("Pomodoro Timer", () => insertPico8("Pomodoro", "pomodorotimerv1")),
|
|
||||||
new MenuItem("Ohm Calculator", () => insertPico8("Resistor Calculator", "picoohm")),
|
|
||||||
new MenuItem("Wobblepaint ", () => insertPico8("Wobblepaint", "wobblepaint")),
|
|
||||||
];
|
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
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 = {
|
||||||
@@ -1057,20 +1066,23 @@ 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTestEnvironment() {
|
function isTestEnvironment() {
|
||||||
return window.location.hostname === "127.0.0.1";
|
return window.location.hostname === "127.0.0.1"
|
||||||
|
|| window.location.hostname === "localhost"
|
||||||
|
|| window.location.hostname.startsWith("192.168.");
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -1078,14 +1090,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) {
|
||||||
@@ -1093,17 +1109,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,
|
||||||
@@ -1113,9 +1131,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");
|
||||||
@@ -1128,7 +1147,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");
|
||||||
@@ -1154,6 +1173,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return settings().birbMode ? "Birb" : "Bird";
|
return settings().birbMode ? "Birb" : "Bird";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (window !== window.top) {
|
||||||
|
// Skip installation if within an iframe
|
||||||
|
log("In iframe, skipping Birb script initialization");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("Sprite sheets loaded successfully, initializing bird...");
|
||||||
|
|
||||||
|
// Preload font
|
||||||
|
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
||||||
|
const fontLink = document.createElement("link");
|
||||||
|
fontLink.rel = "stylesheet";
|
||||||
|
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
||||||
|
document.head.appendChild(fontLink);
|
||||||
|
|
||||||
|
// Add stylesheet font-face
|
||||||
|
const fontFace = `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Monocraft';
|
||||||
|
src: url(${MONOCRAFT_SRC}) format('opentype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const fontStyle = document.createElement("style");
|
||||||
|
fontStyle.innerHTML = fontFace;
|
||||||
|
document.head.appendChild(fontStyle);
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
styleElement.innerHTML = STYLESHEET;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
canvas.id = "birb";
|
||||||
|
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
||||||
|
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(document, (e) => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
||||||
|
removeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onClick(canvas, () => {
|
||||||
|
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
|
||||||
|
// Currently being pet, don't open menu
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("mouseover", () => {
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
petStack.push(Date.now());
|
||||||
|
if (petStack.length > 10) {
|
||||||
|
petStack.shift();
|
||||||
|
}
|
||||||
|
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
||||||
|
if (pets >= 3) {
|
||||||
|
pet();
|
||||||
|
// Clear the stack
|
||||||
|
petStack = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener("touchmove", (e) => {
|
||||||
|
pet();
|
||||||
|
});
|
||||||
|
|
||||||
|
drawStickyNotes();
|
||||||
|
|
||||||
|
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
setInterval(() => {
|
||||||
|
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||||
|
if (currentUrl !== lastUrl) {
|
||||||
|
log("URL changed, updating sticky notes");
|
||||||
|
lastUrl = currentUrl;
|
||||||
|
drawStickyNotes();
|
||||||
|
}
|
||||||
|
}, URL_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
setInterval(update, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
ticks++;
|
||||||
|
|
||||||
|
// Hide bird if the browser is fullscreen
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
hideBirb();
|
||||||
|
// Won't be restored on fullscreen exit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
||||||
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
|
hop();
|
||||||
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
|
// Idle for a while, do something
|
||||||
|
if (focusedElement === null) {
|
||||||
|
// Fly to an element
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
|
||||||
|
// Fly to another element if idle for a longer while
|
||||||
|
focusOnElement();
|
||||||
|
lastActionTimestamp = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentState === States.HOP) {
|
||||||
|
if (updateParabolicPath(HOP_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double the chance of a feather if recently pet
|
||||||
|
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
|
||||||
|
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
||||||
|
lastPetTimestamp = 0;
|
||||||
|
activateFeather();
|
||||||
|
}
|
||||||
|
updateFeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFocusedElementBounds();
|
||||||
|
|
||||||
|
// Update the bird's position
|
||||||
|
if (currentState === States.IDLE) {
|
||||||
|
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
birdY = getFocusedY();
|
||||||
|
} else if (currentState === States.FLYING) {
|
||||||
|
// Fly to target location (even if in the air)
|
||||||
|
if (updateParabolicPath(FLY_SPEED)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTargetY = targetY;
|
||||||
|
targetY = getFocusedY();
|
||||||
|
// Adjust startY to account for scrolling
|
||||||
|
startY += targetY - oldTargetY;
|
||||||
|
if (targetY < 0 || targetY > window.innerHeight) {
|
||||||
|
// Fly to ground if the focused element moves out of bounds
|
||||||
|
focusOnGround();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
||||||
|
setAnimation(Animations.STILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update HTML element position
|
||||||
|
setX(birdX);
|
||||||
|
setY(birdY);
|
||||||
|
}
|
||||||
|
|
||||||
function newStickyNote() {
|
function newStickyNote() {
|
||||||
const id = Date.now().toString();
|
const id = Date.now().toString();
|
||||||
const site = window.location.href;
|
const site = window.location.href;
|
||||||
@@ -1254,17 +1445,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);
|
||||||
|
|
||||||
@@ -1276,88 +1458,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (window !== window.top) {
|
|
||||||
// Skip installation if within an iframe
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload font
|
|
||||||
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
|
||||||
const fontLink = document.createElement("link");
|
|
||||||
fontLink.rel = "stylesheet";
|
|
||||||
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
|
|
||||||
document.head.appendChild(fontLink);
|
|
||||||
|
|
||||||
// Add stylesheet font-face
|
|
||||||
const fontFace = `
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Monocraft';
|
|
||||||
src: url(${MONOCRAFT_SRC}) format('opentype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const fontStyle = document.createElement("style");
|
|
||||||
fontStyle.innerHTML = fontFace;
|
|
||||||
document.head.appendChild(fontStyle);
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
styleElement.innerHTML = STYLESHEET;
|
|
||||||
document.head.appendChild(styleElement);
|
|
||||||
|
|
||||||
canvas.id = "birb";
|
|
||||||
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
|
|
||||||
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(document, (e) => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
|
||||||
removeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onClick(canvas, () => {
|
|
||||||
insertMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
petStack.push(Date.now());
|
|
||||||
if (petStack.length > 10) {
|
|
||||||
petStack.shift();
|
|
||||||
}
|
|
||||||
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
|
|
||||||
if (pets >= 3) {
|
|
||||||
setAnimation(Animations.HEART);
|
|
||||||
// Clear the stack
|
|
||||||
petStack = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
drawStickyNotes();
|
|
||||||
|
|
||||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
setInterval(() => {
|
|
||||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
|
||||||
if (currentUrl !== lastUrl) {
|
|
||||||
log("URL changed, updating sticky notes");
|
|
||||||
lastUrl = currentUrl;
|
|
||||||
drawStickyNotes();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setInterval(update, 1000 / 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawStickyNotes() {
|
function drawStickyNotes() {
|
||||||
// Remove all existing sticky notes
|
// Remove all existing sticky notes
|
||||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||||
@@ -1370,89 +1470,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
|
||||||
ticks++;
|
|
||||||
|
|
||||||
// Hide bird if the browser is fullscreen
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
hideBirb();
|
|
||||||
// Won't be restored on fullscreen exit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
|
|
||||||
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
|
|
||||||
hop();
|
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
|
||||||
// Idle for a while, do something
|
|
||||||
if (focusedElement === null) {
|
|
||||||
// Fly to an element
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
} else if (Math.random() < 1 / (60 * 20)) {
|
|
||||||
// Fly to another element if idle for a longer while
|
|
||||||
focusOnElement();
|
|
||||||
lastActionTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentState === States.HOP) {
|
|
||||||
if (updateParabolicPath(HOP_SPEED)) {
|
|
||||||
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;
|
|
||||||
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
|
|
||||||
lastPetTimestamp = 0;
|
|
||||||
activateFeather();
|
|
||||||
}
|
|
||||||
updateFeather();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFocusedElementBounds();
|
|
||||||
|
|
||||||
// Update the bird's position
|
|
||||||
if (currentState === States.IDLE) {
|
|
||||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
birdY = getFocusedY();
|
|
||||||
} else if (currentState === States.FLYING) {
|
|
||||||
// Fly to target location (even if in the air)
|
|
||||||
if (updateParabolicPath(FLY_SPEED)) {
|
|
||||||
setState(States.IDLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldTargetY = targetY;
|
|
||||||
targetY = getFocusedY();
|
|
||||||
// Adjust startY to account for scrolling
|
|
||||||
startY += targetY - oldTargetY;
|
|
||||||
if (targetY < 0 || targetY > window.innerHeight) {
|
|
||||||
// Fly to ground if the focused element moves out of bounds
|
|
||||||
focusOnGround();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
|
|
||||||
setAnimation(Animations.STILL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update HTML element position
|
|
||||||
setX(birdX);
|
|
||||||
setY(birdY);
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an HTML element with the specified parameters
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -1472,6 +1489,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");
|
||||||
@@ -1551,21 +1604,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
|
||||||
*/
|
*/
|
||||||
@@ -1582,28 +1630,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1712,60 +1746,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertPico8();
|
|
||||||
|
|
||||||
function isSafari() {
|
function isSafari() {
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} pid
|
|
||||||
*/
|
|
||||||
function insertPico8(name, pid) {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">${name}</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-pico-8-content">
|
|
||||||
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=${pid}" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
|
|
||||||
</div>`
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertMusicPlayer() {
|
|
||||||
let html = `
|
|
||||||
<div class="birb-window-header">
|
|
||||||
<div class="birb-window-title">Music Player</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content birb-music-player-content">
|
|
||||||
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/31FWVQBp3WQydWLNhO0ACi?utm_source=generator" width="250" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
|
|
||||||
</div>`;
|
|
||||||
const pico8 = makeElement("birb-window");
|
|
||||||
pico8.innerHTML = html;
|
|
||||||
document.body.appendChild(pico8);
|
|
||||||
makeDraggable(pico8.querySelector(".birb-window-header"));
|
|
||||||
const close = pico8.querySelector(".birb-window-close");
|
|
||||||
if (close) {
|
|
||||||
makeClosable(() => {
|
|
||||||
pico8.remove();
|
|
||||||
}, close);
|
|
||||||
}
|
|
||||||
centerElement(pico8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
*/
|
*/
|
||||||
@@ -1879,7 +1863,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
*/
|
*/
|
||||||
function onClick(element, action) {
|
function onClick(element, action) {
|
||||||
element.addEventListener("click", (e) => action(e));
|
element.addEventListener("click", (e) => action(e));
|
||||||
element.addEventListener("touchstart", (e) => action(e));
|
element.addEventListener("touchend", (e) => {
|
||||||
|
if (e instanceof TouchEvent === false) {
|
||||||
|
return;
|
||||||
|
} else if (element instanceof HTMLElement === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = e.changedTouches[0];
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
touch.clientX >= rect.left &&
|
||||||
|
touch.clientX <= rect.right &&
|
||||||
|
touch.clientY >= rect.top &&
|
||||||
|
touch.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
|
action(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2039,7 +2039,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
function getFullWindowHeight() {
|
function getFullWindowHeight() {
|
||||||
return document.documentElement.clientHeight;
|
return document.documentElement.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusOnGround() {
|
function focusOnGround() {
|
||||||
console.log("Focusing on ground");
|
console.log("Focusing on ground");
|
||||||
focusedElement = null;
|
focusedElement = null;
|
||||||
@@ -2054,12 +2054,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;
|
||||||
}
|
}
|
||||||
@@ -2076,12 +2075,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() {
|
||||||
@@ -2109,7 +2104,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pet() {
|
function pet() {
|
||||||
if (currentState === States.IDLE) {
|
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
|
||||||
setAnimation(Animations.HEART);
|
setAnimation(Animations.HEART);
|
||||||
lastPetTimestamp = Date.now();
|
lastPetTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
@@ -2189,47 +2184,53 @@ 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 };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the birb
|
||||||
|
init();
|
||||||
|
draw();
|
||||||
|
}).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
|
* @returns {boolean} Whether the user is on a mobile device
|
||||||
*/
|
*/
|
||||||
|
|||||||
36
manifest.json
Normal file
36
manifest.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Pocket Bird",
|
||||||
|
"description": "It's a bird, in your browser. What more could you want?",
|
||||||
|
"version": "2025.10.25.130",
|
||||||
|
"homepage_url": "https://idreesinc.com",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"./dist/birb.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab"
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"images/*"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "birb@idreesinc.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -241,8 +241,8 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-left: 15px;
|
padding-left: 10px;
|
||||||
padding-right: 15px;
|
padding-right: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,12 +289,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.birb-field-guide-description {
|
.birb-field-guide-description {
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 20px);
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: rgb(124, 108, 75);
|
color: rgb(124, 108, 75);
|
||||||
|
|||||||
Reference in New Issue
Block a user