feat: bees and frogs

This commit is contained in:
Dan Finch 2026-08-07 12:49:13 +02:00
parent 46e7070b6b
commit 16f205babe
13 changed files with 540 additions and 31 deletions

View file

@ -77,28 +77,44 @@ rules live in `.agents/rules/*.md`.
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
`Boulder.build` appends into a shared mesh), `Bush` (cluster of small leaf
blobs, shares the oak leaf texture/mesh), `Flower` (thin stem + colored bloom;
samples a 2x2 color-atlas texture, drawn double-sided).
samples a 2x2 color-atlas texture, drawn double-sided), `Mob` (a **roaming**
creature — `frog` hops the ground, `bee` hovers/darts — the engine's only
moving geometry. Unlike the baked props, a mob's low-poly mesh is built once
per kind in **local space**; `Mob.build` bakes the two canonical meshes,
`Mob.update` steps the wander AI (leashed to a home anchor, deterministic via
an evolving per-mob seed) each frame, and the live `position`/`heading`/`scale`
become a per-frame model matrix at draw time).
- `app/` — browser glue.
- `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
interval, visible chunks / LOD-aware tri count — the real profiler in play).
Also owns the **mob sim**: each frame it steps `Mob.update` for every mob,
rebuilds the near-player mob colliders into `level.colliders`, and culls the
mobs (`visibleMobs`) so only the visible transforms get dispatched.
- `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 it renders inline. `dispatch`/`done`
are non-blocking so the caller paces on rAF. `?bench=st|mt` A/Bs the paths.
- `renderScene.ts``renderBand(fb, scene, …, y0, y1)`: the single source of
render truth (sky + room + culled chunks + sprite + quantize for a row band).
Used full-height by the inline path, per-band by each worker. `Scene` bundles
the meshes/textures so it clones to a worker whole.
are non-blocking so the caller paces on rAF. Per-frame inputs ride shared
arrays: camera/matrix/visible-chunk list, plus the visible **mob transforms**
(`mobState`, count in the `MOBVIS` control slot). `?bench=st|mt` A/Bs the paths.
- `renderScene.ts``renderBand(fb, scene, …, mobDraws, …, y0, y1)`: the single
source of render truth (sky + room + culled chunks + 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 two canonical mob
meshes) so it clones to a worker whole; each mob is drawn double-sided through
its own `viewProj × Mat4.compose(...)` model matrix, and `visibleMobs`
frustum-culls the moving mobs per frame.
- `assets.ts` — load `/assets/*.png``Texture` (zero-copy; ImageData bytes
are already the `Color` layout).
- `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`), then `buildChunks` bakes
`TREE_/BOULDER_/BUSH_/FLOWER_COUNT`/`_SEED`/`_REACH`) and the roaming mobs
scattered (`placeMobs`; `FROG_/BEE_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs move,
so they carry no baked colliders), then `buildChunks` bakes
terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each =
per-texture meshes grass/bark/leaf/needle/rock/flowers + a tight AABB) that
`main` frustum-culls; bushes fold into the leaf mesh, flowers get their own
@ -118,16 +134,19 @@ rules live in `.agents/rules/*.md`.
`#fps` meter div (styled inline).
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`.
- `assets/` — generated `floor/grass/bark/leaf/needle/rock/flower/wall/crate/npc`
- `assets/` — generated `floor/grass/bark/leaf/needle/rock/flower/wall/crate/npc/frog/bee`
PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`leaf`/`needle` =
tree trunk/oak/spruce, `rock` = boulders, `flower` = 2x2 bloom-color atlas).
tree trunk/oak/spruce, `rock` = boulders, `flower` = 2x2 bloom-color atlas,
`frog`/`bee` = mob skin atlases: frog green + eye tone; bee stripe bands +
head-dark + wing-pale regions).
Swap for real art anytime;
filenames are the contract.
- `server/` — Bun server stub. `shared/` — isomorphic slot.
## Frame pipeline (`app/main.ts` `tick`)
`Player.update` → build `Camera``Camera.viewProjection``visibleChunks`
`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) →
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
@ -148,8 +167,9 @@ floor/walls/crate (room, always) → for each visible `Chunk` draw grass, then
per chunk via the pure `chunkFar` test (dist² from camera to the chunk AABB vs
`lodDistance²`) — either the full rock/bark/leaf/needle + flowers, or the cheap
`rockFar/barkFar/needleFar/leafFar` impostor meshes (foliage detail dropped).
All backface-culled except double-sided flowers → `Sprite.billboard(npc)`
`Framebuffer.quantize`. `chunkFar` is pure (camera + baked bounds + config
All backface-culled except double-sided flowers → `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
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
@ -306,6 +326,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 / moving enemies. `shared/`
lighting; more cloud types; more props / a weapon; more mob kinds + 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).