meat/AGENTS.md
2026-08-04 12:51:07 +02:00

166 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# meat
A from-scratch browser **first-person-shooter game engine** with a configurable
**PS1 aesthetic**. Everything is hand-written: a **software rasterizer** draws
textured triangles into a low-res CPU framebuffer, then Canvas2D blits it
upscaled. No WebGL/WebGPU, no game-engine dependencies.
This file is the durable project brief (checked in, auto-loaded). The per-topic
rules live in `.agents/rules/*.md`.
## Guiding decisions (the "why", not derivable from code)
- **PS1 look is a set of live knobs**, dial-able from full-PS1 to clean. See
`RenderConfig`. Nothing about the era is hardcoded into the renderer.
- **Software rasterizer on purpose** — the PS1 look leans on rasterizer traits
(vertex snap, low-res + dither/banding, no mipmaps). Raytracing was considered
and cut (it removes the very artifacts we want). Note: **affine texture "swim"
was dropped** — texturing is always perspective-correct now (it warped badly on
the big outdoor terrain); the other era knobs stay.
- **Minimal architecture.** Scene-graph-lite / plain data + functions.
Deliberately **not** ECS or any "Big Game Architecture." Prefer the smallest
clear structure; add knobs to experiment rather than abstractions.
- **2D assets only.** Sprites/billboards (PS1-style), **no 3D model loading**.
- **Engine is headless.** `engine/` has no DOM types and could run server-side;
all browser glue (canvas, input, image decode) lives in `app/`.
## Stack & tooling
- **Bun** runtime + `bun test`. **TypeScript 7** (native `tsc`), strict,
`moduleResolution: bundler`. **Vite 8** serves/builds the client.
- **oxc** toolchain: `oxlint` + `oxfmt` (no eslint/prettier).
- commitlint + husky (Conventional Commits), OpenSpec change workflow,
forge-sync (Forgejo). Templates generated by `regime` from a `sigitex:` source.
## Commands
- `bun start` — Vite dev server; open the printed URL. Edits hot-reload.
- `bun run build` — production build to `dist/`.
- `bun run assets` — regenerate the placeholder PNGs in `/assets`.
- `bunx tsc --build tsconfig.app.json` — **typecheck the app+engine graph. Use
this**, not `bun run check` (see Caveats).
- `bunx oxlint engine app` — lint.
- `bun test` — tests (none yet).
- `bun run serve` — Bun server (`server/server.ts`, a stub for now).
## Layout
- `engine/` — headless engine, consumed by `app/` via tsconfig project ref.
- `math/``Vec2`, `Vec3`, `Mat4` (column-major, OpenGL-style; verified).
- `render/``Color` (packed RGBA, little-endian = canvas ImageData order),
`Framebuffer` (Uint32 color + Float32 1/w depth; `quantize` = color-depth +
Bayer dither), `RenderConfig` (the look dials + presets), `Rasterizer`,
`Texture` (nearest/bilinear, wrapping, no mipmaps), `Sky` (gradient + sun +
procedural clouds).
- `scene/``Camera` (fps yaw/pitch; far plane reaches the outdoor peaks),
`Mesh` (indexed tris), `Sprite` (Y-axis billboard), `Terrain` (procedural
heightfield around the room: flat clearing in the center, rolling hills, tall
edge peaks. `Terrain.ground` builds the outdoor mesh with a hole for the room;
`Terrain.height` is the shared ground-height sampler for the player).
- `app/` — browser glue.
- `main.ts` — game loop, input, preset switching, canvas blit, FPS meter.
- `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 walls,
north side open) in the center of a big grassy `Terrain` world (~20x across).
Per-texture meshes incl. the outdoor grass `ground`, `Aabb` colliders, NPC
position, `Terrain` config + `GROUND_DIVISIONS`/`GROUND_UV`, sky/cloud config.
The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it stays clean over the
terrain skirt that laps under the room edges. Room surfaces are single flat
quads -- no subdivision needed since texturing is perspective-correct; ground
triangle count is `GROUND_DIVISIONS` (fixed grid, so world size sets cell
chunkiness, not tri count).
- `player.ts` — feet-cylinder player: gravity/jump + circle-vs-AABB/-circle
collision; ground height comes from `Terrain.height` (plus standable AABBs).
- `index.html` — Vite entry at repo root; holds the `#screen` canvas and the
`#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/wall/crate/npc` PNGs (`floor` = room stone,
`grass` = outdoor ground). Swap for real art anytime;
filenames are the contract.
- `server/` — Bun server stub. `shared/` — isomorphic slot.
## Frame pipeline (`app/main.ts` `frame`)
`Player.update` → build `Camera``Camera.viewProjection`
`Sky.render` (fills color + resets depth, replaces a clear) →
`Rasterizer.draw` ground, floor, walls, crate (one call per texture) →
`Sprite.billboard(npc)` drawn via `Rasterizer.draw`
`Framebuffer.quantize``present` (integer-scale, letterboxed blit;
`imageSmoothingEnabled` follows `upscaleFilter`).
Rasterizer specifics: near-plane clip (Sutherland-Hodgman), **1/w z-buffer**,
perspective-correct UVs, screen-space vertex snap, flat directional lighting,
distance fog, **alpha cutout** (discard texel alpha < 128, for sprites),
**double-sided** (no backface culling).
## The look — where to tune
- **`engine/render/RenderConfig.ts`** per-frame render dials + presets:
`standard` (384×216, the startup default), `soft`, `clean`, plus an unbound
`ps1` (320×240). In-app keys **1/2/3** switch standard/soft/clean live.
Knobs: `internalWidth/Height`, `upscaleFilter`, `colorDepth`, `dither`,
`vertexSnap`, `textureFilter`, `lighting`, `fog`. (Texturing is always
perspective-correct the affine-swim dial was removed.)
- **`app/level.ts` `GROUND_UV`** (0.25) outdoor ground texture tiles per world
unit. Lower = the stone tiles bigger and less busy = less far-distance moire
(there are no mipmaps); higher = finer but shimmerier.
- **`app/level.ts` `GROUND_DIVISIONS`** (56) outdoor ground grid resolution and
the **main outdoor FPS lever**. The open vista is **transform-bound** on the
ground's triangles (no frustum culling every tri is projected each frame), so
cost is ~linear in this: measured ~45 fps at 48, ~28 fps at 64, ~24 fps at 96
(headless, `standard`). Lower it for FPS, raise for finer terrain. Draw distance
(`fog.far` + `Camera` far plane, pushed out to ~200/260 for this scene) is
comparatively cheap since far ground is a thin horizon band. Cranking
`TERRAIN.peakHeight`/`outer` costs almost nothing (same tri count).
## Clouds
Procedural, moving, in the sky pass (`engine/render/Sky.ts`). Two styles picked
by a `CloudLayer` discriminated union `kind`:
- **`basicCumulus`** flat hard-thresholded white puffs, 1 noise lookup/pixel.
- **`fancyCumulus`** domain-warped + heightfield-shaded fake volume,
~5 lookups/pixel (pricier; watch the FPS meter).
Both are exported presets in `app/level.ts`; the active one is set in
`buildLevel`'s `sky.clouds`. Add new cloud types by extending the union and
branching in the cloud shader. Cost scales with sky resolution fine at
`standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky).
## Controls
WASD move · 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.
## Code style (oxlint-enforced — match it)
No semicolons, double quotes, 2-space indent, trailing commas. **`type`, never
`interface`.** Function declarations (arrows allowed). Uppercase hex (`0xFF`).
Curly braces required on all `if`. No `void` operator. `T[]` not `Array<T>`.
Prefer `globalThis` over `window`. Domain behavior lives in a `type` +
matching `namespace` (see `Vec3`, `Color`, `Rasterizer`).
## Caveats
- No mipmaps, so distant textures shimmer (period-correct); far cloud/geometry
is hidden by fog.
## Debugging the renderer without a browser (key workflow)
The engine is pure, so you can **render headless to a PNG and inspect it**. A
throwaway `bun` script can: `buildLevel()`, decode `assets/*.png` (they're
filter-0 RGBA trivial to inflate), set a `Camera` pose, run
`Sky.render` + `Rasterizer.draw` into a `Framebuffer`, encode the color buffer
to PNG (same hand-rolled encoder as `scripts/gen-assets.ts`), write it, and read
it back. Sampling individual pixels this way tuned the clouds, framed the terrain
peaks, and confirmed the ground textures flat (no swim). Put temp scripts in the
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/`
is nearly empty. The FPS meter is static HTML + `textContent` writes only no
DOM-built UI yet (deliberate).