Repeated inputs
This commit is contained in:
parent
2ace096907
commit
d2003bc639
|
|
@ -484,8 +484,8 @@ export class TextRegion {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
chars: Char[][] | string | string[],
|
chars: Char[][] | string | string[],
|
||||||
fg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_BG,
|
fg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_FG,
|
||||||
bg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_FG,
|
bg: ColorLike | ColorLike[] | ColorLike[][] = DEFAULT_BG,
|
||||||
) {
|
) {
|
||||||
if (typeof chars === 'string') {
|
if (typeof chars === 'string') {
|
||||||
chars = chars.split('\n');
|
chars = chars.split('\n');
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { nextFrame } from "./utils";
|
||||||
|
|
||||||
interface FrameMeta {
|
interface FrameMeta {
|
||||||
fps: number;
|
fps: number;
|
||||||
|
now: number;
|
||||||
}
|
}
|
||||||
type Awaitable<T> = PromiseLike<T> | T;
|
type Awaitable<T> = PromiseLike<T> | T;
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
|
||||||
let prevFrame = performance.now();
|
let prevFrame = performance.now();
|
||||||
let fpsCounter = 0;
|
let fpsCounter = 0;
|
||||||
let fpsTimer = 0;
|
let fpsTimer = 0;
|
||||||
const meta: FrameMeta = { fps: 0 };
|
const meta: FrameMeta = { fps: 0, now: 0 };
|
||||||
while (true) {
|
while (true) {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
Input.updateKeys();
|
Input.updateKeys();
|
||||||
|
|
@ -41,6 +42,7 @@ export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>)
|
||||||
const dt = (now - prevFrame) / 1000;
|
const dt = (now - prevFrame) / 1000;
|
||||||
|
|
||||||
if (dt < 1) { // skip long pause to avoid blowing up values
|
if (dt < 1) { // skip long pause to avoid blowing up values
|
||||||
|
meta.now += dt;
|
||||||
const newState = await frame(dt, state, meta);
|
const newState = await frame(dt, state, meta);
|
||||||
if (newState) {
|
if (newState) {
|
||||||
state = newState;
|
state = newState;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ interface IKeyState {
|
||||||
|
|
||||||
released?: boolean;
|
released?: boolean;
|
||||||
pressed?: boolean;
|
pressed?: boolean;
|
||||||
|
repeated?: boolean;
|
||||||
held?: boolean;
|
held?: boolean;
|
||||||
|
|
||||||
|
changedAt: number;
|
||||||
|
eventAt: number;
|
||||||
|
repeatAt?: number;
|
||||||
}
|
}
|
||||||
namespace Input {
|
namespace Input {
|
||||||
export enum KeyCode {
|
export enum KeyCode {
|
||||||
|
|
@ -16,6 +21,42 @@ namespace Input {
|
||||||
SHIFT = 'Shift',
|
SHIFT = 'Shift',
|
||||||
SHIFT_LEFT = 'ShiftLeft',
|
SHIFT_LEFT = 'ShiftLeft',
|
||||||
SHIFT_RIGHT = 'ShiftRight',
|
SHIFT_RIGHT = 'ShiftRight',
|
||||||
|
A = 'KeyA',
|
||||||
|
B = 'KeyB',
|
||||||
|
C = 'KeyC',
|
||||||
|
D = 'KeyD',
|
||||||
|
E = 'KeyE',
|
||||||
|
F = 'KeyF',
|
||||||
|
G = 'KeyG',
|
||||||
|
H = 'KeyH',
|
||||||
|
I = 'KeyI',
|
||||||
|
J = 'KeyJ',
|
||||||
|
K = 'KeyK',
|
||||||
|
L = 'KeyL',
|
||||||
|
M = 'KeyM',
|
||||||
|
N = 'KeyN',
|
||||||
|
O = 'KeyO',
|
||||||
|
P = 'KeyP',
|
||||||
|
Q = 'KeyQ',
|
||||||
|
R = 'KeyR',
|
||||||
|
S = 'KeyS',
|
||||||
|
T = 'KeyT',
|
||||||
|
U = 'KeyU',
|
||||||
|
V = 'KeyV',
|
||||||
|
W = 'KeyW',
|
||||||
|
X = 'KeyX',
|
||||||
|
Y = 'KeyY',
|
||||||
|
Z = 'KeyZ',
|
||||||
|
NUM_0 = 'Digit0',
|
||||||
|
NUM_1 = 'Digit1',
|
||||||
|
NUM_2 = 'Digit2',
|
||||||
|
NUM_3 = 'Digit3',
|
||||||
|
NUM_4 = 'Digit4',
|
||||||
|
NUM_5 = 'Digit5',
|
||||||
|
NUM_6 = 'Digit6',
|
||||||
|
NUM_7 = 'Digit7',
|
||||||
|
NUM_8 = 'Digit8',
|
||||||
|
NUM_9 = 'Digit9',
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum GamepadAxis {
|
export enum GamepadAxis {
|
||||||
|
|
@ -47,13 +88,21 @@ namespace Input {
|
||||||
|
|
||||||
const DEAD_ZONE = 0.05;
|
const DEAD_ZONE = 0.05;
|
||||||
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
const KEYS: Partial<Record<KeyCode, IKeyState>> = {};
|
||||||
|
let repeatIntervalMs = 10;
|
||||||
|
let repeatDelayMs = 300;
|
||||||
|
|
||||||
const onStateChange = (keyId: KeyCode, state: boolean) => {
|
const onStateChange = (keyId: KeyCode, state: boolean) => {
|
||||||
console.debug(`[Input] ${state ? 'Pressed' : 'Released'} ${keyId}`);
|
console.debug(`[Input] ${state ? 'Pressed' : 'Released'} ${keyId}`);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
if (KEYS[keyId]) {
|
if (KEYS[keyId]) {
|
||||||
|
if (state !== KEYS[keyId].state) {
|
||||||
|
KEYS[keyId].changedAt = now;
|
||||||
|
}
|
||||||
KEYS[keyId].state = state;
|
KEYS[keyId].state = state;
|
||||||
|
KEYS[keyId].eventAt = now;
|
||||||
} else {
|
} else {
|
||||||
KEYS[keyId] = { state };
|
KEYS[keyId] = { state, changedAt: now, eventAt: now };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyId === KeyCode.SHIFT_LEFT || keyId === KeyCode.SHIFT_RIGHT) {
|
if (keyId === KeyCode.SHIFT_LEFT || keyId === KeyCode.SHIFT_RIGHT) {
|
||||||
|
|
@ -65,9 +114,15 @@ namespace Input {
|
||||||
document.body.addEventListener('keydown', (e) => onStateChange(e.code as KeyCode, true));
|
document.body.addEventListener('keydown', (e) => onStateChange(e.code as KeyCode, true));
|
||||||
document.body.addEventListener('keyup', (e) => onStateChange(e.code as KeyCode, false));
|
document.body.addEventListener('keyup', (e) => onStateChange(e.code as KeyCode, false));
|
||||||
|
|
||||||
export const isPressed = (key: KeyCode): boolean => KEYS[key]?.pressed ?? false;
|
export const isPressed = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.pressed);
|
||||||
export const isReleased = (key: KeyCode): boolean => KEYS[key]?.released ?? false;
|
export const isReleased = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.released);
|
||||||
export const isHeld = (key: KeyCode): boolean => KEYS[key]?.held ?? false;
|
export const isRepeated = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.repeated);
|
||||||
|
export const isHeld = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.held);
|
||||||
|
|
||||||
|
export const hasPressed = () => Object.values(KEYS).some(k => k.pressed);
|
||||||
|
export const hasReleased = () => Object.values(KEYS).some(k => k.released);
|
||||||
|
export const hasRepeated = () => Object.values(KEYS).some(k => k.repeated);
|
||||||
|
export const hasHeld = () => Object.values(KEYS).some(k => k.held);
|
||||||
|
|
||||||
export const getGamepad = () => navigator.getGamepads().find(g => g != null);
|
export const getGamepad = () => navigator.getGamepads().find(g => g != null);
|
||||||
|
|
||||||
|
|
@ -93,6 +148,14 @@ namespace Input {
|
||||||
+ getGamepadAxis(GamepadAxis.LY)
|
+ getGamepadAxis(GamepadAxis.LY)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const setRepeatInterval = (intervalMs: number) => {
|
||||||
|
repeatIntervalMs = intervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setRepeatDelay = (delayMs: number) => {
|
||||||
|
repeatDelayMs = delayMs;
|
||||||
|
}
|
||||||
|
|
||||||
export function updateKeys() {
|
export function updateKeys() {
|
||||||
for (const key of Object.values(KEYS)) {
|
for (const key of Object.values(KEYS)) {
|
||||||
key.released = false;
|
key.released = false;
|
||||||
|
|
@ -102,10 +165,15 @@ namespace Input {
|
||||||
key.pressed = !key.held;
|
key.pressed = !key.held;
|
||||||
key.held = true;
|
key.held = true;
|
||||||
} else {
|
} else {
|
||||||
key.released = true;
|
key.released = key.held;
|
||||||
key.held = false;
|
key.held = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
key.repeated = key.held && (now - (key.repeatAt ?? 0) > repeatIntervalMs) && (now - key.changedAt > repeatDelayMs);
|
||||||
|
if (key.repeated) {
|
||||||
|
key.repeatAt = now;
|
||||||
|
}
|
||||||
key.prevState = key.state;
|
key.prevState = key.state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,50 @@ function createPlayer(world: World, x = 0, y = 0) {
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMovement() {
|
||||||
|
let dx = 0;
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
const isReleasedOrRepeated = (...keys: Input.KeyCode[]) =>
|
||||||
|
keys.some(key => Input.isReleased(key) || Input.isRepeated(key));
|
||||||
|
|
||||||
|
const left = isReleasedOrRepeated(Input.KeyCode.LEFT, Input.KeyCode.H);
|
||||||
|
const right = isReleasedOrRepeated(Input.KeyCode.RIGHT, Input.KeyCode.L);
|
||||||
|
const up = isReleasedOrRepeated(Input.KeyCode.UP, Input.KeyCode.K);
|
||||||
|
const down = isReleasedOrRepeated(Input.KeyCode.DOWN, Input.KeyCode.J);
|
||||||
|
|
||||||
|
const upLeft = isReleasedOrRepeated(Input.KeyCode.Y);
|
||||||
|
const upRight = isReleasedOrRepeated(Input.KeyCode.U);
|
||||||
|
const downLeft = isReleasedOrRepeated(Input.KeyCode.B);
|
||||||
|
const downRight = isReleasedOrRepeated(Input.KeyCode.N);
|
||||||
|
|
||||||
|
if (left) dx -= 1;
|
||||||
|
if (right) dx += 1;
|
||||||
|
if (up) dy -= 1;
|
||||||
|
if (down) dy += 1;
|
||||||
|
|
||||||
|
if (upLeft) {
|
||||||
|
dx -= 1;
|
||||||
|
dy -= 1;
|
||||||
|
}
|
||||||
|
if (upRight) {
|
||||||
|
dx += 1;
|
||||||
|
dy -= 1;
|
||||||
|
}
|
||||||
|
if (downLeft) {
|
||||||
|
dx -= 1;
|
||||||
|
dy += 1;
|
||||||
|
}
|
||||||
|
if (downRight) {
|
||||||
|
dx += 1;
|
||||||
|
dy += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dx, dy };
|
||||||
|
}
|
||||||
|
|
||||||
export default gameLoop(() => {
|
export default gameLoop(() => {
|
||||||
|
Input.setRepeatInterval(50);
|
||||||
const world = new World();
|
const world = new World();
|
||||||
const display = world.addSystem(new TextDisplaySystem(100, 25)).display;
|
const display = world.addSystem(new TextDisplaySystem(100, 25)).display;
|
||||||
|
|
||||||
|
|
@ -82,7 +125,7 @@ export default gameLoop(() => {
|
||||||
});
|
});
|
||||||
const player = createPlayer(world, startCell.x, startCell.y);
|
const player = createPlayer(world, startCell.x, startCell.y);
|
||||||
|
|
||||||
return {
|
const state = {
|
||||||
display,
|
display,
|
||||||
world,
|
world,
|
||||||
map, mapData,
|
map, mapData,
|
||||||
|
|
@ -90,54 +133,16 @@ export default gameLoop(() => {
|
||||||
random,
|
random,
|
||||||
viewport,
|
viewport,
|
||||||
player,
|
player,
|
||||||
lastMove: 0, now: 0,
|
|
||||||
isWall: (x: number, y: number): boolean => {
|
isWall: (x: number, y: number): boolean => {
|
||||||
const [ch] = mapData.get(x, y);
|
const [ch] = state.mapData.get(x, y);
|
||||||
return ch === WALL;
|
return ch === WALL;
|
||||||
},
|
},
|
||||||
};
|
updateMask: () => {
|
||||||
}, (dt, state) => {
|
if (!state.maskDirty) return;
|
||||||
const {
|
|
||||||
world,
|
const { mapData, maskData, player, isWall } = state;
|
||||||
viewport,
|
|
||||||
player,
|
|
||||||
lastMove, now,
|
|
||||||
mapData, maskData,
|
|
||||||
isWall,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
let dx = -Math.sign(Input.getHorizontal());
|
|
||||||
let dy = -Math.sign(Input.getVertical());
|
|
||||||
const playerPos = getPosition(player)!;
|
const playerPos = getPosition(player)!;
|
||||||
|
|
||||||
if (now - lastMove > 0.05) {
|
|
||||||
if (isWall(playerPos.x + dx, playerPos.y)) {
|
|
||||||
dx = 0;
|
|
||||||
}
|
|
||||||
if (isWall(playerPos.x, playerPos.y + dy)) {
|
|
||||||
dy = 0;
|
|
||||||
}
|
|
||||||
if (isWall(playerPos.x + dx, playerPos.y + dy)) {
|
|
||||||
dx = 0
|
|
||||||
dy = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dx = 0;
|
|
||||||
dy = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dx || dy) {
|
|
||||||
move(player, dx, dy);
|
|
||||||
move(viewport, dx, dy);
|
|
||||||
|
|
||||||
playerPos.x + dx;
|
|
||||||
playerPos.y + dy;
|
|
||||||
|
|
||||||
state.maskDirty = true;
|
|
||||||
state.lastMove = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.maskDirty) {
|
|
||||||
for (let x = 0; x < mapData.width; x++) {
|
for (let x = 0; x < mapData.width; x++) {
|
||||||
for (let y = 0; y < mapData.height; y++) {
|
for (let y = 0; y < mapData.height; y++) {
|
||||||
const [ch, fg] = maskData.get(x + mapData.width, y + mapData.height);
|
const [ch, fg] = maskData.get(x + mapData.width, y + mapData.height);
|
||||||
|
|
@ -151,8 +156,51 @@ export default gameLoop(() => {
|
||||||
}
|
}
|
||||||
state.maskDirty = false;
|
state.maskDirty = false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}, (dt, state) => {
|
||||||
|
const {
|
||||||
|
world,
|
||||||
|
viewport,
|
||||||
|
player,
|
||||||
|
updateMask,
|
||||||
|
isWall,
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const hasInput = Input.hasReleased() || Input.hasRepeated();
|
||||||
|
const playerPos = getPosition(player)!;
|
||||||
|
|
||||||
|
if (hasInput) {
|
||||||
|
if (Input.isHeld(Input.KeyCode.SHIFT)) {
|
||||||
|
} else {
|
||||||
|
let { dx, dy } = handleMovement();
|
||||||
|
|
||||||
|
if (isWall(playerPos.x + dx, playerPos.y)) {
|
||||||
|
dx = 0;
|
||||||
|
}
|
||||||
|
if (isWall(playerPos.x, playerPos.y + dy)) {
|
||||||
|
dy = 0;
|
||||||
|
}
|
||||||
|
if (isWall(playerPos.x + dx, playerPos.y + dy)) {
|
||||||
|
dx = 0
|
||||||
|
dy = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dx || dy) {
|
||||||
|
move(player, dx, dy);
|
||||||
|
move(viewport, dx, dy);
|
||||||
|
|
||||||
|
playerPos.x + dx;
|
||||||
|
playerPos.y + dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO environment step
|
||||||
|
|
||||||
|
state.maskDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMask();
|
||||||
world.update(dt);
|
world.update(dt);
|
||||||
|
|
||||||
state.now += dt;
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue