Merge pull request #9 from IdreesInc/new-birds

New birds and new logic!
This commit is contained in:
Idrees
2026-03-29 14:45:03 -07:00
committed by GitHub
67 changed files with 2705 additions and 597 deletions

4
.gitignore vendored
View File

@@ -4,3 +4,7 @@
obsidian-test.sh obsidian-test.sh
build-cache.json build-cache.json
.vscode/settings.json .vscode/settings.json
aseprite/birb-test.aseprite
aseprite/wren.aseprite
aseprite/birb-no-shoulder.aseprite
aseprite/birb-fat.aseprite

Binary file not shown.

BIN
dist/extension.zip vendored

Binary file not shown.

546
dist/extension/birb.js vendored
View File

@@ -237,6 +237,8 @@
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -249,6 +251,8 @@
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -262,6 +266,8 @@
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -278,6 +284,8 @@
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -292,6 +300,8 @@
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -311,6 +321,8 @@
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -327,6 +339,8 @@
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -340,6 +354,8 @@
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -349,22 +365,11 @@
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -372,12 +377,15 @@
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -392,6 +400,8 @@
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -402,14 +412,249 @@
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
}; };
/** const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -420,23 +665,36 @@
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
const SPRITE_SHEET_COLOR_MAP = { const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -449,13 +707,24 @@
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -463,16 +732,52 @@
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
({
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
});
const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
class BirdType { class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -484,15 +789,27 @@
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -556,7 +873,7 @@
const SPECIES = Object.fromEntries( const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );
@@ -1253,37 +1570,42 @@
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }
@@ -2047,7 +2369,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -2057,7 +2379,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -2089,7 +2411,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -2108,6 +2430,15 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -2119,7 +2450,14 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -2132,7 +2470,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {
@@ -2181,7 +2519,7 @@
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
}`; }`;
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABFFJREFUeJztnb9rE2EYx79vK1gUQSRLL64apxZcdC84dNBk0LiIoIJCwUGwlP4BUjqIFpSKLioOdYlV6ObUpV26ORRX0yoEHezQgvZxSN7rm+v9SGzu3vea7wdC31wued7k3ueT5727XAFCCCGEENJfKNsdIO4jIhL1mFKKY4jkFg7eHGBTQDp2pVgEANTq9bZ2Fn0ghPQp0qLseVL2vH3tODn2Kn7Z82RlZERkYmJfO+34hKTJgO0OkGQqxSKmCgXUKpV97SxZXV7GVKHgtwnJOxRgjrAloFq9jplGo23ZTKPhT4EJISQVzCnwysiIf8tqChzWhyxjE0IcQELIOrZNAZl9oPwI6TNEpLnz3/ibdXzbArIhf0LS5IjtDuSN1dFRK3GVUkpExOapJzzdhZA+RVc+uvpjFURI/uE3eheY0mM1lD3BLx1uA3JQOIBILtDy084TkcwFSAH3KWFHQDkFJBaQF/WyALB2AKgV2+nxzzztnMQToc1vXvOGhN+o9hIKmADAi3pZN60cAFJK+X1wrfoL5oWNPM0jsUeBzQ/TGHzQy0TEXCeVARGc+hjLISLi2kAk6XG3+MHqtrYp4E4wU6E6u+Evo/+iiRSgKZ716m3gAXB29+TeCvVy2/6YNGTkgoAJ0dgWcAyilPKld9gZHlSy+Vd6si1iK8CB4x7UsWGc+7QEANi98QTeq+sYn98FloDq7CaQkoxcEDAhOUCixPf12+/MO5M2w4NKxud3sXRvoCcSjHwBU0C3X+4voW+NVzD37rl/f2HS08+LDtaFoEREBo57bcvaBAxg69emH9uMSxGSPiFWfmtzJbg6XXeF2A+nVVVBflzFnY/vAQCvLl8DAFx//TT0OVqEIa/VDNihnGwLmBCXqc5uxO7YS1uAUfFbeZibPOtIgNuLYzh68RQAYGflJwDg5vrbjgLoMnxtrtT1uVs2BUyIy0QJyMy3lEUUWX3mSYId/xZYi0+zMOnh/P11nDl9InT9wIY40JGonZWfvviC/QgS3Ci6HzwaRg4Tr0s32oqQYL5lIaCv335H5n9e+O+LIWwvjmHoSgm4vx76eJj4DlJ92RQwIS7y7E8FE0dqQMbiS6I6u4GFSU9c6EsSiVNgtKqn7cWxtse2vmwBAArTq1HP3Qvyn+Izp+BhDF35jPMZCZgQ12g8uuAP7kvf3/jL1+ZKWY1ziSpCzAOTLuddYsfCJKTlF0ZherVnb9y2gAlxHVOCaOZD1mNddBESFKGWoMv515EA0ZJQ49GF2HV7KT8zvi0BE0I6QgAgKMI8CDBxH6C+EGfrjURKME3x6NhofuNFrkf5EWIFhebUu5l8Ebulco15NYza44f+VTF0O80LFAR+6B0aP+0+EEKSCV4xx/V87OrfYiqlUHv80L9vtpFi1aVfNy4+Kz9C3KD101QgB/nY1Wkw5lQ02M6CpPiuf9iEHHbyloNddTaunM3ijduOTwg5XPwDDgmrxnQErq8AAAAASUVORK5CYII="; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABORJREFUeJztnU9IHFccx79vE6g0BEK7BJzNwR6apReFQElKjpZaeol7EHuRQhFaECIEIkF6LGkQGqyQ0EBa+u8iHjZCCfXQUw7qRVDwYKQEirtt0yVtMRaFdn857L7x7Tizf3Rn3oz7/cDi8+3s/t44733m9+afACGEEEII6SyU7QaQ+CMiEvSeUop9iCQWdt4EYFNAOnYukwEA5AuFmnIUbSCEdChSZdBxZNBxDpTrybFd8QcdR5Z6e0XGxg6Uw45PSJikbDeANCaXyeBGOo18LnegHCXLjx7hRjrtlglJOhRggrAloHyhgFulUk3drVLJnQITQkgomFPgpd5e9xXVFNivDVHGJoTEAPEh6tg2BWS2gfIjpMMQkcrBf+Nn1PFtC8iG/AkJk5O2G5A0lvv6rMRVSikREZuXnvByF0I6FJ356OyPWRAhyYd79BYwpcdsKHq8Ox1uA3JU2IFIItDy084TkcgFSAF3KH5nQDkFJBaQe4VBAWDtBFA1dqz7P8dp8zS8ENrc85ovNLhHtZ1QwAQA7hUGddHKCSCllNuGuGV/3nFhY5wmkbpngc0/ptH5oOtEBCIiYXYG79THqA89NokXH2UeWN3WNgXcDOZQGJ4qunX0XzCBAjTFs3DpbfRcexXny2eA1zKAcxYo1EoQIewV4yBgQjS2BVwHUUq50jvudJ9Q8tv/0pZtUTcDTJ1yoF7uxrtr68DaOsoj03A+fx/FNz7DuR+yGFrdrjko3U4RxkHAhCQACRLf5tZ25I0Jm+4TSt77soyHH6faIsHALzAF9NPkP279v38Cbz6dw693f8R06Xu3fq7vtP5ccLAWBCUikjrl1NSVR6bh3K8I+GHXX/jmq09841KEpEOoK7+VmSziOl2PC4EZoL7zAAAGxkex8MV9AEDu01HsnQWure3L7/KTHaSMYw5+HGa6Wt4p1gh4AcCdD//G1tM5nLm7BJSAodXtmrhmRui3Ts3GJiTODE8VrR7YG1rdduOnFvYzzdkJB0mSbt2GVoWF3fl+vHTpFQDA3tIzAMBIzwNcfrKDxcflA59763zKrddp+MpMtuVrt3R8+WPIFfDAeEXAI2tfu8uZ7ahuAL/vqqwwJUiOAUECNMdbyCIKzD6TJMGm7wXW4tPM9Z3GL1c38Pq5yhTUlN7i47J3QxzpTNTe0jMMjI/WtqNnX3yLxrLDU0VfAfNsGDlOfJsdwQcb+7Mw73iLQkCbW9vu+E8qh34Ywu58P7quZIGrGwCAza3a9/3Ed5Tsy6aACYkjd/7LYexkHohYfI0YnipidsKROLSlEQ2nwKhmT7vz/TXvPV9/DgBITy4HfXY/yCHFZ07B/ei68jMuVAXspd0CJiRulG5edDv3O79/59avzGSj6udywUhCTGYnnEQcdmrYMD8Jafn5kZ5cbtuK2xYwIXHHlCAq4yHqvi46CfGKUEswzuOvKQGiKqHSzYt1l22n/Mz4tgRMCGkKAQCvCJMgwIbHAPXlMNUVCZRgmOLRsVHZ4wUuR/kRYgWFytS7MvgCDkslGvNpGPnb192nYuhymA8o8Nzo7Rs/7DYQQhrjfWJO3MdjS/8WUymF/O3r7u9mGSFmXfp768Vn5kdIPKjejAAkYDy2dBmMORX1lqOgUfy4/7EJOe4kbQy21Nh66WwUK247PiHkePECZQPi+PbreqwAAAAASUVORK5CYII=";
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 HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg=="; const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg==";
@@ -2211,6 +2549,7 @@
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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -2317,7 +2656,7 @@
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("2026.3.11", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.11"); }, false), new MenuItem("2026.3.29", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.29"); }, false),
]; ];
/** @type {Birb} */ /** @type {Birb} */
@@ -2507,7 +2846,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -2526,11 +2867,11 @@
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -2566,7 +2907,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -2582,7 +2923,7 @@
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -2660,7 +3001,8 @@
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -2855,9 +3197,23 @@
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -2873,14 +3229,26 @@
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -2902,7 +3270,11 @@
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -3084,26 +3456,6 @@
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -3135,20 +3487,20 @@
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -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.3.11", "version": "2026.3.29",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"icons": { "icons": {
"48": "images/icons/transparent/48x48x1.png", "48": "images/icons/transparent/48x48x1.png",

548
dist/obsidian/main.js vendored
View File

@@ -1,7 +1,7 @@
const { Plugin, Notice } = require('obsidian'); const { Plugin, Notice } = require('obsidian');
module.exports = class PocketBird extends Plugin { module.exports = class PocketBird extends Plugin {
onload() { onload() {
console.log("Loading Pocket Bird version 2026.3.11..."); console.log("Loading Pocket Bird version 2026.3.29...");
const OBSIDIAN_PLUGIN = this; const OBSIDIAN_PLUGIN = this;
(function () { (function () {
'use strict'; 'use strict';
@@ -242,6 +242,8 @@ module.exports = class PocketBird extends Plugin {
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -254,6 +256,8 @@ module.exports = class PocketBird extends Plugin {
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -267,6 +271,8 @@ module.exports = class PocketBird extends Plugin {
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -283,6 +289,8 @@ module.exports = class PocketBird extends Plugin {
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -297,6 +305,8 @@ module.exports = class PocketBird extends Plugin {
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -316,6 +326,8 @@ module.exports = class PocketBird extends Plugin {
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -332,6 +344,8 @@ module.exports = class PocketBird extends Plugin {
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -345,6 +359,8 @@ module.exports = class PocketBird extends Plugin {
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -354,22 +370,11 @@ module.exports = class PocketBird extends Plugin {
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -377,12 +382,15 @@ module.exports = class PocketBird extends Plugin {
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -397,6 +405,8 @@ module.exports = class PocketBird extends Plugin {
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -407,14 +417,249 @@ module.exports = class PocketBird extends Plugin {
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
}; };
/** const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -425,23 +670,36 @@ module.exports = class PocketBird extends Plugin {
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
const SPRITE_SHEET_COLOR_MAP = { const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -454,13 +712,24 @@ module.exports = class PocketBird extends Plugin {
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -468,16 +737,52 @@ module.exports = class PocketBird extends Plugin {
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
({
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
});
const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
class BirdType { class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -489,15 +794,27 @@ module.exports = class PocketBird extends Plugin {
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -561,7 +878,7 @@ module.exports = class PocketBird extends Plugin {
const SPECIES = Object.fromEntries( const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );
@@ -1258,37 +1575,42 @@ module.exports = class PocketBird extends Plugin {
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }
@@ -2080,7 +2402,7 @@ module.exports = class PocketBird extends Plugin {
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -2090,7 +2412,7 @@ module.exports = class PocketBird extends Plugin {
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -2122,7 +2444,7 @@ module.exports = class PocketBird extends Plugin {
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -2141,6 +2463,15 @@ module.exports = class PocketBird extends Plugin {
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -2152,7 +2483,14 @@ module.exports = class PocketBird extends Plugin {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -2165,7 +2503,7 @@ module.exports = class PocketBird extends Plugin {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {
@@ -2214,7 +2552,7 @@ module.exports = class PocketBird extends Plugin {
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
}`; }`;
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABFFJREFUeJztnb9rE2EYx79vK1gUQSRLL64apxZcdC84dNBk0LiIoIJCwUGwlP4BUjqIFpSKLioOdYlV6ObUpV26ORRX0yoEHezQgvZxSN7rm+v9SGzu3vea7wdC31wued7k3ueT5727XAFCCCGEENJfKNsdIO4jIhL1mFKKY4jkFg7eHGBTQDp2pVgEANTq9bZ2Fn0ghPQp0qLseVL2vH3tODn2Kn7Z82RlZERkYmJfO+34hKTJgO0OkGQqxSKmCgXUKpV97SxZXV7GVKHgtwnJOxRgjrAloFq9jplGo23ZTKPhT4EJISQVzCnwysiIf8tqChzWhyxjE0IcQELIOrZNAZl9oPwI6TNEpLnz3/ibdXzbArIhf0LS5IjtDuSN1dFRK3GVUkpExOapJzzdhZA+RVc+uvpjFURI/uE3eheY0mM1lD3BLx1uA3JQOIBILtDy084TkcwFSAH3KWFHQDkFJBaQF/WyALB2AKgV2+nxzzztnMQToc1vXvOGhN+o9hIKmADAi3pZN60cAFJK+X1wrfoL5oWNPM0jsUeBzQ/TGHzQy0TEXCeVARGc+hjLISLi2kAk6XG3+MHqtrYp4E4wU6E6u+Evo/+iiRSgKZ716m3gAXB29+TeCvVy2/6YNGTkgoAJ0dgWcAyilPKld9gZHlSy+Vd6si1iK8CB4x7UsWGc+7QEANi98QTeq+sYn98FloDq7CaQkoxcEDAhOUCixPf12+/MO5M2w4NKxud3sXRvoCcSjHwBU0C3X+4voW+NVzD37rl/f2HS08+LDtaFoEREBo57bcvaBAxg69emH9uMSxGSPiFWfmtzJbg6XXeF2A+nVVVBflzFnY/vAQCvLl8DAFx//TT0OVqEIa/VDNihnGwLmBCXqc5uxO7YS1uAUfFbeZibPOtIgNuLYzh68RQAYGflJwDg5vrbjgLoMnxtrtT1uVs2BUyIy0QJyMy3lEUUWX3mSYId/xZYi0+zMOnh/P11nDl9InT9wIY40JGonZWfvviC/QgS3Ci6HzwaRg4Tr0s32oqQYL5lIaCv335H5n9e+O+LIWwvjmHoSgm4vx76eJj4DlJ92RQwIS7y7E8FE0dqQMbiS6I6u4GFSU9c6EsSiVNgtKqn7cWxtse2vmwBAArTq1HP3Qvyn+Izp+BhDF35jPMZCZgQ12g8uuAP7kvf3/jL1+ZKWY1ziSpCzAOTLuddYsfCJKTlF0ZherVnb9y2gAlxHVOCaOZD1mNddBESFKGWoMv515EA0ZJQ49GF2HV7KT8zvi0BE0I6QgAgKMI8CDBxH6C+EGfrjURKME3x6NhofuNFrkf5EWIFhebUu5l8Ebulco15NYza44f+VTF0O80LFAR+6B0aP+0+EEKSCV4xx/V87OrfYiqlUHv80L9vtpFi1aVfNy4+Kz9C3KD101QgB/nY1Wkw5lQ02M6CpPiuf9iEHHbyloNddTaunM3ijduOTwg5XPwDDgmrxnQErq8AAAAASUVORK5CYII="; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABORJREFUeJztnU9IHFccx79vE6g0BEK7BJzNwR6apReFQElKjpZaeol7EHuRQhFaECIEIkF6LGkQGqyQ0EBa+u8iHjZCCfXQUw7qRVDwYKQEirtt0yVtMRaFdn857L7x7Tizf3Rn3oz7/cDi8+3s/t44733m9+afACGEEEII6SyU7QaQ+CMiEvSeUop9iCQWdt4EYFNAOnYukwEA5AuFmnIUbSCEdChSZdBxZNBxDpTrybFd8QcdR5Z6e0XGxg6Uw45PSJikbDeANCaXyeBGOo18LnegHCXLjx7hRjrtlglJOhRggrAloHyhgFulUk3drVLJnQITQkgomFPgpd5e9xXVFNivDVHGJoTEAPEh6tg2BWS2gfIjpMMQkcrBf+Nn1PFtC8iG/AkJk5O2G5A0lvv6rMRVSikREZuXnvByF0I6FJ356OyPWRAhyYd79BYwpcdsKHq8Ox1uA3JU2IFIItDy084TkcgFSAF3KH5nQDkFJBaQe4VBAWDtBFA1dqz7P8dp8zS8ENrc85ovNLhHtZ1QwAQA7hUGddHKCSCllNuGuGV/3nFhY5wmkbpngc0/ptH5oOtEBCIiYXYG79THqA89NokXH2UeWN3WNgXcDOZQGJ4qunX0XzCBAjTFs3DpbfRcexXny2eA1zKAcxYo1EoQIewV4yBgQjS2BVwHUUq50jvudJ9Q8tv/0pZtUTcDTJ1yoF7uxrtr68DaOsoj03A+fx/FNz7DuR+yGFrdrjko3U4RxkHAhCQACRLf5tZ25I0Jm+4TSt77soyHH6faIsHALzAF9NPkP279v38Cbz6dw693f8R06Xu3fq7vtP5ccLAWBCUikjrl1NSVR6bh3K8I+GHXX/jmq09841KEpEOoK7+VmSziOl2PC4EZoL7zAAAGxkex8MV9AEDu01HsnQWure3L7/KTHaSMYw5+HGa6Wt4p1gh4AcCdD//G1tM5nLm7BJSAodXtmrhmRui3Ts3GJiTODE8VrR7YG1rdduOnFvYzzdkJB0mSbt2GVoWF3fl+vHTpFQDA3tIzAMBIzwNcfrKDxcflA59763zKrddp+MpMtuVrt3R8+WPIFfDAeEXAI2tfu8uZ7ahuAL/vqqwwJUiOAUECNMdbyCIKzD6TJMGm7wXW4tPM9Z3GL1c38Pq5yhTUlN7i47J3QxzpTNTe0jMMjI/WtqNnX3yLxrLDU0VfAfNsGDlOfJsdwQcb+7Mw73iLQkCbW9vu+E8qh34Ywu58P7quZIGrGwCAza3a9/3Ed5Tsy6aACYkjd/7LYexkHohYfI0YnipidsKROLSlEQ2nwKhmT7vz/TXvPV9/DgBITy4HfXY/yCHFZ07B/ei68jMuVAXspd0CJiRulG5edDv3O79/59avzGSj6udywUhCTGYnnEQcdmrYMD8Jafn5kZ5cbtuK2xYwIXHHlCAq4yHqvi46CfGKUEswzuOvKQGiKqHSzYt1l22n/Mz4tgRMCGkKAQCvCJMgwIbHAPXlMNUVCZRgmOLRsVHZ4wUuR/kRYgWFytS7MvgCDkslGvNpGPnb192nYuhymA8o8Nzo7Rs/7DYQQhrjfWJO3MdjS/8WUymF/O3r7u9mGSFmXfp768Vn5kdIPKjejAAkYDy2dBmMORX1lqOgUfy4/7EJOe4kbQy21Nh66WwUK247PiHkePECZQPi+PbreqwAAAAASUVORK5CYII=";
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 HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg=="; const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg==";
@@ -2244,6 +2582,7 @@ module.exports = class PocketBird extends Plugin {
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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -2350,7 +2689,7 @@ module.exports = class PocketBird extends Plugin {
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("2026.3.11", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.11"); }, false), new MenuItem("2026.3.29", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.29"); }, false),
]; ];
/** @type {Birb} */ /** @type {Birb} */
@@ -2540,7 +2879,9 @@ module.exports = class PocketBird extends Plugin {
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -2559,11 +2900,11 @@ module.exports = class PocketBird extends Plugin {
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -2599,7 +2940,7 @@ module.exports = class PocketBird extends Plugin {
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -2615,7 +2956,7 @@ module.exports = class PocketBird extends Plugin {
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -2693,7 +3034,8 @@ module.exports = class PocketBird extends Plugin {
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -2888,9 +3230,23 @@ module.exports = class PocketBird extends Plugin {
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -2906,14 +3262,26 @@ module.exports = class PocketBird extends Plugin {
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -2935,7 +3303,11 @@ module.exports = class PocketBird extends Plugin {
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -3117,26 +3489,6 @@ module.exports = class PocketBird extends Plugin {
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -3168,20 +3520,20 @@ module.exports = class PocketBird extends Plugin {
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

View File

@@ -1,7 +1,7 @@
{ {
"id": "pocket-bird", "id": "pocket-bird",
"name": "Pocket Bird", "name": "Pocket Bird",
"version": "2026.3.11", "version": "2026.3.29",
"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",

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version 2026.3.11 // @version 2026.3.29
// @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?
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
@@ -251,6 +251,8 @@
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -263,6 +265,8 @@
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -276,6 +280,8 @@
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -292,6 +298,8 @@
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -306,6 +314,8 @@
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -325,6 +335,8 @@
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -341,6 +353,8 @@
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -354,6 +368,8 @@
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -363,22 +379,11 @@
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -386,12 +391,15 @@
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -406,6 +414,8 @@
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -416,14 +426,249 @@
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
}; };
/** const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -434,23 +679,36 @@
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
const SPRITE_SHEET_COLOR_MAP = { const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -463,13 +721,24 @@
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -477,16 +746,52 @@
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
({
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
});
const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
class BirdType { class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -498,15 +803,27 @@
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -570,7 +887,7 @@
const SPECIES = Object.fromEntries( const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );
@@ -1267,37 +1584,42 @@
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }
@@ -2042,7 +2364,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -2052,7 +2374,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -2084,7 +2406,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -2103,6 +2425,15 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -2114,7 +2445,14 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -2127,7 +2465,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {
@@ -2176,7 +2514,7 @@
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
}`; }`;
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABFFJREFUeJztnb9rE2EYx79vK1gUQSRLL64apxZcdC84dNBk0LiIoIJCwUGwlP4BUjqIFpSKLioOdYlV6ObUpV26ORRX0yoEHezQgvZxSN7rm+v9SGzu3vea7wdC31wued7k3ueT5727XAFCCCGEENJfKNsdIO4jIhL1mFKKY4jkFg7eHGBTQDp2pVgEANTq9bZ2Fn0ghPQp0qLseVL2vH3tODn2Kn7Z82RlZERkYmJfO+34hKTJgO0OkGQqxSKmCgXUKpV97SxZXV7GVKHgtwnJOxRgjrAloFq9jplGo23ZTKPhT4EJISQVzCnwysiIf8tqChzWhyxjE0IcQELIOrZNAZl9oPwI6TNEpLnz3/ibdXzbArIhf0LS5IjtDuSN1dFRK3GVUkpExOapJzzdhZA+RVc+uvpjFURI/uE3eheY0mM1lD3BLx1uA3JQOIBILtDy084TkcwFSAH3KWFHQDkFJBaQF/WyALB2AKgV2+nxzzztnMQToc1vXvOGhN+o9hIKmADAi3pZN60cAFJK+X1wrfoL5oWNPM0jsUeBzQ/TGHzQy0TEXCeVARGc+hjLISLi2kAk6XG3+MHqtrYp4E4wU6E6u+Evo/+iiRSgKZ716m3gAXB29+TeCvVy2/6YNGTkgoAJ0dgWcAyilPKld9gZHlSy+Vd6si1iK8CB4x7UsWGc+7QEANi98QTeq+sYn98FloDq7CaQkoxcEDAhOUCixPf12+/MO5M2w4NKxud3sXRvoCcSjHwBU0C3X+4voW+NVzD37rl/f2HS08+LDtaFoEREBo57bcvaBAxg69emH9uMSxGSPiFWfmtzJbg6XXeF2A+nVVVBflzFnY/vAQCvLl8DAFx//TT0OVqEIa/VDNihnGwLmBCXqc5uxO7YS1uAUfFbeZibPOtIgNuLYzh68RQAYGflJwDg5vrbjgLoMnxtrtT1uVs2BUyIy0QJyMy3lEUUWX3mSYId/xZYi0+zMOnh/P11nDl9InT9wIY40JGonZWfvviC/QgS3Ci6HzwaRg4Tr0s32oqQYL5lIaCv335H5n9e+O+LIWwvjmHoSgm4vx76eJj4DlJ92RQwIS7y7E8FE0dqQMbiS6I6u4GFSU9c6EsSiVNgtKqn7cWxtse2vmwBAArTq1HP3Qvyn+Izp+BhDF35jPMZCZgQ12g8uuAP7kvf3/jL1+ZKWY1ziSpCzAOTLuddYsfCJKTlF0ZherVnb9y2gAlxHVOCaOZD1mNddBESFKGWoMv515EA0ZJQ49GF2HV7KT8zvi0BE0I6QgAgKMI8CDBxH6C+EGfrjURKME3x6NhofuNFrkf5EWIFhebUu5l8Ebulco15NYza44f+VTF0O80LFAR+6B0aP+0+EEKSCV4xx/V87OrfYiqlUHv80L9vtpFi1aVfNy4+Kz9C3KD101QgB/nY1Wkw5lQ02M6CpPiuf9iEHHbyloNddTaunM3ijduOTwg5XPwDDgmrxnQErq8AAAAASUVORK5CYII="; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABORJREFUeJztnU9IHFccx79vE6g0BEK7BJzNwR6apReFQElKjpZaeol7EHuRQhFaECIEIkF6LGkQGqyQ0EBa+u8iHjZCCfXQUw7qRVDwYKQEirtt0yVtMRaFdn857L7x7Tizf3Rn3oz7/cDi8+3s/t44733m9+afACGEEEII6SyU7QaQ+CMiEvSeUop9iCQWdt4EYFNAOnYukwEA5AuFmnIUbSCEdChSZdBxZNBxDpTrybFd8QcdR5Z6e0XGxg6Uw45PSJikbDeANCaXyeBGOo18LnegHCXLjx7hRjrtlglJOhRggrAloHyhgFulUk3drVLJnQITQkgomFPgpd5e9xXVFNivDVHGJoTEAPEh6tg2BWS2gfIjpMMQkcrBf+Nn1PFtC8iG/AkJk5O2G5A0lvv6rMRVSikREZuXnvByF0I6FJ356OyPWRAhyYd79BYwpcdsKHq8Ox1uA3JU2IFIItDy084TkcgFSAF3KH5nQDkFJBaQe4VBAWDtBFA1dqz7P8dp8zS8ENrc85ovNLhHtZ1QwAQA7hUGddHKCSCllNuGuGV/3nFhY5wmkbpngc0/ptH5oOtEBCIiYXYG79THqA89NokXH2UeWN3WNgXcDOZQGJ4qunX0XzCBAjTFs3DpbfRcexXny2eA1zKAcxYo1EoQIewV4yBgQjS2BVwHUUq50jvudJ9Q8tv/0pZtUTcDTJ1yoF7uxrtr68DaOsoj03A+fx/FNz7DuR+yGFrdrjko3U4RxkHAhCQACRLf5tZ25I0Jm+4TSt77soyHH6faIsHALzAF9NPkP279v38Cbz6dw693f8R06Xu3fq7vtP5ccLAWBCUikjrl1NSVR6bh3K8I+GHXX/jmq09841KEpEOoK7+VmSziOl2PC4EZoL7zAAAGxkex8MV9AEDu01HsnQWure3L7/KTHaSMYw5+HGa6Wt4p1gh4AcCdD//G1tM5nLm7BJSAodXtmrhmRui3Ts3GJiTODE8VrR7YG1rdduOnFvYzzdkJB0mSbt2GVoWF3fl+vHTpFQDA3tIzAMBIzwNcfrKDxcflA59763zKrddp+MpMtuVrt3R8+WPIFfDAeEXAI2tfu8uZ7ahuAL/vqqwwJUiOAUECNMdbyCIKzD6TJMGm7wXW4tPM9Z3GL1c38Pq5yhTUlN7i47J3QxzpTNTe0jMMjI/WtqNnX3yLxrLDU0VfAfNsGDlOfJsdwQcb+7Mw73iLQkCbW9vu+E8qh34Ywu58P7quZIGrGwCAza3a9/3Ed5Tsy6aACYkjd/7LYexkHohYfI0YnipidsKROLSlEQ2nwKhmT7vz/TXvPV9/DgBITy4HfXY/yCHFZ07B/ei68jMuVAXspd0CJiRulG5edDv3O79/59avzGSj6udywUhCTGYnnEQcdmrYMD8Jafn5kZ5cbtuK2xYwIXHHlCAq4yHqvi46CfGKUEswzuOvKQGiKqHSzYt1l22n/Mz4tgRMCGkKAQCvCJMgwIbHAPXlMNUVCZRgmOLRsVHZ4wUuR/kRYgWFytS7MvgCDkslGvNpGPnb192nYuhymA8o8Nzo7Rs/7DYQQhrjfWJO3MdjS/8WUymF/O3r7u9mGSFmXfp768Vn5kdIPKjejAAkYDy2dBmMORX1lqOgUfy4/7EJOe4kbQy21Nh66WwUK247PiHkePECZQPi+PbreqwAAAAASUVORK5CYII=";
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 HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg=="; const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg==";
@@ -2206,6 +2544,7 @@
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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -2312,7 +2651,7 @@
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("2026.3.11", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.11"); }, false), new MenuItem("2026.3.29", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.29"); }, false),
]; ];
/** @type {Birb} */ /** @type {Birb} */
@@ -2502,7 +2841,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -2521,11 +2862,11 @@
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -2561,7 +2902,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -2577,7 +2918,7 @@
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -2655,7 +2996,8 @@
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -2850,9 +3192,23 @@
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -2868,14 +3224,26 @@
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -2897,7 +3265,11 @@
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -3079,26 +3451,6 @@
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -3130,20 +3482,20 @@
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

546
dist/web/birb.embed.js vendored
View File

@@ -237,6 +237,8 @@
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -249,6 +251,8 @@
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -262,6 +266,8 @@
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -278,6 +284,8 @@
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -292,6 +300,8 @@
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -311,6 +321,8 @@
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -327,6 +339,8 @@
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -340,6 +354,8 @@
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -349,22 +365,11 @@
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -372,12 +377,15 @@
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -392,6 +400,8 @@
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -402,14 +412,249 @@
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
}; };
/** const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -420,23 +665,36 @@
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
const SPRITE_SHEET_COLOR_MAP = { const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -449,13 +707,24 @@
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -463,16 +732,52 @@
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
({
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
});
const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
class BirdType { class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -484,15 +789,27 @@
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -556,7 +873,7 @@
const SPECIES = Object.fromEntries( const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );
@@ -1253,37 +1570,42 @@
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }
@@ -2022,7 +2344,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -2032,7 +2354,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -2064,7 +2386,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -2083,6 +2405,15 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -2094,7 +2425,14 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -2107,7 +2445,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {
@@ -2156,7 +2494,7 @@
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
}`; }`;
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABFFJREFUeJztnb9rE2EYx79vK1gUQSRLL64apxZcdC84dNBk0LiIoIJCwUGwlP4BUjqIFpSKLioOdYlV6ObUpV26ORRX0yoEHezQgvZxSN7rm+v9SGzu3vea7wdC31wued7k3ueT5727XAFCCCGEENJfKNsdIO4jIhL1mFKKY4jkFg7eHGBTQDp2pVgEANTq9bZ2Fn0ghPQp0qLseVL2vH3tODn2Kn7Z82RlZERkYmJfO+34hKTJgO0OkGQqxSKmCgXUKpV97SxZXV7GVKHgtwnJOxRgjrAloFq9jplGo23ZTKPhT4EJISQVzCnwysiIf8tqChzWhyxjE0IcQELIOrZNAZl9oPwI6TNEpLnz3/ibdXzbArIhf0LS5IjtDuSN1dFRK3GVUkpExOapJzzdhZA+RVc+uvpjFURI/uE3eheY0mM1lD3BLx1uA3JQOIBILtDy084TkcwFSAH3KWFHQDkFJBaQF/WyALB2AKgV2+nxzzztnMQToc1vXvOGhN+o9hIKmADAi3pZN60cAFJK+X1wrfoL5oWNPM0jsUeBzQ/TGHzQy0TEXCeVARGc+hjLISLi2kAk6XG3+MHqtrYp4E4wU6E6u+Evo/+iiRSgKZ716m3gAXB29+TeCvVy2/6YNGTkgoAJ0dgWcAyilPKld9gZHlSy+Vd6si1iK8CB4x7UsWGc+7QEANi98QTeq+sYn98FloDq7CaQkoxcEDAhOUCixPf12+/MO5M2w4NKxud3sXRvoCcSjHwBU0C3X+4voW+NVzD37rl/f2HS08+LDtaFoEREBo57bcvaBAxg69emH9uMSxGSPiFWfmtzJbg6XXeF2A+nVVVBflzFnY/vAQCvLl8DAFx//TT0OVqEIa/VDNihnGwLmBCXqc5uxO7YS1uAUfFbeZibPOtIgNuLYzh68RQAYGflJwDg5vrbjgLoMnxtrtT1uVs2BUyIy0QJyMy3lEUUWX3mSYId/xZYi0+zMOnh/P11nDl9InT9wIY40JGonZWfvviC/QgS3Ci6HzwaRg4Tr0s32oqQYL5lIaCv335H5n9e+O+LIWwvjmHoSgm4vx76eJj4DlJ92RQwIS7y7E8FE0dqQMbiS6I6u4GFSU9c6EsSiVNgtKqn7cWxtse2vmwBAArTq1HP3Qvyn+Izp+BhDF35jPMZCZgQ12g8uuAP7kvf3/jL1+ZKWY1ziSpCzAOTLuddYsfCJKTlF0ZherVnb9y2gAlxHVOCaOZD1mNddBESFKGWoMv515EA0ZJQ49GF2HV7KT8zvi0BE0I6QgAgKMI8CDBxH6C+EGfrjURKME3x6NhofuNFrkf5EWIFhebUu5l8Ebulco15NYza44f+VTF0O80LFAR+6B0aP+0+EEKSCV4xx/V87OrfYiqlUHv80L9vtpFi1aVfNy4+Kz9C3KD101QgB/nY1Wkw5lQ02M6CpPiuf9iEHHbyloNddTaunM3ijduOTwg5XPwDDgmrxnQErq8AAAAASUVORK5CYII="; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABORJREFUeJztnU9IHFccx79vE6g0BEK7BJzNwR6apReFQElKjpZaeol7EHuRQhFaECIEIkF6LGkQGqyQ0EBa+u8iHjZCCfXQUw7qRVDwYKQEirtt0yVtMRaFdn857L7x7Tizf3Rn3oz7/cDi8+3s/t44733m9+afACGEEEII6SyU7QaQ+CMiEvSeUop9iCQWdt4EYFNAOnYukwEA5AuFmnIUbSCEdChSZdBxZNBxDpTrybFd8QcdR5Z6e0XGxg6Uw45PSJikbDeANCaXyeBGOo18LnegHCXLjx7hRjrtlglJOhRggrAloHyhgFulUk3drVLJnQITQkgomFPgpd5e9xXVFNivDVHGJoTEAPEh6tg2BWS2gfIjpMMQkcrBf+Nn1PFtC8iG/AkJk5O2G5A0lvv6rMRVSikREZuXnvByF0I6FJ356OyPWRAhyYd79BYwpcdsKHq8Ox1uA3JU2IFIItDy084TkcgFSAF3KH5nQDkFJBaQe4VBAWDtBFA1dqz7P8dp8zS8ENrc85ovNLhHtZ1QwAQA7hUGddHKCSCllNuGuGV/3nFhY5wmkbpngc0/ptH5oOtEBCIiYXYG79THqA89NokXH2UeWN3WNgXcDOZQGJ4qunX0XzCBAjTFs3DpbfRcexXny2eA1zKAcxYo1EoQIewV4yBgQjS2BVwHUUq50jvudJ9Q8tv/0pZtUTcDTJ1yoF7uxrtr68DaOsoj03A+fx/FNz7DuR+yGFrdrjko3U4RxkHAhCQACRLf5tZ25I0Jm+4TSt77soyHH6faIsHALzAF9NPkP279v38Cbz6dw693f8R06Xu3fq7vtP5ccLAWBCUikjrl1NSVR6bh3K8I+GHXX/jmq09841KEpEOoK7+VmSziOl2PC4EZoL7zAAAGxkex8MV9AEDu01HsnQWure3L7/KTHaSMYw5+HGa6Wt4p1gh4AcCdD//G1tM5nLm7BJSAodXtmrhmRui3Ts3GJiTODE8VrR7YG1rdduOnFvYzzdkJB0mSbt2GVoWF3fl+vHTpFQDA3tIzAMBIzwNcfrKDxcflA59763zKrddp+MpMtuVrt3R8+WPIFfDAeEXAI2tfu8uZ7ahuAL/vqqwwJUiOAUECNMdbyCIKzD6TJMGm7wXW4tPM9Z3GL1c38Pq5yhTUlN7i47J3QxzpTNTe0jMMjI/WtqNnX3yLxrLDU0VfAfNsGDlOfJsdwQcb+7Mw73iLQkCbW9vu+E8qh34Ywu58P7quZIGrGwCAza3a9/3Ed5Tsy6aACYkjd/7LYexkHohYfI0YnipidsKROLSlEQ2nwKhmT7vz/TXvPV9/DgBITy4HfXY/yCHFZ07B/ei68jMuVAXspd0CJiRulG5edDv3O79/59avzGSj6udywUhCTGYnnEQcdmrYMD8Jafn5kZ5cbtuK2xYwIXHHlCAq4yHqvi46CfGKUEswzuOvKQGiKqHSzYt1l22n/Mz4tgRMCGkKAQCvCJMgwIbHAPXlMNUVCZRgmOLRsVHZ4wUuR/kRYgWFytS7MvgCDkslGvNpGPnb192nYuhymA8o8Nzo7Rs/7DYQQhrjfWJO3MdjS/8WUymF/O3r7u9mGSFmXfp768Vn5kdIPKjejAAkYDy2dBmMORX1lqOgUfy4/7EJOe4kbQy21Nh66WwUK247PiHkePECZQPi+PbreqwAAAAASUVORK5CYII=";
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 HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg=="; const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg==";
@@ -2186,6 +2524,7 @@
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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -2292,7 +2631,7 @@
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("2026.3.11", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.11"); }, false), new MenuItem("2026.3.29", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.29"); }, false),
]; ];
/** @type {Birb} */ /** @type {Birb} */
@@ -2482,7 +2821,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -2501,11 +2842,11 @@
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -2541,7 +2882,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -2557,7 +2898,7 @@
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -2635,7 +2976,8 @@
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -2830,9 +3172,23 @@
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -2848,14 +3204,26 @@
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -2877,7 +3245,11 @@
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -3059,26 +3431,6 @@
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -3110,20 +3462,20 @@
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

546
dist/web/birb.js vendored
View File

@@ -237,6 +237,8 @@
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -249,6 +251,8 @@
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -262,6 +266,8 @@
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -278,6 +284,8 @@
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -292,6 +300,8 @@
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -311,6 +321,8 @@
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -327,6 +339,8 @@
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -340,6 +354,8 @@
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -349,22 +365,11 @@
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -372,12 +377,15 @@
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -392,6 +400,8 @@
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -402,14 +412,249 @@
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
}; };
/** const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -420,23 +665,36 @@
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
const SPRITE_SHEET_COLOR_MAP = { const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -449,13 +707,24 @@
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -463,16 +732,52 @@
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
({
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
});
const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
class BirdType { class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -484,15 +789,27 @@
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -556,7 +873,7 @@
const SPECIES = Object.fromEntries( const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );
@@ -1253,37 +1570,42 @@
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }
@@ -2022,7 +2344,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -2032,7 +2354,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -2064,7 +2386,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -2083,6 +2405,15 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -2094,7 +2425,14 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -2107,7 +2445,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {
@@ -2156,7 +2494,7 @@
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
}`; }`;
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABFFJREFUeJztnb9rE2EYx79vK1gUQSRLL64apxZcdC84dNBk0LiIoIJCwUGwlP4BUjqIFpSKLioOdYlV6ObUpV26ORRX0yoEHezQgvZxSN7rm+v9SGzu3vea7wdC31wued7k3ueT5727XAFCCCGEENJfKNsdIO4jIhL1mFKKY4jkFg7eHGBTQDp2pVgEANTq9bZ2Fn0ghPQp0qLseVL2vH3tODn2Kn7Z82RlZERkYmJfO+34hKTJgO0OkGQqxSKmCgXUKpV97SxZXV7GVKHgtwnJOxRgjrAloFq9jplGo23ZTKPhT4EJISQVzCnwysiIf8tqChzWhyxjE0IcQELIOrZNAZl9oPwI6TNEpLnz3/ibdXzbArIhf0LS5IjtDuSN1dFRK3GVUkpExOapJzzdhZA+RVc+uvpjFURI/uE3eheY0mM1lD3BLx1uA3JQOIBILtDy084TkcwFSAH3KWFHQDkFJBaQF/WyALB2AKgV2+nxzzztnMQToc1vXvOGhN+o9hIKmADAi3pZN60cAFJK+X1wrfoL5oWNPM0jsUeBzQ/TGHzQy0TEXCeVARGc+hjLISLi2kAk6XG3+MHqtrYp4E4wU6E6u+Evo/+iiRSgKZ716m3gAXB29+TeCvVy2/6YNGTkgoAJ0dgWcAyilPKld9gZHlSy+Vd6si1iK8CB4x7UsWGc+7QEANi98QTeq+sYn98FloDq7CaQkoxcEDAhOUCixPf12+/MO5M2w4NKxud3sXRvoCcSjHwBU0C3X+4voW+NVzD37rl/f2HS08+LDtaFoEREBo57bcvaBAxg69emH9uMSxGSPiFWfmtzJbg6XXeF2A+nVVVBflzFnY/vAQCvLl8DAFx//TT0OVqEIa/VDNihnGwLmBCXqc5uxO7YS1uAUfFbeZibPOtIgNuLYzh68RQAYGflJwDg5vrbjgLoMnxtrtT1uVs2BUyIy0QJyMy3lEUUWX3mSYId/xZYi0+zMOnh/P11nDl9InT9wIY40JGonZWfvviC/QgS3Ci6HzwaRg4Tr0s32oqQYL5lIaCv335H5n9e+O+LIWwvjmHoSgm4vx76eJj4DlJ92RQwIS7y7E8FE0dqQMbiS6I6u4GFSU9c6EsSiVNgtKqn7cWxtse2vmwBAArTq1HP3Qvyn+Izp+BhDF35jPMZCZgQ12g8uuAP7kvf3/jL1+ZKWY1ziSpCzAOTLuddYsfCJKTlF0ZherVnb9y2gAlxHVOCaOZD1mNddBESFKGWoMv515EA0ZJQ49GF2HV7KT8zvi0BE0I6QgAgKMI8CDBxH6C+EGfrjURKME3x6NhofuNFrkf5EWIFhebUu5l8Ebulco15NYza44f+VTF0O80LFAR+6B0aP+0+EEKSCV4xx/V87OrfYiqlUHv80L9vtpFi1aVfNy4+Kz9C3KD101QgB/nY1Wkw5lQ02M6CpPiuf9iEHHbyloNddTaunM3ijduOTwg5XPwDDgmrxnQErq8AAAAASUVORK5CYII="; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABORJREFUeJztnU9IHFccx79vE6g0BEK7BJzNwR6apReFQElKjpZaeol7EHuRQhFaECIEIkF6LGkQGqyQ0EBa+u8iHjZCCfXQUw7qRVDwYKQEirtt0yVtMRaFdn857L7x7Tizf3Rn3oz7/cDi8+3s/t44733m9+afACGEEEII6SyU7QaQ+CMiEvSeUop9iCQWdt4EYFNAOnYukwEA5AuFmnIUbSCEdChSZdBxZNBxDpTrybFd8QcdR5Z6e0XGxg6Uw45PSJikbDeANCaXyeBGOo18LnegHCXLjx7hRjrtlglJOhRggrAloHyhgFulUk3drVLJnQITQkgomFPgpd5e9xXVFNivDVHGJoTEAPEh6tg2BWS2gfIjpMMQkcrBf+Nn1PFtC8iG/AkJk5O2G5A0lvv6rMRVSikREZuXnvByF0I6FJ356OyPWRAhyYd79BYwpcdsKHq8Ox1uA3JU2IFIItDy084TkcgFSAF3KH5nQDkFJBaQe4VBAWDtBFA1dqz7P8dp8zS8ENrc85ovNLhHtZ1QwAQA7hUGddHKCSCllNuGuGV/3nFhY5wmkbpngc0/ptH5oOtEBCIiYXYG79THqA89NokXH2UeWN3WNgXcDOZQGJ4qunX0XzCBAjTFs3DpbfRcexXny2eA1zKAcxYo1EoQIewV4yBgQjS2BVwHUUq50jvudJ9Q8tv/0pZtUTcDTJ1yoF7uxrtr68DaOsoj03A+fx/FNz7DuR+yGFrdrjko3U4RxkHAhCQACRLf5tZ25I0Jm+4TSt77soyHH6faIsHALzAF9NPkP279v38Cbz6dw693f8R06Xu3fq7vtP5ccLAWBCUikjrl1NSVR6bh3K8I+GHXX/jmq09841KEpEOoK7+VmSziOl2PC4EZoL7zAAAGxkex8MV9AEDu01HsnQWure3L7/KTHaSMYw5+HGa6Wt4p1gh4AcCdD//G1tM5nLm7BJSAodXtmrhmRui3Ts3GJiTODE8VrR7YG1rdduOnFvYzzdkJB0mSbt2GVoWF3fl+vHTpFQDA3tIzAMBIzwNcfrKDxcflA59763zKrddp+MpMtuVrt3R8+WPIFfDAeEXAI2tfu8uZ7ahuAL/vqqwwJUiOAUECNMdbyCIKzD6TJMGm7wXW4tPM9Z3GL1c38Pq5yhTUlN7i47J3QxzpTNTe0jMMjI/WtqNnX3yLxrLDU0VfAfNsGDlOfJsdwQcb+7Mw73iLQkCbW9vu+E8qh34Ywu58P7quZIGrGwCAza3a9/3Ed5Tsy6aACYkjd/7LYexkHohYfI0YnipidsKROLSlEQ2nwKhmT7vz/TXvPV9/DgBITy4HfXY/yCHFZ07B/ei68jMuVAXspd0CJiRulG5edDv3O79/59avzGSj6udywUhCTGYnnEQcdmrYMD8Jafn5kZ5cbtuK2xYwIXHHlCAq4yHqvi46CfGKUEswzuOvKQGiKqHSzYt1l22n/Mz4tgRMCGkKAQCvCJMgwIbHAPXlMNUVCZRgmOLRsVHZ4wUuR/kRYgWFytS7MvgCDkslGvNpGPnb192nYuhymA8o8Nzo7Rs/7DYQQhrjfWJO3MdjS/8WUymF/O3r7u9mGSFmXfp768Vn5kdIPKjejAAkYDy2dBmMORX1lqOgUfy4/7EJOe4kbQy21Nh66WwUK247PiHkePECZQPi+PbreqwAAAAASUVORK5CYII=";
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 HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg=="; const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAAAMCAYAAACjpxUSAAAAAXNSR0IArs4c6QAAA29JREFUWIXtl11oW2UYx3+v3ZCuc1ktIpvowrCBjmkdtl6ouBthWMeGTkQxuA9QKCiym20OBfVG3YUVOobYIszLyXY16HYxvOicns3NRoR267ZG04aOdc3Jx8lJsujjRZOzeE5y+raETjR/CJyc9/98vv/zvOdAAw000EADDWhB3e0E6gURkfK1Uuo/U9dS4567GFs0OHqOymJIKud/JeoV5/8AjyBaV7bJhrWdEmxtl3pumgv195tUEO1wRAGwdzg4F6whCm14BHFvU/NCfYjrp4WWF05TV2EEBIKjfFN4CUpi6Fy3mf0/hfys3Hnr1rDQenW4futLJmiPIO5vaSNrZ3XtZebJ5/k51M13wY3IhQA6hXf1GgQCqxaTb1U47wwBYc8DJ/h0YhOd6zaz65GjtK18yNf2Un/Yyavv1Wd0wkksFsMwDAzDQKfeQmSQxNkvAETMqnwZvvgrNYTpXvPYLuaBrIVl7htlMUQT4wt2drinFbkAqjspfi+sZ187SPvXn+u6dRdZ1a9SSpWPhoPrR4ARdslR9m38ft4Al/rDZFImFyOJeXOJxWLE43HGx7X6I4XIIFY6RcacRkwg2oGYo6JW1+yPHO47gplI8sEn788vttFDpNIWtpXEti1CPQM1ex9sbZcVzSugtM8Ve+zwPYI4/9R6bs3Okk88CsATV6/qFO5AdSd9C+jqNdj0bROUjg1raIufeCR84EcArv8+TTGf4vyJnb5iU0px8r1tjI1dRimFiFT76nAafeXUhHNzjXe90k6Mjh1ce/ZtJuNTACRu5+gnxLtcqZaTAJzbdryCH+IdcxS12pv3WHSKgeND5P+YwEwkSc6m2f36Wzz9ysuMRaeq9gZgePsxUnkb888imcl05VrNHvmdAI5RWT1ZO0smlwJgxr5RletOys+vm9/Va2AVmvjlzf20fbYPa2iLL/+5HX1YmQT54nIAfjvzoR9fF1JxVJBJmc51eUrsPfaDO44zHcooT4lwOOzhFiKDAFjp1J045rRz/fDWQx6bj/q+Ij85STH/FzO3bpLNZVnz+GN8+fEBr/9rg1CYnZsOudvY6ZtzKyVWqGfAbcOL7W9IJmuRy88JIkcGgMjMOYe31N/rugKab1QuOm8RkdLEWGgMLf6/0L8APHjfWpqXtVB5ZNhFixvp+D+M/gZZI68eaJ1OpQAAAABJRU5ErkJggg==";
@@ -2186,6 +2524,7 @@
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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -2292,7 +2631,7 @@
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("2026.3.11", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.11"); }, false), new MenuItem("2026.3.29", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.3.29"); }, false),
]; ];
/** @type {Birb} */ /** @type {Birb} */
@@ -2482,7 +2821,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -2501,11 +2842,11 @@
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -2541,7 +2882,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -2557,7 +2898,7 @@
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -2635,7 +2976,8 @@
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -2830,9 +3172,23 @@
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -2848,14 +3204,26 @@
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -2877,7 +3245,11 @@
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -3059,26 +3431,6 @@
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -3110,20 +3462,20 @@
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

View File

@@ -1,5 +1,5 @@
// @ts-check // @ts-check
import { SPRITE_SHEET_COLOR_MAP, PALETTE, loadSpriteSheetPixels } from '../src/animation/sprites.js'; import { SPRITE_SHEET_COLOR_MAP, PALETTE, DEFAULT_COLOR_OVERRIDES, loadSpriteSheetPixels } from '../src/animation/sprites.js';
import Layer, { TAG } from '../src/animation/layer.js'; import Layer, { TAG } from '../src/animation/layer.js';
import Frame from '../src/animation/frame.js'; import Frame from '../src/animation/frame.js';
import { Directions, getLayerPixels } from '../src/shared.js'; import { Directions, getLayerPixels } from '../src/shared.js';
@@ -10,15 +10,6 @@ import species from '../src/species.js';
const COLOR_MAP = SPRITE_SHEET_COLOR_MAP; const COLOR_MAP = SPRITE_SHEET_COLOR_MAP;
const SPRITE_PATH = "../sprites/birb.png"; const SPRITE_PATH = "../sprites/birb.png";
const SPRITE_SIZE = 32; const SPRITE_SIZE = 32;
/** @type {Record<string, string>} */
const DEFAULT_OVERRIDES = {
"hood": "face",
"eyebrow": "face",
"nose": "face",
"cheek": "face",
"scruff": "face",
"collar": "face",
};
const IGNORED_PARTS = new Set( const IGNORED_PARTS = new Set(
["transparent", "border", "heart", "heart-border", "heart-shine", "feather-spine"] ["transparent", "border", "heart", "heart-border", "heart-shine", "feather-spine"]
); );
@@ -161,8 +152,9 @@ function getColor(part) {
if (currentSpecies.colors[part]) { if (currentSpecies.colors[part]) {
return currentSpecies.colors[part]; return currentSpecies.colors[part];
} }
if (DEFAULT_OVERRIDES[part]) { const override = DEFAULT_COLOR_OVERRIDES[/** @type {keyof typeof DEFAULT_COLOR_OVERRIDES} */ (part)];
return getColor(DEFAULT_OVERRIDES[part]); if (override) {
return getColor(override);
} }
for (const [color, partName] of Object.entries(COLOR_MAP)) { for (const [color, partName] of Object.entries(COLOR_MAP)) {
if (partName === part) { if (partName === part) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,10 +1,6 @@
import species from "../species.js" import species from "../species.js"
/** export const PALETTE = Object.freeze(/** @type {const} */ ({
* Palette color names
* @type {Record<string, string>}
*/
export const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -15,23 +11,36 @@ export const PALETTE = {
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow", EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek", CHEEK: "cheek",
SCRUFF: "scruff", SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar", COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
}; }));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, string>} * @type {Record<string, PaletteColor>}
*/ */
export const SPRITE_SHEET_COLOR_MAP = { export const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -44,13 +53,24 @@ export const SPRITE_SHEET_COLOR_MAP = {
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW, "#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK, "#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF, "#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR, "#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -58,16 +78,52 @@ export const SPRITE_SHEET_COLOR_MAP = {
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
export const DEFAULT_COLOR_OVERRIDES = {
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
};
export const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
export class BirdType { export class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, colors, tags = []) { constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -79,15 +135,27 @@ export class BirdType {
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face, [PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face, [PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face, [PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face, [PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
@@ -151,6 +219,6 @@ export function loadSpriteSheetPixels(src, templateColors = true) {
export const SPECIES = Object.fromEntries( export const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [ Object.entries(species).map(([id, data]) => [
id, id,
new BirdType(data.name, data.description, data.colors, data.tags ?? []), new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]), ]),
); );

View File

@@ -24,8 +24,8 @@ import {
} from './shared.js'; } from './shared.js';
import { import {
PALETTE, PALETTE,
SPRITE_SHEET_COLOR_MAP,
SPECIES, SPECIES,
RARITY,
loadSpriteSheetPixels, loadSpriteSheetPixels,
} from './animation/sprites.js'; } from './animation/sprites.js';
import { import {
@@ -110,6 +110,7 @@ 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 UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
@@ -406,7 +407,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); flyToElement(true);
// TODO: Remove
insertFieldGuide();
} }
function update() { function update() {
@@ -425,11 +428,11 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
focusOnElement(); flyToElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -465,7 +468,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flySomewhere(); flyToElement();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -481,7 +484,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flySomewhere(); flyToElement();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -562,7 +565,8 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -758,9 +762,23 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content"); const familiarBirds = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content); contentContainer.appendChild(familiarLabel);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -776,14 +794,26 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em"; const spacerOne = document.createElement("div");
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacer); fragment.appendChild(spacerOne);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -805,7 +835,11 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement); let section = familiarBirds;
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -987,26 +1021,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -1044,20 +1058,20 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
/** /**
* Focus on an element within the viewport * Fly to an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found * @returns Whether an element to fly to was found (null if flying to the ground)
*/ */
function focusOnElement(teleport = false) { function flyToElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else { } else if (focusedElement !== previousElement) {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;

View File

@@ -8,36 +8,41 @@ export class Birdsong {
audioContext; audioContext;
chirp() { chirp() {
if (!this.audioContext) { const count = Math.floor(1 + Math.random() * 1.5);
this.audioContext = new AudioContext(); for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count,
2100 + Math.random() * 200 * count,
1600 + Math.random() * 400 * count];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
} }
const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200,
3500 + Math.random() * 600,
2100 + Math.random() * 200,
1600 + Math.random() * 400];
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine";
const gain = this.audioContext.createGain();
oscillator.connect(gain);
gain.connect(this.audioContext.destination);
const now = this.audioContext.currentTime;
for (let i = 0; i < TIMES.length; i++) {
const time = TIMES[i] + now;
if (i === 0) {
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
gain.gain.setValueAtTime(VOLUMES[i], time);
} else {
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
}
}
oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]);
} }
} }

View File

@@ -9,6 +9,8 @@ export default {
"bluebird": { "bluebird": {
"name": "Eastern Bluebird", "name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.", "description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#639bff", "face": "#639bff",
@@ -21,6 +23,8 @@ export default {
"shimaEnaga": { "shimaEnaga": {
"name": "Shima Enaga", "name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", "description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffffff", "face": "#ffffff",
@@ -34,6 +38,8 @@ export default {
"tuftedTitmouse": { "tuftedTitmouse": {
"name": "Tufted Titmouse", "name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", "description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#c7cad7", "face": "#c7cad7",
@@ -50,6 +56,8 @@ export default {
"europeanRobin": { "europeanRobin": {
"name": "European Robin", "name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", "description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#ffaf34", "face": "#ffaf34",
@@ -64,6 +72,8 @@ export default {
"redCardinal": { "redCardinal": {
"name": "Red Cardinal", "name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.", "description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": { "colors": {
"beak": "#d93619", "beak": "#d93619",
"foot": "#af8e75", "foot": "#af8e75",
@@ -83,6 +93,8 @@ export default {
"americanGoldfinch": { "americanGoldfinch": {
"name": "American Goldfinch", "name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", "description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": { "colors": {
"beak": "#ffaf34", "beak": "#ffaf34",
"foot": "#af8e75", "foot": "#af8e75",
@@ -99,6 +111,8 @@ export default {
"barnSwallow": { "barnSwallow": {
"name": "Barn Swallow", "name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", "description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#db7c4d", "face": "#db7c4d",
@@ -112,6 +126,8 @@ export default {
"mistletoebird": { "mistletoebird": {
"name": "Mistletoebird", "name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", "description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": { "colors": {
"foot": "#6c6a7c", "foot": "#6c6a7c",
"face": "#352e6d", "face": "#352e6d",
@@ -121,22 +137,11 @@ export default {
"wing-edge": "#282065" "wing-edge": "#282065"
} }
}, },
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f"
}
},
"scarletRobin": { "scarletRobin": {
"name": "Scarlet Robin", "name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.", "description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": { "colors": {
"foot": "#494949", "foot": "#494949",
"face": "#3d3d3d", "face": "#3d3d3d",
@@ -144,12 +149,15 @@ export default {
"underbelly": "#dcdcdc", "underbelly": "#dcdcdc",
"wing": "#2b2b2b", "wing": "#2b2b2b",
"wing-edge": "#ebebeb", "wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633" "theme-highlight": "#fc5633"
} }
}, },
"americanRobin": { "americanRobin": {
"name": "American Robin", "name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", "description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": { "colors": {
"beak": "#e89f30", "beak": "#e89f30",
"foot": "#9f8075", "foot": "#9f8075",
@@ -164,6 +172,8 @@ export default {
"carolinaWren": { "carolinaWren": {
"name": "Carolina Wren", "name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.", "description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": { "colors": {
"foot": "#af8e75", "foot": "#af8e75",
"face": "#edc7a9", "face": "#edc7a9",
@@ -174,5 +184,244 @@ export default {
"wing": "#c58a5b", "wing": "#c58a5b",
"wing-edge": "#866348" "wing-edge": "#866348"
} }
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5fdf44",
"chin": "#f12d3e",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#7029b8",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#aa60e6"
},
"rarity": "uncommon"
} }
} }

View File

@@ -252,7 +252,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-rows: repeat(3, auto); grid-template-columns: repeat(4, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -262,7 +262,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: row;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -294,7 +294,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid rgb(255, 207, 144); border: var(--birb-border-size) solid #ffcf90;
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -313,6 +313,15 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -324,7 +333,14 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: #7c6c4b;
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -337,7 +353,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: rgb(124, 108, 75); color: #7c6c4b;
} }
.birb-sticky-note { .birb-sticky-note {