# PLAN — Tower Defense (Brief 04)

## Goal

Build a tower-defense game in a Vite + TypeScript + Vitest project. The single
hard interface the harness imports is `src/lib/engine-core.ts`. The rest is a
real playable game on Canvas that demonstrates the engine systems under load.

## Architecture overview

```
src/
  lib/                 ← pure, no DOM, no canvas, no globals
    engine-core.ts     ← ACCEPTANCE CONTRACT (World, findPath, nearestInRange)
    astar.ts           ← binary-heap A* used by engine-core.findPath
    pool.ts            ← generic object pool for entity data + free list
  sim/                 ← game-side use of the engine-core World
    components.ts      ← component name constants + TS types
    systems.ts         ← movement, tower targeting, projectile, wave, lives, game-loop
    waves.ts           ← wave spec table (count, hp curve, interval, bounty)
    game.ts            ← GameState factory + per-tick update orchestrator
  render/
    canvas-renderer.ts ← single 2D context, batched fillRect/arc by color
    hud.ts             ← text overlay (lives, money, wave, fps) drawn into same canvas
  input/
    pointer.ts         ← click + hover for tower placement / range preview
  perf/
    probe.ts           ← window.__buildoff = { warmup, workload }
  main.ts              ← entry: wires game + renderer + input + probe
  style.css            ← minimal layout
tests/
  engine-core.test.ts  ← own tests for World, findPath, nearestInRange
  astar.test.ts        ← additional A* edge cases
  pool.test.ts         ← pool acquire/release semantics
  perf.test.ts         ← smoke test of the probe shape
```

## Engine-core design (the contract)

### `World` — sparse-set ECS core, no DOM

- `Map<componentName, Map<entityId, data>>` — sparse-set storage so a system
  can iterate one component cheaply and filter the rest in O(k) per entity.
- Entity ids are dense integers; a free list of destroyed ids is reused so the
  pool can churn without growing heap.
- `query(...names)` picks the smallest component map, then filters by
  membership in the rest. Always returns ids in ascending numeric order
  (stable for diffing, cache-friendly for tests).
- `count()` returns the number of *live* entities (alive = created and not
  destroyed).
- `addComponent` replaces if present; `removeComponent` deletes and clears the
  archetype entry; `destroyEntity` removes from all component maps and pushes
  the id back to the free list.

### `findPath(blocked, start, goal)` — A* on a 4-connected grid

- Binary heap priority queue (open set), Manhattan heuristic (admissible on
  4-connected uniform cost), closed set as a `Uint8Array` keyed by `y*W+x`.
- Returns the path **inclusive of start and goal**, with optimal length, or
  `[]` if unreachable. Among optimal paths any valid one is acceptable (we
  tie-break by lower f then lower h then lower y then lower x for determinism).
- `blocked` is `boolean[][]` per the contract; we still convert to a typed
  view internally for speed on hot paths.
- Validates bounds; returns `[]` for out-of-grid start/goal.

### `nearestInRange(x, y, range, candidates, positions)` — Euclidean

- Iterates candidates once, tracks the nearest by squared distance to avoid
  sqrt. Returns the entity id with smallest distance ≤ range², or `-1`.
- Deterministic tie-break on id ascending when distances are equal.

## Game-side use of the engine

- `components.ts` defines the names (`position`, `velocity`, `health`,
  `path`, `tower`, `projectile`, `enemy`, `lifetime`, `damage`, `sprite`).
- `Game` in `sim/game.ts` owns a `World`, a `Uint8Array` of blocked cells,
  the current path, and the wave schedule. It exposes `update(dt, tickBudget)`
  that runs a fixed-timestep step and `placeTower`, `startNextWave`, etc.
- **Systems** (in `sim/systems.ts`):
  - `systemMovement` — follow next waypoint, advance along path.
  - `systemTowerTargeting` — every fire interval pick `nearestInRange`
    candidate; spawn a projectile entity.
  - `systemProjectile` — straight-line flight, hit detection by Manhattan
    (cheap) or by simple AABB at the end of the step.
  - `systemWaveSpawner` — spawns enemies on a schedule per wave spec.
  - `systemLives` — enemy reaching the end of path deals 1 damage and is
    destroyed.
  - `systemLifetime` — projectile expires after its lifetime component.
