Last Updated: 2026-03-30 (Yoshi shield HP + dismount via Shift/B button) Status: Early development — Galaxy-style gravity, character controller, Goomba enemies with AI, Yoshi NPC (shield HP, mount/dismount), Bowser Jr. boss (defeat sequence + victory flow), placement editor, coin system, logo letter platforms, visual polish (hemisphere lighting, env map, Fresnel rim), mobile performance (distance-based simulation culling, Rapier SIMD), AI-assisted level construction tools, Galaxy-style spin attack (combat, particles, hit-stop, rewards). ECS architecture review remediation complete (18 tasks).
A 3D browser game built with Three.js, Pixi.js (shared WebGL context), and Rapier physics. TypeScript monorepo with npm workspaces (apps/*, packages/*).
- Original 2817 monorepo:
/Users/tommy/Documents/two8one7/2817/— source of all@2817/*packages; port from here when bringing in new packages - Super Mario Movie 1 (SMM1):
/Users/tommy/Documents/two8one7/2817/apps/supermariomovie/— the previous Mario game; reference for game mechanics, level design patterns, and asset conventions - Star Spangled Solitaire:
/Users/tommy/Documents/games/star-spangled-solitaire/star-spangled-solitaire/— recently shipped game; reference for production patterns, game loop structure, and release polish - Library optimization audit:
plans/library-optimization-audit.md— performance fixes to apply when porting packages
- Rendering: Three.js (3D scene) + Pixi.js (2D UI overlays) sharing one WebGL2 context
- Physics: Rapier3D (WASM) — deterministic, fixed-timestep
- Game loop:
@2817/gameloop— fixed update (60Hz) with variable render and interpolation alpha - ECS:
@2817/ecs— custom SoA (Structure of Arrays) entity-component-system - FSM:
@2817/fsm— finite state machine with function states and transition queuing - Process:
@2817/process— composable sequential/parallel process orchestration. Read theprocess-system-designskill before writing any process code. - Audio:
@2817/audio— Web Audio API wrapper via@2817/resourcesasset pipeline - Frontend: Astro (static shell) hosting the canvas-based game
ecs-design-patterns— Universal ECS architecture principles. Read before writing any ECS code.ecs-2817-rapier-threejs— Practical ECS patterns for this stack. Includes/ecs-reviewchecklist.process-system-design— Authoritative patterns for@2817/process. A process IS a temporary update loop.ExecuteActionis instant only. Never pass the game loop around.level-construction-tool— Read before any level design work. CLI tool + dev server auto-save.- GLB Model Inspection — Never guess model properties. Run:
node ~/.claude/skills/glb-inspection/glb-inspection/scripts/inspect-glb.mjs <path>. Every model-derived constant must cite inspection output.
SoA storage: component[field][worldIndex][entityIndex] — three levels of array indirection. Direct array access is the only API (no getter/setter wrappers):
const x = Position.x[worldIdx][entity.index]
Position.x[worldIdx][entity.index] = 10For queries, IDs, and component events API details, see the ecs-2817-rapier-threejs skill.
- Systems are stateless functions — called each frame, return
(dt: number) => void - Entity state lives in components — cooldowns, timers, per-entity flags in SoA fields
- System-internal state lives inside the system — hysteresis maps, cached lookups, elapsed timers
- Shared dependencies are injected — only what is genuinely shared across multiple systems
- Config components over imperative setup — data describes intent, systems execute on
query.added()
These apply to all game-runtime code (anything in a game loop, system, or hot path):
- Zero allocations in hot loops — no object literals, array spreads, or closures per frame
- Direct array access over method calls —
arr[i]notarr.get(i) - Avoid Map.get() in per-frame code — cache lookups in local variables
for...ofover arrays is fine — V8 optimizes this; avoidforEachcallbacks- Mutable singletons for per-frame data — callers must not hold refs across frames
- Fixed timestep with accumulator cap —
MAX_DT = 0.25sprevents spiral-of-death
Fixed 60Hz update / variable render with interpolation alpha. Three.js renders first, Pixi on top (shared WebGL2 context). See docs/render-pipeline.md for pipeline optimizations, gravity shadow multi-pass, bloom, and occlusion silhouette.
createGame() in index.ts delegates to focused setup files. Key principle: composition root stays under ~600 lines. See docs/composition-root.md for file table, bridge systems, late-bound systems, and loop forwarding.
Per-entity (ECS): Enum-field pattern — numeric state field on component, SoA data fields, pure functions in system's if/else chain. Reference: DonutBlock (6 states), LiftPlatform (2 states).
Non-entity (UI, game flow): @2817/fsm library — createMachine<StateName, TransitionName>() with function states and queued transitions.
Detailed architecture for each feature lives in docs/:
| Feature | Doc |
|---|---|
| Camera system | docs/camera-system.md |
| Physics & gravity | docs/physics-and-gravity.md |
| Character controller & ECS systems | docs/ecs-systems.md |
| Spin attack | docs/spin-attack.md |
| Enemy AI | docs/enemy-ai.md |
| Coin system | docs/coin-system.md |
| Brick system | docs/adding-placement-objects.md |
| Debug editor | docs/debug-editor.md |
| Placement editor | docs/placement-editor.md |
| Logo letter platforms | docs/logo-letter-platforms.md |
| Render pipeline | docs/render-pipeline.md |
| Composition root | docs/composition-root.md |
| Scene manager | docs/scene-manager.md |
Rapier SIMD: @dimforge/rapier3d-simd via Vite alias in astro.config.mjs — all @dimforge/rapier3d imports resolve to SIMD at build time. 2-5x physics speedup, same API.
NPC System (Yoshi): Own collision group (GROUP_NPC = 0x0008). Placed via npc-spawn editor type. Eye material needs runtime polygon offset fix. Shield HP: Yoshi has ShieldHealth component (default 3 HP). When mounted, damage depletes shield before Mario's HP (overflow carries to Mario). Shield break triggers forced dismount + poof via shieldBreakResponseSystem. Shield HP persists on Yoshi across voluntary dismount/remount. Dismount triggered by Shift key (B button on gamepad/virtual controls). UI: inner ring in HP meter shows shield HP; center number shows total (Mario + shield). Mushroom healing priority: Mario HP first, then shield. Shared dismount helpers in dismount-helpers.ts.
Simulation Zone: Distance-based culling using the camera's focus point (camera pos + 8u along look direction), not player position — prevents entity pop-in during warp teleports. All enemies/NPCs get SimulationZone + Orientation except Bowser Jr (boss visibility managed by intro sequence). Never add to player or static objects. agentFacingSystem writes Orientation ECS fields (not display.quaternion) and snaps orientation on zone wake (prevZone >= 2).
Level Construction: CLI tool (tools/level.mjs) + dev server auto-save (vite-editor-save-plugin.mjs). Both read/write the same JSON files. Reload browser to see CLI changes.
- Path aliases in root
tsconfig.json(e.g.@2817/ecs→packages/ecs/src/index.ts) - Each package with tests needs its own
vitest.config.tswith resolve aliases matching root tsconfig npm install --legacy-peer-deps(koota benchmark adapter has React peer dep)- Debug features lazy-load via dynamic
import()behind?debugURL param
Hard-won lessons from bugs and edge cases. Full categorized list: docs/constraints.md. The most critical universal constraints:
- Never guess model dimensions — always run
inspect-glb.mjsfirst - Never wrap ECS array access in getter/setter functions — raw access is intentional
- ECS SoA arrays return
T | undefined— useas number/as booleanto assert - Files in
packages/use tabs — Edit tool struggles with tab matching; prefer Write for full-file rewrites - Never use
Group.clone()on GLTF with armatures — useSkeletonUtils.clone() - Initialize unused InstancedMesh slots with zero-scale matrices to prevent phantom rendering
- NEVER use
pow()inonBeforeCompileshaders through UnrealBloomPass — Apple Silicon flicker - Don't move Fixed body colliders after
characterControllerSystem— CC resolves against previous-step positions - Stored direction vectors must be re-projected to current tangent plane each frame
- Systems that smoothly rotate entities (slerp) must read/write
OrientationECS component, NOTdisplay.quaternion—displaySyncSystemoverwrites display from Orientation every frame - Hit-stop freezes game time (
gameDt=0) but NOT render time — guard Rapier step:if (gameDt > 0) physics.step() - Boss entities must NOT have
SimulationZone— breaks scripted intro sequences - Shadow system must read
GravityBody.shadowUpX/Y/Z(stable, quantized), NEVERupX/Y/Z(spring-damped, drifts continuously) - Shadow candidate collection must only run on shadow render frames — otherwise caster layer assignments desync with shadow maps
- Shadow field updates (
shadowFieldId) must defer until gravity spring converges — prevents oscillation during field transitions - InstancedMesh shadow pass assignment must iterate ALL instances (no sample cap) — instances span multiple gravity fields. Per-instance
instanceShadowPassattribute filters each instance to its correct pass in the vertex shader; object-level layer mask is a coarse pre-filter only - Shadow frustum size is derived from the camera's FOV/aspect/distance each frame — not a fixed constant. Ensures shadow coverage matches what's visible on screen
- When adding a new ECS component to the ecosystem, audit ALL entity creation files — easy to miss one (e.g. Bowser Jr was skipped when Orientation was added to all other enemies)
- When removing an entity in the same frame as spawning a poof, use
processManager.add(new PoofProcess(...))directly — do NOT use thePoofRequestcomponent, becausepoofSpawnSystemruns earlier in the frame (line 578) and won't see a request added after damage response (line 591). The death system follows this same pattern. replace_allin Edit tool replaces ALL substring occurrences including inside other words (e.g. replacing "pi" also hits "rapier") — use Write for full-file rewrites when renaming short identifiers
Active plans live in plans/. When working from a plan:
- Check the plan file for current status before starting work
- After completing any plan item, update the plan file — mark sections/rows as done with commit hashes
- If a plan step reveals new constraints, add them to the Constraints section above
Active plans:
plans/library-optimization-audit.md— porting and optimizing@2817/*packagesplans/ecs-refactor-phases.md— ECS refactor (7 phases). Check per-phase files for task-level status.
Completed plans:
plans/ecs-review-remediation.md— 18 tasks, all complete. Key changes: utility extraction, ShellManager, generic enemy scene factory, AIBehavior decomposition.
Update this file when:
- You make a mistake (add to Constraints or docs/constraints.md)
- You discover a pattern worth documenting
- Phase status changes