Add hat item

This commit is contained in:
Idrees Hassan
2026-01-21 22:25:49 -05:00
parent f5742ac3a7
commit 3b2081943d
11 changed files with 1034 additions and 252 deletions

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @name Pocket Bird
// @namespace https://idreesinc.com
// @version 2026.1.20
// @version 2026.1.21
// @description It's a pet bird in your browser, what more could you want?
// @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
@@ -643,6 +643,7 @@
FLOWER_HAT: "flower-hat"
};
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = {
[HAT.NONE]: {
name: "Invisible Hat",
@@ -698,62 +699,108 @@
}
const index = i - 1;
const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1);
const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer);
}
return hatLayers;
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatId
* @returns {Anim}
*/
function createHatItemAnimation(hatId, spriteSheet) {
const hatLayer = buildHatItemLayer(spriteSheet, hatId);
const frames = [
new Frame([hatLayer])
];
return new Anim(frames, [1000], true);
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatName
* @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0]
* @returns {Layer}
*/
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) {
function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6;
const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = [];
let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING);
hatPixels = drawOutline(hatPixels, false);
return new Layer(hatPixels, hatName);
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatId
* @returns {Layer}
*/
function buildHatItemLayer(spriteSheet, hatId) {
if (hatId === HAT.NONE) {
return new Layer([], TAG.DEFAULT);
}
const hatIndex = Object.keys(HAT).indexOf(hatId) - 1;
let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
hatPixels = pad(hatPixels, 1, 1, 1, 1);
hatPixels = drawOutline(hatPixels, true);
hatPixels = pushToBottom(hatPixels);
return new Layer(hatPixels, TAG.DEFAULT);
}
/**
* Add transparent padding around the pixel array
* @param {string[][]} pixels
* @param {number} top
* @param {number} bottom
* @param {number} left
* @param {number} right
* @returns {string[][]}
*/
function pad(pixels, top, bottom, left, right) {
const paddedPixels = [];
const rowLength = pixels[0].length + left + right;
// Top padding
for (let y = 0; y < TOP_PADDING; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING)
.fill(PALETTE.TRANSPARENT)
);
for (let y = 0; y < top; y++) {
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
}
// Left and right padding
for (let y = 0; y < hatPixels.length; y++) {
for (let y = 0; y < pixels.length; y++) {
const row = [];
for (let x = 0; x < LEFT_PADDING; x++) {
for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT);
}
for (let x = 0; x < hatPixels[y].length; x++) {
row.push(hatPixels[y][x]);
for (let x = 0; x < pixels[y].length; x++) {
row.push(pixels[y][x]);
}
for (let x = 0; x < RIGHT_PADDING; x++) {
for (let x = 0; x < right; x++) {
row.push(PALETTE.TRANSPARENT);
}
paddedHatPixels.push(row);
paddedPixels.push(row);
}
// Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING)
.fill(PALETTE.TRANSPARENT)
);
for (let y = 0; y < bottom; y++) {
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
}
return paddedPixels;
}
// Add outline
/**
* Draw an outline around non-transparent pixels
* @param {string[][]} pixels
* @param {boolean} [outlineBottom=false]
* @return {string[][]}
*/
function drawOutline(pixels, outlineBottom = false) {
let neighborOffsets = [
[-1, 0],
[1, 0],
@@ -764,21 +811,42 @@
if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
}
for (let y = 0; y < paddedHatPixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) {
const pixel = paddedHatPixels[y][x];
for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < pixels[y].length; x++) {
const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) {
const newX = x + dx;
const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER;
if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
pixels[newY][newX] = PALETTE.BORDER;
}
}
}
}
}
return new Layer(paddedHatPixels, hatName);
return pixels;
}
/**
* Trim transparent rows from the bottom and push them to the top
* @param {string[][]} pixels
* @returns {string[][]}
*/
function pushToBottom(pixels) {
let trimmedPixels = pixels.slice();
let trimCount = 0;
while (trimmedPixels.length > 1) {
const firstRow = trimmedPixels[trimmedPixels.length - 1];
if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) {
trimmedPixels.pop();
trimCount++;
} else {
break;
}
}
trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0);
return trimmedPixels;
}
/**
@@ -1607,6 +1675,16 @@
z-index: 2147483630 !important;
}
.birb-item {
image-rendering: pixelated;
position: absolute;
bottom: 0;
transform-origin: bottom;
transform: scale(calc(var(--birb-scale) * 1.5)) !important;
transform-origin: bottom;
z-index: 2147483630 !important;
}
.birb-window {
font-family: "Monocraft", monospace !important;
line-height: initial !important;
@@ -1955,6 +2033,7 @@
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE;
@@ -2073,7 +2152,7 @@
insertModal(`${birdBirb()} Mode`, message);
}),
new Separator(),
new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false),
new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false),
];
const styleElement = document.createElement("style");
@@ -2262,6 +2341,9 @@
setInterval(update, UPDATE_INTERVAL);
focusOnElement(true);
// TODO: This is for testing
generateHat();
}
function update() {
@@ -2442,6 +2524,47 @@
}
}
/**
* Insert the hat as an item element in the document if possible
*/
function generateHat() {
if (document.querySelector("#" + HAT_ID)) {
return;
}
// Select a random hat
const hatKeys = Object.keys(HAT);
const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1];
// Find a random valid element to place the hat on
const element = getRandomValidElement();
if (!element) {
return;
}
// Create hat element
const hatCanvas = document.createElement("canvas");
hatCanvas.id = HAT_ID;
hatCanvas.classList.add("birb-item");
hatCanvas.width = 14 * CANVAS_PIXEL_SIZE;
hatCanvas.height = 14 * CANVAS_PIXEL_SIZE;
const hatCtx = hatCanvas.getContext("2d");
if (!hatCtx) {
return;
}
// Create hat animation
const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET);
hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]);
// Position hat above the element
const rect = element.getBoundingClientRect();
hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px";
hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px";
// Append to document
document.body.appendChild(hatCanvas);
}
/**
* @param {string} birdType
*/
@@ -2773,14 +2896,9 @@
}
/**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
* @returns {HTMLElement|null} The random element, or null if no valid element was found
*/
function focusOnElement(teleport = false) {
if (frozen) {
return false;
}
function getRandomValidElement() {
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => {
@@ -2802,10 +2920,22 @@
}
});
if (nonFixedElements.length === 0) {
return false;
return null;
}
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
focusedElement = randomElement;
return randomElement;
}
/**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
*/
function focusOnElement(teleport = false) {
if (frozen) {
return false;
}
focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds();
if (teleport) {
@@ -2813,7 +2943,7 @@
} else {
flyTo(getFocusedElementRandomX(), getFocusedY());
}
return randomElement !== null;
return focusedElement !== null;
}
/**