Skip to content

Architecture

ArdaDDemir edited this page Jul 16, 2026 · 1 revision

Architecture

The canonical, always-current version of this page lives in the repository: docs/ARCHITECTURE.md. This page is the short orientation.

Three worlds that don't touch

The one idea worth carrying around: LuminaReef runs three systems at very different rates, and they talk as little as possible.

Rate Rules
React — HUD, trophies, toasts Rare (on campaign events) useReefStore(selector) is fine here
The frame loopuseFrame in 3D components 60 Hz Never allocate. Never subscribe. Read via getState()
LightCurrentField — the player's stroke Continuous, outside React A plain mutable class, passed by reference

LightCurrentField is the blackboard between your hand and the fish. The pointer writes to it, the fish read from it, the renderer draws from its Float32Arrays. It is deliberately not React state — a stroke updates many times a second, and routing that through setState would re-render the tree at 60 Hz.

The rescue, end to end

The whole game is this loop:

  1. CurrentController ray-picks a fish on pointer down and pins it as the sole responder, then feeds pointer positions into the field.
  2. FishSchool reports every lost fish's live position into the field each frame, which is what step 1 picks against.
  3. The pinned fish asks assessRescuePath() whether the stroke means it — starts at me, continuous, directional, reaches the Heart, doesn't wander.
  4. If valid, the fish rides sampleFlow(): a target ahead of it on the stroke, plus the stroke's tangent.
  5. It's rescued only after physically travelling ≥66% of the path and arriving within 3 units of the Heart.
  6. claimRescue() caps it at one fish per stroke, then store.rescueFish() computes the combo, Lumina, achievements and journal line.

Step 6 is why the frame loop stays clean: everything gamified happens once, in an event, not sixty times a second.

Layout

app/page.tsx              Dynamically imports the scene (ssr: false) + the HUD
components/3d/            Canvas, water, shafts, corals, fish, current, finale
components/ui/            HUD, trophies, toasts, journal, intro, finale card
lib/achievements.ts       Tide marks + combo and run-time rules
lib/restoration.ts        The five named gardens
stores/useReefStore.ts    One zustand store: campaign, combo, records, saving
docs/adr/                 9 ADRs — the design history
public/models/fish/       Web-optimized *_web.glb only (~270 KB each)

R3F can't be server-rendered, so the scene is client-only behind a deep-sea gradient placeholder — there's never a white flash.

The trap

Never enable vertexColors on the fish material. Their GLBs ship no COLOR_0 attribute, and an unbound colour attribute reads (0,0,0): black fish. Per-instance tinting comes from instanceColor.

See also

  • Performance — where the frames actually go
  • Customizing — the data files that shape the reef
  • ADRs — why any of this is the way it is

Clone this wiki locally