Skip to content

House rules

These are invariants the code relies on, with the file that holds each one.

  • One renderer change at a time. src/gpu/webgpu-renderer.ts (about 61k lines) is a shared hotspot. Work that touches it runs one package at a time; pure-module work may run in parallel.
  • The density function exists three times (WGSL src/gpu/density-mc-shader.ts, JS src/chunking/density.ts, Rust density_field.rs in the physics fork, shipped prebuilt in vendor/rapier3d/). Change all three together, layer order included. The lattice, hash and gate decisions are bit-exact; transcendentals may differ by a few f32 ULPs between JS and Rust by design. npm run test:densityparity pins the Rust output against a committed golden and bounds the JS/Rust delta.
  • Determinism. Seeded randomness goes through hashSeed/makeRng (src/structures/lcg-rng.ts), with ONE ’:‘-delimited string key per hashSeed call: hashSeed concatenates its parts with no separator, so ('ab','c') and ('a','bc') collide. No Math.random in game logic. An NPC key such as npc:{bodyId}:{townId}:{localIdx} names a slot, not a person: the roster is re-derived from the seed on every load, so data keyed on a slot must be dropped when its resident dies (forgetDead in src/game/dialogue/llm/llm-memory.ts does this for conversation memory). The world is regenerated from the seed; the save stores what the player changed.
  • Saves. SAVE_VERSION is 4 (src/game/save/game-save.ts); purely additive fields may arrive under the current version when they migrate by being absent, as that file documents. Anything loaded from a save that reaches the UI or a prompt is validated, never shape-cast. Every quest mutation sits behind _questsMutable().
  • Animation and combat. CharacterState (src/character/character-state-machine.ts) is append-only at the tail, so existing numeric state values stay stable. Attack properties are index-aligned tables in src/game/combat/attack-timing.ts, pinned by attackTablesAligned() in test:combat; never branch on index thresholds. GROUNDED_LOCO (src/animation/character-animator.ts) holds grounded locomotion only; air states never go in it. Every shipped character clip needs a recorded source: scripts/check-animation-provenance.mjs runs inside npm run build and fails on Mixamo data or an unrecorded clip.
  • Creature brains. In the creature steer brain (src/gpu/webgpu-renderer.ts), new steer branches sit below the fleeUntil branch and below _groundEscapeStuck, write behaviour, target and gait on the shared steer object, and never scan every active creature per creature.
  • Input and physics. While uiHold is set (src/character/character-input.ts), input is still polled so one-shot presses drain instead of firing after the UI closes, but the result is neutral. Rapier colliders are player-scoped: a far ray that returns nothing means “no information”, not “clear”, so distant queries use the density fallback. Spell lights never cast shadows (src/game/combat/spells.ts).
  • Spoken text. Anything Kokoro speaks has no digits and no em dashes; fixed menu labels go into PLAYER_STOCK_LINES (src/game/dialogue/deterministic-brain.ts) so they are synthesised ahead of time. src/game/dialogue/dialog-styles.ts and src/ui/window-sections.ts hold large template literals: no backticks inside them, comments included. After a scripted regex edit, scan for stray control bytes (below 0x20).
  • Gates. npx tsc --noEmit, npm run build, the test:* suites covering what you touched, and every suite a work package lists.

Written by release:site-sync from docs/rpg-next-steps.md at v2026.09.22. Edit the engine document, not this page.