diff --git a/app/src/js/game.ts b/app/src/js/game.ts index e69de29..ddb581d 100644 --- a/app/src/js/game.ts +++ b/app/src/js/game.ts @@ -0,0 +1,151 @@ +import type { Message } from "../shared/types.js" +import { GameContext, type InputState } from "../shared/game.js" +import { focusInput } from "./focus.js" +import { $$, scrollback } from "./dom.js" +import { randomId } from "../shared/utils.js" +import { setStatus, addOutput } from "./scrollback.js" +import { browserCommands } from "./commands.js" + +const FPS = 30 +const HEIGHT = 540 +const WIDTH = 980 + +type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void } + +let oldMode = "cinema" +let running = false +let canvas: HTMLCanvasElement +const pressedStack = new Set() + +let pressed: InputState = { + key: "", + shift: false, + ctrl: false, + meta: false, + pressed: pressedStack, + prevPressed: new Set(), + justPressed: new Set(), + justReleased: new Set(), +} + +export async function handleGameStart(msg: Message) { + const msgId = msg.id as string + const name = msg.data as string + + let game + try { + game = await import(`/command/${name}`) + } catch (err: any) { + setStatus(msgId, "error") + addOutput(msgId, `Error: ${err.message ? err.message : err}`) + return + } + + if (document.body.dataset.mode === "tall") { + browserCommands.mode?.() + oldMode = "tall" + } + + canvas = createCanvas() + canvas.focus() + setStatus(msgId, "ok") + canvas.addEventListener("keydown", handleKeydown) + canvas.addEventListener("keyup", handleKeyup) + window.addEventListener("resize", resizeCanvas) + resizeCanvas() + gameLoop(new GameContext(canvas.getContext("2d")!), game) +} + +function createCanvas(): HTMLCanvasElement { + const canvas = $$("canvas.game.active") as HTMLCanvasElement + canvas.id = randomId() + canvas.height = HEIGHT + canvas.width = WIDTH + canvas.tabIndex = 0 + + const main = document.querySelector("main") + main?.parentNode?.insertBefore(canvas, main) + + return canvas +} + +function handleKeydown(e: KeyboardEvent) { + e.preventDefault() + + if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) { + endGame() + } else { + pressedStack.add(e.key) + pressed.key = e.key + pressed.ctrl = e.ctrlKey + pressed.shift = e.shiftKey + pressed.meta = e.metaKey + } +} + +function handleKeyup(e: KeyboardEvent) { + pressedStack.delete(e.key) + if (pressedStack.size === 0) { + pressed.key = "" + pressed.ctrl = false + pressed.shift = false + pressed.meta = false + } +} + +function updateInputState() { + pressed.justPressed = new Set([...pressed.pressed].filter(k => !pressed.prevPressed.has(k))) + pressed.justReleased = new Set([...pressed.prevPressed].filter(k => !pressed.pressed.has(k))) + pressed.prevPressed = new Set(pressed.pressed) +} + +function resizeCanvas() { + const scale = Math.min( + window.innerWidth / 960, + window.innerHeight / 540 + ) + + canvas.width = 960 * scale + canvas.height = 540 * scale + + const ctx = canvas.getContext("2d")! + ctx.setTransform(scale, 0, 0, scale, 0, 0) +} + +function gameLoop(ctx: GameContext, game: Game) { + running = true + let last = 0 + + if (game.init) game.init() + + function loop(ts: number) { + if (!running) return + + const delta = ts - last + if (delta >= 1000 / FPS) { + updateInputState() + if (game.update) game.update(delta, pressed) + if (game.draw) game.draw(ctx) + last = ts + } + requestAnimationFrame(loop) + } + + requestAnimationFrame(loop) +} + +function endGame() { + running = false + + if (oldMode === "tall") browserCommands.mode?.() + + canvas.classList.remove("active") + canvas.style.height = HEIGHT / 2 + "px" + canvas.style.width = WIDTH / 2 + "px" + + const output = $$("li.output") + output.append(canvas) + scrollback.append(output) + + focusInput() +} \ No newline at end of file