mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-26 04:07:24 +00:00
Make menu extensible and add PICO-8 games
This commit is contained in:
245
birb.js
245
birb.js
@@ -37,7 +37,8 @@ const HOP_DISTANCE = settings.hopDistance;
|
|||||||
// Time in milliseconds until the user is considered AFK
|
// Time in milliseconds until the user is considered AFK
|
||||||
const AFK_TIME = 1000 * 30;
|
const AFK_TIME = 1000 * 30;
|
||||||
const SPRITE_HEIGHT = 32;
|
const SPRITE_HEIGHT = 32;
|
||||||
const START_MENU_ID = "birb-start-menu";
|
const MENU_ID = "birb-menu";
|
||||||
|
const MENU_EXIT_ID = "birb-menu-exit";
|
||||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||||
const FEATHER_ID = "birb-feather";
|
const FEATHER_ID = "birb-feather";
|
||||||
|
|
||||||
@@ -96,6 +97,20 @@ const styles = `
|
|||||||
transition-timing-function: ease-in;
|
transition-timing-function: ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#${MENU_ID} {
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#${MENU_EXIT_ID} {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999999997;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pop-in {
|
@keyframes pop-in {
|
||||||
0% { opacity: 1; transform: scale(0.1); }
|
0% { opacity: 1; transform: scale(0.1); }
|
||||||
100% { opacity: 1; transform: scale(1); }
|
100% { opacity: 1; transform: scale(1); }
|
||||||
@@ -192,6 +207,8 @@ const styles = `
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-window-list-item:hover {
|
.birb-window-list-item:hover {
|
||||||
@@ -199,14 +216,18 @@ const styles = `
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.birb-window-list-item-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-window-separator {
|
.birb-window-separator {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1.5px;
|
height: 1.5px;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
opacity: 0.45;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
#${FIELD_GUIDE_ID} {
|
#${FIELD_GUIDE_ID} {
|
||||||
@@ -332,14 +353,6 @@ class Frame {
|
|||||||
}
|
}
|
||||||
this.#pixelsByTag[tag] = this.pixels.map(row => row.slice());
|
this.#pixelsByTag[tag] = this.pixels.map(row => row.slice());
|
||||||
}
|
}
|
||||||
// Surround non-transparent pixels with border
|
|
||||||
// for (let y = 0; y < this.pixels.length; y++) {
|
|
||||||
// for (let x = 0; x < this.pixels[y].length; x++) {
|
|
||||||
// if (this.pixels[y][x] === TRANSPARENT && this.hasAdjacent(x, y)) {
|
|
||||||
// this.pixels[y][x] = BORDER;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,20 +363,6 @@ class Frame {
|
|||||||
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
|
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasAdjacent(x, y) {
|
|
||||||
// for (let i = -1; i <= 1; i++) {
|
|
||||||
// for (let j = -1; j <= 1; j++) {
|
|
||||||
// if (i === 0 && j === 0) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// if (this.#pixels[y + i] && this.#pixels[y + i][x + j] && this.#pixels[y + i][x + j] !== TRANSPARENT && this.#pixels[y + i][x + j] !== BORDER) {
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
* @param {number} direction
|
* @param {number} direction
|
||||||
@@ -691,6 +690,47 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MenuItem {
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {() => void} action
|
||||||
|
* @param {boolean} [removeMenu]
|
||||||
|
*/
|
||||||
|
constructor(text, action, removeMenu = true) {
|
||||||
|
this.text = text;
|
||||||
|
this.action = action;
|
||||||
|
this.removeMenu = removeMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Separator extends MenuItem {
|
||||||
|
constructor() {
|
||||||
|
super("", () => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
new MenuItem("Pet Birb", pet),
|
||||||
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
|
// new MenuItem("Decorations", insertDecoration),
|
||||||
|
new MenuItem("Programs", () => switchMenuItems(programItems), false),
|
||||||
|
new Separator(),
|
||||||
|
new MenuItem("Settings", () => {}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const programItems = [
|
||||||
|
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
||||||
|
new Separator(),
|
||||||
|
new MenuItem("Pico Dino", () => insertPico8("Pico Dino", "picodino")),
|
||||||
|
new MenuItem("Tetraminis", () => insertPico8("Tetraminis", "tetraminisdeffect")),
|
||||||
|
new MenuItem("Woodworm", () => insertPico8("Woodworm", "woodworm")),
|
||||||
|
new MenuItem("Wobblepaint ", () => insertPico8("Wobblepaint", "wobblepaint")),
|
||||||
|
new MenuItem("Terra Nova Pinball", () => insertPico8("Terra Nova Pinball", "terra_nova_pinball")),
|
||||||
|
new MenuItem("Pico and Chill", () => insertPico8("Pico and Chill", "picochill")),
|
||||||
|
new MenuItem("Celeste 2", () => insertPico8("Celeste 2", "celeste_classic_2")),
|
||||||
|
new MenuItem("Pool", () => insertPico8("Pool", "mot_pool")),
|
||||||
|
];
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
|
|
||||||
@@ -751,13 +791,13 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
timeOfLastAction = Date.now();
|
timeOfLastAction = Date.now();
|
||||||
if (e.target instanceof Node && !canvas.contains(e.target) && !document.querySelector(".birb-window")?.contains(e.target)) {
|
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
||||||
removeStartMenu();
|
removeMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("click", () => {
|
canvas.addEventListener("click", () => {
|
||||||
insertStartMenu();
|
insertMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
canvas.addEventListener("mouseover", () => {
|
||||||
@@ -782,7 +822,7 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
function update() {
|
function update() {
|
||||||
ticks++;
|
ticks++;
|
||||||
if (currentState === States.IDLE) {
|
if (currentState === States.IDLE) {
|
||||||
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isStartMenuOpen()) {
|
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isMenuOpen()) {
|
||||||
hop();
|
hop();
|
||||||
}
|
}
|
||||||
} else if (currentState === States.HOP) {
|
} else if (currentState === States.HOP) {
|
||||||
@@ -812,7 +852,7 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (focusedElement === null) {
|
if (focusedElement === null) {
|
||||||
if (Date.now() - timeOfLastAction > AFK_TIME && !isStartMenuOpen()) {
|
if (Date.now() - timeOfLastAction > AFK_TIME && !isMenuOpen()) {
|
||||||
// Fly to an element if the user is AFK
|
// Fly to an element if the user is AFK
|
||||||
focusOnElement();
|
focusOnElement();
|
||||||
timeOfLastAction = Date.now();
|
timeOfLastAction = Date.now();
|
||||||
@@ -1053,6 +1093,19 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} closeButton
|
||||||
|
* @param {() => void} func
|
||||||
|
*/
|
||||||
|
function makeClosable(closeButton, func) {
|
||||||
|
closeButton.addEventListener("click", func);
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function removeFieldGuide() {
|
function removeFieldGuide() {
|
||||||
const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID);
|
const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID);
|
||||||
if (fieldGuide) {
|
if (fieldGuide) {
|
||||||
@@ -1060,20 +1113,24 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insertPico8();
|
// insertPico8();
|
||||||
|
|
||||||
function isSafari() {
|
function isSafari() {
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertPico8() {
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} pid
|
||||||
|
*/
|
||||||
|
function insertPico8(name, pid) {
|
||||||
let html = `
|
let html = `
|
||||||
<div class="birb-window-header">
|
<div class="birb-window-header">
|
||||||
<div class="birb-window-title">PICO-8: Woodworm</div>
|
<div class="birb-window-title">PICO-8: ${name}</div>
|
||||||
<div class="birb-window-close">x</div>
|
<div class="birb-window-close">x</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="birb-window-content birb-pico-8-content">
|
<div class="birb-window-content birb-pico-8-content">
|
||||||
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=woodworm" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
|
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=${pid}" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
|
||||||
</div>`
|
</div>`
|
||||||
const pico8 = makeElement("birb-window");
|
const pico8 = makeElement("birb-window");
|
||||||
pico8.innerHTML = html;
|
pico8.innerHTML = html;
|
||||||
@@ -1097,42 +1154,38 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the start menu to the page if it doesn't already exist
|
* Add the menu to the page if it doesn't already exist
|
||||||
*/
|
*/
|
||||||
function insertStartMenu() {
|
function insertMenu() {
|
||||||
if (document.querySelector("#" + START_MENU_ID)) {
|
if (document.querySelector("#" + MENU_ID)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let startMenu = makeElement("birb-window", undefined, START_MENU_ID);
|
let menu = makeElement("birb-window", undefined, MENU_ID);
|
||||||
let header = makeElement("birb-window-header");
|
let header = makeElement("birb-window-header");
|
||||||
header.innerHTML = '<div class="birb-window-title">birbOS</div>';
|
header.innerHTML = '<div class="birb-window-title">birbOS</div>';
|
||||||
let content = makeElement("birb-window-content");
|
let content = makeElement("birb-window-content");
|
||||||
let petButton = makeElement("birb-window-list-item", "Pet Birb");
|
for (const item of menuItems) {
|
||||||
petButton.addEventListener("click", () => {
|
content.appendChild(makeMenuItem(item));
|
||||||
removeStartMenu();
|
}
|
||||||
pet();
|
menu.appendChild(header);
|
||||||
});
|
menu.appendChild(content);
|
||||||
content.appendChild(petButton);
|
document.body.appendChild(menu);
|
||||||
let fieldGuideButton = makeElement("birb-window-list-item", "Field Guide");
|
|
||||||
fieldGuideButton.addEventListener("click", () => {
|
|
||||||
removeStartMenu();
|
|
||||||
insertFieldGuide();
|
|
||||||
});
|
|
||||||
content.appendChild(fieldGuideButton);
|
|
||||||
let decorationsButton = makeElement("birb-window-list-item", "Decorations");
|
|
||||||
decorationsButton.addEventListener("click", () => {
|
|
||||||
removeStartMenu();
|
|
||||||
insertDecoration();
|
|
||||||
});
|
|
||||||
content.appendChild(decorationsButton);
|
|
||||||
content.appendChild(makeElement("birb-window-list-item", "Programs"));
|
|
||||||
content.appendChild(makeElement("birb-window-separator"));
|
|
||||||
content.appendChild(makeElement("birb-window-list-item", "Settings"));
|
|
||||||
startMenu.appendChild(header);
|
|
||||||
startMenu.appendChild(content);
|
|
||||||
document.body.appendChild(startMenu);
|
|
||||||
makeDraggable(document.querySelector(".birb-window-header"));
|
makeDraggable(document.querySelector(".birb-window-header"));
|
||||||
|
|
||||||
|
let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
|
||||||
|
menuExit.addEventListener("click", () => {
|
||||||
|
removeMenu();
|
||||||
|
});
|
||||||
|
document.body.appendChild(menuExit);
|
||||||
|
|
||||||
|
updateMenuLocation(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the menu's location based on the bird's position
|
||||||
|
* @param {HTMLElement} menu
|
||||||
|
*/
|
||||||
|
function updateMenuLocation(menu) {
|
||||||
let x = birdX;
|
let x = birdX;
|
||||||
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
|
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
|
||||||
const offset = 20;
|
const offset = 20;
|
||||||
@@ -1141,34 +1194,76 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
|
|||||||
x += offset;
|
x += offset;
|
||||||
} else {
|
} else {
|
||||||
// Right side
|
// Right side
|
||||||
x -= startMenu.offsetWidth + offset;
|
x -= menu.offsetWidth + offset;
|
||||||
}
|
}
|
||||||
if (y > window.innerHeight / 2) {
|
if (y > window.innerHeight / 2) {
|
||||||
// Top side
|
// Top side
|
||||||
y -= startMenu.offsetHeight + offset + 10;
|
y -= menu.offsetHeight + offset + 10;
|
||||||
} else {
|
} else {
|
||||||
// Bottom side
|
// Bottom side
|
||||||
y += offset;
|
y += offset;
|
||||||
}
|
}
|
||||||
startMenu.style.left = `${x}px`;
|
menu.style.left = `${x}px`;
|
||||||
startMenu.style.top = `${y}px`;
|
menu.style.top = `${y}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the start menu from the page
|
* @param {MenuItem[]} menuItems
|
||||||
*/
|
*/
|
||||||
function removeStartMenu() {
|
function switchMenuItems(menuItems) {
|
||||||
const startMenu = document.querySelector("#" + START_MENU_ID);
|
const menu = document.querySelector("#" + MENU_ID);
|
||||||
if (startMenu) {
|
if (!menu || !(menu instanceof HTMLElement)) {
|
||||||
startMenu.remove();
|
return;
|
||||||
|
}
|
||||||
|
const content = menu.querySelector(".birb-window-content");
|
||||||
|
if (!content) {
|
||||||
|
console.error("Content not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.innerHTML = "";
|
||||||
|
for (const item of menuItems) {
|
||||||
|
content.appendChild(makeMenuItem(item));
|
||||||
|
}
|
||||||
|
updateMenuLocation(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MenuItem} item
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
function makeMenuItem(item) {
|
||||||
|
if (item instanceof Separator) {
|
||||||
|
return makeElement("birb-window-separator");
|
||||||
|
}
|
||||||
|
let menuItem = makeElement("birb-window-list-item", item.text);
|
||||||
|
menuItem.addEventListener("click", () => {
|
||||||
|
if (item.removeMenu) {
|
||||||
|
removeMenu();
|
||||||
|
}
|
||||||
|
item.action();
|
||||||
|
});
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the menu from the page
|
||||||
|
*/
|
||||||
|
function removeMenu() {
|
||||||
|
const menu = document.querySelector("#" + MENU_ID);
|
||||||
|
if (menu) {
|
||||||
|
menu.remove();
|
||||||
|
}
|
||||||
|
const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
|
||||||
|
if (exitMenu) {
|
||||||
|
exitMenu.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether the start menu element is on the page
|
* @returns {boolean} Whether the menu element is on the page
|
||||||
*/
|
*/
|
||||||
function isStartMenuOpen() {
|
function isMenuOpen() {
|
||||||
return document.querySelector("#" + START_MENU_ID) !== null;
|
return document.querySelector("#" + MENU_ID) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user