1
0
Fork 0

Refactor input handling

This commit is contained in:
Pabloader 2025-07-05 09:02:40 +00:00
parent 0977914f20
commit 01c601424f
7 changed files with 196 additions and 64 deletions

View File

@ -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=="],

33
src/common/game.ts Normal file
View File

@ -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();
}
}
};

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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 {