- 19 TypeScript modules under src/ (constants, entities, room, game, render) - Canvas 2D rendering with dark fantasy palette - Room-based navigation, random 7x7 map generation - Two combat modes: ranged (pistol) and melee (knife) - Wall collision with door opening support - Minimap, HP bar, enemy AI - Bun build pipeline: src/main.ts -> dist/main.js + dist/index.html
5.1 KiB
5.1 KiB
Architecture
Overview
The game is a single-page Canvas 2D application. The source is written
in TypeScript and split into ~19 modules under src/, bundled by Bun
into a single dist/main.js. The HTML shell in dist/index.html loads
the bundle.
Module Dependency Graph
main.ts
├── input.ts (global KEYS, setupInput)
└── Game.ts
├── constants.ts (all shared numeric constants)
├── types.ts (Dir, RoomType, CombatMode, Box, Doors)
├── math.ts (shuffle, rand, ri, dist, overlap)
├── RoomMap.ts → Room.ts, tiles.ts, doors.ts
├── Player.ts
├── Enemy.ts
├── Tear.ts
├── MeleeSwing.ts
├── collision.ts → Room.ts, tiles.ts
├── transitions.ts → Game.ts (type only), input.ts
├── spawner.ts → Room.ts, Enemy.ts
└── render/*
→ roomRenderer.ts, entityRenderer.ts,
hudRenderer.ts, minimapRenderer.ts
Game Loop
Every frame (requestAnimationFrame):
- Timers — decrement
invTimer,atkCD,transCD - Movement — read WASD, apply velocity, resolve wall collisions
- Attack — read arrow keys / Space, fire tear or create melee swing
- Melee Update — decrement swing life, apply damage + knockback
- Tear Update — move projectiles, check wall/enemy collisions
- Enemy AI — chase player, contact damage with cooldown
- Room Clear — if all enemies dead →
room.cleared = true, rebuild tiles with doors - Transition Check — if player on door tile + key pressed → load adjacent room
- Win Check — if boss room cleared →
won = true - Render — draw room tiles → entities → HUD → minimap → overlays
Transition System
Room transitions are the most complex subsystem. Key design:
- Door geometry:
DOORconstant defines 3-tile-wide openings at each cardinal edge (top: cols 6-8, row 0; bottom: cols 6-8, row 10; left: rows 4-6, col 0; right: rows 4-6, col 14). - Detection:
checkTransition()computes the player's tile position (col, row). If they're on a door tile AND pressing the matching movement key, the transition fires. - Cooldown:
transCD = 15prevents re-entry within 15 frames. - Movement lock: Transition only works in cleared rooms
(
room.cleared === true). - Collision bypass:
isBlocked()returnsfalsefor out-of-bounds tiles at door openings, letting the player's bounding box extend beyond the room boundary.
Collision System
isBlocked(room, col, row)— per-tile check. Returnstruefor wall tiles and out-of-bounds positions, except at door openings.collidesWall(box, room, ox, oy)— iterates all tiles covered by the entity's bounding box. If any tile is blocked, returnstrue.- Movement resolves axis-independently: apply X, revert if collision; apply Y, revert if collision.
Room Generation
RoomMap.generate() uses a random walk:
- Start at (0,0) with a spawn room.
- Maintain a frontier list of rooms that have room to expand.
- Each step, pick a random frontier room, shuffle directions, and attempt to add a new room in an unoccupied adjacent cell within the 7×7 grid.
- Room types are assigned probabilistically (boss at ~20%, treasure at ~12%, rest normal).
- If no boss room was generated, one normal room is promoted.
Rendering
Rendering is split into 4 stateless functions, each taking
CanvasRenderingContext2D as the first argument:
- roomRenderer — tile loop with wall/door/floor styles
- entityRenderer — enemies (3 types), player (body + weapon), tears, melee swing arc
- hudRenderer — HP bar, mode indicator, enemy count, room label
- minimapRenderer — 7×7 grid with visited/current room highlights
Weapon Visuals
- Pistol (ranged): Barrel line + body rect, rotated toward facing
direction. Muzzle flash circle at
atkCD > 8. - Knife (melee): Triangular blade + handle + guard rects, offset in facing direction.
Enemy Types
| Type | Size | HP | Speed | Damage | Visual |
|---|---|---|---|---|---|
| Normal | 32px | 3 | 1.15 | 1 | Brown block, yellow eyes |
| Fast | 26px | 2 | 1.9 | 1 | Red circle, small eyes |
| Boss | 46px | 10 | 0.9 | 2 | Large red circle, horns, HP bar |
State Management
GAME_OVER/WON— boolean flags checked in loop and render.KEYS— global mutable map, reset on window blur.- Room state (
cleared,visited,enemies[],tears[]) — per-room. - Player state (
hp,mode,facing,transCD, etc.) — singlePlayerinstance. - Cooldowns (
invTimer,atkCD,transCD) decrement each frame.
Build Pipeline
bun build src/main.ts --outdir dist --target browser --minify
cp src/index.html dist/
- Bundler: Bun's native bundler (esbuild under the hood).
- Target: browser (ES module → IIFE/global wrapper).
- Result: single
dist/main.js(~30 KB) +dist/index.html. - Open
dist/index.htmldirectly in any modern browser.