ecs-observable/examples/tetris/commands.ts

125 lines
3.7 KiB
TypeScript

import { defineComponent } from "../../src/component";
import type { World } from "../../src/index";
import type { CommandQueue } from "../../src/commands/index";
import {
Board,
Piece,
Score,
GameOver,
Paused,
TickTimer,
createPieceHelpers,
} from "./components";
import {
collides,
lockPiece,
clearLines,
scoreForLines,
tryRotate,
} from "./game";
// ── Command definitions ──────────────────────────────
/** Move the active piece left. */
export const MoveLeft = defineComponent("moveLeft", {});
/** Move the active piece right. */
export const MoveRight = defineComponent("moveRight", {});
/** Rotate the active piece clockwise. */
export const Rotate = defineComponent("rotate", {});
/** Soft drop — move piece down one row immediately. */
export const SoftDrop = defineComponent("softDrop", {});
/** Hard drop — slam piece to the bottom instantly. */
export const HardDrop = defineComponent("hardDrop", {});
/** Pause / unpause the game. */
export const TogglePause = defineComponent("togglePause", {});
/** Restart after game over. */
export const Restart = defineComponent("restart", {});
// ── Command handlers ─────────────────────────────────
export function registerCommands(
world: World,
commands: CommandQueue,
pieces: ReturnType<typeof createPieceHelpers>,
): void {
const hasActivePiece = () => world.hasSingleton(Piece);
const isBlocked = () =>
world.hasSingleton(GameOver) || world.hasSingleton(Paused);
commands.handle(MoveLeft, () => {
if (!hasActivePiece() || isBlocked()) return;
const piece = world.getSingleton(Piece);
const board = world.getSingleton(Board);
if (!collides(board.grid, piece.shape, piece.x - 1, piece.y)) {
piece.x--;
}
});
commands.handle(MoveRight, () => {
if (!hasActivePiece() || isBlocked()) return;
const piece = world.getSingleton(Piece);
const board = world.getSingleton(Board);
if (!collides(board.grid, piece.shape, piece.x + 1, piece.y)) {
piece.x++;
}
});
commands.handle(Rotate, () => {
if (!hasActivePiece() || isBlocked()) return;
const piece = world.getSingleton(Piece);
const board = world.getSingleton(Board);
const result = tryRotate(board.grid, piece.shape, piece.x, piece.y);
if (result) {
piece.shape = result.shape;
piece.x = result.x;
}
});
commands.handle(SoftDrop, () => {
if (!hasActivePiece() || isBlocked()) return;
const piece = world.getSingleton(Piece);
const board = world.getSingleton(Board);
if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
piece.y++;
}
});
commands.handle(HardDrop, () => {
if (!hasActivePiece() || isBlocked()) return;
const piece = world.getSingleton(Piece);
const board = world.getSingleton(Board);
while (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
piece.y++;
}
pieces.lockAndSpawn();
});
commands.handle(TogglePause, () => {
if (world.hasSingleton(GameOver)) return;
if (world.hasSingleton(Paused)) {
world.removeSingleton(Paused);
} else {
world.addSingleton(Paused);
}
});
commands.handle(Restart, () => {
if (!world.hasSingleton(GameOver)) return;
const board = world.getSingleton(Board);
for (let r = 0; r < board.grid.length; r++) {
board.grid[r].fill(0);
}
world.setSingleton(Score, { points: 0, lines: 0, level: 1 });
world.setSingleton(TickTimer, { accumulator: 0, interval: 800 });
world.removeSingleton(GameOver);
if (world.hasSingleton(Piece)) world.removeSingleton(Piece);
pieces.spawnPiece();
});
}