mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-25 12:17:22 +00:00
Compare commits
39 Commits
birdsong
...
soft-outli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddcd7a693d | ||
|
|
868cd06210 | ||
|
|
307d4a8895 | ||
|
|
5a33cef4d5 | ||
|
|
7b4ebf7ab8 | ||
|
|
e393013b27 | ||
|
|
db1a3dcbb6 | ||
|
|
8cd93bb623 | ||
|
|
a3a09c6819 | ||
|
|
912327a348 | ||
|
|
d54f208cc4 | ||
|
|
cb1f2f605f | ||
|
|
2ee6ea84a7 | ||
|
|
5e04727a1b | ||
|
|
7b1df9bc4f | ||
|
|
130fae6e0c | ||
|
|
3b2081943d | ||
|
|
f5742ac3a7 | ||
|
|
867d214292 | ||
|
|
d97e39449e | ||
|
|
7628ee2c87 | ||
|
|
3227167cb5 | ||
|
|
e0fae3781a | ||
|
|
2773538a6c | ||
|
|
2a90a56a2b | ||
|
|
94454a2338 | ||
|
|
cf968dfec4 | ||
|
|
4838457054 | ||
|
|
e09d4f9eea | ||
|
|
7c38bf9164 | ||
|
|
8263fadfba | ||
|
|
9f7d864e57 | ||
|
|
579967a302 | ||
|
|
ca1495a9f1 | ||
|
|
fd865cacb8 | ||
|
|
5e94998410 | ||
|
|
e13a67e967 | ||
|
|
e1759bc235 | ||
|
|
1d818d83cf |
@@ -95,6 +95,7 @@ If you are running Pocket bird on a browser, the extension needs these permissio
|
|||||||
Here are some websites where you can find Pocket Bird hopping around:
|
Here are some websites where you can find Pocket Bird hopping around:
|
||||||
|
|
||||||
- [https://grepjason.sh](https://grepjason.sh)
|
- [https://grepjason.sh](https://grepjason.sh)
|
||||||
|
- [https://binarydigit.dev](https://binarydigit.dev)
|
||||||
|
|
||||||
*If you've added Pocket Bird to your website, let me know and I'll add it to this list!*
|
*If you've added Pocket Bird to your website, let me know and I'll add it to this list!*
|
||||||
|
|
||||||
|
|||||||
BIN
aseprite/birb-white.aseprite
Normal file
BIN
aseprite/birb-white.aseprite
Normal file
Binary file not shown.
Binary file not shown.
BIN
aseprite/hats.aseprite
Normal file
BIN
aseprite/hats.aseprite
Normal file
Binary file not shown.
4
build.js
4
build.js
@@ -46,6 +46,10 @@ const spriteSheets = [
|
|||||||
{
|
{
|
||||||
key: "__FEATHER_SPRITE_SHEET__",
|
key: "__FEATHER_SPRITE_SHEET__",
|
||||||
path: SPRITES_DIR + "/feather.png"
|
path: SPRITES_DIR + "/feather.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "__HATS_SPRITE_SHEET__",
|
||||||
|
path: SPRITES_DIR + "/hats.png"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
918
dist/extension/birb.js
vendored
918
dist/extension/birb.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"description": "It's a pet bird in your browser, what more could you want?",
|
"description": "It's a pet bird in your browser, what more could you want?",
|
||||||
"version": "2026.1.4",
|
"version": "2026.1.24",
|
||||||
"homepage_url": "https://idreesinc.com",
|
"homepage_url": "https://idreesinc.com",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "images/icons/transparent/48x48x1.png",
|
"48": "images/icons/transparent/48x48x1.png",
|
||||||
|
|||||||
920
dist/obsidian/main.js
vendored
920
dist/obsidian/main.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/obsidian/manifest.json
vendored
2
dist/obsidian/manifest.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "pocket-bird",
|
"id": "pocket-bird",
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"version": "2026.1.4",
|
"version": "2026.1.24",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Add a pet bird to fly around your notes and keep you company!",
|
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||||
"author": "Idrees Hassan",
|
"author": "Idrees Hassan",
|
||||||
|
|||||||
920
dist/userscript/birb.user.js
vendored
920
dist/userscript/birb.user.js
vendored
File diff suppressed because it is too large
Load Diff
918
dist/web/birb.embed.js
vendored
918
dist/web/birb.embed.js
vendored
File diff suppressed because it is too large
Load Diff
918
dist/web/birb.js
vendored
918
dist/web/birb.js
vendored
File diff suppressed because it is too large
Load Diff
BIN
sprites/birb.png
BIN
sprites/birb.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
sprites/hats.png
Normal file
BIN
sprites/hats.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 939 B |
76
src/Frame.js
76
src/Frame.js
@@ -1,76 +0,0 @@
|
|||||||
import { Directions } from './shared.js';
|
|
||||||
import { Sprite, BirdType } from './sprites.js';
|
|
||||||
import Layer from './layer.js';
|
|
||||||
|
|
||||||
class Frame {
|
|
||||||
|
|
||||||
/** @type {{ [tag: string]: string[][] }} */
|
|
||||||
#pixelsByTag = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Layer[]} layers
|
|
||||||
*/
|
|
||||||
constructor(layers) {
|
|
||||||
/** @type {Set<string>} */
|
|
||||||
let tags = new Set();
|
|
||||||
for (let layer of layers) {
|
|
||||||
tags.add(layer.tag);
|
|
||||||
}
|
|
||||||
tags.add("default");
|
|
||||||
for (let tag of tags) {
|
|
||||||
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
|
|
||||||
if (layers[0].tag !== "default") {
|
|
||||||
throw new Error("First layer must have the 'default' tag");
|
|
||||||
}
|
|
||||||
this.pixels = layers[0].pixels.map(row => row.slice());
|
|
||||||
// Pad from top with transparent pixels
|
|
||||||
while (this.pixels.length < maxHeight) {
|
|
||||||
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
|
|
||||||
}
|
|
||||||
// Combine layers
|
|
||||||
for (let i = 1; i < layers.length; i++) {
|
|
||||||
if (layers[i].tag === "default" || layers[i].tag === tag) {
|
|
||||||
let layerPixels = layers[i].pixels;
|
|
||||||
let topMargin = maxHeight - layerPixels.length;
|
|
||||||
for (let y = 0; y < layerPixels.length; y++) {
|
|
||||||
for (let x = 0; x < layerPixels[y].length; x++) {
|
|
||||||
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.#pixelsByTag[tag] = this.pixels.map(row => row.slice());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} [tag]
|
|
||||||
* @returns {string[][]}
|
|
||||||
*/
|
|
||||||
getPixels(tag = "default") {
|
|
||||||
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
|
||||||
* @param {BirdType} [species]
|
|
||||||
* @param {number} direction
|
|
||||||
* @param {number} canvasPixelSize
|
|
||||||
*/
|
|
||||||
draw(ctx, direction, canvasPixelSize, species) {
|
|
||||||
// Clear the canvas before drawing the new frame
|
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
||||||
|
|
||||||
const pixels = this.getPixels(species?.tags[0]);
|
|
||||||
for (let y = 0; y < pixels.length; y++) {
|
|
||||||
const row = pixels[y];
|
|
||||||
for (let x = 0; x < pixels[y].length; x++) {
|
|
||||||
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
|
|
||||||
ctx.fillStyle = species?.colors[cell] ?? cell;
|
|
||||||
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Frame;
|
|
||||||
12
src/Layer.js
12
src/Layer.js
@@ -1,12 +0,0 @@
|
|||||||
class Layer {
|
|
||||||
/**
|
|
||||||
* @param {string[][]} pixels
|
|
||||||
* @param {string} [tag]
|
|
||||||
*/
|
|
||||||
constructor(pixels, tag = "default") {
|
|
||||||
this.pixels = pixels;
|
|
||||||
this.tag = tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layer;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Frame from "./frame.js";
|
import Frame from "./frame.js";
|
||||||
import { BirdType } from "./sprites";
|
import { BirdType } from "./sprites.js";
|
||||||
|
|
||||||
class Anim {
|
class Anim {
|
||||||
/**
|
/**
|
||||||
@@ -59,10 +59,11 @@ class Anim {
|
|||||||
* @param {number} direction
|
* @param {number} direction
|
||||||
* @param {number} timeStart The start time of the animation in milliseconds
|
* @param {number} timeStart The start time of the animation in milliseconds
|
||||||
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
|
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
|
||||||
* @param {BirdType} [species] The species to use for the animation
|
* @param {{ [key: string]: string }} colorScheme The color scheme to use for the animation
|
||||||
|
* @param {string[]} tags The tags to use for the animation
|
||||||
* @returns {boolean} Whether the animation is complete
|
* @returns {boolean} Whether the animation is complete
|
||||||
*/
|
*/
|
||||||
draw(ctx, direction, timeStart, canvasPixelSize, species) {
|
draw(ctx, direction, timeStart, canvasPixelSize, colorScheme, tags) {
|
||||||
// Reset cache if animation was restarted
|
// Reset cache if animation was restarted
|
||||||
if (this.lastTimeStart !== timeStart) {
|
if (this.lastTimeStart !== timeStart) {
|
||||||
this.#clearCache();
|
this.#clearCache();
|
||||||
@@ -79,7 +80,7 @@ class Anim {
|
|||||||
const currentFrameIndex = this.getCurrentFrameIndex(time);
|
const currentFrameIndex = this.getCurrentFrameIndex(time);
|
||||||
|
|
||||||
if (this.#shouldRedraw(currentFrameIndex, direction)) {
|
if (this.#shouldRedraw(currentFrameIndex, direction)) {
|
||||||
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
|
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, colorScheme, tags);
|
||||||
this.lastFrameIndex = currentFrameIndex;
|
this.lastFrameIndex = currentFrameIndex;
|
||||||
this.lastDirection = direction;
|
this.lastDirection = direction;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Directions } from './shared.js';
|
import { Directions } from '../shared.js';
|
||||||
import { Sprite, BirdType } from './sprites.js';
|
import { PALETTE, BirdType } from './sprites.js';
|
||||||
import Layer from './layer.js';
|
import Layer, { TAG } from './layer.js';
|
||||||
|
|
||||||
class Frame {
|
class Frame {
|
||||||
|
|
||||||
@@ -16,25 +16,25 @@ class Frame {
|
|||||||
for (let layer of layers) {
|
for (let layer of layers) {
|
||||||
tags.add(layer.tag);
|
tags.add(layer.tag);
|
||||||
}
|
}
|
||||||
tags.add("default");
|
tags.add(TAG.DEFAULT);
|
||||||
for (let tag of tags) {
|
for (let tag of tags) {
|
||||||
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
|
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
|
||||||
if (layers[0].tag !== "default") {
|
if (layers[0].tag !== TAG.DEFAULT) {
|
||||||
throw new Error("First layer must have the 'default' tag");
|
throw new Error("First layer must have the 'default' tag");
|
||||||
}
|
}
|
||||||
this.pixels = layers[0].pixels.map(row => row.slice());
|
this.pixels = layers[0].pixels.map(row => row.slice());
|
||||||
// Pad from top with transparent pixels
|
// Pad from top with transparent pixels
|
||||||
while (this.pixels.length < maxHeight) {
|
while (this.pixels.length < maxHeight) {
|
||||||
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
|
this.pixels.unshift(new Array(this.pixels[0].length).fill(PALETTE.TRANSPARENT));
|
||||||
}
|
}
|
||||||
// Combine layers
|
// Combine layers
|
||||||
for (let i = 1; i < layers.length; i++) {
|
for (let i = 1; i < layers.length; i++) {
|
||||||
if (layers[i].tag === "default" || layers[i].tag === tag) {
|
if (layers[i].tag === TAG.DEFAULT || layers[i].tag === tag) {
|
||||||
let layerPixels = layers[i].pixels;
|
let layerPixels = layers[i].pixels;
|
||||||
let topMargin = maxHeight - layerPixels.length;
|
let topMargin = maxHeight - layerPixels.length;
|
||||||
for (let y = 0; y < layerPixels.length; y++) {
|
for (let y = 0; y < layerPixels.length; y++) {
|
||||||
for (let x = 0; x < layerPixels[y].length; x++) {
|
for (let x = 0; x < layerPixels[y].length; x++) {
|
||||||
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
|
this.pixels[y + topMargin][x] = layerPixels[y][x] !== PALETTE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,29 +44,36 @@ class Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} [tag]
|
* @param {string[]} [tags]
|
||||||
* @returns {string[][]}
|
* @returns {string[][]}
|
||||||
*/
|
*/
|
||||||
getPixels(tag = "default") {
|
getPixels(tags = [TAG.DEFAULT]) {
|
||||||
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
|
for (let i = tags.length - 1; i >= 0; i--) {
|
||||||
|
const tag = tags[i];
|
||||||
|
if (this.#pixelsByTag[tag]) {
|
||||||
|
return this.#pixelsByTag[tag];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.#pixelsByTag[TAG.DEFAULT];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
* @param {BirdType} [species]
|
* @param {number} direction
|
||||||
* @param {number} direction
|
|
||||||
* @param {number} canvasPixelSize
|
* @param {number} canvasPixelSize
|
||||||
|
* @param {{ [key: string]: string }} colorScheme
|
||||||
|
* @param {string[]} tags
|
||||||
*/
|
*/
|
||||||
draw(ctx, direction, canvasPixelSize, species) {
|
draw(ctx, direction, canvasPixelSize, colorScheme, tags) {
|
||||||
// Clear the canvas before drawing the new frame
|
// Clear the canvas before drawing the new frame
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
|
||||||
const pixels = this.getPixels(species?.tags[0]);
|
const pixels = this.getPixels(tags);
|
||||||
for (let y = 0; y < pixels.length; y++) {
|
for (let y = 0; y < pixels.length; y++) {
|
||||||
const row = pixels[y];
|
const row = pixels[y];
|
||||||
for (let x = 0; x < pixels[y].length; x++) {
|
for (let x = 0; x < pixels[y].length; x++) {
|
||||||
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
|
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
|
||||||
ctx.fillStyle = species?.colors[cell] ?? cell;
|
ctx.fillStyle = colorScheme[cell] ?? cell;
|
||||||
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
|
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
export const TAG = {
|
||||||
|
DEFAULT: "default",
|
||||||
|
TUFT: "tuft",
|
||||||
|
};
|
||||||
|
|
||||||
class Layer {
|
class Layer {
|
||||||
/**
|
/**
|
||||||
* @param {string[][]} pixels
|
* @param {string[][]} pixels
|
||||||
* @param {string} [tag]
|
* @param {string} [tag]
|
||||||
*/
|
*/
|
||||||
constructor(pixels, tag = "default") {
|
constructor(pixels, tag = TAG.DEFAULT) {
|
||||||
this.pixels = pixels;
|
this.pixels = pixels;
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
209
src/animation/sprites.js
Normal file
209
src/animation/sprites.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { TAG } from "./layer.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Palette color names
|
||||||
|
* @type {Record<string, string>}
|
||||||
|
*/
|
||||||
|
export const PALETTE = {
|
||||||
|
THEME_HIGHLIGHT: "theme-highlight",
|
||||||
|
TRANSPARENT: "transparent",
|
||||||
|
OUTLINE: "outline",
|
||||||
|
BORDER: "border",
|
||||||
|
FOOT: "foot",
|
||||||
|
BEAK: "beak",
|
||||||
|
EYE: "eye",
|
||||||
|
FACE: "face",
|
||||||
|
HOOD: "hood",
|
||||||
|
NOSE: "nose",
|
||||||
|
BELLY: "belly",
|
||||||
|
UNDERBELLY: "underbelly",
|
||||||
|
WING: "wing",
|
||||||
|
WING_EDGE: "wing-edge",
|
||||||
|
HEART: "heart",
|
||||||
|
HEART_BORDER: "heart-border",
|
||||||
|
HEART_SHINE: "heart-shine",
|
||||||
|
FEATHER_SPINE: "feather-spine",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of sprite sheet colors to palette colors
|
||||||
|
* @type {Record<string, string>}
|
||||||
|
*/
|
||||||
|
export const SPRITE_SHEET_COLOR_MAP = {
|
||||||
|
"transparent": PALETTE.TRANSPARENT,
|
||||||
|
"#fff000": PALETTE.THEME_HIGHLIGHT,
|
||||||
|
"#ffffff": PALETTE.BORDER,
|
||||||
|
"#000000": PALETTE.OUTLINE,
|
||||||
|
"#010a19": PALETTE.BEAK,
|
||||||
|
"#190301": PALETTE.EYE,
|
||||||
|
"#af8e75": PALETTE.FOOT,
|
||||||
|
"#639bff": PALETTE.FACE,
|
||||||
|
"#99e550": PALETTE.HOOD,
|
||||||
|
"#d95763": PALETTE.NOSE,
|
||||||
|
"#f8b143": PALETTE.BELLY,
|
||||||
|
"#ec8637": PALETTE.UNDERBELLY,
|
||||||
|
"#578ae6": PALETTE.WING,
|
||||||
|
"#326ed9": PALETTE.WING_EDGE,
|
||||||
|
"#c82e2e": PALETTE.HEART,
|
||||||
|
"#501a1a": PALETTE.HEART_BORDER,
|
||||||
|
"#ff6b6b": PALETTE.HEART_SHINE,
|
||||||
|
"#373737": PALETTE.FEATHER_SPINE,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BirdType {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} description
|
||||||
|
* @param {Record<string, string>} colors
|
||||||
|
* @param {string[]} [tags]
|
||||||
|
*/
|
||||||
|
constructor(name, description, colors, tags = []) {
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
const defaultColors = {
|
||||||
|
[PALETTE.TRANSPARENT]: "transparent",
|
||||||
|
[PALETTE.OUTLINE]: "#000000",
|
||||||
|
[PALETTE.BORDER]: "#ffffff",
|
||||||
|
[PALETTE.BEAK]: "#000000",
|
||||||
|
[PALETTE.EYE]: "#000000",
|
||||||
|
[PALETTE.HEART]: "#c82e2e",
|
||||||
|
[PALETTE.HEART_BORDER]: "#501a1a",
|
||||||
|
[PALETTE.HEART_SHINE]: "#ff6b6b",
|
||||||
|
[PALETTE.FEATHER_SPINE]: "#373737",
|
||||||
|
[PALETTE.HOOD]: colors.face,
|
||||||
|
[PALETTE.NOSE]: colors.face,
|
||||||
|
};
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||||
|
this.tags = tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, BirdType>} */
|
||||||
|
export const SPECIES = {
|
||||||
|
bluebird: new BirdType("Eastern Bluebird",
|
||||||
|
"Native to North American and very social, though can be timid around people.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#639bff",
|
||||||
|
[PALETTE.BELLY]: "#f8b143",
|
||||||
|
[PALETTE.UNDERBELLY]: "#ec8637",
|
||||||
|
[PALETTE.WING]: "#578ae6",
|
||||||
|
[PALETTE.WING_EDGE]: "#326ed9",
|
||||||
|
}),
|
||||||
|
shimaEnaga: new BirdType("Shima Enaga",
|
||||||
|
"Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#ffffff",
|
||||||
|
[PALETTE.BELLY]: "#ebe9e8",
|
||||||
|
[PALETTE.UNDERBELLY]: "#ebd9d0",
|
||||||
|
[PALETTE.WING]: "#f3d3c1",
|
||||||
|
[PALETTE.WING_EDGE]: "#2d2d2d",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#d7ac93",
|
||||||
|
}),
|
||||||
|
tuftedTitmouse: new BirdType("Tufted Titmouse",
|
||||||
|
"Native to the eastern United States, full of personality, and notably my wife's favorite bird.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#c7cad7",
|
||||||
|
[PALETTE.BELLY]: "#e4e5eb",
|
||||||
|
[PALETTE.UNDERBELLY]: "#d7cfcb",
|
||||||
|
[PALETTE.WING]: "#b1b5c5",
|
||||||
|
[PALETTE.WING_EDGE]: "#9d9fa9",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#b9abcf",
|
||||||
|
}, [TAG.TUFT]),
|
||||||
|
europeanRobin: new BirdType("European Robin",
|
||||||
|
"Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#ffaf34",
|
||||||
|
[PALETTE.HOOD]: "#aaa094",
|
||||||
|
[PALETTE.BELLY]: "#ffaf34",
|
||||||
|
[PALETTE.UNDERBELLY]: "#babec2",
|
||||||
|
[PALETTE.WING]: "#aaa094",
|
||||||
|
[PALETTE.WING_EDGE]: "#888580",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#ffaf34",
|
||||||
|
}),
|
||||||
|
redCardinal: new BirdType("Red Cardinal",
|
||||||
|
"Native to the eastern United States, this strikingly red bird is hard to miss.", {
|
||||||
|
[PALETTE.BEAK]: "#d93619",
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#31353d",
|
||||||
|
[PALETTE.HOOD]: "#e83a1b",
|
||||||
|
[PALETTE.BELLY]: "#e83a1b",
|
||||||
|
[PALETTE.UNDERBELLY]: "#dc3719",
|
||||||
|
[PALETTE.WING]: "#d23215",
|
||||||
|
[PALETTE.WING_EDGE]: "#b1321c",
|
||||||
|
}, [TAG.TUFT]),
|
||||||
|
americanGoldfinch: new BirdType("American Goldfinch",
|
||||||
|
"Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", {
|
||||||
|
[PALETTE.BEAK]: "#ffaf34",
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#fff255",
|
||||||
|
[PALETTE.NOSE]: "#383838",
|
||||||
|
[PALETTE.HOOD]: "#383838",
|
||||||
|
[PALETTE.BELLY]: "#fff255",
|
||||||
|
[PALETTE.UNDERBELLY]: "#f5ea63",
|
||||||
|
[PALETTE.WING]: "#e8e079",
|
||||||
|
[PALETTE.WING_EDGE]: "#191919",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#ffcc00"
|
||||||
|
}),
|
||||||
|
barnSwallow: new BirdType("Barn Swallow",
|
||||||
|
"Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#db7c4d",
|
||||||
|
[PALETTE.BELLY]: "#f7e1c9",
|
||||||
|
[PALETTE.UNDERBELLY]: "#ebc9a3",
|
||||||
|
[PALETTE.WING]: "#2252a9",
|
||||||
|
[PALETTE.WING_EDGE]: "#1c448b",
|
||||||
|
[PALETTE.HOOD]: "#2252a9",
|
||||||
|
}),
|
||||||
|
mistletoebird: new BirdType("Mistletoebird",
|
||||||
|
"Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", {
|
||||||
|
[PALETTE.FOOT]: "#6c6a7c",
|
||||||
|
[PALETTE.FACE]: "#352e6d",
|
||||||
|
[PALETTE.BELLY]: "#fd6833",
|
||||||
|
[PALETTE.UNDERBELLY]: "#e6e1d8",
|
||||||
|
[PALETTE.WING]: "#342b7c",
|
||||||
|
[PALETTE.WING_EDGE]: "#282065",
|
||||||
|
}),
|
||||||
|
redAvadavat: new BirdType("Red Avadavat",
|
||||||
|
"Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", {
|
||||||
|
[PALETTE.BEAK]: "#f71919",
|
||||||
|
[PALETTE.FOOT]: "#af7575",
|
||||||
|
[PALETTE.FACE]: "#cb092b",
|
||||||
|
[PALETTE.BELLY]: "#ae1724",
|
||||||
|
[PALETTE.UNDERBELLY]: "#831b24",
|
||||||
|
[PALETTE.WING]: "#7e3030",
|
||||||
|
[PALETTE.WING_EDGE]: "#490f0f",
|
||||||
|
}),
|
||||||
|
scarletRobin: new BirdType("Scarlet Robin",
|
||||||
|
"Native to Australia, this striking robin can be found in Eucalyptus forests.", {
|
||||||
|
[PALETTE.FOOT]: "#494949",
|
||||||
|
[PALETTE.FACE]: "#3d3d3d",
|
||||||
|
[PALETTE.BELLY]: "#fc5633",
|
||||||
|
[PALETTE.UNDERBELLY]: "#dcdcdc",
|
||||||
|
[PALETTE.WING]: "#2b2b2b",
|
||||||
|
[PALETTE.WING_EDGE]: "#ebebeb",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#fc5633",
|
||||||
|
}),
|
||||||
|
americanRobin: new BirdType("American Robin",
|
||||||
|
"While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", {
|
||||||
|
[PALETTE.BEAK]: "#e89f30",
|
||||||
|
[PALETTE.FOOT]: "#9f8075",
|
||||||
|
[PALETTE.FACE]: "#2d2d2d",
|
||||||
|
[PALETTE.BELLY]: "#eb7a3a",
|
||||||
|
[PALETTE.UNDERBELLY]: "#eb7a3a",
|
||||||
|
[PALETTE.WING]: "#444444",
|
||||||
|
[PALETTE.WING_EDGE]: "#232323",
|
||||||
|
[PALETTE.THEME_HIGHLIGHT]: "#eb7a3a",
|
||||||
|
}),
|
||||||
|
carolinaWren: new BirdType("Carolina Wren",
|
||||||
|
"Native to the eastern United States, these little birds are known for their curious and energetic nature.", {
|
||||||
|
[PALETTE.FOOT]: "#af8e75",
|
||||||
|
[PALETTE.FACE]: "#edc7a9",
|
||||||
|
[PALETTE.NOSE]: "#f7eee5",
|
||||||
|
[PALETTE.HOOD]: "#c58a5b",
|
||||||
|
[PALETTE.BELLY]: "#e1b796",
|
||||||
|
[PALETTE.UNDERBELLY]: "#c79e7c",
|
||||||
|
[PALETTE.WING]: "#c58a5b",
|
||||||
|
[PALETTE.WING_EDGE]: "#866348",
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Frame from './frame.js';
|
import Frame from './animation/frame.js';
|
||||||
import Layer from './layer.js';
|
import Layer, { TAG } from './animation/layer.js';
|
||||||
import Anim from './anim.js';
|
import Anim from './animation/anim.js';
|
||||||
import { Birb, Animations } from './birb.js';
|
import { Birb, Animations } from './birb.js';
|
||||||
import { Birdsong } from './sound.js';
|
import { Birdsong } from './sound.js';
|
||||||
import { Context, ObsidianContext } from './context.js';
|
import { Context, ObsidianContext } from './context.js';
|
||||||
@@ -19,14 +19,14 @@ import {
|
|||||||
log,
|
log,
|
||||||
debug,
|
debug,
|
||||||
error,
|
error,
|
||||||
getLayer,
|
getLayerPixels,
|
||||||
getWindowHeight
|
getWindowHeight
|
||||||
} from './shared.js';
|
} from './shared.js';
|
||||||
import {
|
import {
|
||||||
Sprite,
|
PALETTE,
|
||||||
SPRITE_SHEET_COLOR_MAP,
|
SPRITE_SHEET_COLOR_MAP,
|
||||||
SPECIES
|
SPECIES
|
||||||
} from './sprites.js';
|
} from './animation/sprites.js';
|
||||||
import {
|
import {
|
||||||
StickyNote,
|
StickyNote,
|
||||||
createNewStickyNote,
|
createNewStickyNote,
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
switchMenuItems,
|
switchMenuItems,
|
||||||
MENU_EXIT_ID
|
MENU_EXIT_ID
|
||||||
} from './menu.js';
|
} from './menu.js';
|
||||||
|
import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +54,8 @@ import {
|
|||||||
* @typedef {Object} BirbSaveData
|
* @typedef {Object} BirbSaveData
|
||||||
* @property {string[]} unlockedSpecies
|
* @property {string[]} unlockedSpecies
|
||||||
* @property {string} currentSpecies
|
* @property {string} currentSpecies
|
||||||
|
* @property {string[]} unlockedHats
|
||||||
|
* @property {string} currentHat
|
||||||
* @property {Partial<Settings>} settings
|
* @property {Partial<Settings>} settings
|
||||||
* @property {SavedStickyNote[]} [stickyNotes]
|
* @property {SavedStickyNote[]} [stickyNotes]
|
||||||
*/
|
*/
|
||||||
@@ -78,12 +81,16 @@ const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
|
|||||||
const STYLESHEET = `___STYLESHEET___`;
|
const STYLESHEET = `___STYLESHEET___`;
|
||||||
const SPRITE_SHEET = "__SPRITE_SHEET__";
|
const SPRITE_SHEET = "__SPRITE_SHEET__";
|
||||||
const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__";
|
const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__";
|
||||||
|
const HATS_SPRITE_SHEET = "__HATS_SPRITE_SHEET__";
|
||||||
|
|
||||||
// Element IDs
|
// Element IDs
|
||||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||||
const FEATHER_ID = "birb-feather";
|
const FEATHER_ID = "birb-feather";
|
||||||
|
const WARDROBE_ID = "birb-wardrobe";
|
||||||
|
const HAT_ID = "birb-hat";
|
||||||
|
|
||||||
const DEFAULT_BIRD = "bluebird";
|
const DEFAULT_BIRD = "bluebird";
|
||||||
|
const DEFAULT_HAT = HAT.NONE;
|
||||||
|
|
||||||
// Birb movement
|
// Birb movement
|
||||||
const HOP_SPEED = 0.07;
|
const HOP_SPEED = 0.07;
|
||||||
@@ -92,8 +99,8 @@ const HOP_DISTANCE = 35;
|
|||||||
|
|
||||||
// Timing constants (in milliseconds)
|
// Timing constants (in milliseconds)
|
||||||
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
||||||
const AFK_TIME = isDebug() ? 0 : 1000 * 5;
|
const AFK_TIME = isDebug() ? 0 : 1000 * 5; // 5 seconds
|
||||||
const PET_BOOST_DURATION = 1000 * 60 * 5;
|
const SUPER_AFK_TIME = 1000 * 60 * 60; // 1 hour
|
||||||
const PET_MENU_COOLDOWN = 1000;
|
const PET_MENU_COOLDOWN = 1000;
|
||||||
const URL_CHECK_INTERVAL = 150;
|
const URL_CHECK_INTERVAL = 150;
|
||||||
const HOP_DELAY = 500;
|
const HOP_DELAY = 500;
|
||||||
@@ -102,10 +109,15 @@ const HOP_DELAY = 500;
|
|||||||
const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds
|
const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds
|
||||||
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds
|
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds
|
||||||
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours
|
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours
|
||||||
|
const HAT_CHANCE = 1 / (60 * 60 * 10); // Every 10 minutes
|
||||||
|
|
||||||
// Feathers
|
// Feathers
|
||||||
const FEATHER_FALL_SPEED = 1;
|
const FEATHER_FALL_SPEED = 1;
|
||||||
|
|
||||||
|
// Petting boosts
|
||||||
|
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||||
const PET_FEATHER_BOOST = 2;
|
const PET_FEATHER_BOOST = 2;
|
||||||
|
const PET_HAT_BOOST = 1.5;
|
||||||
|
|
||||||
// Focus element constraints
|
// Focus element constraints
|
||||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||||
@@ -123,20 +135,23 @@ export async function initializeApplication(context) {
|
|||||||
log("Loading sprite sheets...");
|
log("Loading sprite sheets...");
|
||||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
startApplication(birbPixels, featherPixels);
|
const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET);
|
||||||
|
startApplication(birbPixels, featherPixels, hatsPixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[][]} birbPixels
|
* @param {string[][]} birbPixels
|
||||||
* @param {string[][]} featherPixels
|
* @param {string[][]} featherPixels
|
||||||
|
* @param {string[][]} hatsPixels
|
||||||
*/
|
*/
|
||||||
function startApplication(birbPixels, featherPixels) {
|
function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
|
const HATS_SPRITE_SHEET = hatsPixels;
|
||||||
|
|
||||||
const featherLayers = {
|
const featherLayers = {
|
||||||
feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
|
feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const featherFrames = {
|
const featherFrames = {
|
||||||
@@ -154,6 +169,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
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("Wardrobe", insertWardrobe),
|
||||||
new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()),
|
new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()),
|
||||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
||||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||||
@@ -164,6 +180,9 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
for (let type in SPECIES) {
|
for (let type in SPECIES) {
|
||||||
unlockBird(type);
|
unlockBird(type);
|
||||||
}
|
}
|
||||||
|
for (let hat in HAT) {
|
||||||
|
unlockHat(HAT[hat]);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
new DebugMenuItem("Add Feather", () => {
|
new DebugMenuItem("Add Feather", () => {
|
||||||
activateFeather();
|
activateFeather();
|
||||||
@@ -195,6 +214,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
|
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||||
new MenuItem("__VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, false),
|
new MenuItem("__VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, false),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -232,6 +252,8 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
let petStack = [];
|
let petStack = [];
|
||||||
let currentSpecies = DEFAULT_BIRD;
|
let currentSpecies = DEFAULT_BIRD;
|
||||||
let unlockedSpecies = [DEFAULT_BIRD];
|
let unlockedSpecies = [DEFAULT_BIRD];
|
||||||
|
let unlockedHats = [DEFAULT_HAT];
|
||||||
|
let currentHat = DEFAULT_HAT;
|
||||||
// let visible = true;
|
// let visible = true;
|
||||||
let lastPetTimestamp = 0;
|
let lastPetTimestamp = 0;
|
||||||
/** @type {StickyNote[]} */
|
/** @type {StickyNote[]} */
|
||||||
@@ -250,6 +272,8 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
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;
|
||||||
|
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||||
|
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||||
stickyNotes = [];
|
stickyNotes = [];
|
||||||
|
|
||||||
if (saveData.stickyNotes) {
|
if (saveData.stickyNotes) {
|
||||||
@@ -262,13 +286,16 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
|
|
||||||
log(stickyNotes.length + " sticky notes loaded");
|
log(stickyNotes.length + " sticky notes loaded");
|
||||||
switchSpecies(currentSpecies);
|
switchSpecies(currentSpecies);
|
||||||
|
switchHat(currentHat);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
/** @type {BirbSaveData} */
|
/** @type {BirbSaveData} */
|
||||||
const saveData = {
|
const saveData = {
|
||||||
unlockedSpecies,
|
unlockedSpecies: unlockedSpecies,
|
||||||
currentSpecies,
|
currentSpecies: currentSpecies,
|
||||||
|
unlockedHats: unlockedHats,
|
||||||
|
currentHat: currentHat,
|
||||||
settings: userSettings
|
settings: userSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,7 +348,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
styleElement.textContent = STYLESHEET;
|
styleElement.textContent = STYLESHEET;
|
||||||
document.head.appendChild(styleElement);
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT);
|
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET);
|
||||||
birb.setAnimation(Animations.BOB);
|
birb.setAnimation(Animations.BOB);
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
window.addEventListener("scroll", () => {
|
||||||
@@ -342,6 +369,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
// Currently being pet, don't open menu
|
// Currently being pet, don't open menu
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation);
|
insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,7 +399,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const currentPath = getContext().getPath().split("?")[0];
|
const currentPath = getContext().getPath().split("?")[0];
|
||||||
if (currentPath !== lastPath) {
|
if (currentPath !== lastPath) {
|
||||||
log("Path changed, updating sticky notes: " + currentPath);
|
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||||
lastPath = currentPath;
|
lastPath = currentPath;
|
||||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||||
}
|
}
|
||||||
@@ -412,12 +440,17 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double the chance of a feather if recently pet
|
if (birb.isVisible() && Date.now() - lastActionTimestamp < SUPER_AFK_TIME) {
|
||||||
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
|
if (Math.random() < FEATHER_CHANCE * (isPetBoostActive() ? PET_FEATHER_BOOST : 1)) {
|
||||||
if (birb.isVisible() && Math.random() < FEATHER_CHANCE * petMod) {
|
lastPetTimestamp = 0;
|
||||||
lastPetTimestamp = 0;
|
activateFeather();
|
||||||
activateFeather();
|
}
|
||||||
|
if (Math.random() < (HAT_CHANCE * (isPetBoostActive() ? PET_HAT_BOOST : 1))) {
|
||||||
|
lastPetTimestamp = 0;
|
||||||
|
insertHat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFeather();
|
updateFeather();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +485,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
flySomewhere();
|
flySomewhere();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (birb.draw(SPECIES[currentSpecies])) {
|
if (birb.draw(SPECIES[currentSpecies], currentHat)) {
|
||||||
birb.setAnimation(Animations.STILL);
|
birb.setAnimation(Animations.STILL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +577,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
if (!featherCtx) {
|
if (!featherCtx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type);
|
FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type.colors, type.tags);
|
||||||
document.body.appendChild(featherCanvas);
|
document.body.appendChild(featherCanvas);
|
||||||
onClick(featherCanvas, () => {
|
onClick(featherCanvas, () => {
|
||||||
unlockBird(birdType);
|
unlockBird(birdType);
|
||||||
@@ -563,12 +596,62 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert the hat as an item element in the document if possible
|
||||||
|
*/
|
||||||
|
function insertHat() {
|
||||||
|
if (document.querySelector("#" + HAT_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Select a random hat that hasn't been unlocked yet
|
||||||
|
const availableHats = Object.values(HAT)
|
||||||
|
.filter(hat => hat !== HAT.NONE && !unlockedHats.includes(hat));
|
||||||
|
if (availableHats.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hatId = availableHats[Math.floor(Math.random() * availableHats.length)];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
onClick(hatCanvas, () => {
|
||||||
|
unlockHat(hatId);
|
||||||
|
hatCanvas.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
* @param {string} birdType
|
||||||
*/
|
*/
|
||||||
function unlockBird(birdType) {
|
function unlockBird(birdType) {
|
||||||
if (!unlockedSpecies.includes(birdType)) {
|
if (!unlockedSpecies.includes(birdType)) {
|
||||||
unlockedSpecies.push(birdType);
|
unlockedSpecies.push(birdType);
|
||||||
|
save();
|
||||||
const message = makeElement("birb-message-content");
|
const message = makeElement("birb-message-content");
|
||||||
message.appendChild(document.createTextNode("You've found a "));
|
message.appendChild(document.createTextNode("You've found a "));
|
||||||
const bold = document.createElement("b");
|
const bold = document.createElement("b");
|
||||||
@@ -577,7 +660,24 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species."));
|
message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species."));
|
||||||
insertModal("New Bird Unlocked!", message);
|
insertModal("New Bird Unlocked!", message);
|
||||||
}
|
}
|
||||||
save();
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} hatId
|
||||||
|
*/
|
||||||
|
function unlockHat(hatId) {
|
||||||
|
if (!unlockedHats.includes(hatId)) {
|
||||||
|
unlockedHats.push(hatId);
|
||||||
|
save();
|
||||||
|
switchHat(hatId);
|
||||||
|
const message = makeElement("birb-message-content");
|
||||||
|
message.appendChild(document.createTextNode("You've unlocked the "));
|
||||||
|
const bold = document.createElement("b");
|
||||||
|
bold.textContent = HAT_METADATA[hatId].name;
|
||||||
|
message.appendChild(bold);
|
||||||
|
message.appendChild(document.createTextNode("! To see all of your unlocked accessories, click the Wardrobe from the menu."));
|
||||||
|
insertModal("New Hat Found!", message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFeather() {
|
function updateFeather() {
|
||||||
@@ -644,6 +744,8 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Remove wardrobe if open
|
||||||
|
removeWardrobe();
|
||||||
|
|
||||||
const contentContainer = document.createElement("div");
|
const contentContainer = document.createElement("div");
|
||||||
const content = makeElement("birb-grid-content");
|
const content = makeElement("birb-grid-content");
|
||||||
@@ -691,7 +793,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
if (!speciesCtx) {
|
if (!speciesCtx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
|
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
|
||||||
speciesElement.appendChild(speciesCanvas);
|
speciesElement.appendChild(speciesCanvas);
|
||||||
content.appendChild(speciesElement);
|
content.appendChild(speciesElement);
|
||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
@@ -724,13 +826,114 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertWardrobe() {
|
||||||
|
console.log("Inserting wardrobe");
|
||||||
|
if (document.querySelector("#" + WARDROBE_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove field guide if open
|
||||||
|
removeFieldGuide();
|
||||||
|
|
||||||
|
const contentContainer = document.createElement("div");
|
||||||
|
const content = makeElement("birb-grid-content");
|
||||||
|
const description = makeElement("birb-field-guide-description");
|
||||||
|
contentContainer.appendChild(content);
|
||||||
|
contentContainer.appendChild(description);
|
||||||
|
|
||||||
|
const wardrobe = createWindow(
|
||||||
|
WARDROBE_ID,
|
||||||
|
"Wardrobe",
|
||||||
|
contentContainer
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateDescription = (/** @type {string} */ hat) => {
|
||||||
|
const metadata = HAT_METADATA[hat] ?? { name: "Unknown Hat", description: "todo" };
|
||||||
|
const unlocked = unlockedHats.includes(hat);
|
||||||
|
|
||||||
|
const boldName = document.createElement("b");
|
||||||
|
boldName.textContent = metadata.name;
|
||||||
|
|
||||||
|
const spacer = document.createElement("div");
|
||||||
|
spacer.style.height = "0.3em";
|
||||||
|
|
||||||
|
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : metadata.description);
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
fragment.appendChild(boldName);
|
||||||
|
fragment.appendChild(spacer);
|
||||||
|
fragment.appendChild(descText);
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
description.appendChild(generateDescription(currentHat));
|
||||||
|
for (const hat of Object.values(HAT)) {
|
||||||
|
const unlocked = unlockedHats.includes(hat);
|
||||||
|
const hatElement = makeElement("birb-grid-item");
|
||||||
|
if (hat === currentHat) {
|
||||||
|
hatElement.classList.add("birb-grid-item-selected");
|
||||||
|
}
|
||||||
|
const hatCanvas = document.createElement("canvas");
|
||||||
|
hatCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
|
||||||
|
hatCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
|
||||||
|
const hatCtx = hatCanvas.getContext("2d");
|
||||||
|
if (!hatCtx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
birb.getFrames().base.draw(
|
||||||
|
hatCtx,
|
||||||
|
Directions.RIGHT,
|
||||||
|
CANVAS_PIXEL_SIZE,
|
||||||
|
SPECIES[currentSpecies].colors,
|
||||||
|
[...SPECIES[currentSpecies].tags, hat]
|
||||||
|
);
|
||||||
|
hatElement.appendChild(hatCanvas);
|
||||||
|
content.appendChild(hatElement);
|
||||||
|
if (unlocked) {
|
||||||
|
onClick(hatElement, () => {
|
||||||
|
switchHat(hat);
|
||||||
|
document.querySelectorAll(".birb-grid-item").forEach((element) => {
|
||||||
|
element.classList.remove("birb-grid-item-selected");
|
||||||
|
});
|
||||||
|
hatElement.classList.add("birb-grid-item-selected");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hatElement.classList.add("birb-grid-item-locked");
|
||||||
|
}
|
||||||
|
hatElement.addEventListener("mouseover", () => {
|
||||||
|
description.textContent = "";
|
||||||
|
description.appendChild(generateDescription(hat));
|
||||||
|
});
|
||||||
|
hatElement.addEventListener("mouseout", () => {
|
||||||
|
description.textContent = "";
|
||||||
|
description.appendChild(generateDescription(currentHat));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
centerElement(wardrobe);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWardrobe() {
|
||||||
|
const wardrobe = document.querySelector("#" + WARDROBE_ID);
|
||||||
|
if (wardrobe) {
|
||||||
|
wardrobe.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
*/
|
*/
|
||||||
function switchSpecies(type) {
|
function switchSpecies(type) {
|
||||||
currentSpecies = type;
|
currentSpecies = type;
|
||||||
// Update CSS variable --birb-highlight to be wing color
|
// Update CSS variable --birb-highlight to be wing color
|
||||||
document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]);
|
document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[PALETTE.THEME_HIGHLIGHT]);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} hat
|
||||||
|
*/
|
||||||
|
function switchHat(hat) {
|
||||||
|
currentHat = hat;
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,14 +998,9 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focus on an element within the viewport
|
* @returns {HTMLElement|null} The random element, or null if no valid element was found
|
||||||
* @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) {
|
function getRandomValidElement() {
|
||||||
if (frozen) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
||||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||||
const inWindow = Array.from(elements).filter((img) => {
|
const inWindow = Array.from(elements).filter((img) => {
|
||||||
@@ -830,10 +1028,22 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
return style.position !== "fixed" && style.position !== "sticky";
|
return style.position !== "fixed" && style.position !== "sticky";
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
|
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);
|
log("Focusing on element: ", focusedElement);
|
||||||
updateFocusedElementBounds();
|
updateFocusedElementBounds();
|
||||||
if (teleport) {
|
if (teleport) {
|
||||||
@@ -841,7 +1051,7 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
} else {
|
} else {
|
||||||
flyTo(getFocusedElementRandomX(), getFocusedY());
|
flyTo(getFocusedElementRandomX(), getFocusedY());
|
||||||
}
|
}
|
||||||
return randomElement !== null;
|
return focusedElement !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -913,6 +1123,10 @@ function startApplication(birbPixels, featherPixels) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPetBoostActive() {
|
||||||
|
return Date.now() - lastPetTimestamp < PET_BOOST_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} x
|
* @param {number} x
|
||||||
* @param {number} y
|
* @param {number} y
|
||||||
@@ -1012,7 +1226,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
|||||||
const b = pixels[index + 2];
|
const b = pixels[index + 2];
|
||||||
const a = pixels[index + 3];
|
const a = pixels[index + 3];
|
||||||
if (a === 0) {
|
if (a === 0) {
|
||||||
row.push(Sprite.TRANSPARENT);
|
row.push(PALETTE.TRANSPARENT);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
@@ -1021,8 +1235,9 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
|
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
|
||||||
error(`Unknown color: ${hex}`);
|
// Return the color as-is if not found in the map
|
||||||
row.push(Sprite.TRANSPARENT);
|
row.push(hex);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
|
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/birb.js
61
src/birb.js
@@ -1,8 +1,9 @@
|
|||||||
import { Directions, getLayer, getWindowHeight, getFixedWindowHeight } from './shared.js';
|
import { Directions, getLayerPixels, getWindowHeight, getFixedWindowHeight } from './shared.js';
|
||||||
import Layer from './layer.js';
|
import Layer from './animation/layer.js';
|
||||||
import Frame from './frame.js';
|
import Frame from './animation/frame.js';
|
||||||
import Anim from './anim.js';
|
import Anim from './animation/anim.js';
|
||||||
import { BirdType } from './sprites.js';
|
import { BirdType, PALETTE } from './animation/sprites.js';
|
||||||
|
import { createHatLayers } from './hats.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {keyof typeof Animations} AnimationType
|
* @typedef {keyof typeof Animations} AnimationType
|
||||||
@@ -31,8 +32,9 @@ export class Birb {
|
|||||||
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
|
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
|
||||||
* @param {number} spriteWidth
|
* @param {number} spriteWidth
|
||||||
* @param {number} spriteHeight
|
* @param {number} spriteHeight
|
||||||
|
* @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data
|
||||||
*/
|
*/
|
||||||
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
|
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) {
|
||||||
this.birbCssScale = birbCssScale;
|
this.birbCssScale = birbCssScale;
|
||||||
this.canvasPixelSize = canvasPixelSize;
|
this.canvasPixelSize = canvasPixelSize;
|
||||||
this.windowPixelSize = canvasPixelSize * birbCssScale;
|
this.windowPixelSize = canvasPixelSize * birbCssScale;
|
||||||
@@ -41,28 +43,31 @@ export class Birb {
|
|||||||
|
|
||||||
// Build layers from sprite sheet
|
// Build layers from sprite sheet
|
||||||
this.layers = {
|
this.layers = {
|
||||||
base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)),
|
base: new Layer(getLayerPixels(spriteSheet, 0, this.spriteWidth)),
|
||||||
down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)),
|
down: new Layer(getLayerPixels(spriteSheet, 1, this.spriteWidth)),
|
||||||
heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)),
|
heartOne: new Layer(getLayerPixels(spriteSheet, 2, this.spriteWidth)),
|
||||||
heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)),
|
heartTwo: new Layer(getLayerPixels(spriteSheet, 3, this.spriteWidth)),
|
||||||
heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)),
|
heartThree: new Layer(getLayerPixels(spriteSheet, 4, this.spriteWidth)),
|
||||||
tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"),
|
tuftBase: new Layer(getLayerPixels(spriteSheet, 5, this.spriteWidth), "tuft"),
|
||||||
tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"),
|
tuftDown: new Layer(getLayerPixels(spriteSheet, 6, this.spriteWidth), "tuft"),
|
||||||
wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)),
|
wingsUp: new Layer(getLayerPixels(spriteSheet, 7, this.spriteWidth)),
|
||||||
wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)),
|
wingsDown: new Layer(getLayerPixels(spriteSheet, 8, this.spriteWidth)),
|
||||||
happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)),
|
happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build hat layers
|
||||||
|
const hatLayers = createHatLayers(hatSpriteSheet);
|
||||||
|
|
||||||
// Build frames from layers
|
// Build frames from layers
|
||||||
this.frames = {
|
this.frames = {
|
||||||
base: new Frame([this.layers.base, this.layers.tuftBase]),
|
base: new Frame([this.layers.base, this.layers.tuftBase, ...hatLayers.base]),
|
||||||
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
|
headDown: new Frame([this.layers.down, this.layers.tuftDown, ...hatLayers.down]),
|
||||||
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
|
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, ...hatLayers.base]),
|
||||||
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
|
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, ...hatLayers.down]),
|
||||||
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
|
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartOne]),
|
||||||
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
|
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base,this.layers.heartTwo]),
|
||||||
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
|
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartThree]),
|
||||||
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
|
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartTwo]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build animations from frames
|
// Build animations from frames
|
||||||
@@ -121,14 +126,16 @@ export class Birb {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw the current animation frame
|
* Draw the current animation frame
|
||||||
* @param {BirdType} species The species color data
|
* @param {BirdType} species The species data
|
||||||
|
* @param {string} [hat] The name of the current hat
|
||||||
* @returns {boolean} Whether the animation has completed (for non-looping animations)
|
* @returns {boolean} Whether the animation has completed (for non-looping animations)
|
||||||
*/
|
*/
|
||||||
draw(species) {
|
draw(species, hat) {
|
||||||
const anim = this.animations[this.currentAnimation];
|
const anim = this.animations[this.currentAnimation];
|
||||||
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
|
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species.colors, [...species.tags, hat || '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {AnimationType} The current animation key
|
* @returns {AnimationType} The current animation key
|
||||||
*/
|
*/
|
||||||
|
|||||||
0
src/fieldGuide.js
Normal file
0
src/fieldGuide.js
Normal file
240
src/hats.js
Normal file
240
src/hats.js
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import Anim from "./animation/anim.js";
|
||||||
|
import Frame from "./animation/frame.js";
|
||||||
|
import Layer, { TAG } from "./animation/layer.js";
|
||||||
|
import { PALETTE } from "./animation/sprites.js";
|
||||||
|
import { getLayerPixels } from "./shared.js";
|
||||||
|
|
||||||
|
const HAT_WIDTH = 12;
|
||||||
|
|
||||||
|
export const HAT = {
|
||||||
|
NONE: "none",
|
||||||
|
TOP_HAT: "top-hat",
|
||||||
|
FEZ: "fez",
|
||||||
|
WIZARD_HAT: "wizard-hat",
|
||||||
|
BASEBALL_CAP: "baseball-cap",
|
||||||
|
FLOWER_HAT: "flower-hat",
|
||||||
|
COWBOY_HAT: "cowboy-hat",
|
||||||
|
BEANIE: "beanie",
|
||||||
|
SUN_HAT: "sun-hat",
|
||||||
|
VIKING_HELMET: "viking-helmet",
|
||||||
|
STRAW_HAT: "straw-hat",
|
||||||
|
CORDOVAN_HAT: "cordovan-hat"
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {{ [hatId: string]: { name: string, description: string } }} */
|
||||||
|
export const HAT_METADATA = {
|
||||||
|
[HAT.NONE]: {
|
||||||
|
name: "Invisible Hat",
|
||||||
|
description: "It's like you're wearing nothing at all!"
|
||||||
|
},
|
||||||
|
[HAT.TOP_HAT]: {
|
||||||
|
name: "Top Hat",
|
||||||
|
description: "The mark of a true gentlebird."
|
||||||
|
},
|
||||||
|
[HAT.VIKING_HELMET]: {
|
||||||
|
name: "Viking Helmet",
|
||||||
|
description: "Sure, vikings never actually wore this style of helmet, but why let facts get in the way of good fashion?"
|
||||||
|
},
|
||||||
|
[HAT.COWBOY_HAT]: {
|
||||||
|
name: "Cowboy Hat",
|
||||||
|
description: "You can't jam with the console cowboys without the appropriate attire."
|
||||||
|
},
|
||||||
|
[HAT.FEZ]: {
|
||||||
|
name: "Fez",
|
||||||
|
description: "It's a fez. Fezzes are cool."
|
||||||
|
},
|
||||||
|
[HAT.WIZARD_HAT]: {
|
||||||
|
name: "Wizard Hat",
|
||||||
|
description: "Grants the bearer terrifying mystical power, but luckily birds only use it to summon old ladies with bread crumbs."
|
||||||
|
},
|
||||||
|
[HAT.BASEBALL_CAP]: {
|
||||||
|
name: "Baseball Cap",
|
||||||
|
description: "Birds unfortunately only ever hit 'fowl' balls..."
|
||||||
|
},
|
||||||
|
[HAT.FLOWER_HAT]: {
|
||||||
|
name: "Flower Hat",
|
||||||
|
description: "To be fair, this is less of a hat and more of a dirt clod that your pet happened to pick up."
|
||||||
|
},
|
||||||
|
[HAT.BEANIE]: {
|
||||||
|
name: "Beanie",
|
||||||
|
description: "Keeps feathers warm on those long migrations south!"
|
||||||
|
},
|
||||||
|
[HAT.SUN_HAT]: {
|
||||||
|
name: "Sun Hat",
|
||||||
|
description: "Perfect for frolicking through enchanted flower fields."
|
||||||
|
},
|
||||||
|
[HAT.STRAW_HAT]: {
|
||||||
|
name: "Straw Hat",
|
||||||
|
description: "A classic design, though keep away from water as this particular hat is seemingly unable to float."
|
||||||
|
},
|
||||||
|
[HAT.CORDOVAN_HAT]: {
|
||||||
|
name: "Cordovan Hat",
|
||||||
|
description: "A traditional Spanish hat that stays put even in the wildest of sword fights."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[][]} spriteSheet
|
||||||
|
* @returns {{ base: Layer[], down: Layer[] }}
|
||||||
|
*/
|
||||||
|
export function createHatLayers(spriteSheet) {
|
||||||
|
const hatLayers = {
|
||||||
|
base: [],
|
||||||
|
down: []
|
||||||
|
};
|
||||||
|
for (let i = 0; i < Object.keys(HAT).length; i++) {
|
||||||
|
const hatName = Object.keys(HAT)[i];
|
||||||
|
if (hatName === 'NONE') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const index = i - 1;
|
||||||
|
const hatKey = HAT[hatName];
|
||||||
|
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}
|
||||||
|
*/
|
||||||
|
export 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 {number} [yOffset=0]
|
||||||
|
* @returns {Layer}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
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.values(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; y++) {
|
||||||
|
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
|
||||||
|
}
|
||||||
|
// Left and right padding
|
||||||
|
for (let y = 0; y < pixels.length; y++) {
|
||||||
|
const row = [];
|
||||||
|
for (let x = 0; x < left; x++) {
|
||||||
|
row.push(PALETTE.TRANSPARENT);
|
||||||
|
}
|
||||||
|
for (let x = 0; x < pixels[y].length; x++) {
|
||||||
|
row.push(pixels[y][x]);
|
||||||
|
}
|
||||||
|
for (let x = 0; x < right; x++) {
|
||||||
|
row.push(PALETTE.TRANSPARENT);
|
||||||
|
}
|
||||||
|
paddedPixels.push(row);
|
||||||
|
}
|
||||||
|
// Bottom padding
|
||||||
|
for (let y = 0; y < bottom; y++) {
|
||||||
|
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
|
||||||
|
}
|
||||||
|
return paddedPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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],
|
||||||
|
[0, -1],
|
||||||
|
[-1, -1],
|
||||||
|
[1, -1],
|
||||||
|
];
|
||||||
|
if (outlineBottom) {
|
||||||
|
neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
|
||||||
|
}
|
||||||
|
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 < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
|
||||||
|
pixels[newY][newX] = PALETTE.BORDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -193,7 +193,7 @@ export function error() {
|
|||||||
* @param {number} width The width of each sprite
|
* @param {number} width The width of each sprite
|
||||||
* @returns {string[][]}
|
* @returns {string[][]}
|
||||||
*/
|
*/
|
||||||
export function getLayer(spriteSheet, spriteIndex, width) {
|
export function getLayerPixels(spriteSheet, spriteIndex, width) {
|
||||||
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
|
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
|
||||||
const layer = [];
|
const layer = [];
|
||||||
for (let y = 0; y < width; y++) {
|
for (let y = 0; y < width; y++) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class Birdsong {
|
|||||||
3500 + Math.random() * 600,
|
3500 + Math.random() * 600,
|
||||||
2100 + Math.random() * 200,
|
2100 + Math.random() * 200,
|
||||||
1600 + Math.random() * 400];
|
1600 + Math.random() * 400];
|
||||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
|
||||||
|
|
||||||
const oscillator = this.audioContext.createOscillator();
|
const oscillator = this.audioContext.createOscillator();
|
||||||
oscillator.type = "sine";
|
oscillator.type = "sine";
|
||||||
|
|||||||
199
src/sprites.js
199
src/sprites.js
@@ -1,199 +0,0 @@
|
|||||||
/** Indicators for parts of the base bird sprite sheet */
|
|
||||||
export const Sprite = {
|
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
|
||||||
TRANSPARENT: "transparent",
|
|
||||||
OUTLINE: "outline",
|
|
||||||
BORDER: "border",
|
|
||||||
FOOT: "foot",
|
|
||||||
BEAK: "beak",
|
|
||||||
EYE: "eye",
|
|
||||||
FACE: "face",
|
|
||||||
HOOD: "hood",
|
|
||||||
NOSE: "nose",
|
|
||||||
BELLY: "belly",
|
|
||||||
UNDERBELLY: "underbelly",
|
|
||||||
WING: "wing",
|
|
||||||
WING_EDGE: "wing-edge",
|
|
||||||
HEART: "heart",
|
|
||||||
HEART_BORDER: "heart-border",
|
|
||||||
HEART_SHINE: "heart-shine",
|
|
||||||
FEATHER_SPINE: "feather-spine",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
|
||||||
export const SPRITE_SHEET_COLOR_MAP = {
|
|
||||||
"transparent": Sprite.TRANSPARENT,
|
|
||||||
"#ffffff": Sprite.BORDER,
|
|
||||||
"#000000": Sprite.OUTLINE,
|
|
||||||
"#010a19": Sprite.BEAK,
|
|
||||||
"#190301": Sprite.EYE,
|
|
||||||
"#af8e75": Sprite.FOOT,
|
|
||||||
"#639bff": Sprite.FACE,
|
|
||||||
"#99e550": Sprite.HOOD,
|
|
||||||
"#d95763": Sprite.NOSE,
|
|
||||||
"#f8b143": Sprite.BELLY,
|
|
||||||
"#ec8637": Sprite.UNDERBELLY,
|
|
||||||
"#578ae6": Sprite.WING,
|
|
||||||
"#326ed9": Sprite.WING_EDGE,
|
|
||||||
"#c82e2e": Sprite.HEART,
|
|
||||||
"#501a1a": Sprite.HEART_BORDER,
|
|
||||||
"#ff6b6b": Sprite.HEART_SHINE,
|
|
||||||
"#373737": Sprite.FEATHER_SPINE,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class BirdType {
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} description
|
|
||||||
* @param {Record<string, string>} colors
|
|
||||||
* @param {string[]} [tags]
|
|
||||||
*/
|
|
||||||
constructor(name, description, colors, tags = []) {
|
|
||||||
this.name = name;
|
|
||||||
this.description = description;
|
|
||||||
const defaultColors = {
|
|
||||||
[Sprite.TRANSPARENT]: "transparent",
|
|
||||||
[Sprite.OUTLINE]: "#000000",
|
|
||||||
[Sprite.BORDER]: "#ffffff",
|
|
||||||
[Sprite.BEAK]: "#000000",
|
|
||||||
[Sprite.EYE]: "#000000",
|
|
||||||
[Sprite.HEART]: "#c82e2e",
|
|
||||||
[Sprite.HEART_BORDER]: "#501a1a",
|
|
||||||
[Sprite.HEART_SHINE]: "#ff6b6b",
|
|
||||||
[Sprite.FEATHER_SPINE]: "#373737",
|
|
||||||
[Sprite.HOOD]: colors.face,
|
|
||||||
[Sprite.NOSE]: colors.face,
|
|
||||||
};
|
|
||||||
/** @type {Record<string, string>} */
|
|
||||||
this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
|
||||||
this.tags = tags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {Record<string, BirdType>} */
|
|
||||||
export const SPECIES = {
|
|
||||||
bluebird: new BirdType("Eastern Bluebird",
|
|
||||||
"Native to North American and very social, though can be timid around people.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#639bff",
|
|
||||||
[Sprite.BELLY]: "#f8b143",
|
|
||||||
[Sprite.UNDERBELLY]: "#ec8637",
|
|
||||||
[Sprite.WING]: "#578ae6",
|
|
||||||
[Sprite.WING_EDGE]: "#326ed9",
|
|
||||||
}),
|
|
||||||
shimaEnaga: new BirdType("Shima Enaga",
|
|
||||||
"Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#ffffff",
|
|
||||||
[Sprite.BELLY]: "#ebe9e8",
|
|
||||||
[Sprite.UNDERBELLY]: "#ebd9d0",
|
|
||||||
[Sprite.WING]: "#f3d3c1",
|
|
||||||
[Sprite.WING_EDGE]: "#2d2d2dff",
|
|
||||||
[Sprite.THEME_HIGHLIGHT]: "#d7ac93",
|
|
||||||
}),
|
|
||||||
tuftedTitmouse: new BirdType("Tufted Titmouse",
|
|
||||||
"Native to the eastern United States, full of personality, and notably my wife's favorite bird.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#c7cad7",
|
|
||||||
[Sprite.BELLY]: "#e4e5eb",
|
|
||||||
[Sprite.UNDERBELLY]: "#d7cfcb",
|
|
||||||
[Sprite.WING]: "#b1b5c5",
|
|
||||||
[Sprite.WING_EDGE]: "#9d9fa9",
|
|
||||||
}, ["tuft"]),
|
|
||||||
europeanRobin: new BirdType("European Robin",
|
|
||||||
"Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#ffaf34",
|
|
||||||
[Sprite.HOOD]: "#aaa094",
|
|
||||||
[Sprite.BELLY]: "#ffaf34",
|
|
||||||
[Sprite.UNDERBELLY]: "#babec2",
|
|
||||||
[Sprite.WING]: "#aaa094",
|
|
||||||
[Sprite.WING_EDGE]: "#888580",
|
|
||||||
[Sprite.THEME_HIGHLIGHT]: "#ffaf34",
|
|
||||||
}),
|
|
||||||
redCardinal: new BirdType("Red Cardinal",
|
|
||||||
"Native to the eastern United States, this strikingly red bird is hard to miss.", {
|
|
||||||
[Sprite.BEAK]: "#d93619",
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#31353d",
|
|
||||||
[Sprite.HOOD]: "#e83a1b",
|
|
||||||
[Sprite.BELLY]: "#e83a1b",
|
|
||||||
[Sprite.UNDERBELLY]: "#dc3719",
|
|
||||||
[Sprite.WING]: "#d23215",
|
|
||||||
[Sprite.WING_EDGE]: "#b1321c",
|
|
||||||
}, ["tuft"]),
|
|
||||||
americanGoldfinch: new BirdType("American Goldfinch",
|
|
||||||
"Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", {
|
|
||||||
[Sprite.BEAK]: "#ffaf34",
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#fff255",
|
|
||||||
[Sprite.NOSE]: "#383838",
|
|
||||||
[Sprite.HOOD]: "#383838",
|
|
||||||
[Sprite.BELLY]: "#fff255",
|
|
||||||
[Sprite.UNDERBELLY]: "#f5ea63",
|
|
||||||
[Sprite.WING]: "#e8e079",
|
|
||||||
[Sprite.WING_EDGE]: "#191919",
|
|
||||||
[Sprite.THEME_HIGHLIGHT]: "#ffcc00"
|
|
||||||
}),
|
|
||||||
barnSwallow: new BirdType("Barn Swallow",
|
|
||||||
"Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#db7c4d",
|
|
||||||
[Sprite.BELLY]: "#f7e1c9",
|
|
||||||
[Sprite.UNDERBELLY]: "#ebc9a3",
|
|
||||||
[Sprite.WING]: "#2252a9",
|
|
||||||
[Sprite.WING_EDGE]: "#1c448b",
|
|
||||||
[Sprite.HOOD]: "#2252a9",
|
|
||||||
}),
|
|
||||||
mistletoebird: new BirdType("Mistletoebird",
|
|
||||||
"Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", {
|
|
||||||
[Sprite.FOOT]: "#6c6a7c",
|
|
||||||
[Sprite.FACE]: "#352e6d",
|
|
||||||
[Sprite.BELLY]: "#fd6833",
|
|
||||||
[Sprite.UNDERBELLY]: "#e6e1d8",
|
|
||||||
[Sprite.WING]: "#342b7c",
|
|
||||||
[Sprite.WING_EDGE]: "#282065",
|
|
||||||
}),
|
|
||||||
redAvadavat: new BirdType("Red Avadavat",
|
|
||||||
"Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", {
|
|
||||||
[Sprite.BEAK]: "#f71919",
|
|
||||||
[Sprite.FOOT]: "#af7575",
|
|
||||||
[Sprite.FACE]: "#cb092b",
|
|
||||||
[Sprite.BELLY]: "#ae1724",
|
|
||||||
[Sprite.UNDERBELLY]: "#831b24",
|
|
||||||
[Sprite.WING]: "#7e3030",
|
|
||||||
[Sprite.WING_EDGE]: "#490f0f",
|
|
||||||
}),
|
|
||||||
scarletRobin: new BirdType("Scarlet Robin",
|
|
||||||
"Native to Australia, this striking robin can be found in Eucalyptus forests.", {
|
|
||||||
[Sprite.FOOT]: "#494949",
|
|
||||||
[Sprite.FACE]: "#3d3d3d",
|
|
||||||
[Sprite.BELLY]: "#fc5633",
|
|
||||||
[Sprite.UNDERBELLY]: "#dcdcdc",
|
|
||||||
[Sprite.WING]: "#2b2b2b",
|
|
||||||
[Sprite.WING_EDGE]: "#ebebeb",
|
|
||||||
[Sprite.THEME_HIGHLIGHT]: "#fc5633",
|
|
||||||
}),
|
|
||||||
americanRobin: new BirdType("American Robin",
|
|
||||||
"While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", {
|
|
||||||
[Sprite.BEAK]: "#e89f30",
|
|
||||||
[Sprite.FOOT]: "#9f8075",
|
|
||||||
[Sprite.FACE]: "#2d2d2d",
|
|
||||||
[Sprite.BELLY]: "#eb7a3a",
|
|
||||||
[Sprite.UNDERBELLY]: "#eb7a3a",
|
|
||||||
[Sprite.WING]: "#444444",
|
|
||||||
[Sprite.WING_EDGE]: "#232323",
|
|
||||||
[Sprite.THEME_HIGHLIGHT]: "#eb7a3a",
|
|
||||||
}),
|
|
||||||
carolinaWren: new BirdType("Carolina Wren",
|
|
||||||
"Native to the eastern United States, these little birds are known for their curious and energetic nature.", {
|
|
||||||
[Sprite.FOOT]: "#af8e75",
|
|
||||||
[Sprite.FACE]: "#edc7a9",
|
|
||||||
[Sprite.NOSE]: "#f7eee5",
|
|
||||||
[Sprite.HOOD]: "#c58a5b",
|
|
||||||
[Sprite.BELLY]: "#e1b796",
|
|
||||||
[Sprite.UNDERBELLY]: "#c79e7c",
|
|
||||||
[Sprite.WING]: "#c58a5b",
|
|
||||||
[Sprite.WING_EDGE]: "#866348",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -41,6 +41,22 @@
|
|||||||
z-index: 2147483630 !important;
|
z-index: 2147483630 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.birb-item {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: scale(calc(var(--birb-scale) * 1.5)) !important;
|
||||||
|
transform-origin: bottom;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
z-index: 2147483630 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-item:hover {
|
||||||
|
transform: scale(calc(var(--birb-scale) * 1.9)) !important;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-window {
|
.birb-window {
|
||||||
font-family: "Monocraft", monospace !important;
|
font-family: "Monocraft", monospace !important;
|
||||||
line-height: initial !important;
|
line-height: initial !important;
|
||||||
@@ -238,13 +254,21 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
#birb-field-guide {
|
#birb-field-guide, #birb-wardrobe {
|
||||||
width: 322px !important;
|
width: 322px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#birb-field-guide .birb-grid-content {
|
||||||
|
grid-template-rows: repeat(3, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#birb-wardrobe .birb-grid-content {
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-grid-content {
|
.birb-grid-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(3, auto);
|
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user