import type { Message } from "../shared/types" import { GameContext, type InputState } from "../shared/game" import { focusInput } from "./focus" import { $$ } from "./dom" import { randomId } from "../shared/utils" import { setStatus, addOutput, insert } from "./scrollback" import { browserCommands } from "./commands" 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(`/source/${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") window.addEventListener("keydown", handleKeydown) window.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?.classList.add("game") 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 window.removeEventListener("keydown", handleKeydown) window.removeEventListener("keyup", handleKeyup) window.removeEventListener("resize", resizeCanvas) if (oldMode === "tall") browserCommands.mode?.() const main = document.querySelector("main") main?.classList.remove("game") canvas.classList.remove("active") canvas.style.height = HEIGHT / 2 + "px" canvas.style.width = WIDTH / 2 + "px" const output = $$("li.output") output.append(canvas) insert(output) focusInput() }