Refactor input handling
This commit is contained in:
parent
0977914f20
commit
01c601424f
8
bun.lock
8
bun.lock
|
|
@ -111,7 +111,7 @@
|
|||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
|
||||
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
|
||||
|
||||
"@types/clean-css": ["@types/clean-css@4.2.11", "", { "dependencies": { "@types/node": "*", "source-map": "^0.6.0" } }, "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw=="],
|
||||
|
||||
|
|
@ -123,6 +123,8 @@
|
|||
|
||||
"@types/node": ["@types/node@20.14.10", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/relateurl": ["@types/relateurl@0.2.33", "", {}, "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw=="],
|
||||
|
||||
"@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
|
||||
|
|
@ -145,7 +147,7 @@
|
|||
|
||||
"browser-detect": ["browser-detect@0.2.28", "", { "dependencies": { "core-js": "^2.5.7" } }, "sha512-KeWGHqYQmHDkCFG2dIiX/2wFUgqevbw/rd6wNi9N6rZbaSJFtG5kel0HtprRwCGp8sqpQP79LzDJXf/WCx4WAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
|
||||
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
|
||||
|
||||
"camel-case": ["camel-case@3.0.0", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.1.1" } }, "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w=="],
|
||||
|
||||
|
|
@ -173,6 +175,8 @@
|
|||
|
||||
"cross-spawn": ["cross-spawn@7.0.5", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"delay": ["delay@6.0.0", "", {}, "sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import Input from "./input";
|
||||
import { nextFrame } from "./utils";
|
||||
|
||||
type Setup<T extends Record<string, unknown> | void> = () => Promise<T> | T;
|
||||
type Frame<T extends Record<string, unknown> | void> = (dt: number, state: T) => Promise<void> | void;
|
||||
type GameMain = () => void;
|
||||
|
||||
export function gameLoop<T extends Record<string, unknown> | void>(frame: Frame<T>): GameMain;
|
||||
export function gameLoop<T extends Record<string, unknown> | void>(setup: Setup<T>, frame: Frame<T>): GameMain;
|
||||
export function gameLoop<T extends Record<string, unknown> | void>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): GameMain {
|
||||
return async () => {
|
||||
let state: T;
|
||||
if (frame) {
|
||||
state = await (setupOrFrame as Setup<T>)();
|
||||
} else {
|
||||
frame = setupOrFrame as Frame<T>;
|
||||
state = {} as T;
|
||||
}
|
||||
|
||||
let prevFrame = performance.now();
|
||||
while (true) {
|
||||
await nextFrame();
|
||||
Input.updateKeys();
|
||||
|
||||
const now = performance.now();
|
||||
const dt = (now - prevFrame) / 1000;
|
||||
|
||||
await frame(dt, state);
|
||||
|
||||
prevFrame = performance.now();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -6,45 +6,109 @@ interface IKeyState {
|
|||
pressed?: boolean;
|
||||
held?: boolean;
|
||||
}
|
||||
type KeyCode = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Space';
|
||||
namespace Input {
|
||||
export enum KeyCode {
|
||||
UP = 'ArrowUp',
|
||||
DOWN = 'ArrowDown',
|
||||
LEFT = 'ArrowLeft',
|
||||
RIGHT = 'ArrowRight',
|
||||
SPACE = 'Space',
|
||||
SHIFT = 'Shift',
|
||||
SHIFT_LEFT = 'ShiftLeft',
|
||||
SHIFT_RIGHT = 'ShiftRight',
|
||||
};
|
||||
|
||||
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
||||
|
||||
document.body.addEventListener('keydown', (e) => {
|
||||
const keyId = e.code as KeyCode;
|
||||
console.debug(`[Input] Pressed ${keyId}`);
|
||||
if (KEYS[keyId]) {
|
||||
KEYS[keyId].state = true;
|
||||
} else {
|
||||
KEYS[keyId] = { state: true };
|
||||
export enum GamepadAxis {
|
||||
LX = 0,
|
||||
LY = 1,
|
||||
RX = 2,
|
||||
RY = 3,
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('keyup', (e) => {
|
||||
const keyId = e.code as KeyCode;
|
||||
console.debug(`[Input] Released ${keyId}`);
|
||||
if (KEYS[keyId]) {
|
||||
KEYS[keyId].state = false;
|
||||
export enum GamepadButton {
|
||||
A = 0,
|
||||
B = 1,
|
||||
X = 2,
|
||||
Y = 3,
|
||||
LB = 4,
|
||||
RB = 5,
|
||||
LT = 6,
|
||||
RT = 7,
|
||||
SELECT = 8,
|
||||
START = 9,
|
||||
L3 = 10,
|
||||
R3 = 11,
|
||||
D_UP = 12,
|
||||
D_DOWN = 13,
|
||||
D_LEFT = 14,
|
||||
D_RIGHT = 15,
|
||||
HOME = 16,
|
||||
}
|
||||
});
|
||||
|
||||
export const isPressed = (key: KeyCode): boolean => KEYS[key]?.pressed ?? false;
|
||||
export const isReleased = (key: KeyCode): boolean => KEYS[key]?.released ?? false;
|
||||
export const isHeld = (key: KeyCode): boolean => KEYS[key]?.held ?? false;
|
||||
const DEAD_ZONE = 0.05;
|
||||
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
||||
|
||||
export function updateKeys() {
|
||||
for (const key of Object.values(KEYS)) {
|
||||
key.released = false;
|
||||
key.pressed = false;
|
||||
|
||||
if (key.state) {
|
||||
key.pressed = !key.held;
|
||||
key.held = true;
|
||||
const onStateChange = (keyId: KeyCode, state: boolean) => {
|
||||
console.debug(`[Input] Pressed ${keyId}`);
|
||||
if (KEYS[keyId]) {
|
||||
KEYS[keyId].state = state;
|
||||
} else {
|
||||
key.released = true;
|
||||
key.held = false;
|
||||
KEYS[keyId] = { state };
|
||||
}
|
||||
|
||||
key.prevState = key.state;
|
||||
if (keyId === KeyCode.SHIFT_LEFT || keyId === KeyCode.SHIFT_RIGHT) {
|
||||
const shiftState = Boolean(KEYS[KeyCode.SHIFT_LEFT]?.state || KEYS[KeyCode.SHIFT_RIGHT]?.state);
|
||||
onStateChange(KeyCode.SHIFT, shiftState);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('keydown', (e) => onStateChange(e.code as KeyCode, true));
|
||||
document.body.addEventListener('keyup', (e) => onStateChange(e.code as KeyCode, false));
|
||||
|
||||
export const isPressed = (key: KeyCode): boolean => KEYS[key]?.pressed ?? false;
|
||||
export const isReleased = (key: KeyCode): boolean => KEYS[key]?.released ?? false;
|
||||
export const isHeld = (key: KeyCode): boolean => KEYS[key]?.held ?? false;
|
||||
|
||||
export const getGamepad = () => navigator.getGamepads().find(g => g != null);
|
||||
|
||||
export const getGamepadAxis = (i: GamepadAxis) => {
|
||||
const value = getGamepad()?.axes[i] ?? 0;
|
||||
return Math.abs(value) < DEAD_ZONE ? 0 : value;
|
||||
}
|
||||
}
|
||||
export const getGamepadButton = (btn: GamepadButton) => getGamepad()?.buttons[btn]?.value ?? 0;
|
||||
|
||||
export const getLeft = () => isHeld(KeyCode.LEFT) ? 1 : Math.max(0, -getGamepadAxis(GamepadAxis.LX));
|
||||
export const getRight = () => isHeld(KeyCode.RIGHT) ? 1 : Math.max(0, getGamepadAxis(GamepadAxis.LX));
|
||||
|
||||
export const getUp = () => isHeld(KeyCode.UP) ? 1 : Math.max(0, -getGamepadAxis(GamepadAxis.LY));
|
||||
export const getDown = () => isHeld(KeyCode.DOWN) ? 1 : Math.max(0, getGamepadAxis(GamepadAxis.LY));
|
||||
|
||||
export const getHorizontal = () => (
|
||||
(Number(isHeld(KeyCode.LEFT)) - Number(isHeld(KeyCode.RIGHT)))
|
||||
+ getGamepadAxis(GamepadAxis.LX)
|
||||
);
|
||||
|
||||
export const getVertical = () => (
|
||||
(Number(isHeld(KeyCode.UP)) - Number(isHeld(KeyCode.DOWN)))
|
||||
+ getGamepadAxis(GamepadAxis.LY)
|
||||
);
|
||||
|
||||
export function updateKeys() {
|
||||
for (const key of Object.values(KEYS)) {
|
||||
key.released = false;
|
||||
key.pressed = false;
|
||||
|
||||
if (key.state) {
|
||||
key.pressed = !key.held;
|
||||
key.held = true;
|
||||
} else {
|
||||
key.released = true;
|
||||
key.held = false;
|
||||
}
|
||||
|
||||
key.prevState = key.state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { BrickDisplay } from "@common/display/brick";
|
||||
import { isPressed, updateKeys } from "@common/input";
|
||||
import Input from "@common/input";
|
||||
|
||||
import spritesheetImage from './assets/spritesheet.png';
|
||||
import { ITEMS, Items, MONSTERS, MONSTERS_ORDER, loadData, type Item, type Monster } from "./data";
|
||||
import { randBool, weightedChoice } from "@common/utils";
|
||||
import { gameLoop } from "@common/game";
|
||||
|
||||
let display: BrickDisplay;
|
||||
const spritesheet = BrickDisplay.convertImage(spritesheetImage);
|
||||
|
|
@ -47,9 +48,9 @@ let secondLootItem: Items | null = null;
|
|||
|
||||
let frames = 0;
|
||||
let prevFrameTime: number = 0;
|
||||
async function loop(time: number) {
|
||||
|
||||
async function frame(time: number) {
|
||||
frames++;
|
||||
const dt = time - prevFrameTime;
|
||||
prevFrameTime = time;
|
||||
|
||||
if (frames % 8 == 0) {
|
||||
|
|
@ -105,17 +106,17 @@ async function loop(time: number) {
|
|||
} else if (y === secondLootY) {
|
||||
lootToConfirm = secondLootItem;
|
||||
playerTurn = false;
|
||||
} else if (isPressed('ArrowLeft')) {
|
||||
} else if (Input.isPressed(Input.KeyCode.LEFT)) {
|
||||
selectedSlot = (selectedSlot + inventory.length - 1) % inventory.length;
|
||||
} else if (isPressed('ArrowRight')) {
|
||||
} else if (Input.isPressed(Input.KeyCode.RIGHT)) {
|
||||
selectedSlot = (selectedSlot + 1) % inventory.length;
|
||||
} else if (isPressed('ArrowUp') && (!monsterAlive || y < monsterY - 5)) {
|
||||
} else if (Input.isPressed(Input.KeyCode.UP) && (!monsterAlive || y < monsterY - 5)) {
|
||||
targetY = y + 4;
|
||||
playerTurn = false;
|
||||
} else if (isPressed('ArrowDown')) {
|
||||
} else if (Input.isPressed(Input.KeyCode.DOWN)) {
|
||||
targetY = y - 4;
|
||||
playerTurn = false;
|
||||
} else if (isPressed('Space') && (monsterAlive && (y >= monsterY - 5 || item.ranged || item.heal))) {
|
||||
} else if (Input.isPressed(Input.KeyCode.SPACE) && (monsterAlive && (y >= monsterY - 5 || item.ranged || item.heal))) {
|
||||
if (item.consumable) {
|
||||
inventory.splice(selectedSlot, 1);
|
||||
selectedSlot = (selectedSlot + inventory.length - 1) % inventory.length;
|
||||
|
|
@ -138,7 +139,7 @@ async function loop(time: number) {
|
|||
} else if (lootToConfirm) {
|
||||
console.log('Loot confirm');
|
||||
|
||||
if (isPressed('Space')) {
|
||||
if (Input.isPressed(Input.KeyCode.SPACE)) {
|
||||
const i = ITEMS[lootToConfirm];
|
||||
if (i.instantUse && i.heal) {
|
||||
playerHealth += i.heal;
|
||||
|
|
@ -150,10 +151,10 @@ async function loop(time: number) {
|
|||
}
|
||||
|
||||
lootConfirmed = true;
|
||||
} else if (isPressed('ArrowUp')) {
|
||||
} else if (Input.isPressed(Input.KeyCode.UP)) {
|
||||
targetY = y + 4;
|
||||
playerTurn = true;
|
||||
} else if (isPressed('ArrowDown')) {
|
||||
} else if (Input.isPressed(Input.KeyCode.DOWN)) {
|
||||
targetY = y - 4;
|
||||
playerTurn = true;
|
||||
}
|
||||
|
|
@ -279,9 +280,6 @@ async function loop(time: number) {
|
|||
}
|
||||
|
||||
display.update();
|
||||
|
||||
updateKeys();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function spawnNextMonster() {
|
||||
|
|
@ -321,11 +319,12 @@ function damagePlayer(monster: Monster) {
|
|||
}
|
||||
}
|
||||
|
||||
export default function main() {
|
||||
const setup = () => {
|
||||
display = new BrickDisplay();
|
||||
display.init();
|
||||
|
||||
loadData(spritesheet);
|
||||
spawnNextMonster();
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
}
|
||||
|
||||
export default gameLoop(setup, frame);
|
||||
|
|
@ -1,5 +1,34 @@
|
|||
import awoo from "./awoo.cpp";
|
||||
import { gameLoop } from "@common/game";
|
||||
import Input from "@common/input";
|
||||
|
||||
export default function main() {
|
||||
console.log(awoo, awoo.play());
|
||||
}
|
||||
const setup = () => {
|
||||
let x = window.innerWidth / 2 - 16;
|
||||
let y = window.innerHeight / 2 - 16;
|
||||
const ball = document.createElement('div');
|
||||
|
||||
ball.style.display = 'block';
|
||||
ball.style.width = '32px';
|
||||
ball.style.height = '32px';
|
||||
ball.style.borderRadius = '50%';
|
||||
ball.style.backgroundColor = 'red';
|
||||
ball.style.position = 'absolute';
|
||||
|
||||
document.body.append(ball);
|
||||
|
||||
const speed = Math.min(window.innerHeight, window.innerWidth);
|
||||
|
||||
return { x, y, speed, ball };
|
||||
}
|
||||
|
||||
const frame = (dt: number, state: ReturnType<typeof setup>) => {
|
||||
const dx = Input.getHorizontal();
|
||||
const dy = Input.getVertical();
|
||||
|
||||
state.x += state.speed * dx * dt;
|
||||
state.y += state.speed * dy * dt;
|
||||
|
||||
state.ball.style.left = `${state.x}px`;
|
||||
state.ball.style.top = `${state.y}px`;
|
||||
}
|
||||
|
||||
export default gameLoop(setup, frame);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Color, Direction, getOppositeDirection } from './const';
|
||||
import { drawTextInBox, tick } from './display';
|
||||
import { isHeld, isPressed, updateKeys } from '@common/input';
|
||||
import Input from '@common/input';
|
||||
import { createItems, getItemsCount } from './item';
|
||||
import { GameMap } from './map';
|
||||
import { Player } from './player';
|
||||
|
|
@ -13,30 +13,30 @@ const player = new Player(currentRoom.x + currentRoom.width / 2, currentRoom.y +
|
|||
|
||||
let lastMove = Date.now();
|
||||
function handleInput() {
|
||||
const isSpacePressed = isPressed(' ');
|
||||
const isSpacePressed = Input.isPressed(Input.KeyCode.SPACE);
|
||||
|
||||
if (Date.now() - lastMove < 75 && !isHeld('shift') && !isSpacePressed) return;
|
||||
if (Date.now() - lastMove < 75 && !Input.isHeld(Input.KeyCode.SHIFT) && !isSpacePressed) return;
|
||||
lastMove = Date.now();
|
||||
let newX = player.x;
|
||||
let newY = player.y;
|
||||
let moved = isSpacePressed;
|
||||
|
||||
if (isHeld('arrowup')) {
|
||||
if (Input.isHeld(Input.KeyCode.UP)) {
|
||||
newY--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowdown')) {
|
||||
if (Input.isHeld(Input.KeyCode.DOWN)) {
|
||||
newY++;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowleft')) {
|
||||
if (Input.isHeld(Input.KeyCode.LEFT)) {
|
||||
newX--;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (isHeld('arrowright')) {
|
||||
if (Input.isHeld(Input.KeyCode.RIGHT)) {
|
||||
newX++;
|
||||
moved = true;
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@ export async function main() {
|
|||
drawTextInBox(8, 11, 'Use arrow keys to move\nSHIFT to run\nSPACE to activate\nAWOO!', { fg: Color.CYAN });
|
||||
|
||||
while (true) {
|
||||
updateKeys();
|
||||
Input.updateKeys();
|
||||
handleInput();
|
||||
handleLogic();
|
||||
draw();
|
||||
|
|
|
|||
|
|
@ -84,7 +84,10 @@ export default class Player extends Character {
|
|||
}
|
||||
|
||||
public spendItem(item: Item | null | undefined): boolean {
|
||||
return Boolean(item?.isSpendingWeapon) && this.removeItem(item);
|
||||
if (!item) return false;
|
||||
if (item.isSpendingWeapon) return this.removeItem(item);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public removeItem(item: Item | null | undefined): boolean {
|
||||
|
|
|
|||
Loading…
Reference in New Issue