- Object pooling: enemies + projectiles are pulled from a pre-allocated pool
  (cap 512), so per-spawn we don't allocate.
- Fixed-timestep loop with accumulator (60 Hz). `requestAnimationFrame`
  drives the accumulator; rendering is decoupled from simulation (F7).

## Rendering

- One `<canvas>` sized to `innerWidth × innerHeight`. DPR-aware: we set
  `canvas.width = cssW * dpr` and scale the context once.
- Batched draws: towers are drawn as filled squares + range ring on hover;
  enemies as filled circles colored by HP bucket; projectiles as small
  circles. No DOM nodes per entity (F8).
- HUD text in the same canvas — no extra DOM dependencies.

## Input

- Single `pointermove` + `pointerdown` on the canvas; cell under cursor is
  computed from `event.offsetX / cellSize`. Hover draws the tower's range
  circle. Click places a tower if affordable and cell is empty & not on path.

## Performance strategy (rubric A4 / A5)

- **Sparse-set ECS** so system iteration touches only the component it needs.
- **Typed arrays** for the blocked grid (`Uint8Array`), per-component
  position/velocity pools where hot (`Float32Array` chunks inside pools).
- **Object pooling** for enemies + projectiles (cap 512) so wave pressure
  doesn't churn the GC.
- **Cached A\* path** with a dirty flag — we only re-run A* when a tower is
  placed or removed; otherwise reuse the cached path for all enemies.
- **Single canvas**, batched fills, no per-entity DOM (F8, F10).
- **Fixed-timestep sim** (F7) means render can drop frames without
  corrupting gameplay.
- **Probe** at `window.__buildoff.warmup/workload` for the harness.

## Libraries

- `vite` — build/dev (required by contract)
- `typescript` — required by contract (strict:true)
- `vitest` — required by contract
- `@vitest/coverage-v8` — only if tests need it (omit to keep bundle small)
- `eslint`, `typescript-eslint`, `@eslint/js`, `prettier` — required by
  contract
- **No runtime libraries** — vanilla TS keeps the bundle tiny and the
  perf predictable.

## File layout (final)

```
package.json          # scripts: dev / build / test / lint
tsconfig.json         # copied from shared-configs
vite.config.ts        # base './', build.outDir='dist', vitest config inline
eslint.config.js      # copied from shared-configs
.prettierrc.json      # copied from shared-configs
index.html            # vite entry; root div + canvas
src/
  lib/{engine-core,astar,pool}.ts
  sim/{components,systems,waves,game}.ts
  render/{canvas-renderer,hud}.ts
  input/pointer.ts
  perf/probe.ts
  main.ts
  style.css
tests/{engine-core,astar,pool,perf}.test.ts
PLAN.md
REASONING.md
SELF.md
```

## Risks

1. **Hidden acceptance tests**: they import the exact exports from
   `engine-core.ts`. We must keep the file dependency-free and the API
   signatures stable. Mitigated by writing our own thorough tests first
   against the contract surface, then running them.
2. **A\* optimal-length correctness**: I will test on grids with multiple
   shortest paths, blocked obstacles forcing detours, and unreachable
   goals. I will verify the returned length is the known optimum.
3. **Bundle size (A5)**: keeping dependencies minimal and not pulling in
   framework code.
4. **Heap churn at 200+ entities**: pooling + typed-array hot paths; we
   measure with `performance.memory` if available in the probe.
5. **Lint strictness**: `no-explicit-any`, `no-unused-vars`,
   `no-non-null-assertion`, `no-console` are warns. We avoid `any`,
   unused locals, and `!` non-null assertions in production code.
6. **Bundle/build time (A4)**: Vite + tsc is fast; we have no heavy deps.

## Acceptance strategy

- Acceptance contract is implemented **first** and tested with our own
  Vitest suite that mirrors the same shape the hidden harness will probe.
- Then the playable game is layered on top using only the contract API.
- Final verification: `npm ci && npm run build && npm test && npm run lint`
  must all succeed; `dist/` must exist.
