refactor: move world concepts into engine
This commit is contained in:
parent
eeedcb8e48
commit
2d15c7ab8d
52 changed files with 3298 additions and 1558 deletions
143
AGENTS.md
143
AGENTS.md
|
|
@ -26,14 +26,19 @@ rules live in `.agents/rules/*.md`.
|
|||
frame; on-screen solids also **backface-cull**. This is what keeps a dense world
|
||||
(thousands of trees/rocks) affordable — off-screen content costs ~nothing.
|
||||
- **2D assets only.** Sprites/billboards (PS1-style), **no 3D model loading**.
|
||||
- **Three layers, one-way deps: `engine` ← `game` ← `app`.** `engine/` is the
|
||||
reusable, content-agnostic **mechanism** (rasterizer, framebuffer, culling, the
|
||||
`Actor`/`Material` interfaces) — no DOM, no game content, could run server-side.
|
||||
`game/` is **this game's content** (the creatures/props, terrain, level, scene
|
||||
assembly + render orchestration) built on the engine interfaces. `app/` is
|
||||
**browser glue** (canvas, input, image decode, the worker render driver + loop).
|
||||
- **Three layers, one-way deps: `engine` ← `game` ← `app`.** `engine/` owns
|
||||
reusable, content-agnostic **concepts and mechanisms**: levels, terrain, actors,
|
||||
prefabs, collision, render scenes, chunking/LOD, rasterization, and worker render
|
||||
transport. `game/` owns **this game's concrete content**: actual level values,
|
||||
placements, materials, and frog/bee/robin or oak/spruce/birch definitions.
|
||||
`app/` is browser glue (canvas, input mapping, image decode, workers, frame loop).
|
||||
Enforced: `engine/` imports nothing from `game`/`app`, `game/` nothing from `app`
|
||||
(`tests/layering.test.ts`). Both `engine` and `game` are DOM-free (tsconfig).
|
||||
- **No closed game-content kinds or registries.** Instances reference concrete
|
||||
`ActorDefinition`/`Prefab` objects directly. Do not add `MobKind`, `TreeKind`,
|
||||
`*_KINDS`, content dispatch switches, or stable content-order protocols. Numeric
|
||||
prototype indexes are scene-local engine transport details only. Closed unions
|
||||
remain valid for finite engine capabilities such as collider or cloud shape.
|
||||
|
||||
## Stack & tooling
|
||||
|
||||
|
|
@ -57,7 +62,7 @@ rules live in `.agents/rules/*.md`.
|
|||
- `bunx tsc --build tsconfig.app.json` — **typecheck the engine+game+app graph. Use
|
||||
this**, not `bun run check` (see Caveats).
|
||||
- `bunx oxlint engine game app` — lint.
|
||||
- `bun test` — tests (registry id-order + engine↛game layering guards).
|
||||
- `bun test` — tests (world compilation, render protocol, boundary guards).
|
||||
- `bun run serve` — Bun server (`server/server.ts`, a stub for now).
|
||||
|
||||
## Layout
|
||||
|
|
@ -71,73 +76,51 @@ rules live in `.agents/rules/*.md`.
|
|||
(optional backface cull per draw), `Frustum` (6 planes from the viewProj +
|
||||
AABB test, for chunk culling), `Texture` (nearest/bilinear, wrapping, no
|
||||
mipmaps), `Material` (texture + cull flag; a `DrawGroup` pairs a mesh with one,
|
||||
so the renderer draws by list, not by named texture), `Sky` (gradient + sun +
|
||||
procedural clouds; renders at 1/`step` res).
|
||||
so the renderer draws by list, not by named texture), `Chunk`/`ChunkBuilder`
|
||||
(spatial batches + two-level LOD), `RenderScene` (clone-safe scene projection,
|
||||
culling + band rendering), `RenderProtocol` (shared worker frame layout), and
|
||||
`Sky` (full-resolution gradient + sun, procedural clouds sampled at 1/`step`).
|
||||
- `scene/` — `Camera` (fps yaw/pitch; far plane reaches the outdoor peaks),
|
||||
`Mesh` (indexed tris; verts stored flat: `STRIDE` floats x,y,z,u,v per vertex,
|
||||
no per-vertex objects — cache-friendly + alloc-free to draw), `Sprite`
|
||||
(Y-axis billboard), `Actor` (the generic `Entity<State, World>` interface —
|
||||
build + update + bounds — that a content kind implements; the engine dispatches
|
||||
through it, never a `kind` switch).
|
||||
- `game/` — this game's content + world assembly, on the engine interfaces
|
||||
(Y-axis billboard), `MeshBuilder` (generic quad/slab/box construction), `Actor`
|
||||
(open behavior + render/collider contract), and `Prefab` (open static-content
|
||||
contract + type-erased placed value).
|
||||
- `world/` — `Terrain` (height contract, built-in rolling generator, generic
|
||||
patch meshing), `Collider`/`CollisionWorld`, `CharacterController`, and `Level`
|
||||
(live actor/collision state + clone-safe `RenderScene` projection).
|
||||
- `game/` — this game's definitions and level data, on the engine interfaces
|
||||
(headless: no DOM, imports nothing from `app`).
|
||||
- `actors/` — the placeable things. `Mob` (a **roaming** creature — `frog` hops
|
||||
the ground, `bee` hovers/darts, `robin` mostly hops but now and then takes a
|
||||
short powered flight — the only moving geometry; each kind an `Entity` in
|
||||
`mobs/<Kind>.ts` + shared `mobs/mobkit.ts`, assembled by the thin `Mob` registry.
|
||||
Its local-space mesh is built once per kind; `Mob.update` steps the wander AI
|
||||
(leashed to a home anchor, deterministic per evolving `seed`) each frame and the
|
||||
live `position`/`heading`/`scale` become a per-frame model matrix at draw.
|
||||
`MOB_KINDS` is the SAB id order). `Tree` (oak/spruce/birch, each a `TreeSpecies`
|
||||
in `trees/<Kind>.ts` + `trees/treekit.ts` — see the Trees section), `Boulder`
|
||||
short powered flight — the only moving geometry; each module exports an
|
||||
`ActorDefinition` factory and instances hold the resulting object directly.
|
||||
`Tree` is shared placement state; Oak/Spruce/Birch modules export material-bound
|
||||
`Prefab<Tree>` factories with no species registry. `Boulder`
|
||||
(squashed jittered part-buried sphere), `Bush` (leaf-blob cluster, shares the
|
||||
leaf mesh), `Flower` (stem + colored bloom, 2x2 atlas, double-sided). Baked props
|
||||
append into shared per-material meshes; mobs draw live.
|
||||
- `Terrain.ts` — procedural heightfield around the room (flat clearing, rolling
|
||||
hills, tall edge peaks). `Terrain.patch` builds one ground patch over a rectangle
|
||||
(per chunk, welds crack-free, hole for the room); `Terrain.height` is the shared
|
||||
ground sampler for the player + mobs.
|
||||
- `level.ts` — builds the playground: a flat stone-floored room (three thick
|
||||
walls via `slab`, north side open) always drawn, in the center of a big grassy
|
||||
`Terrain` world (~20x across). Props are placed first (`placeTrees` /
|
||||
`placeBoulders` / `placeBushes` / `placeFlowers` → instance lists + colliders;
|
||||
`TREE_/BOULDER_/BUSH_/FLOWER_COUNT`/`_SEED`/`_REACH`) and the roaming mobs
|
||||
scattered (`placeMobs`; `FROG_/BEE_/ROBIN_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs
|
||||
move, so no baked colliders), then `buildChunks` bakes terrain + props into a
|
||||
`CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each = a tight AABB + two
|
||||
`DrawGroup[]` lists `near`/`far`; the baker accumulates one mesh per **material
|
||||
key** (`MAT_ORDER`) and routes each prop by its declared material, so it names no
|
||||
texture) that `main` frustum-culls. Trees + boulders bake **twice** — full into
|
||||
`near`, a low-poly impostor into `far` — so a far chunk swaps to the cheap set
|
||||
with no per-frame work (`chunkFar` / `RenderConfig.lodDistance`).
|
||||
`buildLevel(textures)` binds the ground/prop `Material`s once + shares them.
|
||||
Also: `Aabb` colliders, NPC position, `TERRAIN`/`TERRAIN_SUBDIV`/`GROUND_UV`,
|
||||
sky/cloud config, `FLOOR_LIFT` (a z-bias lifting the stone floor over the terrain
|
||||
skirt). Room surfaces are single flat quads (texturing is perspective-correct).
|
||||
- `renderScene.ts` — `renderBand(fb, scene, …, mobDraws, …, y0, y1)`: the single
|
||||
source of render truth (sky + room + culled chunk draw-groups + sprite + roaming
|
||||
mobs + quantize for a row band). Used full-height by the inline path, per-band by
|
||||
each worker. `Scene` bundles the static meshes/textures (incl. the canonical mob
|
||||
meshes) so it clones to a worker whole; each mob draws double-sided through its
|
||||
own `viewProj × Mat4.compose(...)` model matrix, and `visibleChunks`/`visibleMobs`
|
||||
frustum-cull per frame. `textures.ts` holds the `Textures` palette type.
|
||||
- `player.ts` — feet-cylinder player: gravity/jump + Shift-run (`RUN_MULTIPLIER`)
|
||||
+ circle-vs-AABB/-circle collision, substepped so fast running can't tunnel
|
||||
walls; ground height from `Terrain.height` (plus standable AABBs).
|
||||
- `level.ts` — concrete playground values and placement policy: room dimensions,
|
||||
rolling-terrain parameters, counts/seeds/reach, material bindings, sky, and
|
||||
spawn definition lists. It places direct `Prefab` objects, submits the resulting
|
||||
values to engine `ChunkBuilder`, then creates engine `Level`; no game renderer or material-key
|
||||
registry exists. Trees + boulders bake full and far-impostor geometry; bushes
|
||||
and flowers provide near geometry only.
|
||||
- `player.ts` — concrete player tuning only; movement and collision live in
|
||||
engine `CharacterController`.
|
||||
- `app/` — browser glue only (top layer; depends on `game` + `engine`).
|
||||
- `main.ts` — game loop: input, sim, preset switching, per-frame culling, then
|
||||
the non-blocking pump (`renderer.dispatch`/`done`) + `present` (GPU/CSS upscale)
|
||||
+ a multi-line frame HUD (`work + present` critical-path ms, vsync, visible
|
||||
chunks / LOD-aware tris). Owns the **mob sim**: steps `Mob.update` for every mob,
|
||||
rebuilds near-player mob colliders into `level.colliders`, culls mobs
|
||||
(`visibleMobs`) so only visible transforms dispatch.
|
||||
chunks / LOD-aware tris). Calls engine `Level` simulation/collider extraction,
|
||||
maps browser keys into `CharacterInput`, and dispatches visible render instances.
|
||||
- `renderer.ts` — the render driver. When the page is cross-origin-isolated it runs
|
||||
a pool of `render-worker.ts` threads (`MAX_WORKERS`) over a `SharedArrayBuffer`
|
||||
framebuffer, each owning a disjoint row band, synced by a lock-free `Atomics`
|
||||
barrier; otherwise inline. `dispatch`/`done` are non-blocking so the caller paces
|
||||
on rAF. Per-frame inputs ride shared arrays: camera/matrix/visible-chunk list +
|
||||
visible **mob transforms** (`mobState`, count in `MOBVIS`). `?bench=st|mt` A/Bs
|
||||
the paths.
|
||||
visible engine instance prototype indexes + transforms. `?bench=st|mt` A/Bs
|
||||
the paths. Renderer and worker import no game modules.
|
||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes are
|
||||
already the `Color` layout); returns the `game` `Textures` palette.
|
||||
- `index.html` — Vite entry at repo root; holds the `#screen` canvas and the
|
||||
|
|
@ -155,9 +138,10 @@ rules live in `.agents/rules/*.md`.
|
|||
|
||||
## Frame pipeline (`app/main.ts` `tick`)
|
||||
|
||||
`Mob.update` (all mobs) + rebuild near-player mob colliders → `Player.update` →
|
||||
build `Camera` → `Camera.viewProjection` → `visibleChunks` + `visibleMobs`
|
||||
(frustum-cull, once on the main thread) → `renderer.dispatch` (non-blocking) →
|
||||
`Level.update` (all actors) + `Level.refreshActorColliders` →
|
||||
`CharacterController.update` → build `Camera` → `Camera.viewProjection` →
|
||||
`RenderScene.visibleChunks` + `Level.visibleInstances` (frustum-cull, once on the
|
||||
main thread) → `renderer.dispatch` (non-blocking) →
|
||||
next rAF: `renderer.done()` ? `present` : skip this vsync. Frame N is presented
|
||||
while N+1 is dispatched; the pump never blocks or async-awaits, so it can't
|
||||
desync from rAF.
|
||||
|
|
@ -171,16 +155,15 @@ an integer multiple (crisp letterbox, centered) once per resize/config, and
|
|||
`linear`). This replaced a per-frame main-thread `drawImage` that scaled to the
|
||||
whole window (cost grew with window size); present is now ~0.2ms.
|
||||
|
||||
`renderBand` runs `renderScene.renderBand` for rows [y0,y1): `Sky.render` at
|
||||
1/`SKY_STEP` res (fills color + resets depth, replaces a clear) → `Rasterizer.draw`
|
||||
`renderBand` runs `RenderScene.renderBand` for rows [y0,y1): `Sky.render` with
|
||||
clouds sampled at 1/`SKY_STEP` res (fills color + resets depth, replaces a clear) → `Rasterizer.draw`
|
||||
floor/walls/crate (room, always) → for each visible `Chunk`, loop its draw-groups —
|
||||
`near` or `far` chosen by the pure `chunkFar` test (dist² from camera to the chunk
|
||||
`near` or `far` chosen by the pure `Chunk.isFar` test (dist² from camera to the chunk
|
||||
AABB vs `lodDistance²`): `near` is grass + full trees/rocks + flowers, `far` is grass
|
||||
+ the cheap impostors (foliage/flowers dropped). Each group draws with its own
|
||||
`Material` (cull per-material, so solids backface-cull and flowers stay double-sided)
|
||||
→ `Sprite.billboard(npc)` →
|
||||
the roaming mobs (each: shared local mesh × its `Mat4.compose` model matrix,
|
||||
double-sided) → `Framebuffer.quantize`. `chunkFar` is pure (camera + baked bounds + config
|
||||
→ each billboard → each visible actor prototype instance (shared local draw groups ×
|
||||
its `Mat4.compose` model matrix) → `Framebuffer.quantize`. `Chunk.isFar` is pure (camera + baked bounds + config
|
||||
only), so every worker band picks the same LOD for a chunk → no horizontal seam.
|
||||
Multi-threaded: N workers each run `renderBand` over
|
||||
their band of the shared framebuffer in parallel; single-threaded: one call over
|
||||
|
|
@ -225,9 +208,9 @@ off-screen or fogged each frame, so several things keep it cheap:
|
|||
chunks that fall outside the view. Behind you + off to the sides = free.
|
||||
- **Backface culling** (`draw(..., true)`) — ~halves fill on solid geometry
|
||||
(terrain, foliage, rock). See the Rasterizer note re winding.
|
||||
- **Half-res sky** (`SKY_STEP` in `main`, default 2) — the cloud fbm runs per
|
||||
- **Half-res clouds** (`SKY_STEP` in `app/renderer.ts`, default 2) — cloud fbm runs per
|
||||
pixel and dominated the frame; sampling once per 2×2 block quarters it.
|
||||
- **Distance LOD** (`RenderConfig.lodDistance`, `chunkFar` in `renderScene`) —
|
||||
- **Distance LOD** (`RenderConfig.lodDistance`, `Chunk.isFar`) —
|
||||
past `lodDistance` a chunk's trees + boulders swap to pre-baked low-poly
|
||||
impostors and its bushes/flowers drop; both meshes are baked once at load, and
|
||||
the near/far pick is a pure function of camera + chunk bounds, so it costs
|
||||
|
|
@ -282,11 +265,10 @@ branching in the cloud shader. Cost scales with sky resolution — fine at
|
|||
|
||||
## Trees (`game/actors/Tree.ts` + `game/actors/trees/`)
|
||||
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. Each species
|
||||
is a `TreeSpecies` definition in its own `trees/<Kind>.ts` module (geometry +
|
||||
which chunk materials its trunk/foliage bake into); `Tree.ts` just assembles them
|
||||
into a registry (`Tree.species(kind)`, `TREE_KINDS`) and shared primitives live in
|
||||
`trees/treekit.ts`. Three `kind`s carry the species read purely by silhouette:
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. `Tree.ts`
|
||||
contains only shared placement state. Oak, Spruce, and Birch each expose a concrete
|
||||
`Prefab<Tree>` factory from their own module; level data references those definition
|
||||
objects directly, and shared geometry primitives live in `trees/treekit.ts`.
|
||||
- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of
|
||||
lumpy canopy `blob`s (wider than tall, bushy).
|
||||
- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing
|
||||
|
|
@ -298,14 +280,11 @@ into a registry (`Tree.species(kind)`, `TREE_KINDS`) and shared primitives live
|
|||
|
||||
`growth` (0..1) runs **sapling → full grown**: it scales height/girth and adds
|
||||
canopy blobs (oak/birch) / tiers (spruce); `seed` gives each tree its own wobble.
|
||||
A species declares its `trunk`/`foliage` **material keys** (e.g. birch → white
|
||||
`birch` trunk, oak `leaf` foliage); the chunk baker (`level.ts`) accumulates one
|
||||
mesh per material key and routes each tree via `Tree.species(kind)` — so a forest
|
||||
still batches into a few draw calls and the baker names no texture. `game/level.ts`
|
||||
`placeTrees` seeds the forest and rolls the species. **Add a species** = add a
|
||||
`trees/<Kind>.ts` module (its geometry + material keys) + one entry in the `Tree`
|
||||
registry; only a genuinely new material also needs a `Material` in `buildLevel` +
|
||||
its key in `MAT_ORDER`.
|
||||
Each factory receives concrete trunk/foliage `Material` objects and closes over
|
||||
them. `ChunkBuilder` accumulates meshes by material object, so a forest still
|
||||
batches into a few draw groups without string keys or a registry. `game/level.ts`
|
||||
`placeTrees` selects from a weighted list of prefab objects. **Add a species** =
|
||||
add one concrete prefab module and include its object in level data.
|
||||
|
||||
**Boulders** (`game/actors/Boulder.ts`) work the same way: `Boulder.build`
|
||||
appends a squashed, per-vertex-jittered low-poly sphere (seam/pole-safe so it
|
||||
|
|
@ -317,7 +296,7 @@ colliders; add a new prop type by cloning the pattern (generator + scatter).
|
|||
|
||||
## Controls
|
||||
|
||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `game/player.ts`) · mouse
|
||||
WASD move · **Shift** run (speed from `Player.config` in `game/player.ts`) · mouse
|
||||
look (click canvas to pointer-lock) · **Space** jump · **1/2/3** switch look
|
||||
presets. FPS shown bottom-right. The room's north wall is open — walk out onto
|
||||
the terrain and toward the peaks.
|
||||
|
|
@ -349,7 +328,7 @@ job tmp dir, not the repo.
|
|||
## Roadmap / not yet built
|
||||
|
||||
In-browser RenderConfig slider panel; mipmaps; `painter` depth mode; gouraud
|
||||
lighting; more cloud types; more props / a weapon; more mob kinds + smarter mob
|
||||
lighting; more cloud types; more props / a weapon; more mob definitions + smarter mob
|
||||
behavior (they wander + block/stand-on today, but don't yet react to the player). `shared/`
|
||||
is nearly empty. The FPS meter is static HTML + `textContent` writes only — no
|
||||
DOM-built UI yet (deliberate).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue