346 Commits

Author SHA1 Message Date
math-pixel 1325b7b2af Merge branch 'develop' into feat/polish-mission-2
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-03 02:02:37 +02:00
Tom Boullay c2f55e3a2f feat(site): sync naming typewriter to last subtitle cue
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Replace the audio "ended"-based trigger with an SRT-driven one: the
typewriter now starts (lastCue.endTime - typewriterDuration) seconds
into the dialogue so the final letter lands at the moment the
narrator finishes speaking. Char delay shortened from 110ms to 70ms
for a snappier reveal. Fallbacks: audio "ended" when no SRT, 8s
absolute timer otherwise.
2026-06-03 01:56:14 +02:00
math-pixel 63c2b294c1 Merge branch 'develop' into feat/polish-mission-2
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-03 01:52:20 +02:00
Tom Boullay 62d0dcf531 upatde; config ebike
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-03 01:48:31 +02:00
Tom Boullay c75c4e0be6 fix(site): keep white card border visible when selected
Replace the border swap with an outer green outline so the white
border stays in place. Selected = white border + green outline,
unselected = white border only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 01:46:13 +02:00
math-pixel 10b0d4fc16 outro anim + vid 2026-06-03 01:45:43 +02:00
Tom Boullay 5f113cbba4 feat(tutorial): add movement and hand-tracking onboarding overlays
Mount two first-time tutorial overlays driven by the game state machine:

- MovementTutorial: visible during the intro reveal and the free-walk
  step before the ebike mount, dismissed on the first Z/Q/S/D keydown.
- HandTrackingTutorial: visible during the early ebike repair steps
  (fragmented, scanning, inspected), dismissed when MediaPipe detects
  any hand on screen.

Both share a generic TutorialOverlay shell (transparent panel, dark
blue border, lucide-react Hand / inline ZQSD keycap icons, centered
text). The overlay sits at z-index 14, behind Subtitles (15) and
the talkie overlay (16), so dialogue/subtitle UI stays in front.

Dismissals stay persistent for the session: keyboard-triggered uses
event-handler setState; hand-detection uses a guarded effect-driven
setState (same pattern as PylonDownedPylon resync).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 01:43:25 +02:00
math-pixel b1037d5107 split narrator srt 2026-06-03 01:01:48 +02:00
Tom Boullay 1cc3b0e47e feat(audio): swap to repair music while a mission is in repair flow
Switch background music to musique-reparation.mp3 whenever any mission
(ebike / pylon / farm) is in the repair mini-game step range
(inspected → done) and back to musique-jeu.mp3 once the mission
leaves that range. Reuses AudioManager.playMusic which handles the
swap cleanly when the path changes.
2026-06-03 01:00:29 +02:00
Tom Boullay 00b1ff9e93 fix(ebike): unlock walking during breakdown + hide interact prompt + 450m ride
Three fixes for the ebike-breakdown substep:

1. PlayerController: the previous `if (!isEbikeBreakdown)` guard
   zeroed _wishDir for everyone during breakdown, including the
   player after they had been auto-dismounted to walk mode. Narrow
   the guard to `isEbikeMounted && isEbikeBreakdown` so the bike
   stops accepting drive input but the player on foot can move.

2. Ebike: track `window.ebikeBreakdownActive` in component state
   and hide the InteractableObject (and therefore the interact
   prompt UI) while the breakdown sequence is active. The bike must
   read as inert and non-interactive while the panne dialogue plays
   and during the auto-dismount that follows.

3. ebikeConfig: bump EBIKE_INTRO_BREAKDOWN_DISTANCE from 15 m to
   450 m so the panne triggers after a real ride instead of a few
   meters from the parked spawn.
2026-06-03 00:46:30 +02:00
Tom Boullay 675a45f02b Update ebikeConfig.ts 2026-06-03 00:44:00 +02:00
Tom Boullay bbae199105 docs(handtracking): document SVG-primary path and isFist origin
Reflect the current runtime in docs/technical/hand-tracking.md:

- SVG visualizer is now the primary hand UI; the 3D glove is opt-in
  via the Show Model debug toggle.
- Reorder the runtime flow to put HandTrackingVisualizer before
  HandTrackingGlove and make explicit that grab, fist detection, SVG
  and optional 3D glove are independent consumers of the same
  landmark snapshot.
- New Fist Detection section showing how isFist() in
  browserHandTracking.ts derives the flag from landmarks alone (palm
  centroid + 4 fingertip distances), and confirming GrabbableObject
  reads that flag directly - no glove involvement.
- Describe the SVG visualizer styling and the feMorphology outline
  trick.
- Mark HandTrackingFallback and the gant_l/_pad assets as legacy in
  the limitations list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 00:42:14 +02:00
Tom Boullay c4cad629c9 feat(handtracking): redesign SVG hand as primary visualization
Rewrite the live hand visualizer as a light-blue silhouette with a
crisp dark-blue outline, suitable as the primary hand UI (replacing
the buggy 3D glove for the default flow):

- Palm polygon (landmarks 0,1,5,9,13,17) and five finger tubes merged
  via an SVG feMorphology filter, so the outline is a single
  continuous ring with no internal seams.
- Q curves bow out to two synthetic wrist corners (perpendicular to
  the palm centerline) for a rounded heel of palm.
- Straight L edges between MCPs along the top - the filter dilation
  rounds the corners visually, no creux.
- Each finger path starts half a stroke inside the palm so the round
  base cap is hidden under the palm fill.
- Whole silhouette shrunk to 65% of the tracked hand size around the
  centroid, with 0.8 group opacity, and a faint MediaPipe skeleton
  overlay (lines + dots) on top.

Update the static fallback silhouettes (HandTrackingFallback) to a
matching curved-path look in a 100x120 viewBox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 00:42:05 +02:00
math-pixel 18fb5e39e9 edit electricienne + poto 2026-06-03 00:05:07 +02:00
Tom Boullay ff4ead1d24 fix(lint): satisfy react-hooks immutability + set-state-in-effect rules
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
The new react-compiler-aware lint rules flag legitimate Three.js
external-system synchronizations (texture/uniform/AnimationAction
mutations) and a derived-state reset in PylonDownedPylon. None of
these are bugs — they're the canonical way to bridge React state
with imperative graphics objects — so they're annotated with
targeted eslint-disable comments and a small reorder.

- EbikeGPSMap: disable on uniform/texture sync effects
- EbikeSpeedmeter: disable around the canvas+texture useFrame sync
- PylonFarmerNPC: disable around playAnim (drei AnimationAction
  fadeIn/fadeOut/setLoop/clampWhenFinished) and the effects/frame
  callbacks that invoke it
- PylonDownedPylon: move showUpright/isPylonInteractive declarations
  above the useFrame that reads them (fixes access-before-declared)
  and disable set-state-in-effect on the per-step isRaised reset
2026-06-03 00:04:14 +02:00
Tom Boullay 974f340d33 style: prettier reflow pylon config and lighting effect
Mechanical formatting cleanup carried over from the develop merge:
inline single-line tuples and break long lines per project prettier
config. No behavior change.
2026-06-03 00:03:59 +02:00
Tom Boullay c6283d492c refactor(debug): rename hand-tracking SVG toggle to Model
The debug control now reflects what it actually gates: the 3D hand
model rendering (used by World.tsx to decide whether to show the
hand-tracking gloves), not the legacy SVG visualizer.

- Debug.ts: rename showHandTrackingSvg → showHandTrackingModel
  (state, GUI label "Show Model", getter/setter)
- World.tsx: gate showHandTrackingGloves on the new toggle and
  drop the unused HandTrackingGloveHandedness import
2026-06-03 00:03:44 +02:00
Tom Boullay 83194df14f fix(ebike): allow player input during mount/dismount camera transition
Add lockInput option (default true) to animateCameraTransformTransition
so ebike mount/dismount can keep player input active during the 1s
camera tween instead of locking via setCinematicPlaying.

Also drop the unused camPointPos/dropPointPos debug vars and the
matching debugRestingPosition state — the consuming JSX has been
commented out for a while.
2026-06-03 00:03:29 +02:00
Tom Boullay 918ee49d7c Merge branch 'develop' of https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 23:47:10 +02:00
Tom Boullay c0e7567849 fix(ebike): hide interact prompt while actively riding the bike
While the player is mounted on the e-bike and pressing a movement key,
the persistent 'Descendre du bike' prompt was visible on screen and
polluted the view during gameplay. The InteractableObject is now
unmounted as soon as window.ebikeDriveInputActive flips to true and
remounted the moment the bike comes to a stop.

The driving signal is read in a useFrame and only flips React state
on transitions, so this adds zero per-frame re-renders.
2026-06-02 23:36:13 +02:00
Tom Boullay 931308c92c fix(ui): tone down InteractPrompt and support empty label
- Smaller boxes (36x36 key + 36px-tall label) instead of the previous
  oversized white pills.
- Dark translucent background (rgba(10, 12, 20, 0.55)) with a 1px
  white outline (rgba(255, 255, 255, 0.7)), no border-radius and
  white text so the prompt blends with the dark UI instead of being a
  bright blob over the 3D scene.
- Key cube now has a 3D keyboard-key effect (inset top highlight +
  inset bottom darkening + small bottom drop) so it reads as a
  physical key.
- Key and label are visually separated (gap: 8px) but share the same
  height for alignment.
- InteractPrompt no longer renders the label box when focused.label is
  empty/whitespace, so callers can show the key prompt alone.
2026-06-02 23:27:07 +02:00
Tom Boullay 4e1ca708b2 docs(repair-game): document focus bubble + recursive explosion drill
- Add RepairFocusBubble + useRepairFocusStore to the main files table.
- New 'Focus Bubble' section documenting the shroud lifecycle, the
  cocoon decor pass and the vegetation/zone-overlay hide hook.
- Update the 'Fragmented' section to describe the recursive descent in
  ExplodedModel.createParts and the new modelRotation field used to
  align the fragmented model with the world-space source.
- Drop the stale reference to useRepairMovementLocked (removed in a
  prior commit).
2026-06-02 23:00:30 +02:00
Tom Boullay ca6c8e00b6 feat(repair): hide vegetation and zone overlays during repair focus
When the repair focus bubble is active the vegetation system and zone
debug visuals are unmounted so trees and gizmos don't clip through the
dark sphere shroud. Terrain, water, sky, clouds and grass remain
visible behind the bubble per Option (a).
2026-06-02 22:59:04 +02:00
Tom Boullay 220a661d6d feat(repair): introduce focus bubble shroud for repair mini-game
Adds a dark expanding sphere around the repair model when the player
enters the immersive repair phases (fragmented / scanning / repairing /
reassembling). The bubble grows from 0 to 10m using GSAP expo.out over
2.5s and reverses on focus end, visually isolating the player from the
surrounding map.

- New useRepairFocusStore tracks active state + world center.
- New RepairFocusBubble renders a BackSide sphere shell + a soft cocoon
  decor pass (grid floor + directional light + ambient) inside.
- RepairGame drives setFocus from its lifecycle effect.
- Mounted in both GameStageContent and TestMap so behaviour matches in
  the production scene and the physics test scene.

Also drops the now-unused EBIKE_CONFIG_KEY constant in
GameStageContent.tsx (leftover from a previous remount-key strategy).
2026-06-02 22:57:18 +02:00
math-pixel 0a3966a339 animate and fix electricienne
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 22:53:34 +02:00
Tom Boullay be5d03a30c feat(ui): redesign InteractPrompt per Figma DA
- Larger label box and key cube on white-translucent backgrounds
  (rgba(255, 255, 255, 0.92)) with black Inter 900 text and rounded
  12px corners + soft drop shadow.
- Move from bottom: 30% to bottom: 12% so the prompt sits closer to
  the visual center of attention near focused world objects.
- Key cube grown 24x24 -> 64x64 / font 13 -> 32, label padding 0 ->
  16x24 / font 13 -> 22, both bold instead of regular.
2026-06-02 22:53:06 +02:00
Tom Boullay ed0683d814 feat(ebike): rename interact label to 'Lancer le repair game'
Clarifies that interacting with the parked Ebike during the ebike
mission opens the repair mini-game rather than directly performing
a repair action.
2026-06-02 22:52:00 +02:00
Tom Boullay d9a92e336c fix(repair): drill explosion to natural group + apply mission rotation
- ExplodedModel.createParts now descends recursively through single
  mesh-bearing wrapper nodes (e.g. Scene > Moto > Eclatement) until
  reaching a node with multiple mesh-bearing children. Previously the
  first wrapper was used as root, so models with extra Empty/group
  parents fell back to flat leaf meshes lerping in local space.
- Add optional modelRotation field on RepairMissionConfig so fragmented
  + repairing models can match the world-space rotation of the source
  inspection model (parked Ebike).
- Ebike mission now uses EBIKE_WORLD_ROTATION_Y/EBIKE_WORLD_SCALE
  directly so the fragmented bike lines up with the parked bike.
2026-06-02 22:51:35 +02:00
Tom Boullay 89050331df chore(electricienne): switch to idle/walk animations
Replaces the placeholder Dance animation set on the electricienne
character with the standard idle/walk loop used by the other animated
NPCs.
2026-06-02 22:15:36 +02:00
Tom Boullay 0f211cc169 chore(format): apply prettier formatting 2026-06-02 22:15:25 +02:00
Tom Boullay 6a0215d1a6 fix(repair): keep ebike at zone Y in test scene
Adds an opt-out 'snapToTerrain' prop on Ebike so the parked position
keeps the explicit Y supplied by callers instead of resolving against
the world terrain GLTF. TestMap passes snapToTerrain={false} since it
does not render the world terrain — without this the bike was being
positioned at the invisible terrain height, far above the test floor,
and looked missing.
2026-06-02 22:10:31 +02:00
Tom Boullay 2a6a028e1d revert(repair): remove player movement lock during repair
Drops the useRepairMovementLocked hook, the RepairMovementLockIndicator
overlay, and all PlayerController gating tied to repair sub-states.
The repair flow no longer freezes player movement or shows a lock
banner; the player keeps full control while interacting with the case.
2026-06-02 22:04:05 +02:00
Tom Boullay a609314411 feat(repair): mount Ebike on TestMap and snap repair to parked position
The Physique test scene now mounts the real Ebike component for the
ebike repair zone, mirroring GameStageContent so the bike model and
its interactions (mount/dismount, parked position tracking) are
available when testing the repair flow.

RepairGame derives its live world position from
window.ebikeParkedPosition once the ebike mission leaves the
locked/waiting phase, so the repair sequence happens wherever the
player parked the bike rather than at the static zone anchor.
2026-06-02 22:00:01 +02:00
Tom Boullay d1665891f4 feat(repair): filter debug sub-state options by current mission
Pylon-only mission steps (approaching/arrived/npc-return/narrator-outro)
no longer appear in the GameStateDebugPanel sub-state dropdown for the
ebike or farm missions, which use the shorter
locked/waiting/inspected/fragmented/scanning/repairing/reassembling/done
flow.
2026-06-02 21:59:54 +02:00
math-pixel eb5d4076d1 la correction de merde x)
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 20:54:16 +02:00
math-pixel 5177f43d96 Merge branch 'develop' into feat/polish-mission-2
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 20:43:00 +02:00
math-pixel 7f37f9a747 Merge branch 'develop' into feat/e-bike
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 20:36:04 +02:00
math-pixel ff1ec56729 Merge branch 'develop' into feat/polish-mission-2 2026-06-02 20:27:48 +02:00
math-pixel 386abf06b6 Merge branch 'develop' into feat/e-bike 2026-06-02 19:23:01 +02:00
math-pixel a73f9fb951 fixed ebike 2026-06-02 19:21:52 +02:00
Tom Boullay d29b01e398 feat(repair): broken parts spawn from exploded model node positions
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Render the exploded mission model during the repairing step so broken
nodes (e.g. ebike refroidisseur) stay visible in the world, and surface
their world positions to RepairRepairingStep so broken pieces spawn
from where they belong on the model rather than from a static offset.

- ExplodableModel: add hideNodeNames (mesh visibility off, restored on
  unmount) and nodeAnchorNames + onNodeAnchorsChange (per-frame world
  positions debounced via signature so React state updates only when
  the values actually move).
- RepairGame: render ExplodableModel during 'repairing' with the broken
  node names hidden + anchors forwarded; threads brokenAnchors state
  to RepairRepairingStep.
- RepairScanSequence + RepairRepairingStep: propagate targetNodeName
  through scanned and fallback broken-part lists.
- RepairRepairingStep: broken parts spawn at brokenAnchors[targetNodeName]
  when set, falling back to legacy BROKEN_PART_START_OFFSETS otherwise.
2026-06-02 19:12:38 +02:00
Tom Boullay 6edc5f7972 docs: refresh hand-tracking notes and drop context-lost investigation 2026-06-02 19:06:32 +02:00
Tom Boullay ae35eb1dfb feat(handtracking): restyle svg visualizer and add silhouette fallback 2026-06-02 19:05:39 +02:00
Tom Boullay 4de86f4e58 feat(repair): align mission data with new pylone glb and broken-part workflow
- Update pylone path references from /models/pylone/model.gltf to .glb
  (galleryModels, mapInstancingConfig, repairMissions). The new glb
  exports a single cable node 'cable2' (no cable1).
- Pylon brokenParts: drop lampe and panneau2; the pylon design no longer
  has detached broken pieces (the cable is just missing from the model).
- Pylon replacement cables target the pylon's 'cable2' node world position
  via targetNodeName (used by the upcoming broken-anchor wiring).
- Ebike refroidisseur replacement and brokenPart both target the bike's
  'refroidisseur' node so the broken piece spawns from its original
  location and the replacement can install in the same slot.
2026-06-02 19:01:47 +02:00
Tom Boullay 5b123f9704 feat(repair): soft-lock mutually exclusive replacement parts
When a replacement part with a caseLockGroup is grabbed, sibling parts
sharing the same group become non-interactable and ghosted (35% opacity)
until the held part is released. This implements the pylon cable choice
where the player picks either cable1 or cable2 (both valid) without
being able to grab both simultaneously.

- GrabbableObject: add disabled prop (skips interaction frame logic and
  unmounts InteractableObject so it does not register with the manager)
  and onGrabChange callback fired on press, release, hand grab, and hand
  release. Force-releases when disabled becomes true mid-grab.
- SimpleModel: add opacity prop, traversed onto cloned mesh materials
  (safe because cloneResources clones materials per instance).
- RepairObjectModel: forward ghosted prop as opacity 0.35.
- RepairRepairingStep: track heldPartByLockGroup and pass disabled +
  ghosted to siblings of the currently held part.
2026-06-02 18:46:34 +02:00
Tom Boullay d1bf438465 feat(repair): inject ebike + pylon parts at packderelance anchors
- Ebike replacement parts: cooling core (correct, anchored at refroidisseur)
  + four distractors anchored at cabledroit/cablegauche/pucehaut/pucebas.
  Removes the ad-hoc gant_l/talkie distractors in favor of consistent
  case-anchored visuals.
- Pylon replacement parts: cable1 + cable2 (alternative correct, both with
  caseLockGroup 'pylon-cable' for upcoming soft-lock) + refroidisseur and
  two puce distractors anchored to packderelance.
- Farm replacement parts kept as-is (caseAnchor undefined falls back to
  placeholder slot positions for backward compatibility).
- RepairGame threads anchors from RepairCaseModel through RepairMissionCase
  to RepairRepairingStep; replacement-part initial position now resolves
  to the anchor world position when caseAnchor is set, falling back to the
  legacy slot index otherwise.
2026-06-02 18:37:12 +02:00
Tom Boullay d2ce990165 feat(repair): support multiple required parts and per-part case anchor
- RepairMissionConfig.requiredReplacementPartId (string) is replaced by
  requiredReplacementPartIds (readonly string[]) so a mission can accept
  several alternative correct parts (e.g. pylon will accept either cable).
- RepairMissionPartConfig gains optional caseAnchor (where the standalone
  spawns inside packderelance), caseLockGroup (mutually exclusive parts),
  and targetNodeName (snap onto a node of the broken model rather than a
  placeholder slot in the case).
- RepairScannedBrokenPart gains targetNodeName so scan results can carry
  this hint through to the repairing step.
- RepairRepairingStep validation logic (placed/wrong/feedback) now matches
  any id in requiredReplacementPartIds. Existing data is migrated mechanically
  (single-element arrays); part-level new fields are wired in subsequent
  commits.
2026-06-02 18:26:45 +02:00
Tom Boullay 7d2a257e84 feat(repair): expose case part anchors and fix lid node name
- Fix REPAIR_CASE_LID_NODE_NAME from "partiesup" to "partsup" (the actual
  node name in packderelance.gltf), restoring the lid open/close animation
  that was silently no-oping since introduction.
- Add REPAIR_CASE_PART_ANCHOR_NAMES (cabledroit, cablegauche, pucehaut,
  pucebas, refroidisseur) and REPAIR_CASE_PART_ANCHOR_FALLBACKS for
  case-local positions used when the GLTF lacks a node (refroidisseur).
- RepairCaseModel now resolves these anchor nodes on mount, hides existing
  meshes underneath them, and creates lightweight Object3D placeholders for
  missing names so the anchoring pipeline is uniform.
- Each frame, anchor world positions are converted to step-local space and
  emitted via the new onAnchorsChange callback (debounced via signature like
  placeholders). Consumers added in subsequent commits.
2026-06-02 18:20:43 +02:00
Tom Boullay 58eb60292f update: model pylone 2026-06-02 18:01:14 +02:00
Tom Boullay 73c6d7d50d fix(handtracking): bump browser camera to 640x480 for detection
The browser MediaPipe model (hand_landmarker.task float16) was failing
to detect hands at 320x240 — MediaPipe ran for 10s straight without
ever returning a hand. Bumping the requested camera resolution to
640x480 (browser only — backend still ships 320x240 JPEGs over the
WebSocket) makes the model reliably pick hands up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:39:43 +02:00
Tom Boullay d9cacdad12 fix(handtracking): stabilize provider root and linger enabled
(1) HandTrackingProvider always renders the same JSX root
(HandTrackingRuntime) so toggling `enabled` no longer remounts the
<Canvas> below — that remount was destroying the WebGL context every
time the player entered an interaction zone.

(2) Add HAND_TRACKING_LINGER_MS (2s) cooldown on `enabled` so brief
walk-throughs of a trigger zone don't tear down MediaPipe before it
has time to initialize the webcam + model + first frame (cold start
~800ms).

Resolves the WebGL context lost + respawn loop and restores visible
hand tracking in the backend runtime. Browser JS runtime detection
quality is a separate follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:27:04 +02:00
Tom Boullay ab88ab722f Update intro.mp4 2026-06-02 17:24:56 +02:00
Tom Boullay a30a9a2d29 fix(handtracking): absorb React StrictMode double-mount
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
In dev, <StrictMode> intentionally mounts → unmounts → remounts each
effect to surface non-idempotent code. The hand tracking hooks were
calling getUserMedia and creating MediaPipe / WebSocket runtimes on
every mount, which in practice ran the full start/stop/start cycle
inside a few milliseconds and pushed WebGL over its limit on top
of the loaded scene → context lost.

Add HAND_TRACKING_RUNTIME_START_DELAY_MS (80ms) and delay the actual
start() call behind a setTimeout in both useBrowserHandTracking and
useRemoteHandTracking. The cleanup clears the timer, so a fast
mount/unmount never reaches start(). 80ms is invisible to the user
(<5 frames at 60fps) and also absorbs rapid `nearby` toggles at
trigger borders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:54:28 +02:00
Tom Boullay d217c3376b fix(handtracking): reduce GPU pressure on WebGL context loss
Several mitigations against the WebGL context lost that fires when
hand tracking starts on a loaded scene:

- Canvas: fixed DPR [1,1], antialias off, scoped id="game-canvas",
  context-lost handler releases MediaPipe and logs GPU memory counters
- optimizeGLTFScene: cap anisotropy at 2 and stop forcing mipmaps /
  needsUpdate on every pass — avoids massive texture re-uploads
- MediaPipe: force CPU delegate (HAND_TRACKING_BROWSER_DELEGATE),
  cache the landmarker instance, and expose releaseBrowserHandLandmarker
- useBrowserHandTracking / useRemoteHandTracking: idempotent cleanup
  guarded by a cleanedUp flag, try/catch around the detect loop, and
  release of the landmarker on stop
- World: mount HandTrackingGlove only when the matching hand is
  actually present in the snapshot (status connected + hands.length > 0)
- HandTrackingGlove: drop the eager useGLTF.preload that was running
  at startup whether or not hand tracking was used

Does not yet absorb the React StrictMode double-mount — that is the
follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:48:39 +02:00
Tom Boullay 864e075b42 chore(debug): keep hand tracking source controller display in sync
Store the lil-gui Source controller so setHandTrackingSource() from
the settings menu can refresh its display, and log the transition
for traceability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:34:05 +02:00
Tom Boullay 3fe5b32de2 fix(player): scope pointer lock and ground snap to game scene
PointerLockControls now targets #game-canvas and respects the
settings menu, and document.exitPointerLock() only runs when a
pointer lock is actually active. The terrain ground snap in
PlayerController is gated on sceneMode === "game" so it doesn't
fight the physics test scene's flat floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:33:54 +02:00
Tom Boullay 72cb9f5be6 update: ebike model 2026-06-02 16:33:42 +02:00
Tom Boullay f708c4cd2e chore(debug): tune physics test scene and drop noisy waypoint logs
Lower grabbable spawn, expose GPS preview position/rotation as
constants, fix physics spawn to use PLAYER_EYE_HEIGHT, and silence
the console.log noise around waypoint loading in TestMap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:33:39 +02:00
math-pixel 193fc8b4b6 update 2026-06-02 16:31:36 +02:00
Tom Boullay c61760dafd docs: document webgl context lost investigation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:02:58 +02:00
Tom Boullay 18dd2ae49d fix(electricienne): remove alphaMode BLEND + doubleSided causing face artifacts
Solid character mesh does not need transparency; BLEND combined with doubleSided
caused depth-sort issues rendering inside-faces over front-faces (visible as
triangular orange artifacts on the face). Default OPAQUE single-sided is correct.
2026-06-02 15:36:43 +02:00
Tom Boullay def0609383 chore(talkie): consolidate to single GLB, remove orphan LOD assets
- Replace public/models/talkie/* (PNG textures + .bin + .gltf) with single model.glb
- Delete unused public/models/talkie-LOD/ folder (no longer referenced in MAP_LOD_MODEL_PATHS)
- Remove farm-radio-distractor (talkie) from farm repair mission distractors
2026-06-02 15:36:20 +02:00
Tom Boullay 19a1d20a97 fix(repair): remove talkie LOD + drop talkie distractor from ebike
- Talkie does not need a LOD swap; remove the entry from
  MAP_LOD_MODEL_PATHS so the same model.glb is used at any distance.
- The ebike repair distractors are meant to be other disassembled bike
  parts, not random props. Drop the talkie radio distractor and keep
  only the (thematically plausible) insulation glove alongside the
  correct cooling core replacement.
2026-06-02 15:27:30 +02:00
Tom Boullay 49ebacfbfb chore(talkie): point full-detail talkie path at model.glb
The talkie folder now ships a single binary GLB; update the four call
sites (TalkieModel, gallery, two repair mission entries) to load
model.glb. The talkie-LOD path is unchanged.
2026-06-02 15:06:44 +02:00
Tom Boullay 68253fae41 Update lightingConfig.ts
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 14:52:54 +02:00
Tom Boullay 2dabb73d3d fix(vegetation): scale-correct sapin and buisson LOD meshes
The exported sapin-LOD and buisson-LOD models do not match their full
detail counterpart's baseline scale, so they appear oversized once the
LOD swap kicks in. Add MAP_LOD_SCALE_MULTIPLIERS keyed by map name
(sapin: 0.5, buisson: 0.8) and apply it on top of the chunk's existing
scaleMultiplier whenever the resolved model path is the LOD variant.
2026-06-02 14:50:07 +02:00
Tom Boullay 4f1b3b4ff3 fix(graphics): tune presets, single-line ui, vegetation LOD by nearest instance
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
- Bump high to 30m chunk / HD<20m and ultra to HD<30m so HD models
  persist further before swapping.
- Render the 5 graphics preset cards on a single row.
- Vegetation LOD selection now uses the distance to the nearest instance
  in each chunk instead of the chunk centre, matching MapInstancingSystem
  and avoiding premature LOD swaps when the camera enters a chunk.
2026-06-02 14:33:16 +02:00
Tom Boullay 627c8d4eb9 feat(vegetation): wire arbre/sapin/buisson into LOD system
Register the three new vegetation LOD models in MAP_LOD_MODEL_PATHS and
extend VegetationSystem with per-chunk distance-based LOD selection
(mirroring MapInstancingSystem). Chunk model paths are re-evaluated on
the existing CHUNK_CONFIG.updateInterval cadence and Suspense keys
include the resolved path so a swap unmounts the previous instanced mesh
cleanly.
2026-06-02 14:17:19 +02:00
Tom Boullay 0b801888f0 update: fix name assets + add LOD 2026-06-02 14:13:55 +02:00
Tom Boullay a180b89ee6 fix(pylone): convert lampe BLEND to MASK in GLTFs
Switching alphaMode from BLEND to MASK with a 0.5 cutoff lets the lampe
material participate in instanced shadow rendering correctly. BLEND is
known to interact badly with InstancedMesh shadow casting, producing
incorrect or missing shadows on the pylone lampe.
2026-06-02 13:52:08 +02:00
Tom Boullay 3e66e31117 feat(graphics): add max preset (no chunk streaming, LOD@50m)
Restore ultra to its original behaviour (50m chunk streaming, HD within
20m, no fog) and introduce a new max preset that disables chunk streaming
entirely (loads all chunks unconditionally) and pushes the HD/LOD swap
distance to 50m. Add chunkStreamingEnabled flag to GraphicsPresetConfig
so the streaming gate honours the preset. The settings card label shows
'All' when streaming is off.
2026-06-02 13:51:33 +02:00
Tom Boullay 2c194cdd2e fix: use player ebike speed
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-06-02 09:48:44 +02:00
Tom Boullay feaf502e44 feat: support webm mission notifications 2026-06-02 09:40:18 +02:00
math-pixel 489499f5d2 Merge pull request 'Feat/polish-mission1' (#12) from feat/polish-mission1 into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #12
2026-06-01 21:51:09 +00:00
Tom Boullay 39b996eb31 Update GameMapCollision.tsx
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 23:38:19 +02:00
Tom Boullay 134c0aecb7 fix(world): reallocate shadow map after Suspense + clear LaFabrik doorway
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
Shadows occasionally failed to render on initial load and the Fabrik
doorway sometimes blocked the player. Both issues are tracked down to
geometry that mounts after Lighting:

- Shadows: GLTFs and the merged static map mount imperatively after
  Lighting, so materials get compiled against a renderer state that
  pre-dates the final scene and bake a 'no shadow map' permutation,
  silently dropping shadows. A WebGL context-restore cycle fixes it,
  but is too invasive. New 'useShadowMapWarmup' hook replays it
  cheaply: once the scene mesh count has been stable for ~1s, it
  disposes the directional shadow map (three.js reallocates it on
  the next render) and marks every material 'needsUpdate' so shaders
  rebind to the freshly created shadow sampler.
- Doorway: the door slab + its Solidify-modifier frame (children of
  the 'Thicken' parent in the LaFabrik GLTF) sat inside the doorway
  AABB and prevented the player from walking through. Stripped from
  the collision octree alongside the existing 'porte' slab; visual
  rendering is unaffected.

Also: extract sun-relative-to-camera placement into a small helper,
remove the temporary diagnostic logs, and document the shadow warmup
in three-debugging.md.
2026-06-01 23:37:57 +02:00
Tom Boullay b144dc1c18 Update model.gltf
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 22:42:21 +02:00
Tom Boullay 69c720b86b fix(world): restore multi-frame shadow warmup and unblock fabrik doorway
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
- Lighting: replace single-frame needsUpdate with a 3-rAF warmup that forces
  scene.updateMatrixWorld + sun.shadow.needsUpdate + gl.shadowMap.needsUpdate.
  This restores the SceneShadowWarmup behaviour (deleted in 777e51e) inline,
  so shadows survive Physics Suspense remounts and webglcontextrestored.
- octreeCollisionConfig: remove (comment out) the thin LA_FABRIK interior box
  at x=-6.93 that was sealing the doorway despite the mesh hole; fabrik mesh
  octree already provides surrounding wall collision.
- DebugOctreeVisualization: add Fabrik-only filter to inspect interior
  collisions/non-collisions in isolation.
2026-06-01 22:41:45 +02:00
Tom Boullay 1b57a25e5f fix(world): strip blender-suffixed porte variants from la fabrik collision
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 22:19:58 +02:00
Tom Boullay f6db7d74e2 chore(world): add temporary diagnostics for porte strip, octree, ctx loss 2026-06-01 22:19:27 +02:00
Tom Boullay a1798aecb3 refactor(ui): split talkie dialogue overlay
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 21:43:58 +02:00
Tom Boullay 3b07f40f2d fix(ui): restore talkie idle vs active animation
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
The previous talkie-overlay refactor lost the rest/active behaviour and
left the radio-shake CSS animation running constantly. Restore the
intended polish:

- Position lerp between TALKIE_REST_Y (idle) and TALKIE_ACTIVE_Y
  (raised when narrator is speaking) with a subtle floating bob.
- Subtle z-axis float at idle, faster shake when active.
- Gate the CSS radio-shake on the new --active modifier so the talkie
  is calm when no narrator dialogue is playing.
- Keep the face-camera rotation [0.18, PI, -0.08] from the original
  overlay version.

Visibility still kicks in at the reveal step (no regression on the
recent fix).
2026-06-01 17:01:08 +02:00
Tom Boullay 27416143e3 update: add dialogue 2026-06-01 16:57:22 +02:00
Tom Boullay a2a491bd5c chore(world): add temporary shadow pipeline diagnostics
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
Shadows still go missing intermittently despite the per-frame
needsUpdate fix. Add temporary console logs to narrow down the cause:

- [shadow:mount]: one-shot snapshot of renderer shadow flags and a
  scene.traverse count of meshes with castShadow / receiveShadow at
  Lighting mount time.
- [shadow:tick]: every 2s during useFrame, log shadow map enabled flag,
  autoUpdate, sun.castShadow, sun intensity, shadow map texture
  presence, sun and target world positions, and renderer draw calls.

To be removed once the root cause is identified.
2026-06-01 16:50:21 +02:00
Tom Boullay da7d66e1fd feat(debug): add filters to octree visualization
Default visualization was unreadable because every node from depth 0 to
maxDepth was rendered with rainbow-coloured edges. Add three filters
exposed in the Debug folder:

- Octree Leaves Only (default true): skip internal nodes
- Octree Min Depth (default 4): hide the largest enclosing boxes
- Octree Opacity (default 0.35): tone down line density

Also skip nodes without triangles, drop the per-depth HSL palette in
favour of a uniform cyan, and bump default Octree Max Depth to 8.
2026-06-01 16:50:08 +02:00
Tom Boullay 5faf4b4197 fix(ui): keep talkie overlay visible after reveal step
Regression introduced by the narrator-video revert (1ad0c4d): the talkie
overlay was hidden whenever no narrator subtitle was active. Restore the
prior behaviour where the talkie stays visible from the reveal step
onward and only the --raised modifier and signal lines depend on the
active narrator dialogue.
2026-06-01 16:49:58 +02:00
Tom Boullay bee0c7f223 fix(world): make octree collision proxies solid
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 15:15:55 +02:00
Tom Boullay 216d29ae59 docs(three): document sun shadow needsUpdate fix
Update three-debugging.md to reflect that the shadow intermittence
is resolved by explicit sun.shadow.needsUpdate = true at mount and
in useFrame after updateMatrixWorld.
2026-06-01 14:47:29 +02:00
Tom Boullay e13cf1e4c7 fix(world): force per-frame sun shadow refresh
Restore sun.shadow.needsUpdate = true at mount and in useFrame
after updateMatrixWorld. Lost during SHADOW_CONFIG centralization.
Matches develop's belt-and-suspenders pattern; autoUpdate alone
is insufficient because the sun follows the camera (matrix dirty
every frame) and three.js can skip shadow map re-render.
2026-06-01 14:46:57 +02:00
math-pixel cd0afcda8c feat mission-2 2026-06-01 14:40:17 +02:00
Tom Boullay d20bdc4934 fix(ui): render loading loader as raw image
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 14:39:55 +02:00
Tom Boullay 7c35090dbd fix(ui): animate scene loading logo
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 14:28:48 +02:00
Tom Boullay a766784ce8 docs: update scene runtime and debug toggles
- Drop SceneShadowWarmup section, document centralized shadow config.
- Document the localized Suspense boundaries in World.tsx.
- Document the new player model and octree debug visualizations.
- Open note about intermittent first-load shadow rendering.
2026-06-01 14:16:01 +02:00
Tom Boullay 63952912b5 fix(world): wrap stage and player in suspense to prevent scene remount
Late asset loads inside GameStageContent (e.g. EbikeSpeedometer's
useTexture) and the spawn-player block were bubbling Suspense up to the
root boundary in pages/page.tsx, which unmounted World mid-load and
triggered a redundant octree rebuild + shadow re-config. Localize the
suspension by wrapping each block in its own Suspense fallback.

Also mount DebugOctreeVisualization conditionally on the new debug
toggle.
2026-06-01 14:14:27 +02:00
Tom Boullay fd0b9e2749 feat(debug): add player model and octree visualization toggles
- Show Player Model: render the main character GLTF in camera-local
  space so it stays visible at any pitch.
- Show Octree: overlay the collision octree as colored line segments,
  one wireframe per spatial cell, colored by depth.
- Octree Max Depth: cap recursion to keep the scene readable.
2026-06-01 14:14:20 +02:00
Tom Boullay 777e51efeb fix(world): centralize shadow config and remove warmup
- Extract SHADOW_CONFIG into lightingConfig.ts (bias=0, normalBias=0,
  cameraSize=95) matching the historically working values from develop.
- Drop SceneShadowWarmup; rely on sun.shadow.autoUpdate=true for
  steady-state refresh.
- Enable cloud castShadow and traverse Ebike meshes for cast/receive.
2026-06-01 14:14:14 +02:00
Tom Boullay 1ad0c4de37 revert(ui): remove narrator video on talkie 2026-06-01 14:14:03 +02:00
Tom Boullay 7a378afad3 feat(world): add octree collision proxies 2026-06-01 14:11:23 +02:00
Tom Boullay d52ec7e5a9 fix(model): clean lafabrik props and materials 2026-06-01 13:50:16 +02:00
math-pixel 813c10f3f7 wip mission 2 refine 2026-06-01 11:49:48 +02:00
Tom Boullay 153833deec Update favicon.ico
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 11:32:31 +02:00
Tom Boullay b617885aa2 chore(ui): clean loading overlay logo styling
Show the loading logo as a raw, contained image without rounded corners
or drop shadow, slightly larger to balance the empty space.
2026-06-01 11:28:15 +02:00
Tom Boullay 5d2e7e2aab fix(world): allow walking through la fabrik door
Strip the 'porte' mesh from the cloned scene used to build the la fabrik
collision octree. The wall geometry already has a doorway cutout, so
removing the door slab leaves the opening passable. The visual model is
rendered separately by MergedStaticMapModel and is unaffected.

Drops the stop-gap LA_FABRIK_COLLISION_Y_OFFSET added during debugging.
2026-06-01 11:28:15 +02:00
Tom Boullay de77f76d48 fix(world): restore shadow auto-update
Reverts the manual shadow refresh throttle introduced in 6d58b90 which
prevented shadows from rendering. Renderer and sun shadow now use
autoUpdate=true and a per-frame needsUpdate=true pulse, matching the
behaviour that produced visible shadows before that commit.
2026-06-01 11:28:07 +02:00
Tom Boullay bdc704fe8e feat(ui): show narrator video on talkie
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 10:52:28 +02:00
Tom Boullay bce7d11b66 fix(ebike): snap parked model to terrain 2026-06-01 10:52:17 +02:00
Tom Boullay 8aa755da7a fix(model): replace electricienne animated asset 2026-06-01 10:52:08 +02:00
Tom Boullay 6d58b90856 fix(world): throttle shadows and tune high preset 2026-06-01 10:45:07 +02:00
Tom Boullay bafca5a936 fix(ui): apply mobile blocker globally 2026-06-01 09:45:45 +02:00
Tom Boullay dcf3a8564c feat(ui): add narrator talkie overlay 2026-06-01 01:32:46 +02:00
Tom Boullay bc862960a7 fix(settings): persist pause menu preferences 2026-06-01 01:32:36 +02:00
Tom Boullay 597ebcfbd4 fix(ebike): sync parked position from config 2026-06-01 01:32:29 +02:00
Tom Boullay aa2d411b0c fix(world): stabilize lafabrik spawn and vegetation 2026-06-01 01:32:21 +02:00
Tom Boullay 061e0dc677 feat: update ui and intro sequence 2026-06-01 00:54:59 +02:00
Tom Boullay 9ef94af488 Merge branch 'develop' into feat/polish-mission1
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 00:15:46 +02:00
Tom Boullay 27b4a2c392 upatde(fabrik): zone + herbe
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 00:14:39 +02:00
math-pixel d5feb07ff0 Merge pull request 'Feat/polish-perf' (#13) from feat/polish-perf into develop
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #13
2026-05-31 22:11:58 +00:00
Tom Boullay 7dff4a1238 feat(characters): populate residential zones
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-06-01 00:09:39 +02:00
Tom Boullay a8cd66dcaa fix(model): sync animated character textures 2026-05-31 23:59:36 +02:00
Tom Boullay 116746f838 fix(model): sync animated gerant textures 2026-05-31 23:45:36 +02:00
Tom Boullay a388c02ab3 fix(model): repair gerant materials 2026-05-31 23:36:54 +02:00
Tom Boullay 4b4162b7d2 tune(characters): adjust npc placement 2026-05-31 23:35:39 +02:00
Tom Boullay 4415faa1f1 chore(models): remove lafabrik old assets 2026-05-31 23:35:17 +02:00
Tom Boullay 4c5f08d772 fix(model): restore lafabrik window transparency 2026-05-31 23:07:24 +02:00
Tom Boullay 51569af7b8 feat(ui): add transient loading indicator
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 22:43:48 +02:00
Tom Boullay d26c676edf polish(ui): compact pause menu backdrop 2026-05-31 22:22:29 +02:00
Tom Boullay d3b4a55e71 fix(model): load lafabrik glb 2026-05-31 22:14:13 +02:00
Tom Boullay e212e4bbd5 update(model): use a glb
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 22:08:08 +02:00
Tom Boullay 39ec9feb0e fix(model): externalize lafabrik geometry buffer 2026-05-31 21:58:13 +02:00
Tom Boullay 4a43083178 fix(ui): preserve site cookie on restart
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 21:45:55 +02:00
Tom Boullay efcbf9e972 polish(ui): refine pause settings menu 2026-05-31 21:42:59 +02:00
Tom Boullay f11ed67452 fix(model): reconnect lafabrik textures 2026-05-31 21:37:52 +02:00
Tom Boullay 3e7edcb1b7 docs(world): document map lod system
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 21:23:07 +02:00
Tom Boullay b9c5d0c563 feat(world): add lafabrik lod support 2026-05-31 21:22:48 +02:00
Tom Boullay ebdb72ce0d update(model): add lafabrik lod models 2026-05-31 21:22:24 +02:00
Tom Boullay 34c198ebfd feat(world): add map lod graphics presets 2026-05-31 19:03:55 +02:00
Tom Boullay 564a455520 update(model): add lod models
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 18:42:13 +02:00
Tom Boullay c33d973f12 fix(ui): update logo asset path 2026-05-31 11:51:33 +02:00
Tom Boullay 396e7e4ff0 feat(ebike): add speedometer
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 11:36:19 +02:00
Tom Boullay 2c2a90264d Merge branch 'develop' of https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-05-31 11:02:34 +02:00
Tom Boullay e02d06b8a5 Update hand_landmarker.task 2026-05-31 11:02:31 +02:00
math-pixel 1901075e3a Merge pull request 'Feat/polish-intro' (#11) from feat/polisth-intro into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #11
2026-05-31 09:01:17 +00:00
Tom Boullay e073fc375b fix(world): warm up map shadows from environment
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 11:00:40 +02:00
Tom Boullay bff8a16290 feat(intro): add ebike onboarding sequence
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-31 10:42:46 +02:00
Tom Boullay a3f611e227 fix(webgl): auto-restore context after loss
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
The Canvas onCreated callback used to log Context Lost but never asked
the GPU to restore it, which left the page on a frozen black canvas
until the user reloaded. We now grab the WEBGL_lose_context extension
on mount and call restoreContext() 500ms after a loss, giving the GPU
time to free memory before we ask for a new context. The existing
webglcontextrestored handler reinstates the shadow map settings, so
recovery is transparent to the user.

This does not prevent context loss itself — frequent losses still
indicate VRAM pressure or HMR-driven context churn — but it removes
the need to reload manually when the GPU recycles us.
2026-05-30 20:58:58 +02:00
Tom Boullay b578e68c2e Update SiteTransitionOverlay.tsx 2026-05-30 20:55:51 +02:00
Tom Boullay 7c691a8044 fix: show dialogue subtitles on black screen
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-30 20:25:21 +02:00
Tom Boullay f24704091a chore(logging): downgrade 'lite map skipped' to debug
This log fires every time the lite map loader skips heavy nodes, which
is the expected fast-path. It does not need to show up in a normal
console session — moving it to logger.debug keeps it accessible under
?debug for diagnostics while removing the noise from default runs.
2026-05-30 20:20:15 +02:00
Tom Boullay e6bfcbe960 feat(intro): polish loading transition 2026-05-30 20:11:40 +02:00
Tom Boullay 0fa7a82175 fix(perf): prevent Canvas double-mount on /site redirect
HomePage used to mount the Canvas before its effect fired the redirect
to /site, then unmount it as soon as the route changed. That left the
WebGL context torn down mid-load with GLTF requests still in flight,
which on slow GPUs ended in a 'Context Lost' and a stuck 1 FPS render
once the user came back from /site. The fix is a synchronous cookie
check after all hooks: if the user has not visited /site today we
return null and let the redirect happen without ever creating a GL
context.

Also drops the GameMap 'lite map skipped' log from warn to info: it
is an expected lite-loading path, not a problem worth a yellow warning.
2026-05-30 19:51:57 +02:00
Tom Boullay 82dc47a296 fix(assets): correct texture URIs in gant_r and pylone GLTFs
- gant_r/model.gltf: align casing with the actual files on disk
  (gant_basecolor / gant_occlusionroughnessmetallic) so the GLTFLoader
  stops logging 'Couldn't load texture' on case-sensitive filesystems
- pylone/model.gltf: nine missing Image_N.png references replaced with
  the existing color.png / normal.png so the pylone renders without
  console errors. Material slots are mapped by role: normalTexture ->
  normal.png, baseColorTexture and metallicRoughnessTexture -> color.png
2026-05-30 19:51:51 +02:00
Tom Boullay 970adf4853 feat(a11y): WCAG AA polish on the site onboarding flow
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
- index.css: add visible :focus-visible rings for .site-card-button
  and .site-button so keyboard users can see where focus lives
- SiteCard: drop outline:none, add aria-pressed and aria-label so
  screen readers announce selection state
- SiteButton: add the .site-button class for the shared focus ring
- SiteDisclaimerScreen: keyboard skip via Enter / Space / Escape, a
  role="region" + aria-label wrapper and aria-live="polite" on the
  message; honour prefers-reduced-motion on the fade
- IntroVideoPlayer: role="region" with a skip hint in aria-label,
  preload="auto", and aria-hidden on the decorative caption span
2026-05-30 18:44:03 +02:00
Tom Boullay 07b09c22af fix(site): repair onboarding audio cleanup, redirect, and manifest fetches
- loadDialogueManifest: cache the resolved manifest at module level and
  dedupe concurrent fetches so each screen no longer re-downloads it
- useGameStore: completeIntroState now also advances intro.currentStep
  to "playing" so callers do not need a separate setIntroStep call
- SiteNamingScreen and SiteTransitionOverlay: replace ref-based guards
  with an isCancelled flag captured per effect. The previous guards
  persisted across StrictMode remounts, leaving mount 2 unable to
  re-run the effect after mount 1's chain was cancelled, which broke
  the fade animations, the second narrator dialogue and the redirect.
  Both screens now also call stopCurrentDialogue on unmount so audio
  cannot bleed across routes, and the transition gets a safety timeout
  in case the dialogue audio fails to fire its "ended" event
- SiteTransitionOverlay: keep the <Subtitles /> mount inside the
  overlay so it renders inside the z-index 1000 stacking context
  (above the black screen); the one in SiteLayout sits behind it
- IntroDialogueOverlay: route through playDialogueById instead of
  AudioManager.playSoundWithCallback so the narrator subtitles play
  in sync, and add the same isCancelled cleanup pattern
- IntroRevealOverlay: rely on completeIntro alone now that it advances
  intro.currentStep, and skip the fade when reduced motion is requested
- SiteMobileBlocker: correct logo path from public/... to /...
2026-05-30 18:43:53 +02:00
Tom Boullay 0f6860f1ae refactor(site): extract shared utilities and centralise dialogue IDs
- new src/hooks/ui/useIsMobile.ts (matchMedia + useSyncExternalStore)
  replacing the resize-handler hook inlined inside pages/site/page.tsx
- new src/hooks/ui/usePrefersReducedMotion.ts
- new src/data/site/dialogueIds.ts so site and intro components stop
  carrying hard-coded narrator IDs
- siteConfig: add SITE_BACKGROUND_STYLE shared by SiteLayout and
  SiteMobileBlocker, rename forcedName to presetPlayerName, fix the
  swapped id/label pairing on situation cards
- useSiteStore: rename selectedExperience/Situation to *Index so the
  stored value (an array index) is obvious in callers
- audioConfig: drop dead AUDIO_PATHS placeholders
- propagate the renames and SITE_BACKGROUND_STYLE through SiteLayout,
  SiteWelcomeScreen, SiteSituationScreen and pages/site/page.tsx
2026-05-30 18:43:35 +02:00
Tom Boullay 6ae21a2427 fix(site): unified card styles, import Nersans One font, native naming input
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-30 17:56:42 +02:00
Tom Boullay 29342d796c fix(site): reduce situation card font size 2026-05-30 17:21:44 +02:00
Tom Boullay 60e3c92511 fix(site): update situation cards 2026-05-30 17:06:29 +02:00
Tom Boullay 02c1fb33d0 feat(dialogues): support multi-cue subtitles
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-30 04:00:25 +02:00
Tom Boullay ce5dc8ada0 update: intro flow overlays 2026-05-30 04:00:20 +02:00
Tom Boullay a2cff0567e feat: add site onboarding route 2026-05-30 04:00:09 +02:00
Tom Boullay 8cfee1ac93 update: reorganize public assets 2026-05-30 03:59:45 +02:00
Tom Boullay 4c5e2ed945 feat(types): add SiteStep and refactor GameStep for new intro flow
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-30 02:14:10 +02:00
Tom Boullay 345d49f485 add: cinemactics and assets
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-05-30 02:04:39 +02:00
math-pixel a6cc028848 Merge pull request 'Feat/gallery' (#9) from feat/galerie into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #9
2026-05-29 07:00:36 +00:00
Tom Boullay 52bb1b2915 chore: code quality audit and lint fixes
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
- Fix all 63 ESLint errors across codebase
- Consolidate MaterialWithTextureSlots type in src/types/three/three.ts
- Add CSS custom properties for design tokens
- Extract ebike constants to src/data/ebike/ebikeConfig.ts
- Add proper TypeScript types for window extensions
- Fix React hooks violations (refs during render, setState in effects)
- Remove unused exports and redundant CSS
- Add type guards for Three.js material handling
- Clean up AI slop comments and legacy CSS patterns
2026-05-29 09:00:04 +02:00
Tom Boullay ade301389e merge develop into feat/galerie - resolve model and code conflicts 2026-05-29 02:25:46 +02:00
Tom Boullay 47e50d9318 fix: issue in galley mode 2026-05-29 02:18:17 +02:00
math-pixel c7df58099a Merge pull request 'Feat/map-environment' (#6) from feat/map-environment into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #6
2026-05-29 00:00:50 +00:00
Tom Boullay f7b4a07e41 fix: bug on textute vegetation item
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-29 02:00:35 +02:00
Tom Boullay 89044a18ec merge develop into feat/map-environment
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-29 01:45:08 +02:00
Tom Boullay 95ca1bbfde hore(review): tighten pre-merge audit cleanup
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-29 01:34:10 +02:00
Tom Boullay 093ffd726d fix(review): address audit findings before merge
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-29 01:23:08 +02:00
Tom Boullay 4728690a11 Create outro.mp4
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-29 00:52:53 +02:00
Tom Boullay 343a122c06 fix(editor): restore stable map editing behavior 2026-05-29 00:52:44 +02:00
tom-boullay d5675fe82c feat: restaure l'éditeur map et ajoute les personnages
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 15:49:57 +02:00
tom-boullay fcdbf7270c perf: applique les échelles aux assets instanciés 2026-05-28 15:49:26 +02:00
tom-boullay 0b3d49e8d1 feat: ajoute les potagers à la map 2026-05-28 15:48:33 +02:00
tom-boullay 9bbed06ddc feat: ancre les réparations sur la map chargée 2026-05-28 15:47:53 +02:00
tom-boullay ba50224e6e refactor: nettoie l'architecture monde et les docs 2026-05-28 15:47:16 +02:00
tom-boullay 1a91b1d7ae refactor: clean map gameplay architecture 2026-05-28 11:15:45 +02:00
tom-boullay d9cf87d2d6 feat: launch ebike repair from map interaction
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 10:13:59 +02:00
Tom Boullay d654565f87 chore: address code quality audit findings
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 08:31:42 +02:00
math-pixel fb466a63cb Merge pull request 'Merge e_bike + gps into develop' (#7) from feat/gps into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #7
2026-05-28 05:55:18 +00:00
math-pixel a75c3fd896 uptd : change location of map_backougnround & fix : remove old netshader into world tsx
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 07:54:47 +02:00
math-pixel 603e521714 Merge branch 'develop' into feat/gps 2026-05-28 07:50:25 +02:00
math-pixel 49ef8f58b4 Merge pull request 'Add Net shader into develop' (#8) from feat/shader-net into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
Reviewed-on: #8
2026-05-28 05:47:52 +00:00
Tom Boullay 947025cbf5 Update windConfig.ts 2026-05-28 01:35:43 +02:00
Tom Boullay f2cecfbbc9 fix(lighting): keep shadows enabled after scene loading
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 01:33:15 +02:00
Tom Boullay 1b9ac5c996 feat(environment): apply shared wind to vegetation 2026-05-28 01:28:16 +02:00
Tom Boullay e45fdbf97d fix(lighting): keep shadow map centered on player 2026-05-28 01:20:12 +02:00
Tom Boullay 08b01715c0 feat(map): stream secondary map assets by chunk 2026-05-28 01:16:29 +02:00
Tom Boullay a2fc417be6 fix(environment): preserve grass blade colors
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 01:12:21 +02:00
Tom Boullay 57498b9bb1 tune(environment): rebalance procedural grass zones 2026-05-28 01:08:33 +02:00
Tom Boullay b87a7e929c feat(environment): vary grass density by procedural zones 2026-05-28 01:05:40 +02:00
Tom Boullay ea23b4bb46 fix(environment): tune grass and terrain grounding 2026-05-28 00:59:36 +02:00
Tom Boullay 3881e38a6d feat(collision): include static map models in octree 2026-05-28 00:47:40 +02:00
Tom Boullay 7a72743e5c refactor(environment): use player-centered ghibli grass patch 2026-05-28 00:31:45 +02:00
Tom Boullay 65651405b6 feat(editor): add multi-selection transforms 2026-05-28 00:29:00 +02:00
Tom Boullay 81cd935bba fix(environment): reduce grass generation cost 2026-05-28 00:25:23 +02:00
Tom Boullay fe989c9550 refactor(environment): replace grass with terrain-mesh ghibli shader 2026-05-28 00:20:01 +02:00
Tom Boullay e15fb18d6b update: models
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-28 00:07:17 +02:00
Tom Boullay 0230795f55 feat: add chunked grass and organize environment systems 2026-05-28 00:07:02 +02:00
Tom Boullay b1fca3a25b Merge remote map editor updates 2026-05-27 22:24:59 +02:00
math-pixel 0a322acf88 Merge branch 'develop' into feat/shader-net
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 18:08:46 +02:00
math-pixel a397febd52 fix zoom
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 17:23:06 +02:00
math-pixel c15cad2ab0 fix zoom 2026-05-27 17:22:14 +02:00
tom-boullay 3eba38a80b fix: ebike
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 17:21:11 +02:00
math-pixel 011e7815a2 update gps 2026-05-27 17:15:08 +02:00
tom-boullay 0992aacec6 fix(editor): update transforms while dragging
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 14:22:26 +02:00
tom-boullay bfe184dea4 feat(editor): focus camera on selected object
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 11:58:33 +02:00
tom-boullay 4ee13b0336 fix(editor): keep selection when clicking empty space
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 11:41:42 +02:00
tom-boullay fdd530a3e7 fix(map): prevent visual loading from blocking gameplay 2026-05-27 11:39:44 +02:00
tom-boullay 2b676d985d feat(editor): focus selected model editing 2026-05-27 11:06:14 +02:00
tom-boullay b89eedd5be fix(map): align terrain visual collision and snapping 2026-05-27 09:47:08 +02:00
tom-boullay d38ad242d6 fix(editor): preserve hierarchical map saves 2026-05-27 09:47:01 +02:00
Tom Boullay c2b16434fb feat(editor): edit hierarchical map nodes 2026-05-27 08:30:54 +02:00
Tom Boullay ab100c683f Update fogConfig.ts
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 01:01:32 +02:00
Tom Boullay b8cff43545 fix(environment): disable fog by default 2026-05-27 01:01:25 +02:00
Tom Boullay 25e0f7e062 feat(environment): add adaptive atmospheric fog
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 00:54:17 +02:00
Tom Boullay ab3943eef3 tune(environment): add cloud controls and visibility fixes
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-27 00:34:01 +02:00
Tom Boullay 4ebb5b8c25 feat(environment): add wind-driven cloud system 2026-05-27 00:33:53 +02:00
Tom Boullay a6787a7ecb chore: tune lighting and world plane material 2026-05-27 00:09:26 +02:00
Tom Boullay d816e4b07e feat(lighting): expose light colors in debug controls 2026-05-26 23:53:04 +02:00
Tom Boullay 665d9f9702 fix(map): add world plane collision and respawn 2026-05-26 23:52:12 +02:00
Tom Boullay 0696ca2ae3 perf: stream water shader by distance 2026-05-26 23:19:41 +02:00
Tom Boullay d6d3d5b685 fix: stabilize water depth and rounded mask 2026-05-26 22:56:50 +02:00
Tom Boullay 1c27d55e5a feat(map): add terrain boundary collision 2026-05-26 22:06:13 +02:00
Tom Boullay fd558db034 tune(environment): make water surface adjustable 2026-05-26 22:05:00 +02:00
Tom Boullay 054cb975da fix: hide gallery export planes
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-25 19:13:02 +02:00
Tom Boullay fbe8c0c854 feat(environment): add terrain water shader 2026-05-25 19:09:13 +02:00
Tom Boullay 88b6db6166 fix(map): disable generated path system 2026-05-25 18:42:38 +02:00
Tom Boullay cf71148935 fix: smooth gallery preview seams
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-25 18:02:36 +02:00
Tom Boullay 1b2241df49 feat: add gallery lighting controls 2026-05-25 17:57:51 +02:00
Tom Boullay 2b08665508 fix(map): align path preview with terrain projection 2026-05-25 17:54:57 +02:00
Tom Boullay d7351e5f37 fix: render gallery skybox unlit double-sided 2026-05-25 17:53:46 +02:00
Tom Boullay 4f8355e934 debug(map): add path surface sampling preview 2026-05-25 17:40:46 +02:00
Tom Boullay 417afdc1d5 fix(terrain): map surface colors with configurable projection 2026-05-25 17:40:01 +02:00
Tom Boullay 235a38f67b fix(map): disable generated paths while correcting mapping 2026-05-25 17:39:00 +02:00
Tom Boullay 6a412c7b00 fix: stabilize gallery skybox rendering 2026-05-25 17:31:27 +02:00
Tom Boullay e9fb36f9dc style: simplify gallery UI and rename route 2026-05-25 17:13:21 +02:00
Tom Boullay f54e71fc03 feat(map): generate path tiles from terrain colors 2026-05-25 17:08:07 +02:00
Tom Boullay 36180279b2 docs: document model gallery 2026-05-25 16:24:12 +02:00
Tom Boullay 626dc47bbe feat: add model gallery viewer 2026-05-25 16:23:36 +02:00
Tom Boullay 6d178dc59e feat: expose terrain surface data
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-25 16:03:22 +02:00
Tom Boullay b4a3545460 feat: add terrain surface sampler 2026-05-25 15:55:56 +02:00
Tom Boullay 1f6d9659ed chore: add terrain surface config 2026-05-25 15:51:02 +02:00
Tom Boullay a52d57ae6c fix: keep terrain collision during visual filtering 2026-05-25 01:28:25 +02:00
Tom Boullay d0497ec42c fix(debug): defer hand tracking startup 2026-05-25 01:19:14 +02:00
Tom Boullay 4c42e11268 fix(debug): stabilize map debug controls 2026-05-25 01:10:10 +02:00
Tom Boullay f175ad6240 tune(debug): refine fog and debug controls 2026-05-25 01:00:31 +02:00
Tom Boullay a4383a7cec update: file order
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-25 00:52:06 +02:00
Tom Boullay d07ffc4662 docs: update map performance notes 2026-05-25 00:51:09 +02:00
Tom Boullay d17738eaf1 perf(map): snap assets to terrain 2026-05-25 00:51:03 +02:00
Tom Boullay 50fa94b3ad docs: expose map performance notes 2026-05-25 00:08:29 +02:00
Tom Boullay 44f9d68ef1 tune: expand vegetation streaming range 2026-05-24 23:52:37 +02:00
Tom Boullay e4857135b1 feat: stream vegetation chunks near player 2026-05-24 23:51:40 +02:00
Tom Boullay 1f6335092a fix: harden merged map resource cleanup 2026-05-24 23:51:28 +02:00
Tom Boullay f035195b56 Merge branch 'feat/map-environment' of https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik into feat/map-environment
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-24 22:07:15 +02:00
Tom Boullay b8e5c4d1a9 upadte: gain some fps 2026-05-24 22:06:59 +02:00
math-pixel 970253801a add map on bike 2026-05-22 18:28:05 +02:00
tom-boullay 4e6582b543 add: panneaux solaire model
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-21 15:39:01 +02:00
tom-boullay e4ee2d768b feat(debug): add map performance visibility controls 2026-05-21 15:38:23 +02:00
tom-boullay 5e594c51f7 docs(perf): document map rendering bottlenecks 2026-05-21 15:34:56 +02:00
tom-boullay 26ddbebe14 refactor(map): add generated R3F model for ecole 2026-05-21 15:34:49 +02:00
tom-boullay 48c2b4f0cd perf(map): merge instanced map asset geometry 2026-05-21 15:34:39 +02:00
tom-boullay cf08062def refactor(world): restore sky fallback chain
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-21 12:10:31 +02:00
tom-boullay 4f25b33d3b feat(debug): persist and reset debug game state 2026-05-21 12:09:12 +02:00
tom-boullay 072dec03b4 refactor: tighten terrain and sky model resource ownership
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-21 11:55:49 +02:00
tom-boullay 529c60adae update: add new sounds + comrpessed models
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-21 10:52:14 +02:00
math-pixel 246da0019a add transparency gps 2026-05-20 16:56:01 +02:00
math-pixel 09a9471814 feature gps works 2026-05-20 15:29:23 +02:00
math-pixel 6e9318457a gps component 2026-05-20 14:45:40 +02:00
math-pixel 54a353de03 first implementation of pathfinding 2026-05-20 14:34:26 +02:00
math-pixel 8b619bfc28 feat: add NetShader and UnicolorShader with a debug component for visual testing in the world scene 2026-05-19 22:54:29 +02:00
math-pixel 4faa226326 working move kikle 2026-05-19 17:10:34 +02:00
math-pixel dd66966507 working move kikle 2026-05-19 17:04:01 +02:00
math-pixel 5893afe42a working move kikle 2026-05-19 16:34:48 +02:00
math-pixel 1ead7ab3a7 working move kikle 2026-05-19 16:17:02 +02:00
math-pixel 047c58678b working move kikle 2026-05-19 16:12:58 +02:00
math-pixel ed9051b0dc working move kikle 2026-05-19 16:10:57 +02:00
math-pixel 08be6bee48 add good inclinason cam 2026-05-19 15:54:40 +02:00
math-pixel ce0eb90321 inhance move 2026-05-19 15:50:11 +02:00
math-pixel 96d7ec7fc0 move forward cam 2026-05-19 15:36:50 +02:00
math-pixel 9ab4b4a002 first move with bike 2026-05-19 15:32:59 +02:00
math-pixel d13dd0fda0 wip bike movement 2026-05-17 12:30:40 +02:00
math-pixel fbedb90bca working bike 2026-05-17 08:15:16 +02:00
math-pixel cff7744ad9 wip 2026-05-17 07:41:29 +02:00
Tom Boullay 6957b9e4f0 update: merge instanced map geometry 2026-05-15 23:29:07 +02:00
Tom Boullay 9dff245aab update: instance map renderables 2026-05-15 21:48:13 +02:00
Tom Boullay 27951d13fd upatde: json + models
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-15 00:18:03 +02:00
Tom Boullay cdd919c010 update: clean map hierarchy and mesh model names
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-14 17:25:39 +02:00
Tom Boullay cea2856fd0 update: models 2026-05-14 17:17:54 +02:00
Tom Boullay d245d6b460 Update vegetationConfig.ts 2026-05-14 16:13:34 +02:00
Tom Boullay dba7aec6fa fix: dupplicate buisson and ecole 2026-05-14 16:13:32 +02:00
Tom Boullay d376d0ba6b update: add new models 2026-05-14 16:13:16 +02:00
Tom Boullay 242a3dcd37 refactor: remove vegetation instance limit (will be handled by chunks later) 2026-05-14 00:26:42 +02:00
Tom Boullay 225ac828df feat: enable all vegetation types and remove debug logs 2026-05-14 00:23:18 +02:00
Tom Boullay 28f7db172c upatde: add new models
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-14 00:20:20 +02:00
Tom Boullay 2063656f29 chore: update eslint config and CI workflow 2026-05-14 00:18:02 +02:00
Tom Boullay 592cfa405f feat: add graphics settings hook 2026-05-14 00:17:53 +02:00
Tom Boullay 0a32cd1d21 feat: add map node caching for vegetation system 2026-05-14 00:17:44 +02:00
Tom Boullay 4516cf4ec6 feat: configure shadow map settings for better shadow quality 2026-05-14 00:17:33 +02:00
Tom Boullay c6f60d1ca7 update: vegetation models 2026-05-14 00:17:08 +02:00
Tom Boullay aa35e97cbb update: skybox model files 2026-05-14 00:16:58 +02:00
Tom Boullay 57c142c8ef fix: correct texture paths case sensitivity in GLTF models 2026-05-14 00:16:47 +02:00
Tom Boullay 4843bf1d75 fix: correct skybox model path 2026-05-14 00:16:37 +02:00
Tom Boullay d02cf29a1d debug: add logging for scene loading flow 2026-05-14 00:16:28 +02:00
Tom Boullay 3b4c9c2529 feat: add WebGL context loss handler and GPU performance config 2026-05-14 00:16:18 +02:00
Tom Boullay fdf03349cf fix: enable terrain loading for collision octree build 2026-05-14 00:16:09 +02:00
Tom Boullay 439f9c1dad feat: add VegetationSystem with InstancedMesh rendering
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-14 00:16:00 +02:00
Tom Boullay 260bfea716 fix: add disposal on unmount in SkyModel and SimpleModel 2026-05-14 00:15:52 +02:00
Tom Boullay b3a3f3557c fix: add disposal on unmount in TerrainModel 2026-05-14 00:15:41 +02:00
Tom Boullay 621556b38c fix: add disposal on unmount in useClonedObject hook 2026-05-14 00:15:32 +02:00
Tom Boullay 5c55f2c7f4 feat: add disposeObject3D utility for GPU memory cleanup 2026-05-14 00:15:23 +02:00
Tom Boullay bb4bccb175 Update weekly-branch-promotions.yml 2026-05-13 15:26:36 +02:00
Tom Boullay ae835e5008 update: models 2026-05-13 15:00:53 +02:00
Tom Boullay 1d3aa1c9f2 add: world config (wind, graphics, terrain, fog) 2026-05-13 15:00:13 +02:00
Tom Boullay 23cab2da5e update: CI branch promotions schedule 2026-05-13 15:00:05 +02:00
Tom Boullay 44216f9395 update: models 2026-05-13 14:59:57 +02:00
Tom Boullay 7785a6c9d7 fix: clean formatting issues
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
2026-05-13 11:03:18 +02:00
Tom Boullay 48f8de5ea5 fix: register mission flow docs route 2026-05-13 11:03:17 +02:00
Tom Boullay 73fc9ec677 fix: add temporary lite map reducer 2026-05-13 11:03:17 +02:00
Tom Boullay 90cd4d40cc fix: update sky model loading 2026-05-13 11:03:16 +02:00
Tom Boullay 5c68e4cd79 update: models and map data 2026-05-13 11:03:14 +02:00
Tom Boullay 2b3ccaf67d Merge branch 'develop' of https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 16:59:30 +02:00
Tom Boullay 122af7bd9a update: add new models 2026-05-12 16:58:58 +02:00
math-pixel ae34dc38ed Merge pull request 'Feat/mission-12' (#3) from feat/mission-2 into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
Reviewed-on: #3
2026-05-12 14:52:53 +00:00
math-pixel 1cecead474 Merge branch 'develop' into feat/mission-2
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
2026-05-12 14:52:17 +00:00
math-pixel ceffedf684 fix : lint
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 16:51:35 +02:00
Tom Boullay 43b64172ea Create weekly-branch-promotions.yml
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 16:41:46 +02:00
math-pixel aa2250aba1 Merge pull request 'chore: add three devtools debug mode' (#5) from feat/code-review into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
Reviewed-on: #5
2026-05-12 14:05:33 +00:00
math-pixel 544e540c6a Merge branch 'develop' into feat/code-review
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 14:05:23 +00:00
Tom Boullay 832f8b59be Update repairMissions.ts
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 15:38:04 +02:00
math-pixel 700c088c48 fix : audio and data issues
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 14:29:38 +02:00
math-pixel 490f9627cc merge mission & intro 2026-05-12 14:03:53 +02:00
math-pixel 8d197ba26b PR: refactor state game 2026-05-12 13:47:05 +02:00
math-pixel eab552a09b PR: refactor name 2026-05-12 13:42:23 +02:00
Tom Boullay 8c54451b2e Create code-review-preparation.md
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 11:52:40 +02:00
Tom Boullay c09818aa67 docs: audit app architecture and refresh feature documentation
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 11:46:11 +02:00
Tom Boullay e65ad62b76 chore: add three devtools debug mode 2026-05-12 11:45:43 +02:00
math-pixel b04aa95966 Merge pull request 'Feat/env-manager2' (#4) from feat/env-manager into develop
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
Reviewed-on: #4
2026-05-12 08:59:27 +00:00
Tom Boullay cfb761e66c merge: sync develop into env manager
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 10:56:56 +02:00
Tom Boullay 3bef55e12f fix: restore animated model
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 10:41:31 +02:00
Tom Boullay 15361db203 update: organize editor controls panel
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
2026-05-12 10:18:12 +02:00
Tom Boullay 45fc8e9a83 fix: align router and rapier dependencies
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
2026-05-12 09:50:58 +02:00
Tom Boullay ffca1e9e5f fix: stabilize game scene loading and player spawn 2026-05-11 23:52:57 +02:00
1123 changed files with 105897 additions and 9593 deletions
+16 -52
View File
@@ -15,12 +15,9 @@ export class SomeManager {
return SomeManager._instance;
}
private constructor() {
// init logic
}
private constructor() {}
destroy(): void {
// cleanup logic
SomeManager._instance = null;
}
}
@@ -28,43 +25,12 @@ export class SomeManager {
## Managers in this project
| Manager | File | Role |
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
| `InteractionManager` | `src/managers/InteractionManager.ts` | Focus, nearby, trigger, grab, and hand-grab interaction state. |
| `GameManager` | target-state only | Future single source of truth for phase, zone, mission, input lock, dialogue. |
| `CinematicManager` | target-state only | Future GSAP timeline orchestrator. |
| `ZoneManager` | target-state only | Future zone entry/exit detection and LOD triggers. |
| Manager | File | Role |
| -------------------- | ------------------------------------ | -------------------------------------------------------------- |
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
| `InteractionManager` | `src/managers/InteractionManager.ts` | Focus, nearby, trigger, grab, and hand-grab interaction state. |
## Target-State GameManager
`GameManager` does not exist in the current implementation. The following pattern is target-state guidance only and should not be applied until the manager exists in code.
```ts
export class GameManager {
cinematic!: CinematicManager;
audio!: AudioManager;
zone!: ZoneManager;
private constructor() {
this.cinematic = CinematicManager.getInstance();
this.audio = AudioManager.getInstance();
this.zone = ZoneManager.getInstance();
}
}
```
When a `GameManager` exists, components and hooks should access other managers through it:
```ts
// Correct
GameManager.getInstance().cinematic.play("intro");
// Wrong — never import sub-managers directly in components
CinematicManager.getInstance().play("intro");
```
## Target-State Subscribe Pattern
## Subscribe Pattern
```ts
private listeners = new Set<() => void>()
@@ -79,28 +45,26 @@ private emit(): void {
}
```
In that target-state manager, every `set*()` method calls `this.emit()` to notify subscribers.
Managers that expose state to React call `this.emit()` from every `set*()` method that changes subscribed state.
## Target-State React Bridge Hook
## React Bridge Hook
```ts
// hooks/useGameState.ts
export function useGameState() {
const game = GameManager.getInstance();
const [state, setState] = useState(game.getState());
// hooks/interaction/useInteraction.ts
const manager = InteractionManager.getInstance();
useEffect(() => {
return game.subscribe(() => setState({ ...game.getState() }));
}, [game]);
return state;
export function useInteraction(): InteractionSnapshot {
return useSyncExternalStore(
manager.subscribe.bind(manager),
manager.getState.bind(manager),
);
}
```
## Rules
- Do not add a `GameManager` unless the feature requires a real shared gameplay state owner.
- Current managers may be imported directly until the target-state orchestrator exists.
- Current managers may be imported directly.
- Keep singleton managers limited to side-effect services or shared interaction state.
- Always call `destroy()` on cleanup when a manager owns external resources.
- Never create manager instances with `new` — always use `.getInstance()`.
+3
View File
@@ -23,3 +23,6 @@
# Video (cinematics)
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
# ML models
*.task filter=lfs diff=lfs merge=lfs -text
+8 -3
View File
@@ -69,16 +69,21 @@ jobs:
- name: 📥 Install
run: npm ci
- name: 🧹 Lint
run: npm run lint
- name: 🎨 Format check
run: npm run format:check
- name: 📦 Build
run: npm run build
- name: 📏 Check bundle size
run: |
# Check generated app assets only; public/ model files are runtime assets copied to dist.
SIZE=$(du -k dist/assets | cut -f1)
# Check generated JS/CSS bundles only; public runtime assets are copied to dist/assets too.
SIZE=$(node -e "const fs=require('fs');const path=require('path');function walk(dir){return fs.readdirSync(dir,{withFileTypes:true}).flatMap((entry)=>{const file=path.join(dir,entry.name);return entry.isDirectory()?walk(file):file;});}const bytes=walk('dist/assets').filter((file)=>/\.(js|css)$/.test(file)).reduce((sum,file)=>sum+fs.statSync(file).size,0);console.log(Math.ceil(bytes/1024));")
echo "Bundle size: ${SIZE}KB"
# Threshold: 5000KB (configurable)
THRESHOLD=5000
if [ "$SIZE" -gt "$THRESHOLD" ]; then
@@ -0,0 +1,111 @@
name: 🔁 Branch Promotions
on:
schedule:
- cron: "0 6 * * 1,4" # Lundi et Jeudi à 6h UTC (design → develop)
- cron: "0 6 * * 1" # Lundi à 6h UTC (develop → main)
workflow_dispatch:
inputs:
promotion:
description: "Which promotion to run"
required: true
type: choice
options:
- design-to-develop
- develop-to-main
- both
permissions:
contents: read
pull-requests: write
concurrency:
group: branch-promotions
cancel-in-progress: false
jobs:
design-to-develop:
name: Open design → develop
runs-on: ubuntu-latest
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.promotion == 'design-to-develop' || github.event.inputs.promotion == 'both'))
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔁 Open promotion PR
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git fetch origin develop design
if git merge-base --is-ancestor origin/design origin/develop; then
echo "No promotion needed: develop already contains design."
exit 0
fi
existing_pr="$(gh pr list \
--state open \
--base develop \
--head "$GITHUB_REPOSITORY_OWNER:design" \
--json number \
--jq '.[0].number // empty')"
if [ -n "$existing_pr" ]; then
echo "Promotion PR already open: #$existing_pr."
exit 0
fi
gh pr create \
--base develop \
--head design \
--title "chore: merge design into develop" \
--body "Automated promotion PR from \`design\` to \`develop\`."
develop-to-main:
name: Open develop → main
runs-on: ubuntu-latest
if: |
(github.event_name == 'schedule' && github.event.schedule == '0 6 * * 1') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.promotion == 'develop-to-main' || github.event.inputs.promotion == 'both'))
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔁 Open promotion PR
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git fetch origin main develop
if git merge-base --is-ancestor origin/develop origin/main; then
echo "No promotion needed: main already contains develop."
exit 0
fi
existing_pr="$(gh pr list \
--state open \
--base main \
--head "$GITHUB_REPOSITORY_OWNER:develop" \
--json number \
--jq '.[0].number // empty')"
if [ -n "$existing_pr" ]; then
echo "Promotion PR already open: #$existing_pr."
exit 0
fi
gh pr create \
--base main \
--head develop \
--title "chore: merge develop into main" \
--body "Automated weekly promotion PR from \`develop\` to \`main\`."
-187
View File
@@ -1,187 +0,0 @@
# Game Flow - La Fabrik
## Étapes du jeu
```
intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik
```
---
## Détail des étapes
### 1. `intro` (initial)
- État initial au chargement du jeu
- Aucune action, juste une étape de départ
- Transition automatique vers `start-intro`
### 2. `start-intro`
- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée
- **Action** : Joue l'audio d'intro (`intro`)
- **Attente** : Attend que l'audio se termine
- **Transition** : Vers `naming` quand l'audio se termine
### 3. `naming`
- **Déclenchement** : Quand l'audio d'intro se termine
- **Action** : Affiche un input pour demander le prénom du joueur
- **Attente** : L'utilisateur entre son prénom et valide
- **Transition** : Vers `bienvenue` quand l'utilisateur valide
### 4. `bienvenue`
- **Déclenchement** : Quand l'utilisateur valide son prénom
- **Actions** :
- Affiche "Bienvenue {prénom} !" à l'écran
- Joue l'audio de bienvenue
- **Attente** : Attend que l'audio se termine
- **Transition** : Vers `star-move` quand l'audio se termine
### 5. `star-move`
- **Déclenchement** : Quand l'audio de bienvenue se termine
- **Action** : Active le mouvement du joueur (`setCanMove(true)`)
- **État** : Le joueur peut maintenant se déplacer librement
- **Zone** : La détection de zone devient active (ZoneDetection)
### 6. `mission2`
- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`)
- **Actions** :
- Stocke `activityCity: false` dans le store Zustand
- Joue l'audio `alertCentral`
- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations
- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem`
### 7. `searching_problem`
- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`)
- **Actions** :
- Joue l'audio `searchingProblem`
- Affiche l'objet "central" (position: `[1, 15, -45]`)
- **Attente** : Le joueur interagit avec l'objet "central"
### 8. `preparation`
- **Déclenchement** : Quand le joueur interagit avec l'objet "central"
- **Actions** :
- Bloque le mouvement (`setCanMove(false)`)
- Cache l'objet "central"
### 9. `outOfFabrik`
- **Déclenchement** : (non implémenté pour le moment)
- **Action** : Transition vers l'étape finale
---
## Fichiers clés
| Fichier | Rôle |
| --------------------------------------- | --------------------------------------------------------- |
| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu |
| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand |
| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio |
| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue |
| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone |
| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 |
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
| `src/data/zones.ts` | Configuration des zones de transition |
| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville |
---
## Configuration audio
```typescript
// src/data/audioConfig.ts
export const AUDIO_PATHS = {
intro: "/sounds/fa.mp3",
bienvenue: "/sounds/fa.mp3",
alertCentral: "/sounds/fa.mp3",
searchingProblem: "/sounds/fa.mp3",
};
```
---
## Configuration des zones
```typescript
// src/data/zones.ts
export const ZONES: Zone[] = [
{
id: "fabrikExit",
position: [-5, 25, -15],
radius: 10,
height: 20,
targetStep: "mission2",
},
{
id: "searchingProblemZone",
position: [-5, 25, -30],
radius: 10,
height: 20,
targetStep: "searching_problem",
},
];
```
---
## Store Zustand
```typescript
// src/stores/gameStore.ts
interface GameState {
step: GameStep;
activityCity: boolean;
playerName: string;
canMove: boolean;
setStep: (step: GameStep) => void;
setActivityCity: (value: boolean) => void;
setPlayerName: (name: string) => void;
setCanMove: (canMove: boolean) => void;
}
```
---
## Hooks personnalisés
### useActivityCity
Permet aux objets 3D de réagir au changement d'activité de la ville :
```typescript
import { useActivityCity } from "@/hooks/useActivityCity";
function MyAnimatedObject() {
const activityCity = useActivityCity(); // true par défaut, false en mission2
// L'animation se déclenche quand activityCity change à false
// Utiliser useEffect pour réagir au changement
}
```
---
## Debug
En mode debug (`?debug` dans l'URL), on peut voir :
- **Game Step** : L'étape actuelle dans le panneau lil-gui
- **Player Position** : Position X, Y, Z du joueur en temps réel
- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents
---
## Notes techniques
- Le mouvement du joueur est bloqué tant que `canMove` est `false`
- Le store Zustand (`useGameStore`) est la source principale de vérité
- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions
- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store
- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques
+152 -103
View File
@@ -1,124 +1,173 @@
# La-Fabrik
An interactive 3D web experience for La Fabrik Durable — a low-tech repair and transformation service in Altera, a post-capitalist city rebuilt in 2039. Players step into the role of a newly onboarded technician and experience a day at the service: repairing an e-bike, fixing a power grid, and upgrading a vertical farm's irrigation system.
La-Fabrik is an interactive 3D web experience built with React, Vite, Three.js, React Three Fiber, Rapier, GSAP, MediaPipe, and Zustand.
Built with React, Three.js, and Vite. Runs in the browser, no installation required.
The current prototype puts the player in a repair-oriented world where they progress through a short mission chain: intro, e-bike repair, power pylon repair, vertical farm repair, then outro. The project also includes a local editor for map, dialogue, subtitle, and cinematic data.
## 📦 Tech Stack
## Current Scope
### Build & Language
- Playable fullscreen 3D scene at `/`
- Production map loaded from `public/map.json`
- Progressive map/model/collision/stage loading overlay
- Player controller with pointer lock, `ZQSD` movement, jump, octree collision, trigger input, and grab input
- Reusable repair-game flow for `ebike`, `pylon`, and `farm`
- Repair case animation, exploded model scan, broken-part markers, grabbable replacements, snap-to-placeholder placement, install validation, reassembly, and completion
- Shared interaction system for trigger and grab objects
- Rapier physics for gameplay objects while the player keeps a Three.js octree collision controller
- Hand tracking through either a local Python WebSocket backend or browser-side MediaPipe
- Category-based audio manager for music, SFX, and dialogue
- Dialogue manifest, SRT subtitles, subtitle overlay, and dialogue queueing
- Cinematic manifest with GSAP camera keyframes and optional dialogue cues
- In-game settings menu for volumes, subtitles, and subtitle language
- Debug mode with `?debug`, lil-gui controls, game-state panel, hand-tracking panel, debug camera, physics playground, and R3F perf
- `/editor` route for map transforms, SRT editing, dialogue manifest editing, cinematic manifest editing, preview, validation, export, and dev-server saves
- `/docs` route that renders the repository documentation inside the app
| Package |
| -------------------------------------------------- |
| [TypeScript](https://www.typescriptlang.org/docs/) |
| [React](https://react.dev/learn) |
| [Vite](https://vite.dev/guide/) |
| [ESLint](https://eslint.org/docs/latest/) |
| [Prettier](https://prettier.io/docs/) |
## Routes
### 3D Engine
| Route | Purpose |
| ---------- | --------------------------------------------------- |
| `/` | Playable 3D experience |
| `/?debug` | Playable scene with debug GUI and overlays |
| `/editor` | Local map, dialogue, subtitle, and cinematic editor |
| `/gallery` | 3D model gallery for browsing project assets |
| `/docs` | In-app documentation index |
| Package |
| ----------------------------------------------------------------------------------------- |
| [Three.js](https://threejs.org/docs/) |
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
| [@react-three/drei](https://pmndrs.github.io/drei) |
| [@react-three/rapier](https://rapier.rs/docs/) |
| [GSAP](https://gsap.com/docs/v3/Installation/) |
## Tech Stack
### Performance & Effects
| Area | Packages |
| --------------------- | ------------------------------------------------------------------------------ |
| App | React 19, TypeScript, Vite, TanStack Router |
| 3D | Three.js, React Three Fiber, drei |
| Physics and animation | `@react-three/rapier`, GSAP, Three.js `AnimationMixer` |
| State | Zustand, custom singleton managers where imperative runtime objects are needed |
| Hand tracking | `@mediapipe/tasks-vision`, optional FastAPI backend |
| Docs | `react-markdown`, `remark-gfm` |
| Quality | ESLint, Prettier, TypeScript project build |
| Package |
| --------------------------------------------------------------------------- |
| [r3f-perf](https://github.com/utsuboco/r3f-perf) |
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
## Project Structure
## 🗂 Project Structure
```
la-fabrik/
├── public/
│ ├── models/
│ │ ├── map/ # Base map — loaded once at start
│ │ ├── workshop/
│ │ ├── powerGrid/
│ │ └── farm/
│ ├── textures/
└── sounds/
└── src/
├── world/ # Persistent 3D world composition
│ ├── World.tsx # Active scene composition
│ ├── GameMap.tsx # Map loading and progressive rendering
│ ├── GameMapCollision.tsx # Collision-only octree source
│ ├── Lighting.tsx # Ambient, directional, point lights
│ ├── Environment.tsx # Scene background / sky model
│ ├── GameMusic.tsx # Game scene music lifecycle
│ ├── debug/ # Debug-only test scene
│ │ └── TestMap.tsx
│ └── player/
│ ├── Player.tsx # Player rig composition
│ ├── PlayerCamera.tsx # Player camera mount
│ └── PlayerController.tsx # Pointer lock movement and inputs
├── components/
│ ├── three/ # Shared R3F components by domain
│ │ ├── gameplay/ # Core repair gameplay prototype
│ │ ├── handTracking/ # R3F hand tracking debug models
│ │ ├── interaction/ # Trigger, grab, focus wrappers
│ │ ├── models/ # GLTF model components
│ │ └── world/ # Environment-specific 3D objects
│ └── ui/ # HTML overlays — outside Canvas
│ ├── Crosshair.tsx
│ ├── debug/ # Debug-only HTML overlay panels
│ │ ├── DebugOverlayLayout.tsx
│ │ ├── GameStateDebugPanel.tsx
│ │ └── HandTrackingDebugPanel.tsx
│ ├── HandTrackingVisualizer.tsx
│ └── InteractPrompt.tsx
├── managers/ # Current singleton-style services
│ ├── AudioManager.ts # Music and SFX playback
│ └── InteractionManager.ts # Focus, nearby, grab state
├── hooks/ # React hooks by domain
│ ├── debug/ # Debug state and GUI folders
│ ├── docs/ # Docs language context access
│ ├── editor/ # Editor loading and history
│ ├── gameplay/ # Repair gameplay helpers
│ ├── handTracking/ # Webcam/WebSocket hand tracking
│ ├── interaction/ # Interaction manager subscriptions
│ └── three/ # Three.js/R3F helpers
├── data/
│ ├── interaction/ # Interaction tuning
│ ├── player/ # Player tuning
│ ├── gameplay/ # Repair gameplay static config
│ └── world/ # Environment and lighting config
├── utils/
│ ├── core/ # Logger and generic utilities
│ ├── debug/ # Dev-only tools and scene inspection
│ ├── editor/ # Editor-only parsing utilities
│ ├── map/ # Map loading and validation
│ └── three/ # Three.js helpers
├── types/ # Shared TypeScript domain types
├── App.tsx # App bootstrap and route switch
└── main.tsx
```txt
.
|-- backend/ # Optional Python hand-tracking backend
|-- docs/
| +-- technical/ # Architecture and implementation notes
| +-- user/ # Feature and user-facing guides
|-- public/
| +-- assets/ # UI videos, PDFs, logos, world videos
| +-- cinematics.json # Runtime cinematic manifest
| +-- map.json # Runtime/editor map data
| +-- models/ # GLTF/GLB assets resolved by model folder name
| +-- sounds/ # Music, SFX, dialogue audio, SRT subtitles
|-- src/
| +-- components/
| | +-- docs/ # In-app docs layout and renderer
| | +-- editor/ # Editor panels and editor scene
| | +-- three/ # R3F components by domain
| | +-- ui/ # HTML game/debug overlays
| +-- controls/ # Editor fly/player-style controls
| +-- data/ # Static tuning/config per domain
| +-- hooks/ # React hooks by domain
| +-- lib/ # Browser hand-tracking helpers
| +-- managers/ # Audio, interaction, and Zustand stores
| +-- pages/ # Route-level pages
| +-- providers/ # Docs and hand-tracking providers
| +-- routes/ # Lazy route wrappers
| +-- types/ # Shared domain types
| +-- utils/ # Core, map, editor, dialogue, subtitle, Three helpers
| +-- world/ # Production/debug world composition and player
`-- vite.config.ts # Vite config plus local editor save endpoints
```
## 🚀 Getting Started
## Getting Started
Install and run the frontend:
```bash
git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git
cd La-Fabrik
npm install
npm run dev
```
- app: `http://localhost:5173`
- debug mode: `http://localhost:5173?debug`
Open:
## 📜 License
```txt
http://localhost:5173
```
See [LICENSE](./LICENSE) file.
Useful local URLs:
```txt
http://localhost:5173/?debug
http://localhost:5173/editor
http://localhost:5173/gallery
http://localhost:5173/docs
```
Run checks:
```bash
npm run typecheck
npm run lint
npm run format:check
npm run build
```
Regenerate runtime map data after editing `public/map_raw.json` that came from the hierachy node of the model Blocking.gltf:
```bash
npm run map:transform
```
## Optional Hand-Tracking Backend
The app can use the local Python backend, but the default debug source is browser-side MediaPipe.
```bash
python3.11 -m venv backend/.venv
source backend/.venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r backend/requirements.txt
python backend/download_model.py
python -m backend.main
```
Backend endpoints:
```txt
GET http://localhost:8000/health
WS ws://localhost:8000/ws
```
## Documentation Index
| File | Purpose |
| --------------------------------------- | ---------------------------------------------------------- |
| `docs/technical/architecture.md` | Current runtime architecture |
| `docs/technical/scene-runtime.md` | Scene loading, world composition, and player spawn gates |
| `docs/technical/repair-game.md` | Repair-game implementation and state flow |
| `docs/technical/interaction.md` | Trigger, grab, focus, and hand-grab system |
| `docs/technical/target-architecture.md` | Intended medium-term architecture direction |
| `docs/technical/audio.md` | Music, SFX, dialogue, subtitles, and editor validation |
| `docs/technical/hand-tracking.md` | Webcam, backend/browser MediaPipe, glove, and gesture flow |
| `docs/technical/zustand.md` | Game, settings, and subtitle stores |
| `docs/technical/three-debugging.md` | DevTools workflow for stepping into Three.js internals |
| `docs/technical/map-performance.md` | Map draw-call bottlenecks and optimization notes |
| `docs/technical/map-lod.md` | Runtime map LOD presets, paths, and workflow |
| `docs/technical/editor.md` | Editor implementation details |
| `docs/technical/animation.md` | Animated, explodable, and reusable 3D model components |
| `docs/user/features.md` | Implemented feature inventory |
| `docs/user/main-feature.md` | User-facing repair-game walkthrough |
| `docs/user/editor.md` | Editor user guide |
| `docs/user/gallery.md` | Model gallery user guide |
| `docs/code-review-preparation.md` | French code-review preparation support |
## Current Caveats
- This is still a prototype, not a complete game runtime.
- `useRepairMovementLocked()` locks player movement during focused repair steps and drives the repair movement indicator.
- Production editor persistence does not exist. Save endpoints in `vite.config.ts` are local Vite dev-server helpers.
- The player uses octree collision while gameplay objects use Rapier. Keep that boundary deliberate unless the whole player controller is migrated.
## License
See `LICENSE`.
+1 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
from uuid import uuid4
@@ -13,7 +13,6 @@ class ClientConnection:
websocket: WebSocket
is_processing: bool = False
last_frame_at: float = 0.0
metadata: dict[str, Any] = field(default_factory=dict)
class ConnectionManager:
Binary file not shown.
+921
View File
@@ -0,0 +1,921 @@
# Préparation Code Review - La-Fabrik
Ce document est une fiche de révision pour préparer la code review.
Il est basé sur `develop`, après le merge récent autour de l'environnement, du repair game, de l'audio, du store Zustand, du hand tracking, de l'éditeur et de la doc intégrée.
Le but n'est pas de réciter le repo. Le but est de savoir :
- expliquer l'architecture générale ;
- naviguer vite entre les bons fichiers ;
- défendre les choix techniques ;
- montrer une démo propre avec Chrome DevTools ;
- reconnaître honnêtement les limites actuelles.
## Ce qu'il faut retenir en premier
La-Fabrik est une expérience web 3D en React, Vite, Three.js et React Three Fiber.
Le joueur est dans un monde 3D et avance dans une progression de réparation :
```txt
intro -> ebike -> pylon -> farm -> outro
```
Les trois piliers à connaître pour la review :
1. `RepairGame` : boucle de gameplay principale.
2. `AudioManager` : musique, SFX, dialogues et sous-titres.
3. `useGameStore` / Zustand : progression globale du jeu.
Le reste du repo sert à intégrer ces piliers :
- `World` monte la scène 3D ;
- `InteractionManager` relie les objets interactifs aux inputs ;
- `HandTrackingProvider` active la webcam seulement quand elle est utile ;
- `/editor` permet de modifier map, dialogues, SRT et cinématiques ;
- `/docs` rend la documentation Markdown dans l'app.
## Pitch en 30 secondes
Phrase simple à savoir dire :
> La-Fabrik est une expérience 3D interactive en React Three Fiber. Le joueur progresse dans des missions de réparation. Le coeur du gameplay est un `RepairGame` réutilisable pour le vélo, le pylône et la ferme. La progression est centralisée dans Zustand, les interactions passent par un manager commun, les objets manipulables utilisent Rapier, et le joueur garde une collision octree séparée. L'audio est géré par un `AudioManager` avec volumes par catégorie, dialogues et sous-titres.
## Carte mentale du projet
```txt
src/main.tsx
-> App.tsx
-> router.tsx
-> / HomePage + Canvas + World + GameUI
-> /editor EditorPage
-> /docs DocsLayout + Markdown pages
```
Dans la scène jouable :
```txt
HomePage
-> HandTrackingProvider
-> Canvas
-> World
-> GameMap
-> GameStageContent
-> RepairGame ebike/pylon/farm
-> GameMusic
-> GameDialogues
-> Player
-> GameUI
```
## Features à connaître
### Runtime 3D
Fichiers :
- `src/pages/page.tsx`
- `src/world/World.tsx`
- `src/hooks/world/useWorldSceneLoading.ts`
- `src/world/GameMap.tsx`
- `src/world/GameMapCollision.tsx`
Ce que ça fait :
- charge `public/map.json` ;
- résout les modèles dans `public/models/{name}/model.glb` ou `model.gltf` ;
- affiche des cubes fallback si un modèle manque ;
- construit l'octree joueur depuis les nodes de collision ;
- attend que map, collision, stage et octree soient prêts avant de spawn le joueur.
Phrase à retenir :
> `World` ne lance pas tout d'un coup. Il attend que la map, l'octree et le stage gameplay soient prêts avant de monter le joueur, la musique et les dialogues.
Piège à connaître :
Les anciens flags comme `noMusic`, `noMap`, `noDialogues`, `noPlayer` ne sont plus branchés dans `World`. Pour la démo, utiliser surtout `?debug`.
### Player et collision
Fichiers :
- `src/world/player/PlayerController.tsx`
- `src/data/input/keybindings.ts`
- `src/data/player/playerConfig.ts`
- `src/world/GameMapCollision.tsx`
Ce que ça fait :
- déplacement `ZQSD` ;
- souris en pointer lock ;
- saut avec `Space` ;
- interaction avec `E` ;
- grab avec clic gauche ;
- collision joueur via capsule Three.js + octree.
Phrase à retenir :
> Le joueur n'est pas piloté par Rapier. Rapier sert aux objets manipulables, alors que le joueur utilise une capsule et un octree.
Piège à connaître :
`useRepairMovementLocked()` lit maintenant l'étape de mission active et verrouille le déplacement pendant les phases de réparation qui doivent immobiliser le joueur.
### Interaction
Fichiers :
- `src/managers/InteractionManager.ts`
- `src/hooks/interaction/useInteraction.ts`
- `src/components/three/interaction/InteractableObject.tsx`
- `src/components/three/interaction/TriggerObject.tsx`
- `src/components/three/interaction/GrabbableObject.tsx`
- `src/components/ui/InteractPrompt.tsx`
Ce que ça fait :
- détecte si un objet est proche ;
- raycast depuis la caméra pour savoir si le joueur vise l'objet ;
- affiche le prompt `E` pour les triggers ;
- gère les grabs souris et hand tracking ;
- expose un snapshot React via `useSyncExternalStore`.
Phrase à retenir :
> Les interactions sont séparées du gameplay. Un objet dit juste "je suis trigger" ou "je suis grabbable", puis le player controller déclenche l'action selon le focus courant.
### Repair game
Fichiers coeur :
- `src/components/three/gameplay/RepairGame.tsx`
- `src/components/three/gameplay/RepairRepairingStep.tsx`
- `src/components/three/gameplay/RepairScanSequence.tsx`
- `src/components/three/gameplay/RepairMissionCase.tsx`
- `src/components/three/gameplay/RepairCaseModel.tsx`
- `src/components/three/gameplay/RepairCompletionStep.tsx`
Fichiers data/state :
- `src/data/gameplay/repairMissions.ts`
- `src/data/gameplay/repairCaseConfig.ts`
- `src/data/gameplay/repairGameConfig.ts`
- `src/types/gameplay/repairMission.ts`
- `src/managers/stores/useGameStore.ts`
Flow :
```txt
locked
-> waiting
-> inspected
-> fragmented
-> scanning
-> repairing
-> reassembling
-> done
```
Ce que ça fait :
- inspecter l'objet de mission ;
- afficher une mallette ;
- ouvrir/interagir avec la mallette ;
- fragmenter le modèle ;
- scanner les pièces cassées ;
- proposer plusieurs pièces de remplacement ;
- attraper et snapper les pièces ;
- vérifier bonne pièce + pièces cassées rangées ;
- réassembler le modèle ;
- valider la mission et passer à la suivante.
Phrase à retenir :
> `RepairGame` est un orchestrateur de steps. Les variations entre vélo, pylône et ferme sont dans `repairMissions.ts`, pas hardcodées dans trois composants séparés.
### Audio, dialogues, sous-titres
Fichiers :
- `src/managers/AudioManager.ts`
- `src/world/GameMusic.tsx`
- `src/utils/dialogues/playDialogue.ts`
- `src/utils/dialogues/loadDialogueManifest.ts`
- `src/managers/stores/useSettingsStore.ts`
- `src/managers/stores/useSubtitleStore.ts`
- `src/components/ui/Subtitles.tsx`
Ce que ça fait :
- musique en loop ;
- sons one-shot avec pooling ;
- volumes par catégorie : `music`, `sfx`, `dialogue` ;
- dialogues depuis `public/sounds/dialogue/dialogues.json` ;
- SRT par voix et par langue ;
- sous-titres synchronisés avec `audio.currentTime` ;
- queue de dialogues pour éviter les overlaps.
Phrase à retenir :
> L'audio est impératif, donc il est dans un manager. React garde les réglages et les sous-titres, mais les vrais `HTMLAudioElement` sont gérés par `AudioManager`.
### Zustand
Fichiers :
- `src/managers/stores/useGameStore.ts`
- `src/managers/stores/useSettingsStore.ts`
- `src/managers/stores/useSubtitleStore.ts`
- `src/components/ui/debug/GameStateDebugPanel.tsx`
Ce que ça fait :
- `useGameStore` : progression globale ;
- `useSettingsStore` : menu, volumes, sous-titres, runtime repair ;
- `useSubtitleStore` : sous-titre actif ;
- debug panel : manipule le même store que le vrai gameplay.
Phrase à retenir :
> Zustand contient l'état durable. Les valeurs qui changent à chaque frame restent dans des refs, dans R3F ou dans les managers.
### Hand tracking
Fichiers :
- `src/providers/gameplay/HandTrackingProvider.tsx`
- `src/hooks/handTracking/useRemoteHandTracking.ts`
- `src/hooks/handTracking/useBrowserHandTracking.ts`
- `src/hooks/handTracking/useBothFistsHold.ts`
- `src/components/three/handTracking/HandTrackingGlove.tsx`
- `backend/main.py`
Ce que ça fait :
- source `backend` avec Python/FastAPI/MediaPipe ;
- source `browser` avec `@mediapipe/tasks-vision` ;
- activation seulement quand utile ;
- gesture deux poings fermés ;
- grab à la main pour certains objets ;
- visualisation gants et panel debug.
Phrase à retenir :
> Le hand tracking n'est pas actif tout le temps. Le provider l'active quand le contexte le justifie, par exemple pendant certaines étapes du repair game.
### Editor
Fichiers :
- `src/pages/editor/page.tsx`
- `src/components/editor/EditorControls.tsx`
- `src/components/editor/scene/EditorScene.tsx`
- `src/components/editor/scene/EditorMap.tsx`
- `vite.config.ts`
Ce que ça fait :
- édite `public/map.json` ;
- affiche et transforme les objets ;
- édite dialogues, SRT et cinématiques ;
- preview audio/cinématique ;
- sauvegarde locale via endpoints Vite.
Phrase à retenir :
> L'éditeur partage les mêmes formats que le runtime. Il n'y a pas un format map pour l'éditeur et un autre pour le jeu.
Piège à connaître :
Les endpoints `/api/save-*` sont des helpers Vite en dev local, pas une API de production.
## Bloc principal 1 : RepairGame
### Ce qu'il faut comprendre
`RepairGame` ne fait pas "juste afficher une réparation". Il coordonne :
- le state global Zustand ;
- les étapes de mission ;
- les assets GLTF ;
- les prompts vidéo ;
- la mallette ;
- les interactions `E` ;
- les objets Rapier grabbables ;
- le scan des pièces ;
- la validation gameplay.
### Navigation simple
Ouvrir dans cet ordre :
1. `src/world/GameStageContent.tsx`
2. `src/components/three/gameplay/RepairGame.tsx`
3. `src/data/gameplay/repairMissions.ts`
4. `src/components/three/gameplay/RepairRepairingStep.tsx`
5. `src/managers/stores/useGameStore.ts`
### Comment expliquer l'architecture
`GameStageContent` place les missions dans le monde.
`RepairGame` reçoit une mission :
```tsx
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
```
Puis il vérifie :
- est-ce que `mainState` correspond à cette mission ?
- quel est le `step` courant ?
- quelle config utiliser ?
- quel sous-composant monter ?
Les variations mission sont dans `repairMissions.ts` :
- modèle ;
- prompt vidéo ;
- pièces cassées ;
- bonnes pièces ;
- leurres ;
- timings ;
- placeholders.
### Pourquoi c'est bien
- Un seul flow réutilisable.
- Moins de duplication entre `ebike`, `pylon`, `farm`.
- Les règles générales restent dans les composants.
- Les variations restent dans la data.
- Le debug panel peut tester les mêmes steps que le vrai jeu.
### Compromis
Si une future mission devient très différente, la config ne suffira peut-être plus. Il faudra alors créer des composants spécifiques ou un vrai `MissionManager`.
### Points à dire si on ouvre `RepairRepairingStep`
Ce fichier gère l'étape la plus gameplay :
- pièces de remplacement ;
- pièces cassées à déposer ;
- snap vers placeholders ;
- feedback vert/rouge/bleu ;
- validation finale.
État local important :
- `placedPartIds`
- `depositedBrokenPartIds`
- `showBlockedInstallFeedback`
Phrase simple :
> Cette étape garde seulement l'état local de manipulation. La progression globale reste dans Zustand.
## Bloc principal 2 : Audio
### Ce qu'il faut comprendre
`AudioManager` est un singleton parce qu'il manipule des objets impératifs du navigateur :
- `HTMLAudioElement`
- `AudioContext`
- pools audio
- panner stereo
- musique active
### Navigation simple
Ouvrir dans cet ordre :
1. `src/managers/AudioManager.ts`
2. `src/world/GameMusic.tsx`
3. `src/managers/stores/useSettingsStore.ts`
4. `src/utils/dialogues/playDialogue.ts`
5. `src/components/ui/Subtitles.tsx`
### Architecture audio
Catégories :
```txt
music
sfx
dialogue
```
Volumes :
```txt
volume effectif = volume de base * volume catégorie
```
Exemple :
- `GameMusic` lance `/sounds/musique/test.mp3` avec base `0.33`.
- Si le volume musique settings vaut `0.5`, le volume effectif vaut `0.165`.
### Dialogues et sous-titres
Un dialogue référence :
- un `id` ;
- une voix ;
- un fichier audio ;
- un index de cue SRT.
Le SRT est par voix et par langue, pas un fichier SRT par dialogue.
Phrase simple :
> Les dialogues utilisent la catégorie audio `dialogue`, et `playDialogueById` synchronise le sous-titre actif avec le temps courant de l'audio.
### Risques à connaître
- Autoplay navigateur : la musique peut attendre un input utilisateur.
- Un seul track musique actif à la fois.
- Les dialogues sont queue-based, pas un vrai système de priorité.
- L'éditeur audio/SRT sauve via Vite local, pas en prod.
## Bloc principal 3 : Zustand
### Ce qu'il faut comprendre
Zustand contient l'état durable.
Il ne contient pas :
- la velocity joueur ;
- les raycasts ;
- les positions frame par frame ;
- les vecteurs temporaires ;
- l'état interne d'un grab.
### Navigation simple
Ouvrir dans cet ordre :
1. `src/managers/stores/useGameStore.ts`
2. `src/components/ui/debug/GameStateDebugPanel.tsx`
3. `src/components/three/gameplay/RepairGame.tsx`
4. `src/managers/stores/useSettingsStore.ts`
5. `src/managers/stores/useSubtitleStore.ts`
### Game store
Main states :
```txt
intro
ebike
pylon
farm
outro
```
Mission steps :
```txt
locked
waiting
inspected
fragmented
scanning
repairing
reassembling
done
```
Actions importantes :
- `setMissionStep`
- `completeMission`
- `advanceGameState`
- `rewindGameState`
- `resetGame`
Phrase simple :
> Le debug panel et le vrai gameplay utilisent la même source de vérité. Ce n'est pas un debug state séparé.
### Settings store
Gère :
- menu ouvert/fermé ;
- volumes ;
- sous-titres ;
- langue.
### Subtitle store
Gère seulement :
- le sous-titre actif ;
- clear/set.
Phrase simple :
> Le store de sous-titres est volontairement petit parce que le timing reste piloté par l'audio.
## Les pièges à ne pas rater
Si on te pose une question précise, réponds vrai.
| Sujet | Réponse honnête |
| ------------------------- | -------------------------------------------------------------------- |
| Lock mouvement réparation | Les étapes repair actives bloquent le déplacement via le hook dédié. |
| Old debug flags | `noMusic`, `noMap`, `noDialogues`, etc. ne sont plus branchés. |
| Player physics | Le joueur n'est pas Rapier, il utilise capsule + octree. |
| Collision map | L'octree vient de nodes dédiés, actuellement `terrain`. |
| Editor save | Ce sont des endpoints Vite dev, pas une API de prod. |
| Cinematics | `GameCinematics` est monté seulement pendant `outro` dans `World`. |
| Hand tracking depth | Le `z` MediaPipe est relatif, pas une vraie profondeur monde stable. |
## Si l'évaluateur ouvre un fichier au hasard
| Fichier | Ce qu'il faut dire |
| -------------------------- | ----------------------------------------------------------------------------- |
| `World.tsx` | Compositeur de scène. Gère game/debug, loading gates, player spawn. |
| `useWorldSceneLoading.ts` | Évite de spawn le joueur avant map + collision + stage. |
| `GameMap.tsx` | Charge map JSON, modèles, fallback cubes, signale quand les nodes sont prêts. |
| `GameMapCollision.tsx` | Construit l'octree joueur depuis des nodes de collision dédiés. |
| `PlayerController.tsx` | Inputs, pointer lock, mouvement, jump, interaction, collision capsule. |
| `InteractionManager.ts` | État courant des interactions, pas dans Zustand car frame-adjacent. |
| `RepairGame.tsx` | Orchestrateur de steps repair, data-driven par mission. |
| `RepairRepairingStep.tsx` | Validation gameplay : pièces, snap, dépôt, install target. |
| `repairMissions.ts` | Config des missions, évite de hardcoder chaque mission dans le composant. |
| `AudioManager.ts` | Manager impératif pour musique, SFX, dialogue, pooling, volumes. |
| `playDialogue.ts` | Queue dialogue + synchronisation sous-titre. |
| `useGameStore.ts` | Source de vérité progression. |
| `GameStateDebugPanel.tsx` | Outil debug qui manipule le même store que le jeu. |
| `HandTrackingProvider.tsx` | Active la webcam seulement quand utile. |
| `GrabbableObject.tsx` | Grab souris/main, Rapier body, snap target. |
| `EditorControls.tsx` | Panneau éditeur, pas runtime joueur. |
## Démo live avec Chrome DevTools
Cette partie correspond à :
> Session de test / démo live via les devtools, commentée
Le but n'est pas de tout montrer. Le but est de montrer que tu sais observer l'application proprement.
### Préparation
Lancer le front :
```bash
npm run dev
```
Ouvrir :
```txt
http://localhost:5173/?debug
```
Si Vite affiche un autre port, utiliser ce port.
Ouvrir DevTools :
- macOS : `Cmd + Option + I`
- Windows/Linux : `Ctrl + Shift + I`
Disposition conseillée :
- app à gauche ;
- DevTools docké à droite ;
- onglets prêts : `Console`, `Network`, `Sources`, `Application`.
### Étape 1 : Console
Objectif :
- vérifier que l'app ne crashe pas ;
- surveiller les erreurs quand on interagit.
À faire :
1. Ouvrir `Console`.
2. Reload la page.
3. Vérifier s'il y a des erreurs rouges.
4. Garder la console ouverte pendant la démo.
Phrase simple :
> Je commence par la console pour vérifier que la démo ne cache pas une erreur runtime.
### Étape 2 : Network
Objectif :
- montrer que les assets sont chargés ;
- vérifier map, modèles, sons, vidéos.
À faire :
1. Ouvrir `Network`.
2. Cocher `Disable cache`.
3. Reload.
4. Filtrer :
- `map.json`
- `model.gltf`
- `.webm`
- `.mp3`
- `.srt`
Ce que tu peux expliquer :
- `map.json` décrit la scène ;
- `model.gltf` charge les assets 3D ;
- `.webm` sert aux prompts in-game ;
- `.mp3` sert à musique/dialogues/SFX ;
- `.srt` sert aux sous-titres.
Phrase simple :
> Network me permet de vérifier que la feature n'est pas juste du code React, elle dépend aussi d'assets runtime.
### Étape 3 : Sources pour suivre le repair game
Objectif :
- montrer une transition de step ;
- prouver que `RepairGame` écrit dans Zustand.
À faire :
1. Ouvrir `Sources`.
2. `Cmd + P` ou `Ctrl + P`.
3. Chercher `RepairGame.tsx`.
4. Mettre un breakpoint sur un appel à `setMissionStep`.
5. Chercher `useGameStore.ts`.
6. Mettre un breakpoint dans `setMissionStep` ou `completeMission`.
Dans l'app :
1. Activer `?debug`.
2. Dans lil-gui, mettre `Scene = Physics`.
3. Garder `Camera Mode = Player`.
4. Dans le debug overlay, passer `Main state = Bike`.
5. Mettre le sub-state sur `waiting` si besoin.
6. Interagir avec l'objet ou avancer les steps via le debug panel.
Quand le breakpoint pause :
- regarder `mission` ;
- regarder `step` ;
- expliquer la transition.
Phrase simple :
> Là on voit que le composant ne change pas juste son état local. Il déclenche une transition dans le store global.
### Étape 4 : Sources pour suivre l'audio
Objectif :
- montrer qu'un son passe par `AudioManager`.
À faire :
1. Ouvrir `AudioManager.ts`.
2. Mettre un breakpoint dans `playSound`.
3. Déclencher un son, par exemple ouverture/fermeture de mallette.
4. Inspecter :
- `path`
- `volume`
- `options.category`
Ensuite :
1. Ouvrir le menu avec `Escape`.
2. Changer le volume SFX.
3. Mettre un breakpoint dans `setCategoryVolume`.
4. Vérifier que la catégorie change.
Phrase simple :
> Les sliders ne modifient pas directement un audio isolé. Ils mettent à jour un volume de catégorie dans le manager.
### Étape 5 : Application
Objectif :
- montrer les données locales du navigateur.
À faire :
1. Ouvrir `Application`.
2. Regarder `Local Storage`.
3. Chercher la clé de debug :
```txt
la-fabrik-debug-controls
```
Ce que ça montre :
- certains choix debug peuvent être persistés ;
- ce n'est pas la progression du jeu ;
- c'est juste du confort dev.
### Étape 6 : Performance, optionnel
Objectif :
- montrer que tu sais diagnostiquer si ça rame.
À faire seulement si demandé :
1. Ouvrir `Performance`.
2. Record 5 secondes.
3. Bouger dans la scène.
4. Stop.
5. Observer FPS, scripting, rendering.
Phrase simple :
> Si la scène rame, je ne devine pas. Je regarde si le temps part dans le JS, le rendu, ou le chargement d'assets.
## Démo recommandée en 5 minutes
### 1. Ouvrir la scène debug
```txt
http://localhost:5173/?debug
```
Dans lil-gui :
- `Scene = Physics`
- `Camera Mode = Player`
- `Debug Overlay = true`
### 2. Montrer le store
Dans le debug panel :
- passer `Main state = Bike`
- passer le step à `waiting`
- avancer avec `Next step`
Expliquer :
> Le panel debug écrit dans le même store que le vrai gameplay.
### 3. Montrer le repair game
Faire défiler les steps :
```txt
waiting -> inspected -> fragmented -> scanning -> repairing
```
Montrer :
- objet de mission ;
- mallette ;
- modèle éclaté ;
- scan ;
- pièces à grab ;
- validation bloquée si mauvaise pièce.
### 4. Montrer l'audio
Ouvrir DevTools :
- breakpoint dans `AudioManager.playSound` ;
- déclencher un son ;
- montrer `path` et `category`.
### 5. Montrer Zustand
Breakpoint dans `useGameStore`.
Expliquer :
> Quand la mission avance, ce n'est pas chaque composant qui invente son état. La progression passe par des actions centralisées.
## Questions probables et réponses simples
### Pourquoi Zustand ?
Parce qu'on a besoin d'une source de vérité partagée entre UI, monde 3D, debug panel et gameplay.
### Pourquoi pas tout dans Zustand ?
Parce que certaines valeurs changent trop souvent. Les positions, vitesses, raycasts et animations frame par frame restent dans des refs ou dans les composants R3F.
### Pourquoi un `AudioManager` ?
Parce que l'audio navigateur est impératif. On doit gérer des `HTMLAudioElement`, des pools, une musique active et des volumes de catégories.
### Pourquoi séparer octree et Rapier ?
Pour garder un player controller simple tout en utilisant Rapier pour les objets manipulables.
### Pourquoi `RepairGame` est data-driven ?
Pour réutiliser le même flow sur plusieurs missions et garder les variations dans `repairMissions.ts`.
### Qu'est-ce qui est incomplet ?
- pas de vrai `GameManager` global ;
- editor save uniquement en dev ;
- hand tracking encore approximatif sur profondeur et smoothing.
## Checklist avant la review
Commandes :
```bash
npm run format:check
npm run typecheck
npm run lint
npm run build
```
Pages à ouvrir :
- `/`
- `/?debug`
- `/editor`
- `/docs`
- `/docs/code-review`
Fichiers à avoir en tête :
- `src/world/World.tsx`
- `src/components/three/gameplay/RepairGame.tsx`
- `src/components/three/gameplay/RepairRepairingStep.tsx`
- `src/data/gameplay/repairMissions.ts`
- `src/managers/AudioManager.ts`
- `src/managers/stores/useGameStore.ts`
- `src/components/ui/debug/GameStateDebugPanel.tsx`
Réponses pièges à réviser :
- lock mouvement repair actif sur les étapes dédiées ;
- player pas Rapier ;
- save editor pas production ;
- old boot flags non branchés.
## Mini plan de révision
### Session 1 : 20 minutes
Lire :
- cette fiche ;
- `docs/technical/repair-game.md` ;
- `docs/technical/zustand.md`.
Objectif :
- savoir expliquer le repair game et le store.
### Session 2 : 20 minutes
Lire :
- `docs/technical/audio.md` ;
- `docs/technical/interaction.md`.
Objectif :
- savoir expliquer audio + interaction.
### Session 3 : 20 minutes
Faire la démo :
- lancer `npm run dev` ;
- ouvrir `/?debug` ;
- ouvrir DevTools ;
- faire un breakpoint dans `RepairGame` ;
- faire un breakpoint dans `AudioManager`.
Objectif :
- ne pas découvrir DevTools le jour de la review.
## Version ultra-courte à garder en tête
Si tu paniques, reviens à ça :
```txt
World monte la scène.
GameStageContent place les missions.
RepairGame orchestre les steps.
repairMissions fournit la data.
useGameStore garde la progression.
InteractionManager gère focus/trigger/grab.
AudioManager gère sons, musique, dialogues.
DevTools permet de suivre assets, state et appels runtime.
```
+27 -1
View File
@@ -9,7 +9,9 @@ This document describes the 3D components that are currently used in the runtime
| Interaction | `InteractableObject` | Focus detection through distance and raycasting |
| Interaction | `TriggerObject` | Press-to-trigger interactions, optional sound, optional spawned model |
| Interaction | `GrabbableObject` | Physics grab and hand-tracking grab behavior |
| Model | `AnimatedModel` | GLTF animation playback with fade, speed, and context controls |
| Model | `ExplodableModel` | Split/reassemble a GLTF model into separated parts |
| Model | `SimpleModel` | Lightweight static GLTF render helper |
| Gameplay | `RepairCaseModel` | Repair case lid animation, proximity float, and wobble |
## Continuous Animation
@@ -27,6 +29,28 @@ Use GSAP only for discrete timeline-style transitions. Current example:
- `RepairCaseModel` animates the case lid between open and closed rotations.
## Animated Models
`src/components/three/models/AnimatedModel.tsx` wraps drei `useAnimations()` around a loaded GLTF scene.
It supports:
- default animation playback
- optional autoplay
- fade duration
- speed multiplier
- `onLoaded`
- `onAnimationEnd`
- context controls through `AnimatedModelContext`
The debug physics scene currently uses it to preview:
```txt
public/models/electricienne-animated/model.gltf
```
with the `Dance` animation.
## GLTF Reuse
Use `useClonedObject` when a GLTF scene is reused by a component instance. It memoizes `scene.clone(true)` and keeps clone creation out of render churn.
@@ -44,7 +68,9 @@ src/components/three/
│ ├── InteractableObject.tsx
│ └── TriggerObject.tsx
├── models/
── ExplodableModel.tsx
── AnimatedModel.tsx
│ ├── ExplodableModel.tsx
│ └── SimpleModel.tsx
└── world/
└── SkyModel.tsx
```
+267 -106
View File
@@ -4,139 +4,300 @@ This document describes the code that exists today in the repository.
## Runtime Structure
- `src/main.tsx` mounts React.
- `src/App.tsx` mounts the TanStack `RouterProvider`.
- `src/router.tsx` declares the top-level routes:
- `/` mounts the playable 3D scene, debug perf overlay, and HTML overlays.
- `/editor` mounts the map editor page.
- `src/world/World.tsx` composes the active scene, including:
- environment and lighting
- debug helpers and debug camera mode
- either the map scene or the debug physics test scene
- the player rig when the active camera mode is `player`
- `src/hooks/world/useWorldSceneLoading.ts` owns the production scene loading state shared by `World`, `GameMap`, and the player octree readiness.
- `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, renders them progressively, and shows fallback cubes for missing models.
- `src/world/GameMapCollision.tsx` builds the player collision octree from dedicated collision nodes only.
- `src/world/GameStageContent.tsx` is wrapped in Rapier `Physics` in the production game scene so stage gameplay objects can use physics without moving the map or player to Rapier. It now mounts reusable `RepairGame` instances for `bike`, `pylone`, and `ferme` mission states.
- `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map with the existing grab/trigger/model-preview objects plus separate `Bike`, `Pylone`, and `Farm` repair playground zones.
- `src/world/player/Player.tsx` mounts the camera and controller.
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, repair-step movement locking, and interaction input.
- `src/main.tsx` mounts React in `StrictMode`.
- `src/App.tsx` mounts TanStack `RouterProvider`.
- `src/router.tsx` declares `/`, `/editor`, and `/docs`.
- `src/pages/page.tsx` composes the playable route with `HandTrackingProvider`, React Three Fiber `Canvas`, `World`, `DebugPerf`, `GameUI`, and `SceneLoadingOverlay`.
- `src/pages/editor/page.tsx` composes the local editor route.
- `src/components/docs/DocsLayout.tsx` composes the in-app documentation route.
Detailed runtime-loading notes live in `docs/technical/scene-runtime.md`.
## World Composition
`src/world/World.tsx` is the main 3D scene composer.
Always-mounted scene systems:
- `Environment`
- `Lighting`
- debug helpers when `?debug` is active
- optional hand-tracking glove overlays
- optional debug camera controls
Game scene systems:
- `GameMap`
- Rapier `Physics` wrapping `GameStageContent`
- `GameMusic`
- `GameDialogues`
- `GameCinematics` only while `mainState === "outro"`
- `Player` after gameplay is ready
Debug physics scene systems:
- `TestMap`
- `Player` after the debug octree is ready
Debug scene and camera mode are controlled by `src/utils/debug/Debug.ts` and enabled with `?debug`.
## Scene Loading
The production game scene is considered ready only after:
- map data and visible map nodes have settled
- collision source models have settled
- the player octree exists
- the Rapier gameplay stage has mounted
The player is not spawned until that readiness gate is satisfied. This avoids starting player movement, music, dialogue timing, and interactions while the map/stage is still loading.
## Physics Boundaries
The project currently uses two collision layers with separate responsibilities:
The project currently uses two collision systems with separate responsibilities:
- `GameMapCollision` builds an octree used by the player controller for map collision.
- The player octree must be built from a small collision-only subset of map nodes. It currently uses the `terrain` node only instead of traversing the full visible map, because building an octree from all rendered props can overload the browser renderer.
- `GameStageContent` is wrapped in Rapier `Physics` for gameplay objects such as repair triggers, cases, grabbables, and future mission-specific objects.
- `TestMap` owns its own Rapier `Physics` playground so repair gameplay can be tuned per mission state without depending on the production map layout.
- Player movement uses a Three.js `Capsule` and an `Octree`.
- Gameplay objects use Rapier rigid bodies and colliders.
Keep the player and map octree outside the Rapier provider until there is a deliberate migration plan. This avoids mixing player movement rules with object physics before the gameplay systems need it.
`GameMapCollision` builds the player octree from explicit collision nodes. It currently uses only the `terrain` node.
`GameStageContent` is wrapped in Rapier `Physics` so repair cases, triggers, and grabbable parts can use physics without migrating the player controller to Rapier.
This split is deliberate. It keeps the player controller simple while still enabling physical manipulation for gameplay objects.
## Gameplay Layer
The current core gameplay feature is the reusable repair game.
Production placements live in:
```txt
src/world/GameStageContent.tsx
```
The reusable flow lives in:
```txt
src/components/three/gameplay/RepairGame.tsx
```
Mission-specific data lives in:
```txt
src/data/gameplay/repairMissions.ts
```
The repair game supports:
```txt
locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done
```
Detailed repair-game implementation notes live in `docs/technical/repair-game.md`.
## State Management
Durable progression state lives in:
```txt
src/managers/stores/useGameStore.ts
```
It owns:
- `mainState`
- intro state
- `ebike`, `pylon`, and `farm` mission state
- outro state
- `isCinematicPlaying`
- progression actions
- generic mission actions
Settings state lives in:
```txt
src/managers/stores/useSettingsStore.ts
```
Subtitle display state lives in:
```txt
src/managers/stores/useSubtitleStore.ts
```
Detailed Zustand notes live in `docs/technical/zustand.md`.
## Managers
Managers are used for imperative runtime systems that own browser or frame-adjacent objects.
Current managers:
- `src/managers/AudioManager.ts`
- `src/managers/InteractionManager.ts`
`AudioManager` owns `HTMLAudioElement` instances, music playback, one-shot pools, category volumes, and optional stereo panning.
`InteractionManager` owns focused/nearby/holding state for trigger and grab interactions and exposes a snapshot through `useSyncExternalStore`.
## Interaction Model
- `src/managers/InteractionManager.ts` is the current interaction state source.
- `src/components/three/interaction/InteractableObject.tsx` handles focus detection through distance and raycasting.
- `src/components/three/interaction/TriggerObject.tsx` implements trigger-style interactions.
- `src/components/three/interaction/GrabbableObject.tsx` implements hold-and-release interactions.
- `src/hooks/interaction/useInteraction.ts` exposes the interaction snapshot to React UI.
- `src/components/ui/InteractPrompt.tsx` shows the `E` prompt for trigger interactions.
Core interaction files:
## Audio
- `src/components/three/interaction/InteractableObject.tsx`
- `src/components/three/interaction/TriggerObject.tsx`
- `src/components/three/interaction/GrabbableObject.tsx`
- `src/hooks/interaction/useInteraction.ts`
- `src/components/ui/InteractPrompt.tsx`
- `src/managers/AudioManager.ts` provides pooled one-shot playback, looped music playback, category volumes, and optional stereo pan for one-shot sounds.
- Supported audio categories are `music`, `sfx`, and `dialogue`.
- Trigger interactions may play SFX directly through `AudioManager`.
The player controller bridges raw input to semantic interaction actions:
## Settings Menu
- `E` triggers focused trigger objects
- primary mouse button grabs focused grabbable objects
- hand tracking can grab hand-controlled grabbable objects
- `src/managers/stores/useSettingsStore.ts` stores settings for music volume, SFX volume, dialogue volume, subtitle visibility, subtitle language, repair runtime, and menu visibility.
- `src/components/ui/GameSettingsMenu.tsx` renders the in-game options menu.
- `src/components/ui/GameUI.tsx` mounts the settings menu as an HTML overlay outside the canvas.
- `Esc` opens and closes the menu, and `src/world/player/PlayerController.tsx` ignores player input while the menu is open.
- Volume changes are forwarded to `AudioManager` by category.
Detailed interaction notes live in `docs/technical/interaction.md`.
## Dialogues And Subtitles
## Audio, Dialogue, And Subtitles
- `public/sounds/dialogue/dialogues.json` is the runtime dialogue manifest.
- Dialogue audio files live under `public/sounds/dialogue/`.
- Subtitle files live under `public/sounds/dialogue/subtitles/{fr|en}/`.
- The current subtitle model is one SRT file per voice and language.
- `src/types/dialogues/dialogues.ts` contains the dialogue manifest types.
- `src/utils/dialogues/dialogueManifestValidation.ts` validates manifest shape at runtime.
- `src/utils/dialogues/loadDialogueManifest.ts` loads the manifest and SRT cues, with French fallback when the selected language is missing.
- `src/utils/subtitles/parseSrt.ts` parses SRT blocks and timecodes.
- `src/utils/dialogues/playDialogue.ts` plays dialogue audio and synchronizes the active subtitle against the audio element time.
- `src/managers/stores/useSubtitleStore.ts` stores the currently displayed subtitle cue.
- `src/components/ui/Subtitles.tsx` renders the subtitle overlay.
- `src/world/GameDialogues.tsx` currently triggers dialogue entries that define a `timecode`.
- Dialogue playback is queued so multiple dialogue requests do not overlap.
Audio is split into:
- `music`
- `sfx`
- `dialogue`
Runtime dialogue data lives under:
```txt
public/sounds/dialogue/
```
The current subtitle model is one SRT file per voice and language. A dialogue entry references one cue by `subtitleCueIndex`.
`src/utils/dialogues/playDialogue.ts` queues dialogue playback and synchronizes the active subtitle cue against the playing audio element.
Detailed audio notes live in `docs/technical/audio.md`.
## Cinematics
- `public/cinematics.json` is the runtime cinematic manifest.
- `src/types/cinematics/cinematics.ts` contains cinematic manifest types.
- `src/utils/cinematics/cinematicManifestValidation.ts` validates manifest shape at runtime.
- `src/utils/cinematics/loadCinematicManifest.ts` loads `/cinematics.json`.
- `src/world/GameCinematics.tsx` triggers cinematics that define a global `timecode`.
- Cinematics use GSAP timelines to animate the active camera position and look target.
- `dialogueCues` on a cinematic trigger dialogue IDs at times relative to the cinematic start.
- `src/managers/stores/useGameStore.ts` exposes `isCinematicPlaying`, used to lock player input during cinematics.
Runtime cinematic data lives in:
## Debug System
```txt
public/cinematics.json
```
- Debug mode is enabled with `?debug`.
- `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
- `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
- `src/components/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
- `src/components/ui/debug/DebugOverlayLayout.tsx` mounts the compact HTML debug overlay when enabled from `lil-gui`.
- `src/components/ui/debug/GameStateDebugPanel.tsx` exposes current game state, main/sub-state switching, previous/next step controls, and reset.
- `src/components/ui/debug/HandTrackingDebugPanel.tsx` shows hand tracking status, usage, loaded glove model, hand count, and fist state while hand tracking is active.
- `src/components/ui/SceneLoadingOverlay.tsx` displays the fullscreen loading state for 3D scenes, including the production game scene, debug physics scene, and editor scene.
- `src/components/three/handTracking/HandTrackingGlove.tsx` places the rigged `gant_l` and `gant_r` models on detected hands in the debug physics scene.
- `src/components/debug/scene/DebugHelpers.tsx` mounts debug helpers.
- `src/components/debug/scene/DebugCameraControls.tsx` mounts the free debug camera.
- `lil-gui` global debug controls include camera mode, scene mode, `R3F Perf`, and `Debug Overlay`; interaction-specific controls live in the `Interaction` folder.
Cinematics support camera keyframes, GSAP timelines, optional dialogue cues, and `isCinematicPlaying` input locking. Current world integration mounts `GameCinematics` only during the outro state.
## 3D Component Domains
## Hand Tracking
- `src/components/three/models/` contains reusable model helpers such as `ExplodableModel`.
- `src/components/three/interaction/` contains reusable interaction wrappers such as `InteractableObject`, `TriggerObject`, and `GrabbableObject`.
- `src/components/three/handTracking/` contains R3F hand tracking debug models such as the glove overlays.
- `src/components/three/gameplay/` contains the reusable production `RepairGame` flow, repair case, repair steps, and repair prompt components.
- `src/components/three/world/` contains reusable world/environment objects such as `SkyModel`.
Hand tracking can use:
- local Python backend over WebSocket
- browser-side MediaPipe through `@mediapipe/tasks-vision`
Important files:
- `src/providers/gameplay/HandTrackingProvider.tsx`
- `src/hooks/handTracking/useRemoteHandTracking.ts`
- `src/hooks/handTracking/useBrowserHandTracking.ts`
- `src/hooks/handTracking/useBothFistsHold.ts`
- `src/components/three/handTracking/HandTrackingGlove.tsx`
- `backend/main.py`
Hand tracking is activated lazily. In production it is enabled during repair steps that need hand input. In debug physics mode it is enabled when interaction context makes hand input useful.
Detailed hand-tracking notes live in `docs/technical/hand-tracking.md`.
## Editor System
- `src/pages/editor/page.tsx` is the route-level editor page for `/editor`.
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
- `src/components/editor/EditorDialogueManifestPanel.tsx` edits `public/sounds/dialogue/dialogues.json`.
- `src/components/editor/EditorCinematicManifestPanel.tsx` edits `public/cinematics.json`.
- `src/components/editor/EditorSrtPanel.tsx` renders the dialogue SRT editor inside the editor control panel.
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
- `src/utils/map/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
- `src/types/editor/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used across store, config, debug UI, and gameplay components.
The editor route is:
```txt
/editor
```
Important editor files:
- `src/pages/editor/page.tsx`
- `src/components/editor/EditorControls.tsx`
- `src/components/editor/scene/EditorScene.tsx`
- `src/components/editor/scene/EditorMap.tsx`
- `src/components/editor/EditorDialogueManifestPanel.tsx`
- `src/components/editor/EditorCinematicManifestPanel.tsx`
- `src/components/editor/EditorSrtPanel.tsx`
- `src/hooks/editor/useEditorSceneData.ts`
- `src/hooks/editor/useEditorHistory.ts`
- `src/controls/editor/FlyController.tsx`
The editor shares `MapNode` data with the runtime map loader.
Local save endpoints live in `vite.config.ts`:
- `POST /api/save-map`
- `POST /api/save-srt`
- `GET /api/validate-dialogues`
- `POST /api/save-dialogues`
- `POST /api/save-cinematics`
These are Vite dev-server helpers, not production backend APIs.
Detailed editor notes live in `docs/technical/editor.md`.
## Documentation System
The docs route uses:
- `src/components/docs/DocsLayout.tsx`
- `src/components/docs/DocsDocument.tsx`
- `src/data/docs/docsSections.ts`
- `src/routes/DocsRoute.tsx`
- `src/pages/docs/**/page.tsx`
Docs pages import Markdown files with `?raw` and render them through `react-markdown` plus `remark-gfm`.
## 3D Component Domains
`src/components/three/` is organized by domain:
- `gameplay`: repair-game flow and repair components
- `handTracking`: glove overlays
- `interaction`: trigger/grab/focus wrappers
- `models`: animated, simple, and explodable model helpers
- `world`: world/environment objects
## Map Data
- `public/map.json` is expected to be a `MapNode[]`.
- Each map node `name` maps to `public/models/{name}/model.glb` when available, with `public/models/{name}/model.gltf` kept as fallback.
- The editor renders a fallback cube for missing models.
- The game scene renders fallback cubes for nodes whose model cannot be resolved.
- The game scene currently uses `terrain` as the collision source for the player octree. Additional collision nodes should be explicit lightweight collision assets, not arbitrary visible decoration models.
Runtime map data:
```txt
public/map.json
```
Expected shape:
```ts
interface MapNode {
name: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
}
```
Each `name` maps to:
```txt
public/models/{name}/model.glb
public/models/{name}/model.gltf
```
## Current Limitations
- The repository is a prototype, not the full intended game runtime.
- `src/world/debug/TestMap.tsx` is part of the active scene composition.
- There is no central gameplay orchestrator such as `GameManager`.
- Mission state exists in Zustand and the repair flow is implemented as a prototype for the current repair missions.
- Cinematics and dialogues exist as prototype timecode-driven systems; dialogue branching and broader gameplay orchestration are still limited.
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
- The repository is still a prototype.
- There is no central production `GameManager`.
- The repair game is implemented, but broader mission orchestration is still light.
- `useRepairMovementLocked()` locks player movement during focused repair steps.
- Player collision and Rapier gameplay physics are separate systems.
- Editor persistence is local development tooling only.
- Debug systems are still part of active scene composition and should remain easy to identify.
+217
View File
@@ -0,0 +1,217 @@
# Audio Technical Notes
This document describes the audio systems that exist in the current codebase.
## Scope
Audio is currently split into three runtime categories:
- `music`: looped background music
- `dialogue`: spoken dialogue audio linked to subtitles
- `sfx`: one-shot interaction and feedback sounds
The shared runtime service is `src/managers/AudioManager.ts`. User-facing volume settings live in `src/managers/stores/useSettingsStore.ts` and are forwarded to `AudioManager` by category.
## AudioManager
`AudioManager` is a singleton side-effect service. It owns browser audio elements, category volumes, pooled one-shot sounds, music playback, and stereo panning for one-shot sounds.
Supported public methods:
- `playMusic(path, volume)`: starts or updates a looped music track.
- `stopMusic()`: stops the active music track.
- `playSound(path, volume, options)`: plays a pooled one-shot sound and returns its `HTMLAudioElement`.
- `setCategoryVolume(category, volume)`: updates `music`, `sfx`, or `dialogue` volume.
- `getCategoryVolume(category)`: reads the current category volume.
- `destroy()`: stops music, clears pools, closes the audio context, and resets the singleton.
One-shot sounds are pooled by path with a maximum pool size per sound. If every element in a pool is busy, the pool grows until the limit, then recycles an existing element.
Browser autoplay restrictions are handled in `playMusic()`: if playback is blocked by the browser, the manager waits for a user `pointerdown` or `keydown`, then retries the music.
## Music
Runtime music is mounted by `src/world/GameMusic.tsx`.
Current behavior:
- `GameMusic` calls `AudioManager.getInstance().playMusic()` on mount.
- The current music path is `/sounds/musique/test.mp3`.
- The base music volume is `0.33` before category volume is applied.
- On unmount, `GameMusic` calls `stopMusic()`.
Effective music volume is:
```txt
base music volume * settings music volume
```
Use `music` only for long-running looped background tracks. Do not use `playSound()` for music, because one-shot pooling is designed for short overlapping sounds.
## Sound Effects
SFX are short one-shot sounds. They should use `AudioManager.playSound()` with the default category or with `{ category: "sfx" }`.
Example:
```ts
AudioManager.getInstance().playSound("/sounds/sfx/click.mp3", 0.8, {
category: "sfx",
pan: 0,
});
```
Useful options:
- `category`: `sfx` or `dialogue`; defaults to `sfx`.
- `pan`: stereo panning from `-1` left to `1` right.
- `playbackRate`: playback speed multiplier.
SFX volume is controlled by the settings menu through the `sfx` category volume.
## Dialogues
Runtime dialogue data lives under `public/sounds/dialogue/`.
```txt
public/
└── sounds/
└── dialogue/
├── dialogues.json
└── subtitles/
├── fr/
│ ├── narrateur.srt
│ ├── fermier.srt
│ └── electricienne.srt
└── en/
├── narrateur.srt
├── fermier.srt
└── electricienne.srt
```
The dialogue manifest shape is defined in `src/types/dialogues/dialogues.ts`.
Each dialogue entry contains:
- `id`: stable dialogue identifier
- `voice`: voice group, currently `narrateur`, `fermier`, or `electricienne`
- `audio`: runtime audio path
- `subtitleCueIndex`: cue number inside that voice/language SRT file
- `timecode`: optional global trigger time in seconds from scene start
Dialogues are played through `src/utils/dialogues/playDialogue.ts`.
Important functions:
- `playDialogueById(manifest, dialogueId)`: plays a dialogue from an already loaded manifest.
- `queueDialogueById(manifest, dialogueId)`: queues dialogue playback so multiple requests do not overlap.
- `playGameplayDialogueById(dialogueId)`: loads the gameplay manifest once and queues a dialogue by ID.
- `clearQueuedDialogues()`: resolves pending dialogue requests and clears the queue.
Dialogue audio uses `AudioManager.playSound()` with `{ category: "dialogue" }`, so it follows the dialogue volume setting.
## Dialogue And SRT Link
The subtitle model is one SRT file per voice and language, not one SRT file per dialogue.
A dialogue chooses its subtitle by combining:
1. `voice`
2. selected subtitle language from settings
3. `subtitleCueIndex`
For example, this dialogue:
```json
{
"id": "narrateur_bienvenueaaltera",
"voice": "narrateur",
"audio": "/sounds/dialogue/narrateur/bienvenueaaltera.mp3",
"subtitleCueIndex": 1
}
```
loads cue `1` from:
```txt
public/sounds/dialogue/subtitles/fr/narrateur.srt
```
when the subtitle language is French, or from:
```txt
public/sounds/dialogue/subtitles/en/narrateur.srt
```
when the subtitle language is English.
If the selected language is missing, the loader falls back to French. Missing English SRT files are warnings during validation, not runtime errors.
SRT timecodes are relative to the dialogue audio file. They are not relative to the game clock and not relative to a cinematic timeline.
## Subtitle Runtime
`playDialogueById()` loads the matching subtitle cue with `loadDialogueSubtitleCue()` before playing the audio.
While audio plays:
- `timeupdate` checks `audio.currentTime`
- the active subtitle is written to `useSubtitleStore`
- `src/components/ui/Subtitles.tsx` renders the current speaker and text
- `ended` and `pause` clear the subtitle
The subtitle overlay respects settings from `useSettingsStore`, including visibility and selected language.
## Global Timecode Dialogues
`src/world/GameDialogues.tsx` loads the dialogue manifest and triggers entries that define `timecode`.
This is useful for simple global scene timing. It should not be used for dialogue that belongs to a cinematic. Cinematic-owned dialogue should be triggered by `dialogueCues` in `public/cinematics.json` instead, otherwise the same dialogue can play twice.
## Cinematic Dialogue Cues
`public/cinematics.json` can include `dialogueCues`.
Each cue contains:
- `time`: seconds relative to the cinematic start
- `dialogueId`: ID from `dialogues.json`
`src/world/GameCinematics.tsx` uses those cues to play dialogue during camera timelines. This keeps camera movement and dialogue playback synchronized without relying on global scene time.
## Editor Tooling
The `/editor` route provides three audio-related tools:
- `Dialogues`: edits `public/sounds/dialogue/dialogues.json` and previews dialogue playback.
- `SRT`: edits one SRT file at a time and validates dialogue assets.
- `Cinematics`: links dialogue IDs to cinematic timelines through `dialogueCues`.
Dev-only Vite endpoints in `vite.config.ts` support local saves:
- `POST /api/save-dialogues`
- `POST /api/save-srt`
- `GET /api/validate-dialogues`
- `POST /api/save-cinematics`
These endpoints are local development helpers. They are not production APIs.
## Validation
`GET /api/validate-dialogues` validates:
- manifest shape
- referenced dialogue audio files
- French SRT files
- referenced subtitle cue indexes
- optional English SRT files as warnings
Run validation after adding or renaming dialogue audio, changing cue indexes, or editing SRT files.
## Known Limitations
- There is no production persistence for audio manifests or SRT files.
- Dialogue branching is not implemented.
- Dialogue interruption and priority rules are minimal; playback is queue-based.
- SRT editing is text-based and does not yet provide waveform editing.
- Music currently supports one active looped track at a time.
+67 -22
View File
@@ -4,7 +4,7 @@ This document describes the map editor that exists in the current codebase.
## Purpose
The editor is a React route used to inspect and adjust the `public/map.json` scene data from inside the La-Fabrik app. It shares the same `MapNode` data format as the game scene and uses React Three Fiber for rendering.
The editor is a React route used to inspect and adjust the current hierarchical `public/map.json` scene data from inside the La-Fabrik app. It exposes editable object nodes as a flat list for UI selection, while preserving and saving the full map tree.
## Routing
@@ -52,7 +52,7 @@ src/
## Responsibilities
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as primary selected object, selected object indexes, hovered object, transform mode, selection lock, player-mode toggle, cinematic preview requests, and editor scene loading state.
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
@@ -60,9 +60,9 @@ src/
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls. For multi-selection, it attaches `TransformControls` to a temporary group centered on the selected nodes, then decomposes the group delta back into each selected node transform.
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas. The panel is organized into top-level `details` groups: `Editor`, `Cinematics`, `Dialogues`, and `SRT`.
`src/components/editor/EditorDialogueManifestPanel.tsx` renders the dialogue manifest editor. It loads `dialogues.json`, edits dialogue entries, previews selected dialogue playback, creates missing French SRT cues, and saves the manifest through a dev-server endpoint.
@@ -72,7 +72,7 @@ src/
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
`src/utils/map/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.glb` files first, then falls back to `public/models/{name}/model.gltf`.
`src/utils/map/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json`, validates the hierarchical payload, exposes editable nodes with their `sourcePath` back to the tree, and resolves available `public/models/{name}/model.glb` files first, then falls back to `public/models/{name}/model.gltf`.
`src/utils/editor/loadEditorScene.ts` contains editor-only upload handling for user-selected folders.
@@ -87,22 +87,13 @@ interface MapNode {
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
sourcePath?: number[];
}
```
`public/map.json` is expected to be a `MapNode[]`.
`public/map.json` may be hierarchical. The editor keeps the hierarchy in `SceneData.mapTree` and stores editable entries in `SceneData.mapNodes` with a `sourcePath` back to the real tree node.
```json
[
{
"name": "pylone",
"type": "Mesh",
"position": [0, 5, 0],
"rotation": [0, 1.57, 0],
"scale": [1, 1, 1]
}
]
```
Group nodes use `role: "group"`; editable nodes keep `name`, `type`, `position`, `rotation`, and `scale`.
Each node `name` maps to a model folder:
@@ -122,16 +113,26 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
2. `useEditorSceneData` calls `loadMapSceneData()`.
3. `loadMapSceneData()` loads `/map.json` and available model URLs.
4. If `/map.json` is missing, the page displays a folder-upload flow.
5. `EditorScene` renders the grid, lights, camera controls, and map nodes.
6. `EditorControls` exposes transform mode, history actions, export, save, and selection info.
5. The route-level loading overlay reports map JSON loading, then hands off to the editor scene once the map payload is ready.
6. `EditorScene` renders the grid, lights, camera controls, and map nodes inside `Suspense`.
7. `EditorControls` exposes transform mode, terrain snap, terrain-selection lock, add/delete node, precise scale inputs, history actions, camera focus/reset, export, save, JSON preview, selection lock, multi-selection status, and the cinematic/dialogue/SRT editors.
## Controls
- Click: select a node.
- `Shift` + right click: add or remove a node from the multi-selection.
- `Esc`: clear selection.
- Selection lock button: prevent object clicks and `Esc` from changing the current selection.
- Selection clear button: intentionally clear the current selection even when the lock is active.
- `T`: translate mode.
- `R`: rotate mode.
- `S`: scale mode.
- Snap terrain on move: enabled by default and applied while translating an object.
- Multi-selection transforms use a temporary centered group and write the resulting position, rotation, and scale back to every selected map node.
- Lock terrain: enabled by default so terrain remains visible but ignores selection clicks.
- Camera action: centers on the selected object or resets to the editor home view.
- Add node: creates a fallback cube under `blocking` using the requested model folder name.
- Delete selected node: removes the editable node from the preserved map tree.
- `Ctrl+Z` or `Cmd+Z`: undo.
- `Ctrl+Y` or `Cmd+Y`: redo.
- `WASD`, `ZQSD`, or arrow keys: move in player-controller mode.
@@ -142,10 +143,54 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
The editor supports two output paths:
- Export JSON downloads the current `MapNode[]` as `map.json`.
- Save to Server posts the current `MapNode[]` to `/api/save-map`.
- Export JSON downloads the current hierarchical map tree as `map.json`.
- Save to Server posts the current hierarchical map tree to `/api/save-map`.
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It writes to `public/map.json` and enforces a maximum payload size.
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It validates the payload through the shared map parser, writes to `public/map.json`, and enforces a maximum payload size.
## Editor Loading Overlay
The editor uses `SceneLoadingOverlay` like the runtime scene for the route-level map JSON loading phase.
The route tracks the map JSON loading phase:
- map JSON loading through `useEditorSceneData()`
The overlay is rendered outside the canvas so it remains visible while the editor route mounts. Model loading is left to R3F `Suspense` boundaries to avoid progress updates during model render.
## Panel Groups
`EditorControls` uses the local `EditorPanelGroup` helper to keep the side panel navigable as tools grow.
Current group order:
1. `Editor`
2. `Cinematics`
3. `Dialogues`
4. `SRT`
Inside the `Editor` group, the section order is:
1. `Shortcuts`
2. `Transform`
3. `Selection`
4. `View`
5. `JSON`
6. `File`
The `Shortcuts` group is nested and closed by default to reduce visual noise.
## Selection Lock
Selection lock is owned by `EditorPage` through `isSelectionLocked`.
The state is passed to:
- `EditorControls`, to render the lock/unlock button
- `EditorScene`, to block `Esc` deselection when locked
- `EditorMap`, to block object selection when locked
The clear button calls `onClearSelection` directly from `EditorControls`. Clicking empty canvas space does not clear the current selection; use `Esc` or the explicit clear button instead.
## Dialogue SRT Editing
-187
View File
@@ -1,187 +0,0 @@
# Game Flow - La Fabrik
## Étapes du jeu
```
intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik
```
---
## Détail des étapes
### 1. `intro` (initial)
- État initial au chargement du jeu
- Aucune action, juste une étape de départ
- Transition automatique vers `start-intro`
### 2. `start-intro`
- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée
- **Action** : Joue l'audio d'intro (`intro`)
- **Attente** : Attend que l'audio se termine
- **Transition** : Vers `naming` quand l'audio se termine
### 3. `naming`
- **Déclenchement** : Quand l'audio d'intro se termine
- **Action** : Affiche un input pour demander le prénom du joueur
- **Attente** : L'utilisateur entre son prénom et valide
- **Transition** : Vers `bienvenue` quand l'utilisateur valide
### 4. `bienvenue`
- **Déclenchement** : Quand l'utilisateur valide son prénom
- **Actions** :
- Affiche "Bienvenue {prénom} !" à l'écran
- Joue l'audio de bienvenue
- **Attente** : Attend que l'audio se termine
- **Transition** : Vers `star-move` quand l'audio se termine
### 5. `star-move`
- **Déclenchement** : Quand l'audio de bienvenue se termine
- **Action** : Active le mouvement du joueur (`setCanMove(true)`)
- **État** : Le joueur peut maintenant se déplacer librement
- **Zone** : La détection de zone devient active (ZoneDetection)
### 6. `mission2`
- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`)
- **Actions** :
- Stocke `activityCity: false` dans le store Zustand
- Joue l'audio `alertCentral`
- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations
- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem`
### 7. `searching_problem`
- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`)
- **Actions** :
- Joue l'audio `searchingProblem`
- Affiche l'objet "central" (position: `[1, 15, -45]`)
- **Attente** : Le joueur interagit avec l'objet "central"
### 8. `preparation`
- **Déclenchement** : Quand le joueur interagit avec l'objet "central"
- **Actions** :
- Bloque le mouvement (`setCanMove(false)`)
- Cache l'objet "central"
### 9. `outOfFabrik`
- **Déclenchement** : (non implémenté pour le moment)
- **Action** : Transition vers l'étape finale
---
## Fichiers clés
| Fichier | Rôle |
| --------------------------------------- | --------------------------------------------------------- |
| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu |
| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand |
| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio |
| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue |
| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone |
| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 |
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
| `src/data/zones.ts` | Configuration des zones de transition |
| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville |
---
## Configuration audio
```typescript
// src/data/audioConfig.ts
export const AUDIO_PATHS = {
intro: "/sounds/fa.mp3",
bienvenue: "/sounds/fa.mp3",
alertCentral: "/sounds/fa.mp3",
searchingProblem: "/sounds/fa.mp3",
};
```
---
## Configuration des zones
```typescript
// src/data/zones.ts
export const ZONES: Zone[] = [
{
id: "fabrikExit",
position: [-5, 25, -15],
radius: 10,
height: 20,
targetStep: "mission2",
},
{
id: "searchingProblemZone",
position: [-5, 25, -30],
radius: 10,
height: 20,
targetStep: "searching_problem",
},
];
```
---
## Store Zustand
```typescript
// src/stores/gameStore.ts
interface GameState {
step: GameStep;
activityCity: boolean;
playerName: string;
canMove: boolean;
setStep: (step: GameStep) => void;
setActivityCity: (value: boolean) => void;
setPlayerName: (name: string) => void;
setCanMove: (canMove: boolean) => void;
}
```
---
## Hooks personnalisés
### useActivityCity
Permet aux objets 3D de réagir au changement d'activité de la ville :
```typescript
import { useActivityCity } from "@/hooks/useActivityCity";
function MyAnimatedObject() {
const activityCity = useActivityCity(); // true par défaut, false en mission2
// L'animation se déclenche quand activityCity change à false
// Utiliser useEffect pour réagir au changement
}
```
---
## Debug
En mode debug (`?debug` dans l'URL), on peut voir :
- **Game Step** : L'étape actuelle dans le panneau lil-gui
- **Player Position** : Position X, Y, Z du joueur en temps réel
- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents
---
## Notes techniques
- Le mouvement du joueur est bloqué tant que `canMove` est `false`
- Le store Zustand (`useGameStore`) est la source principale de vérité
- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions
- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store
- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques
+110 -34
View File
@@ -10,17 +10,25 @@ It is now also available to the production repair flow when a mission reaches a
## Runtime Flow
1. The browser captures webcam frames in `src/hooks/handTracking/useRemoteHandTracking.ts`.
2. Frames are sent to the local Python backend over WebSocket.
3. The backend runs MediaPipe hand landmark detection.
4. The backend returns hand data including landmarks, handedness, score, center point, and `isFist`.
5. React stores the latest snapshot in the hand tracking provider.
6. `GrabbableObject` reads that snapshot each frame and uses fist state plus raycasting to grab objects.
7. `HandTrackingGlove` reads the same snapshot and places the rigged `gant_l` and `gant_r` models on the detected hands when hand tracking is active.
The frontend can run hand tracking with two interchangeable sources, selected from the debug source controller:
- **Browser JS** (`src/hooks/handTracking/useBrowserHandTracking.ts`) runs MediaPipe `hand_landmarker.task` directly in the browser via `@mediapipe/tasks-vision`. Default for debug.
- **Backend** (`src/hooks/handTracking/useRemoteHandTracking.ts`) sends webcam frames as JPEG over WebSocket to a local Python process that runs MediaPipe and returns landmarks.
Both sources funnel into the same `HandTrackingContext` so all consumers see one shared snapshot:
1. The active source captures or receives landmarks.
2. The hook applies an EMA smoothing pass on the landmarks before publishing the snapshot.
3. `HandTrackingProvider` exposes that snapshot through React context.
4. `GrabbableObject` reads the snapshot each frame and uses `hand.isFist` plus raycasting to grab objects.
5. `HandTrackingVisualizer` paints the SVG hand silhouette overlay on top of the canvas — the primary visualization.
6. `HandTrackingGlove` (opt-in, see UI And Debug) places a rigged 3D glove on each detected hand when enabled via the debug toggle.
All consumers — fist detection, grab raycasting, SVG silhouette, optional 3D glove — read the **same** landmarks from the snapshot. None of them depend on the others.
## Activation Rules
Hand tracking is intentionally gated so the webcam and backend are not used all the time.
Hand tracking is gated so the webcam and runtime are only spun up when actually needed.
The debug activation conditions are:
@@ -28,16 +36,26 @@ The debug activation conditions are:
- scene mode is `physics`
- the player is near an interaction, is holding an object, or is hand-holding an object
This keeps hand tracking active while the player is inside an interaction zone, even if the camera is not aimed directly at the object.
The production repair activation conditions are:
- active `mainState` is `bike`, `pylone`, or `ferme`
- active `mainState` is `ebike`, `pylon`, or `farm`
- the active mission step is `inspected`, `repairing`, `reassembling`, or `done`
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`.
In the current production repair flow, `inspected` uses a two-fists hold gesture to advance to `fragmented`. The hold must last one second and is independent from local object interaction distance once the mission is in the correct state. Keyboard input for the same transition is handled separately by the repair case trigger, so pressing `E` requires the case to be focused through the shared interaction system.
### Linger
Once activation turns off (player walks back out of a trigger zone, or a mission step transitions away), the runtime stays alive for `HAND_TRACKING_LINGER_MS` (2000 ms) before being torn down. This gives MediaPipe enough time to finish initializing the webcam and load the model on a fresh entry — without the linger, a quick walk-through of a trigger zone never produces a detected hand.
## Provider Stability
`HandTrackingProvider` always renders the same JSX root (`HandTrackingRuntime`) and exposes `enabled` as a prop. Returning two different element types (`<HandTrackingContext value=IDLE>` vs `<ActiveHandTrackingProvider>`) used to be the historical shape and was the root cause of WebGL context loss: every `enabled` toggle forced React to remount the entire subtree, including the `<Canvas>`, which destroyed the WebGL renderer.
The two source hooks are therefore mounted in permanence with an `enabled` flag that they early-return on. No webcam or MediaPipe resources are created while `enabled` is false.
## StrictMode Resilience
In development, `<StrictMode>` mounts → unmounts → remounts each effect to surface non-idempotent code. The two source hooks delay their actual `start()` call by `HAND_TRACKING_RUNTIME_START_DELAY_MS` (80 ms) and clear the timer on cleanup, so a StrictMode double-mount or a rapid `nearby` flicker never reaches `getUserMedia` twice.
## Backend
@@ -52,7 +70,27 @@ The Python process uses MediaPipe and the local model file:
backend/hand_landmarker.task
```
The backend sends normalized hand coordinates and landmarks. The frontend treats the values as screen-space inputs, then maps them into world space with the active Three.js camera.
The frontend sends JPEG frames at `HAND_TRACKING_FRAME_WIDTH × HAND_TRACKING_FRAME_HEIGHT` (320×240) to keep WebSocket bandwidth low. The backend sends normalized hand coordinates and landmarks.
## Browser MediaPipe
The browser path uses `hand_landmarker.task` (float16) downloaded from Google's MediaPipe model storage. The requested webcam resolution is **640×480** (`HAND_TRACKING_BROWSER_CAMERA_WIDTH/HEIGHT`), independent from the backend's 320×240. The float16 model is more sensitive than the backend Python model and needs the higher-resolution frame to detect hands reliably.
The MediaPipe delegate is currently `"GPU"`. CPU works too but is significantly slower; on a loaded scene the inference drops to ~5fps and the user feels noticeable lag during grab. MediaPipe creates its own WebGL context separate from Three.js, so there is no direct contention.
A singleton instance of `HandLandmarker` is cached in `src/lib/handTracking/browserHandTracking.ts`. `releaseBrowserHandLandmarker()` is called on cleanup and on WebGL context lost.
## Smoothing
MediaPipe at ~10 fps produces noticeable landmark jitter that, when fed raw into the scene, makes both the glove rig and any grabbed object tremble.
A simple exponential moving average is applied to every landmark before the snapshot is published:
```ts
smoothed.x = previous.x * (1 - factor) + next.x * factor;
```
The factor is `HAND_TRACKING_LANDMARK_SMOOTHING` (0.4). Hands are matched across frames by `handedness` so left/right don't bleed into each other.
## Frontend Data Shape
@@ -72,6 +110,17 @@ interface HandTrackingHand {
`x` and `y` are normalized camera coordinates. `z` is a relative depth value from MediaPipe, not an absolute world-space distance.
## Fist Detection
`isFist` is computed in `src/lib/handTracking/browserHandTracking.ts` (`isFist()` function) from landmarks alone — no model, no glove. The check is:
1. Palm center = mean of landmarks `[0, 5, 9, 13, 17]` (wrist + 4 MCPs).
2. Palm size = distance from wrist (landmark 0) to middle MCP (landmark 9).
3. For each of the four fingertip landmarks `[8, 12, 16, 20]`, check whether its distance to the palm center is less than `1.05 × palmSize`.
4. `isFist === true` iff all four fingertips pass the check.
The flag is attached to each hand on the snapshot at the publish step (`isFist: isFist(normalizedLandmarks)`) and read directly by `GrabbableObject.tsx` — the SVG visualizer and the 3D glove never participate in the gesture decision.
## Grab Targeting
The hand grab logic lives in `src/components/three/interaction/GrabbableObject.tsx`.
@@ -95,44 +144,71 @@ If any ray hits the object while the object is within `INTERACTION_RADIUS`, the
## Depth Handling
Because MediaPipe `z` is relative, the frontend captures the starting depth when the grab begins:
Because MediaPipe `z` is relative and noisy, the current frontend does not use it as a direct world-depth controller for object grabbing.
```txt
initialHandZ = hand.z
initialHoldDistance = hit.distance
```
Instead, `GrabbableObject` computes a ray from the 2D hand center and moves the object toward a configurable hold distance in front of the active camera. That hold distance is shared with the mouse grab path and can be tuned in the debug GUI.
While holding, the object distance from the camera is adjusted by the change in hand depth:
```txt
holdDistance = initialHoldDistance + (hand.z - initialHandZ) * sensitivity
```
The final hold distance is clamped between the configured grab minimum and maximum distances to avoid unstable movement.
This is less expressive than true depth-aware hand movement, but it is more stable for the current first-person prototype.
## UI And Debug
The current debug UI includes:
- `HandTrackingDebugPanel` inside `DebugOverlayLayout` for status, usage, loaded glove model, server state, hand count, and fist state
- `HandTrackingVisualizer` for the SVG landmark wireframe fallback
- `HandTrackingGlove` for the left-hand `gant_l` and right-hand `gant_r` models in the R3F scene
- `HandTrackingVisualizer` for the SVG hand silhouette overlay (always on when tracking is active)
- `HandTrackingFallback` for the last-resort hand silhouette overlay (legacy, see below)
- `HandTrackingGlove` for the per-hand rigged glove models in the R3F scene, opt-in via the **Show Model** toggle
- `r3f-perf` for render performance
- `lil-gui` for scene, camera, lighting, interaction, and grab controls
The hand tracking debug panel is a compact HTML grid outside the canvas. `Model loaded` displays the successfully loaded glove models. The SVG hand wireframe is only a fallback while models are loading or if a glove model fails to load.
### SVG Visualizer
`HandTrackingVisualizer` is the primary hand visualization. It draws a light-blue hand silhouette with a crisp dark-blue outline by:
1. Filling a palm polygon (landmarks `[1, 5, 9, 13, 17]` plus two synthetic wrist corners) and five finger tubes (thick rounded `stroke` along each finger's joint chain).
2. Wrapping the whole thing in an SVG `<filter>` that uses `feMorphology` to dilate the merged alpha by 2 px and subtract the original, producing a single continuous outline around the union — no internal seams where the palm and finger tubes overlap.
3. Shrinking every landmark toward the hand centroid by `RENDER_SCALE = 0.65` so the silhouette stays compact and doesn't dominate the screen.
4. Overlaying the 21 raw landmarks and 21 bones as faint translucent lines and dots, so the user can still see the MediaPipe data feeding the silhouette.
The SVG only displays when MediaPipe is active and the debug **Show Model** toggle is off (default). When the toggle is on, the SVG hides and `HandTrackingGlove` takes over.
### Show Model Toggle
The `Hand Tracking` debug folder exposes a single visualization switch:
- `showHandTrackingModel = false` (default): SVG visualizer renders, 3D glove is not mounted at all.
- `showHandTrackingModel = true`: SVG visualizer hides, 3D glove gets mounted for the detected hand(s).
The 3D glove is treated as opt-in legacy because it had bugs (WebGL context loss, finger rig artefacts) and its hit/grab role was never load-bearing — grab has always read landmarks directly.
### Fallback Overlay (legacy)
`HandTrackingFallback` draws a simple open-hand or fist silhouette positioned on the detected wrist landmark. It renders for any hand whose glove is in the `"error"` state in `useHandTrackingGloveStatus`. Now that the glove is opt-in and rarely mounted, the fallback effectively only fires in the rare case where the user enables `showHandTrackingModel` and the glove fails to load. It is kept on disk for that edge case but is not part of the default visual path.
## Glove Models
The current glove MVP uses `public/models/gant_l/model.gltf` and `public/models/gant_r/model.gltf`, which contain GLTF skins and armatures. Each model is positioned, oriented, and scaled from palm landmarks, then each finger bone chain is rotated toward the matching MediaPipe landmark chain.
The 3D glove is **opt-in** via the `Show Model` debug toggle (see UI And Debug). It is not mounted by default; the SVG visualizer is the primary hand UI. The information below applies only when the toggle is enabled.
The glove models are intentionally smaller than the raw SVG overlay so they do not dominate the camera view.
`HandTrackingGlove` loads `public/models/gant_l/model.gltf` for both hands. The right hand applies `scale.x = -1` at the group level to mirror the mesh, so the thumb ends up on the correct side. Both hands therefore share the same rig and the same material.
The historical `public/models/gant_r/model.gltf` is kept as legacy but is not loaded by the frontend — its GLB embeds three skeletons (`Hand_l`, `Hand_l_pad`, `Hand_r`) plus a `galet` mesh, which made the finger rig unreliable.
The `gant_l` material is set to `alphaMode: OPAQUE` with `doubleSided: true`. The opaque mode prevents transparency sorting issues that made folded fingers disappear behind the palm; the double-sided flag covers the back faces revealed by the mirror scale on the right hand.
Two additional glove variants exist on disk:
- `public/models/gant_l_pad/model.gltf`
- `public/models/gant_r_pad/model.gltf`
They are intended for future swap-by-state usage but are **not yet rigged**. They cannot be animated by MediaPipe landmarks in their current form — re-exporting them from Blender with the same armature structure as `gant_l` is a prerequisite.
## Known Limitations
- Production usage is currently limited to repair mission steps that explicitly need hands.
- MediaPipe depth is relative and can be noisy.
- MediaPipe depth is relative and currently not used for stable object depth control.
- The virtual hit zone is an approximation based on multiple raycasts, not a real 3D collider.
- There is no smoothing layer for hand position or depth yet.
- The SVG hand visualization is a fallback, not the primary display when glove models load correctly.
- The 3D glove is opt-in only (see `Show Model` toggle). Default visual is the SVG silhouette.
- `HandTrackingFallback` is legacy and effectively unused unless the glove toggle is enabled and the glove fails to load.
- The right glove is a mirrored copy of `gant_l` rather than its own mesh; in the future a dedicated right-hand model would give a better visual.
- The `_pad` glove variants are not rigged yet, so swap-by-state (normal ↔ pad) is not wired in.
- Finger bone animation is an approximate landmark-to-bone mapping; it still needs calibration for per-model twist, offsets, and smoothing.
+240
View File
@@ -0,0 +1,240 @@
# Interaction System Technical Notes
This document explains the shared trigger, grab, focus, and hand-grab system.
## Purpose
The app has several ways for the player to affect the 3D scene:
- press `E` on focused trigger objects
- hold the primary mouse button on grabbable objects
- close a tracked hand into a fist to grab hand-controlled objects
- release objects and optionally snap them into target positions
The implementation keeps those rules in a reusable interaction layer so gameplay features such as the repair game do not each create their own input system.
## Main Files
| File | Responsibility |
| --------------------------------------------------------- | ----------------------------------------------- |
| `src/managers/InteractionManager.ts` | Shared interaction state and imperative actions |
| `src/hooks/interaction/useInteraction.ts` | React subscription to the manager |
| `src/components/three/interaction/InteractableObject.tsx` | Distance/raycast focus detection |
| `src/components/three/interaction/TriggerObject.tsx` | Press-to-trigger wrapper |
| `src/components/three/interaction/GrabbableObject.tsx` | Physics-backed grab and hand grab wrapper |
| `src/components/ui/InteractPrompt.tsx` | HTML prompt for focused trigger interactions |
| `src/world/player/PlayerController.tsx` | Keyboard/mouse input bridge |
## Architecture
The interaction system has three layers:
1. R3F objects detect focus and register handles.
2. `InteractionManager` stores the current interaction snapshot.
3. UI and player input read the snapshot and trigger the selected action.
This is intentionally not Zustand. Interaction focus and holding state are short-lived, frame-adjacent runtime state. A small singleton plus `useSyncExternalStore` is a better fit than putting high-frequency interaction details into the durable game progression store.
## Interaction Snapshot
The snapshot type lives in:
```txt
src/types/interaction/interaction.ts
```
```ts
interface InteractionSnapshot {
focused: InteractableHandle | null;
nearby: boolean;
holding: boolean;
handHolding: boolean;
}
```
Meaning:
- `focused`: the interactable currently aimed at by the camera ray
- `nearby`: at least one interactable is within interaction radius
- `holding`: mouse/player-controller grab is active
- `handHolding`: hand-tracking grab is active
`nearby`, `holding`, and `handHolding` are also used by the hand-tracking provider to decide when webcam tracking should stay active in the debug physics scene.
## Focus Detection
Focus detection lives in:
```txt
src/components/three/interaction/InteractableObject.tsx
```
Each frame, it:
1. finds the interactable world position from its Rapier body or group transform
2. checks distance from the camera
3. marks the handle as nearby if it is inside radius
4. raycasts from the camera forward direction
5. sets the focused handle when the ray hits the object
6. clears focus if the object is no longer nearby or no longer aimed at
This gives a simple first-person interaction model: the player must be close enough and looking at the object.
## Trigger Objects
Trigger implementation:
```txt
src/components/three/interaction/TriggerObject.tsx
```
`TriggerObject` wraps children in a fixed Rapier body and exposes a trigger handle.
When triggered, it can:
- play an optional SFX through `AudioManager`
- call `onTrigger`
- spawn an optional model at an offset
Typical users:
- repair-object inspection
- repair-case open/fragment interaction
- install target
- completion target
- debug scene trigger sphere
## Grabbable Objects
Grab implementation:
```txt
src/components/three/interaction/GrabbableObject.tsx
```
`GrabbableObject` wraps children in a dynamic Rapier body and exposes a grab handle.
Mouse/controller grab flow:
1. Player focuses the object.
2. Mouse down calls `InteractionManager.pressInteract()`.
3. The object enters holding mode.
4. Each frame, velocity is pushed toward a hold target in front of the camera.
5. Mouse up calls `releaseInteract()`.
6. The object can snap to the nearest configured target.
Important tuning values live in:
```txt
src/data/interaction/grabConfig.ts
```
The debug GUI exposes hold stiffness, throw boost, and hold distance.
## Snap-To-Target
`GrabbableObject` supports:
- `snapTargets`
- `snapRadius`
- `snapDuration`
- `onSnap`
On release, the object finds the nearest target inside `snapRadius`. If a target is found, GSAP animates the Rapier body translation to that target and calls `onSnap`.
The repair game uses this to place replacement parts and broken parts into case placeholders.
## Hand-Controlled Grab
If `handControlled` is true, `GrabbableObject` also reads:
```txt
useHandTrackingSnapshot()
```
Hand grab flow:
1. Find a detected hand where `hand.isFist` is true.
2. Compute the visual center of the hand from landmark bounds.
3. Convert that screen-space point to a camera ray.
4. Raycast against the object.
5. Use a small set of offset rays around the center to make hit detection more forgiving.
6. If the object is in range and hit, enter `handHolding`.
7. Move the object toward a hold target in front of the camera while the fist remains closed.
8. When the fist opens or disappears, release and snap if possible.
This is an approximation, not a full 3D hand collider. It is a practical prototype compromise because MediaPipe gives normalized camera-space landmarks and relative depth, not stable world-space hand meshes.
## Player Input Bridge
The player controller owns raw keyboard and mouse input:
```txt
src/world/player/PlayerController.tsx
```
It calls:
- `interaction.pressInteract()` when `E` is pressed and the focused handle is a trigger
- `interaction.pressInteract()` on mouse down when the focused handle is a grab
- `interaction.releaseInteract()` on mouse up when a grab is active
Input is ignored while:
- the settings menu is open
- a cinematic is playing
Movement lock is read separately from `useRepairMovementLocked`, which locks the player during focused repair steps.
## UI Prompt
The prompt lives in:
```txt
src/components/ui/InteractPrompt.tsx
```
It appears only when:
- camera mode is `player`
- a focused interaction exists
- the player is not holding an object
- the focused interaction is a trigger
The prompt does not appear for grab objects, because grabs are mouse/hand actions rather than `E` trigger actions.
## Debug Controls
Interaction debugging is split between:
- lil-gui `Interaction` folder for showing interaction spheres
- lil-gui `GrabbableObject` folder for grab tuning
- debug physics scene for live trigger/grab testing
- hand-tracking debug panel for hand grab state
Use:
```txt
http://localhost:5173/?debug
```
Then switch the scene mode to `Physics` from lil-gui.
## Why This Architecture Works
The interaction layer separates concerns:
- R3F objects know their distance/raycast hit state.
- The player controller owns input events.
- UI only subscribes to a snapshot.
- Gameplay objects receive semantic callbacks like `onTrigger`, `onSnap`, or `onPositionChange`.
This keeps the repair game focused on gameplay rules instead of low-level input plumbing.
## Known Limitations
- Only one focused handle is stored at a time.
- The focus rule is camera ray based, so side-facing interactions can feel strict without larger meshes or radii.
- Hand grab uses screen-space raycasts, not physical hand colliders.
- The manager is singleton-based, so tests must call `destroy()` or isolate state when needed.
- `nearby` is boolean, not a list exposed to UI, so the current UI cannot rank multiple nearby objects.
+119
View File
@@ -0,0 +1,119 @@
# Map LOD System
This document describes the runtime LOD system used by the production map.
## Goal
The map now supports two visual versions for selected models:
- the regular model in `public/models/<name>/`
- the lighter model in `public/models/<name>-LOD/`
The runtime chooses between those paths from the active graphics preset. This keeps nearby objects visually richer while reducing the cost of distant objects.
## Graphics Presets
Presets are configured in:
```txt
src/data/world/graphicsConfig.ts
```
Current behavior:
| Preset | Chunk load distance | Fog | LOD behavior |
| -------- | ------------------: | --- | ------------------------------------- |
| `low` | 10m | On | Always use `*-LOD` models |
| `medium` | 20m | On | Always use `*-LOD` models |
| `high` | 35m | Off | Regular model up to 10m, then `*-LOD` |
| `ultra` | 50m | Off | Regular model up to 20m, then `*-LOD` |
The unload distance stays slightly larger than the load distance to avoid rapid mount/unmount flickering when the player stands near a boundary.
## Runtime Selection
LOD path mapping lives in:
```txt
src/data/world/mapLodConfig.ts
```
The main selector is `selectMapModelPathByDistance()`. It receives:
- the current camera distance
- the map model name
- the regular model path
- the active graphics preset
It returns either the regular path or the `*-LOD` path.
## Chunked Instanced Models
Repeated static assets are rendered through:
```txt
src/world/map-instancing/MapInstancingSystem.tsx
```
For each visible chunk, the system checks the nearest instance in that chunk. If the nearest instance is inside the high-detail threshold, the whole chunk uses the regular model. Otherwise, it uses the `*-LOD` model.
This is intentionally chunk-level LOD instead of per-instance LOD. It matches the existing chunk streaming architecture and avoids splitting every object into many tiny batches.
## Single And Generated Models
Single map nodes use:
```txt
src/hooks/world/useMapLodModelPath.ts
src/world/GameMap.tsx
```
Some named map objects are rendered through dedicated generated components instead of the generic `GameMap` path. Those components must call `useMapLodModelPath()` directly.
Current dedicated generated components with LOD support:
```txt
src/components/three/world/EcoleModel.tsx
src/components/three/world/LaFabrikMapModel.tsx
```
This matters for `lafabrik`: adding `public/models/lafabrik-LOD/` is not enough by itself. The component must also be connected to `useMapLodModelPath()`.
## Adding A New LOD Model
To add LOD support for a model:
1. Add the light model in `public/models/<name>-LOD/model.gltf`.
2. Keep the regular model in `public/models/<name>/model.glb` or `public/models/<name>/model.gltf`.
3. Add the mapping in `src/data/world/mapLodConfig.ts`.
4. If the model uses a dedicated component, call `useMapLodModelPath()` in that component.
5. Preload both paths when the component is dedicated and uses `useGLTF.preload()`.
6. Verify the GLTF/GLB references: buffers, textures, opacity maps, and relative paths.
## Current LOD Models
The current explicit LOD mappings are:
```txt
ebike
eolienne
pylone
boiteimmeuble
ecole
immeuble1
lafabrik
maison1
panneauaffichage
talkie
```
## Regression Risks
The most common failure modes are:
- the `*-LOD` folder exists but is missing from `mapLodConfig.ts`
- a dedicated generated component keeps a hardcoded model path
- GLTF references point to textures that were renamed during export
- a model is added to LOD config but does not spawn through `GameMap` or `MapInstancingSystem`
Before committing model changes, validate both the regular and LOD folders for missing GLTF refs.
+268
View File
@@ -0,0 +1,268 @@
# Map Performance Notes
This document tracks the current map-rendering performance pass.
## Current Runtime Path
- `public/map.json` is the source of map transforms.
- `src/world/GameMap.tsx` renders regular visual map nodes.
- `src/world/vegetation/VegetationSystem.tsx` already instances dense vegetation.
- `src/world/map-instancing/MapInstancingSystem.tsx` instances selected repeated static map assets.
- `src/world/GameMapCollision.tsx` keeps terrain collision separate for the player octree.
## Draw-Call Bottlenecks Found
The first performance bottleneck was draw calls. Some assets were exported as many small GLTF primitives even when they used only a few materials.
| Model | Instances | Meshes / primitives | Notes |
| ---------------- | --------: | ------------------: | ------------------------------------------------------------------------------------ |
| `generateur` | 3 | 3152 | Worst draw-call offender. Needs asset-side mesh merging. |
| `lafabrik` | 4 | 474 | High primitive count; current HD GLB has embedded geometry and no external textures. |
| `ecole` | 1 | 107 | One material but many primitives; should be merged. |
| `fermeverticale` | 3 | 1 | Geometry is fine; textures are large for the visible complexity. |
`generateur` was especially expensive because three visible instances could multiply thousands of primitives into thousands of draw calls. Instancing reduces repeated instance cost, but the source asset still needs a cleaner export.
## Runtime Merge Pass
`InstancedMapAsset` now groups source meshes by material and compatible geometry attributes before creating `THREE.InstancedMesh` objects. This reduces the runtime draw groups even when the source GLTF is exported as many small meshes.
Estimated source primitive count versus runtime merged groups:
| Model | Source primitives | Runtime merged groups |
| ------------ | ----------------: | --------------------: |
| `generateur` | 3152 | 8 |
| `ecole` | 107 | 2 |
| `eolienne` | 118 | 8 |
| `lafabrik` | 474 | ~77 |
This is a code-side safety net, not a replacement for clean asset exports. Clean GLB exports with merged meshes and fewer textures remain the preferred long-term path.
## Current Triangle Bottleneck
After the runtime merge pass, draw calls can drop dramatically, but FPS can still stay low because the scene now remains triangle-bound. A debug capture after the merge showed roughly:
```txt
138 draw calls
~69.6M triangles
~10 FPS
```
That means the renderer is no longer mostly blocked by draw-call submission. It is mostly drawing too many visible triangles.
Estimated triangle contribution from `map.json` instance counts:
| Model | Instances | Triangles each | Estimated total triangles |
| ------------------- | --------: | -------------: | ------------------------: |
| `buisson` | 646 | 37 500 | ~24.2M |
| `champdesoja` | 1181 | 16 268 | ~19.2M |
| `arbre` | 291 | 38 906 | ~11.3M |
| `champdeble` | 1307 | 6 260 | ~8.2M |
| `champsdetournesol` | 1163 | 3 264 | ~3.8M |
| `sapin` | 93 | 23 972 | ~2.2M |
These vegetation and crop assets account for almost all of the current `~69M` triangle count. By comparison, the previously suspicious static buildings are much smaller in triangle cost:
| Model | Estimated total triangles |
| ---------------- | ------------------------: |
| `generateur` | ~123k |
| `lafabrik` | ~124k |
| `ecole` | ~5k |
| `fermeverticale` | ~1k |
`InstancedMesh` reduces draw calls, but it does not reduce triangle count. If 646 bushes each contain 37 500 triangles, the GPU still has to draw about 24 million bush triangles when those instances are visible.
## Debug Performance Controls
The debug-only performance folder can isolate model families when `?debug` is enabled.
Proposed controls:
```txt
Performance / Map
- vegetation
- crops
- trees
- buildings
- landmarks
- props
- terrain
- sky
```
Useful per-model toggles:
```txt
buisson
arbre
sapin
champdeble
champdesoja
champsdetournesol
fermeverticale
lafabrik
immeuble1
eolienne
pylone
```
The purpose is diagnostic, not final gameplay behavior. The expected workflow is:
1. Open `/?debug` with R3F perf enabled.
2. Disable one family or model type.
3. Watch `triangles`, `calls`, and FPS.
4. Identify which model groups need LOD, density reduction, or asset re-export.
Recommended implementation files:
```txt
src/managers/stores/useMapPerformanceStore.ts
src/hooks/debug/useMapPerformanceDebug.ts
src/world/vegetation/VegetationSystem.tsx
src/world/map-instancing/MapInstancingSystem.tsx
src/world/GameMap.tsx
```
The store should stay runtime/debug-only. It should not change persisted production map data.
## Triangle-Reduction Follow-Up
Once the expensive model families are isolated, the real triangle fixes are:
1. Lower-poly vegetation and crop exports.
2. LOD variants for trees, bushes, and crop fields.
3. Distance-based culling for vegetation/crop instances.
4. Chunked instancing so Three.js can frustum-cull groups instead of one huge global `InstancedMesh`.
5. Billboard/impostor versions for far vegetation.
Chunked instancing is especially important. A single `InstancedMesh` containing every bush has one global bounding sphere. If that bounding sphere is visible, Three.js may keep the whole batch visible. Splitting instances into grid chunks allows entire offscreen chunks to be skipped.
## Player-Only Vegetation Streaming
The first distance-streaming pass is intentionally limited to vegetation and crop instances:
- `arbre`
- `sapin`
- `buisson`
- `champdeble`
- `champdesoja`
- `champsdetournesol`
The behavior is configured in:
```txt
src/data/world/fogConfig.ts
```
Current runtime values:
```txt
chunkSize: 35
low load/unload radius: 10m / 18m
medium load/unload radius: 20m / 30m
high load/unload radius: 35m / 45m
ultra load/unload radius: 50m / 65m
updateInterval: 250ms
fog near: 30
fog far: 45
```
The streaming and fog are scoped to the production game scene with the player camera only:
```txt
sceneMode === "game" && cameraMode === "player"
```
This matters for debugging. In debug camera mode there is no fog and no distance streaming, so the developer can inspect the full map freely. In player mode, chunks mount and unmount around the camera to reduce visible triangle count while fog hides vegetation pop-in.
Chunk cleanup is handled through React unmounting. `VegetationSystem` removes chunks from the tree, and `InstancedVegetation` removes its `THREE.InstancedMesh` objects from the group while disposing the locally created merged geometries/material clones in its own cleanup path.
## Runtime Texture Filtering
Loaded GLTF textures are normalized in code through:
```txt
src/utils/three/optimizeGLTFScene.ts
```
The runtime pass applies conservative texture filtering:
1. Cap anisotropy to a small value.
2. Enable mipmap generation for regular PNG/JPG/WebP textures.
3. Use trilinear mipmap filtering for minification.
4. Keep existing opacity/alpha material mapping intact.
This mirrors the intent of the designer upload pipeline without rewriting model files at runtime. The sibling `upload-GLTF` project already has the stronger asset-side path: Blender GLB export with Draco, texture resizing, KTX2 generation with mipmaps, WebP fallback, and GLTF JSON URI/extension rewriting for `KHR_texture_basisu`.
Runtime texture filtering improves distant texture stability and GPU sampling behavior, but it does not reduce mesh triangle count. Triangle reduction still comes from streaming, distance unloading, or optimized source assets.
## Terrain-Snapped Map Placement
Map object heights are corrected at runtime through:
```txt
src/hooks/three/useTerrainHeight.ts
```
The terrain raycast is not done every frame. The terrain mesh list is built from the cached terrain GLTF, then each model or instance computes its snapped `y` when it is mounted or when its instance data changes.
Applied paths:
1. Regular `GameMap` model instances.
2. Generated static map models.
3. Instanced static map assets.
4. Vegetation and crop chunks.
Only the `y` coordinate is replaced. `x`, `z`, and rotation stay from `map.json`. Runtime scale is also normalized when a static map node has a non-uniform scale, which prevents exported values like `[1, 2, 1]` from stretching or shrinking a map model unexpectedly.
## Current Code-Side Optimization
Repeated static assets are configured in:
```txt
src/world/map-instancing/mapInstancingConfig.ts
```
Those names are excluded from the regular `GameMap` clone path, then rendered by `MapInstancingSystem` with merged `THREE.InstancedMesh` batches.
This keeps the existing map authoring format while reducing repeated draw calls for selected assets.
## Generated R3F Model Path
Unique static map assets can use explicit R3F components instead of the generic cloned GLTF path. This follows the same intent as `gltfjsx`: expose the model as a React component, then keep control over mesh/material setup in code.
Current generated map-model entry point:
```txt
src/world/map-generated/GeneratedMapNodeInstance.tsx
```
Current generated model component:
```txt
src/components/three/world/EcoleModel.tsx
src/components/three/world/LafabrikModel.tsx
src/components/three/world/FermeVerticaleModel.tsx
src/components/three/world/GenerateurModel.tsx
```
`ecole`, `lafabrik`, `fermeverticale`, and `generateur` use this path. Their components share the same merged static model renderer, which groups compatible geometry by material before mounting meshes.
This path should be used selectively. It improves control and can remove clone overhead, but it does not reduce source triangle count by itself.
## Asset-Side Follow-Up
Design/export should prioritize:
1. Produce lower-poly `buisson`, `arbre`, `sapin`, and crop assets.
2. Add LOD or billboard variants for far vegetation.
3. Merge `generateur` meshes from 3152 primitives to a small number of material groups.
4. Keep `lafabrik` exports texture-light, and merge repeated material primitives where possible.
5. Merge `ecole` primitives because it uses a single material.
6. Prefer runtime `.glb` or compressed runtime textures when the pipeline supports it.
## Safety Rules
- Do not instance `terrain` for player collision without validating `Octree.fromGraphNode` support.
- Do not replace repair-game models with optimized map models unless repair node names are preserved.
- Dispose only GPU resources created locally. Do not dispose textures or geometries owned by `useGLTF`'s cache.
+4 -5
View File
@@ -14,7 +14,6 @@ The store owns the `missionFlow` slice:
```ts
missionFlow: {
step: GameStep;
activityCity: boolean;
playerName: string;
canMove: boolean;
@@ -31,14 +30,14 @@ Managers stay responsible for local runtime services:
- `AudioManager` owns audio elements, audio pools, music playback, category volume, and stereo pan.
- `InteractionManager` owns transient focused/nearby/held interaction handles.
Mission progression is not owned by a manager. Components update the store through explicit actions such as `setFlowStep`, `setCanMove`, `showDialog`, and `hideDialog`.
Mission progression is not owned by a manager. Components update the store through explicit actions such as `setIntroStep`, `setCanMove`, `showDialog`, and `hideDialog`.
## Runtime Components
- `src/components/game/GameFlow.tsx` reacts to `missionFlow.step` and triggers one-off side effects such as intro audio and movement unlocks.
- `src/components/game/GameFlow.tsx` reacts to intro state and triggers one-off side effects such as intro audio and movement unlocks.
- `src/components/zone/ZoneDetection.tsx` reads the camera position and moves the flow to a target step when the player enters a configured zone.
- `src/components/three/interaction/CentralObject.tsx` and `VillageoisHelperObject.tsx` expose temporary interactive mission objects.
- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `BienvenueDisplay`, and `DialogMessage`.
- `src/world/GameStageContent.tsx` mounts repair games and their mission-start triggers.
- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `DialogMessage`, and subtitles.
- `src/world/player/PlayerController.tsx` reads `missionFlow.canMove` as an additional movement lock.
## Step Sequence
+384
View File
@@ -0,0 +1,384 @@
# Repair Game Technical Notes
This document explains the implementation of the reusable repair-game flow.
## Purpose
The repair game is the current core gameplay loop. It gives three missions the same interaction structure while allowing mission-specific assets, broken parts, replacement choices, prompts, and timing to live in data.
Implemented missions:
| Mission | Object | Role |
| ------- | ------------- | --------------------------------------------- |
| `ebike` | E-bike | Repair a damaged cooling core |
| `pylon` | Power pylon | Restore relay/panel-like broken parts |
| `farm` | Vertical farm | Stabilize irrigation/sensor-like broken parts |
## Main Files
| File | Responsibility |
| ----------------------------------------------------- | ------------------------------------------------- |
| `src/components/three/gameplay/RepairGame.tsx` | Orchestrates the repair step machine |
| `src/components/three/gameplay/RepairFocusBubble.tsx` | Dark sphere shroud + cocoon decor during focus |
| `src/managers/stores/useRepairFocusStore.ts` | Global flag + center for the repair focus bubble |
| `src/data/gameplay/repairMissions.ts` | Mission-specific data |
| `src/types/gameplay/repairMission.ts` | Mission ids, step ids, guards |
| `src/managers/stores/useGameStore.ts` | Global progression and mission transitions |
| `src/world/GameStageContent.tsx` | Production placement of the three repair missions |
| `src/world/debug/TestMap.tsx` | Debug repair playground placement |
## State Machine
Repair mission steps are defined in:
```txt
src/types/gameplay/repairMission.ts
```
```txt
locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done
```
The practical flow is:
```mermaid
stateDiagram-v2
[*] --> locked
locked --> waiting: mission unlocked
waiting --> inspected: inspect mission object
inspected --> fragmented: repair-case trigger or two-fists hold
fragmented --> scanning: fragmentation timer
scanning --> repairing: scan sequence complete
repairing --> reassembling: install target validates
reassembling --> done: reassembly timer
done --> [*]: completion target calls completeMission
```
There is no dedicated finite-state-machine library. The state machine is intentionally lightweight and distributed across:
- `MissionStep` union types
- Zustand transition helpers
- conditional rendering in `RepairGame`
- callbacks passed to step components
For the current prototype, this is readable and low overhead. If mission rules become much more branched, a centralized mission orchestrator or FSM library would become more useful.
## Integration With Zustand
The durable state lives in:
```txt
src/managers/stores/useGameStore.ts
```
`RepairGame` reads:
- `mainState`
- current step for its mission
`RepairGame` writes:
- `setMissionStep(mission, nextStep)`
- `completeMission(mission)`
The important architectural choice is that reusable repair components do not call `setEbikeState`, `setPylonState`, or `setFarmState` directly. They use generic mission actions so the same component can run for all three missions.
## Data-Driven Mission Config
Mission variation lives in:
```txt
src/data/gameplay/repairMissions.ts
```
Each mission config defines:
- `id`
- `label`
- `description`
- `modelPath`
- optional `modelScale`
- `stageUiPath`
- `interactUiPath`
- `brokenUiPath`
- repair case transform
- optional scan/reassembly timings
- `requiredReplacementPartId`
- `brokenParts`
- `replacementParts`
The main benefit is that `RepairGame` stays generic. A mission can change broken nodes, replacement choices, or prompt videos without changing the orchestration component.
The tradeoff is that the config can grow complex. If one future mission needs very different rules, create a mission-specific component instead of forcing every exception into the shared config.
## Orchestration Component
`RepairGame.tsx` is a step router.
It:
1. receives a `mission` id and transform props
2. gets `config = REPAIR_MISSIONS[mission]`
3. subscribes to the active `mainState`
4. subscribes to the current mission step
5. preloads mission assets
6. mounts the component for the active step
7. stores local runtime state needed between steps
Local runtime state:
- `casePlaceholders`: placeholder transforms emitted by the repair case GLTF
- `scannedBrokenParts`: output of the scan sequence used by the repair step
Those values are local because they are transient scene/runtime details. They do not need to persist globally in Zustand.
## Step Components
### Waiting
File:
```txt
src/components/three/gameplay/RepairInspectionObject.tsx
```
The mission object is rendered with a 3D prompt video and wrapped in an interaction trigger. Pressing `E` while focused moves the mission to `inspected`.
### Inspected
Files:
```txt
src/components/three/gameplay/RepairMissionCase.tsx
src/components/three/gameplay/RepairCaseModel.tsx
src/hooks/gameplay/useRepairFragmentationInput.ts
```
The repair case appears near the mission object. The player can:
- aim at the case and press `E`
- hold both fists closed for one second when hand tracking is active
Both paths move to `fragmented`.
### Fragmented
File:
```txt
src/components/three/models/ExplodableModel.tsx
```
The mission object is shown split apart. A timer then moves the mission to `scanning`.
`ExplodedModel.createParts` walks the GLTF tree recursively, descending through any single mesh-bearing wrapper node (e.g. `Scene > Moto > Eclatement` for the Ebike) until it reaches a node with multiple mesh-bearing children. Those children are the natural "explosion groups" authored by the modeler. This avoids exploding raw leaf meshes in local space when the model has extra empty wrapper nodes above the intended group.
When mounted, `RepairGame` applies `RepairMissionConfig.modelRotation` and `modelScale` to the fragmented model so it lines up with the source inspection model in world space (e.g. the parked Ebike using `EBIKE_WORLD_ROTATION_Y` / `EBIKE_WORLD_SCALE`).
The default delay comes from:
```txt
REPAIR_FRAGMENTATION_SEQUENCE_SECONDS
```
### Scanning
File:
```txt
src/components/three/gameplay/RepairScanSequence.tsx
```
The scan sequence:
- keeps the exploded model visible
- receives model parts from `ExplodableModel`
- advances an active part index over time
- renders `RepairScanVisual` on the active part
- reveals broken-part highlights when configured broken parts have been reached
- returns `RepairScannedBrokenPart[]` when done
Broken-part lookup first tries `brokenParts[].nodeName`. If no configured node matches, it falls back to the first available exploded parts. This fallback is useful while GLTF node names are still unstable, but precise `nodeName` config is safer for production.
### Repairing
File:
```txt
src/components/three/gameplay/RepairRepairingStep.tsx
```
This is the densest gameplay step.
It renders:
- install target
- placeholder markers
- grabbable replacement parts
- grabbable broken parts to store
- placement feedback
- ready-to-install prompt
Important local state:
- `placedPartIds`: replacement parts that snapped near a placeholder
- `depositedBrokenPartIds`: broken parts stored in the case
- `showBlockedInstallFeedback`: temporary visual feedback when install is attempted too early
Validation:
```txt
correct replacement part placed
AND every scanned broken part deposited
```
Only then does the install target call `onRepair()` and move to `reassembling`.
### Reassembling
File:
```txt
src/components/three/gameplay/RepairReassemblyStep.tsx
```
The exploded model animates back into assembled form and completion particles play. A timer then moves the mission to `done`.
Mission configs can override the default reassembly duration.
### Done
File:
```txt
src/components/three/gameplay/RepairCompletionStep.tsx
```
The repaired object remains visible. The player validates the completion target, then:
1. the repair case closes
2. the case plays its exit animation
3. `completeMission(mission)` advances the global game progression
## Focus Bubble
While the player is in `fragmented`, `scanning`, `repairing` or `reassembling`, `RepairGame` flips `useRepairFocusStore.active = true` and publishes the snapped world center of the repair model.
`RepairFocusBubble` reads the store and:
- renders a `BackSide` sphere (radius 1, scaled 0 → 10m) tinted `#060814` at opacity 0.92
- grows the sphere with GSAP `expo.out` over 2.5 s when focus turns on
- shrinks back with `expo.in` over 1.2 s when focus turns off
- mounts a small "cocoon" decor pass inside (subtle grid floor + soft directional light + ambient) that fades in once the bubble is mostly grown
`Environment.tsx` and `GameStageContent.tsx` consume the same store flag to unmount the vegetation system and the zone debug visuals while the bubble is up, so trees and gizmos do not pierce the shroud. Terrain, water, sky, clouds and grass remain visible behind the bubble.
The bubble is mounted both in `GameStageContent` (production scene) and `TestMap` (physics test scene) so the behaviour matches in both contexts.
## Repair Case Details
The case model implementation lives in:
```txt
src/components/three/gameplay/RepairCaseModel.tsx
```
It handles:
- GLTF loading through `useLoggedGLTF`
- clone creation through `useClonedObject`
- pop-in animation
- lid open/close animation
- open/close SFX through `AudioManager`
- proximity-based floating
- small rotation wobble
- exit animation
- placeholder discovery
Placeholder discovery is data-friendly:
```txt
placeholder_*
```
Any GLTF node whose name starts with that prefix is exported to the repair step as a placement target. This lets artists move placeholder transforms in the model file without hard-coding every placement point in TypeScript.
## Interaction Dependencies
The repair game depends on the shared interaction layer:
- `RepairInspectionObject` uses `InteractableObject`
- `RepairMissionCase` uses `TriggerObject`
- `RepairRepairingStep` uses `GrabbableObject` and `TriggerObject`
- completion uses `TriggerObject`
This keeps the repair game from owning raw keyboard or mouse listeners for every object. The player controller handles input, and interaction components decide what is focused.
## Hand Tracking Dependencies
Hand tracking participates in two places:
- `useRepairFragmentationInput` uses `useBothFistsHold`
- `GrabbableObject` can be `handControlled`
`HandTrackingProvider` enables tracking during the repair steps that are expected to use hands:
```txt
inspected
repairing
reassembling
done
```
This avoids keeping the webcam active for the whole game scene.
## Runtime Placement
Production placement lives in:
```txt
src/world/GameStageContent.tsx
```
Current positions:
```tsx
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
<RepairGame mission="pylon" position={[64, 0, -66]} />
<RepairGame mission="farm" position={[-24, 0, 42]} />
```
Only the repair game whose `mission` matches `useGameStore().mainState` renders active content.
## Debug Placement
Debug placement lives in:
```txt
src/world/debug/TestMap.tsx
```
The debug scene mounts repair playground zones for all missions. Use `?debug`, switch to the physics scene in lil-gui, then use the game-state debug panel to activate the mission you want to test.
## Why This Is A Good Review Focus
This feature shows several important frontend/game architecture skills:
- state-driven scene composition
- data-driven feature variation
- React state for step-local runtime values
- Zustand for durable game progression
- R3F component boundaries
- Rapier object interaction
- hand tracking integration
- audio feedback
- GLTF traversal
- graceful asset fallbacks
## Known Limitations
- Movement lock is currently disabled by an early `return false` in `useRepairMovementLocked`.
- The repair-game runtime setting in the options menu is stored but not consumed by `RepairGame`.
- Broken-part scan fallback can produce incorrect matches if GLTF node names are missing.
- Mission progression is still prototype-level and not owned by a central `GameManager`.
- The same repair flow covers all missions. Very different future missions may need dedicated components.
+273
View File
@@ -0,0 +1,273 @@
# Scene Runtime And Loading
This document explains how the playable route boots the 3D world, loads the map, gates gameplay readiness, and spawns the player.
## Purpose
The playable scene has heavy asynchronous work: map JSON, GLTF models, collision meshes, octree construction, Rapier stage content, audio, dialogues, and the player controller.
The current runtime avoids spawning the player too early. That matters because the player controller needs a ready octree, and the repair game needs the production stage to be mounted before the user starts interacting with objects.
## Entry Flow
```txt
src/main.tsx
-> src/App.tsx
-> src/router.tsx
-> src/pages/page.tsx
-> HandTrackingProvider
-> Canvas
-> World
-> DebugPerf
-> GameUI
-> SceneLoadingOverlay
```
`HomePage` owns the visible loading state and passes `onLoadingStateChange` down to `World`.
The loading progress in `HomePage` is monotonic:
- if the scene is already ready, a late loading event is ignored
- progress can only increase while the scene is booting
This prevents the overlay from jumping backward when nested loaders finish in a slightly different order.
After the initial map boot is complete, late loading signals no longer reopen the full-screen loading overlay. Instead, `HomePage` shows the compact `AppLoadingIndicator` while the game remains visible. This is reserved for explicit runtime reload signals such as graphics preset changes, repair-state transitions, or late world loading events; chunk streaming intentionally does not drive this indicator.
## World Composition
`src/world/World.tsx` is the main scene composer.
Always-mounted systems:
- `Environment`
- `Lighting`
- debug helpers when `?debug` is active
- optional hand-tracking glove overlays
- optional debug camera controls
Game scene systems:
- `GameMap`
- Rapier `Physics` wrapping `GameStageContent`
- `GameMusic`
- `GameDialogues`
- `GameCinematics`, currently only in `mainState === "outro"`
- `Player`
Debug physics scene systems:
- `TestMap`
- `Player`
## Loading State Owner
The world loading gate lives in:
```txt
src/hooks/world/useWorldSceneLoading.ts
```
It tracks:
- `octree`: collision octree built from collision source meshes
- `gameMapLoaded`: map data and visible map nodes settled
- `gameStageLoaded`: Rapier gameplay stage mounted
- `showGameStage`: true when the map is ready enough to mount gameplay content
- `gameplayReady`: true when map, stage, and octree are all ready
The game-scene readiness condition is:
```ts
showGameStage && gameStageLoaded && octree !== null;
```
Shadows are configured once when `Lighting` mounts (renderer `shadowMap.enabled`, sun
`shadow.autoUpdate = true`, bias and frustum from `SHADOW_CONFIG` in
`src/data/world/lightingConfig.ts`). The shadow map then refreshes every frame and
follows the player camera through the sun's `target`. The earlier `SceneShadowWarmup`
step has been removed — the visible loading overlay no longer waits for a forced
shadow refresh because `autoUpdate` covers steady-state rendering.
### Avoiding global scene remounts
Heavy stage components (`GameStageContent`, `Player`, dialogues) load assets via
`useGLTF`/`useTexture` without preload (e.g. `EbikeSpeedometer` calls `useTexture`
when the bike mounts). To prevent any late suspension from bubbling up to the
root `<Suspense>` boundary in `src/pages/page.tsx` and unmounting the entire
world (which would trigger a redundant octree rebuild and shadow re-config), the
game stage block and the spawn-player block are wrapped in their own
`<Suspense fallback={null}>` boundaries inside `src/world/World.tsx`. Any new
sibling that suspends late should be added inside one of these boundaries or get
its own.
The debug physics scene is ready when:
```ts
octree !== null;
```
## Map Loading
Map loading starts in:
```txt
src/world/GameMap.tsx
```
`GameMap` calls:
```txt
src/utils/map/loadMapSceneData.ts
```
That utility:
1. fetches `/map.json`
2. validates it as a `MapNode[]`
3. deduplicates model names
4. checks `public/models/{name}/model.glb`
5. falls back to `public/models/{name}/model.gltf`
6. returns `{ mapNodes, models }`
If a model is missing, the map still renders a fallback cube. This keeps the scene inspectable while assets are incomplete.
## Model Settling
`GameMap` counts settled map nodes.
A node settles when:
- it has no model and renders a fallback cube
- its GLTF model instance has mounted
- a model error boundary catches a load/render error and renders fallback
This prevents `GameMapCollision` from building collision before the visible map has reached a stable state.
## Collision Loading
Collision loading lives in:
```txt
src/world/GameMapCollision.tsx
```
The current production collision source is intentionally small:
```ts
const MAP_COLLISION_NODE_NAMES = new Set(["terrain"]);
```
Only matching map nodes are loaded into the invisible collision group. Then:
```txt
src/hooks/three/useOctreeGraphNode.ts
```
builds the Three.js octree from that group and sends it back through `onOctreeReady`.
This is a performance choice. Building a player collision octree from every visible prop can overload the browser and make the scene fragile.
## Stage Loading
Production gameplay content is mounted by:
```txt
src/world/GameStageContent.tsx
```
`World` wraps it in Rapier `Physics`, but only after `GameMap` reports loaded:
```tsx
{
showGameStage ? (
<Physics>
<GameStageLoaded onLoaded={handleGameStageLoaded} />
<GameStageContent />
</Physics>
) : null;
}
```
`GameStageLoaded` is a tiny component that calls `handleGameStageLoaded()` after mount. It gives the loading hook a clear signal that the Rapier stage has entered the scene graph.
## Player Spawn Gate
The player is spawned only when the active camera mode is not debug and the active scene is ready.
```ts
const spawnPlayer =
cameraMode !== "debug" &&
(sceneMode === "game" ? gameplayReady : octree !== null);
```
This avoids two common bugs:
- the player starts falling or clipping before collision is ready
- gameplay starts while the map/stage is still mounting
The production player spawn uses:
```txt
PLAYER_SPAWN_POSITION_GAME
```
The debug physics scene uses:
```txt
PLAYER_SPAWN_POSITION_PHYSICS
```
## Audio And Narrative Mounting
`GameMusic`, `GameDialogues`, and `Player` mount together after `spawnPlayer` is true.
This means background music and global dialogue timecode processing do not start while the loading overlay is still preparing the scene.
`GameCinematics` is currently gated further:
```tsx
{
mainState === "outro" ? <GameCinematics /> : null;
}
```
So cinematic playback is part of the outro path today, not a global always-on system.
## Debug Modes
Debug is enabled with:
```txt
http://localhost:5173/?debug
```
`src/utils/debug/Debug.ts` provides:
- camera mode: `player` or `debug`
- scene mode: `game` or `physics`
- R3F perf toggle
- debug overlay toggle
- hand-tracking source
- hand SVG visibility
- interaction sphere visibility
Important current detail: the older boot flags such as `noMusic`, `noCinematics`, `noMap`, `noDialogues`, `noOctree`, and `noPlayer` are not part of the current `develop` runtime path.
## Why This Architecture Works
The runtime uses React composition as the scene orchestration layer:
- if JSX is mounted, the Three/Rapier object exists
- if JSX is unmounted, the object leaves the scene
- loading gates are explicit booleans instead of hidden timing assumptions
This keeps the prototype understandable while still preventing expensive systems from starting too early.
## Risks And Watch Points
- Loading progress is manually estimated, not measured from every asset byte.
- The production collision source is currently only `terrain`; extra collision needs explicit lightweight nodes.
- Rapier gameplay physics and player octree collision are separate systems and can diverge if future features assume they are the same world.
- `GameCinematics` is not globally mounted anymore; docs or tests that expect intro cinematics to auto-run should be updated before relying on that path.
- Scene readiness is stored in React state, so remounting the route restarts the loading flow.
+82
View File
@@ -0,0 +1,82 @@
# Three Debugging
Use the dedicated debug mode when you need Chrome DevTools to step into Three.js internals.
```bash
npm run dev:three-debug
```
This mode aliases `three` to `node_modules/three/src/Three.js` and disables Vite dependency pre-bundling for Three. In DevTools, open `node_modules/three/src/renderers/WebGLRenderer.js` and place a breakpoint inside:
```js
this.render = function (scene, camera) {
```
Reload the page or trigger a frame. When the breakpoint hits, inspect `scene`, `camera`, renderer state, visible objects, matrices, materials, and `this.info.render`.
If DevTools still opens a bundled file, stop the dev server, clear Vite's cached deps, and restart:
```bash
rm -rf node_modules/.vite
npm run dev:three-debug
```
## Visual debug toggles
The `Debug` folder of the runtime debug GUI exposes inspection toggles backed by
`src/managers/stores/useDebugVisualsStore.ts`:
- **Show Player Model** — renders the main character GLTF in front of the
current camera (`src/components/debug/DebugPlayerModel.tsx`). The model is
positioned in camera-local space so it stays visible regardless of pitch.
- **Show Octree** — overlays the collision octree as colored line segments,
one wireframe per spatial cell (`src/components/debug/DebugOctreeVisualization.tsx`).
Cells are colored by depth. Use it to inspect collision precision around
doorways or passages.
- **Octree Max Depth** — caps how deep the octree visualization recurses
(default 6). Increase to see leaf-level subdivisions; decrease to keep the
scene readable when the tree is large.
The octree visualization reads the live `Octree` instance from `World`. The
mesh uses `depthTest: false` and a high `renderOrder`, so cells stay visible
through opaque geometry.
## Shadow rendering intermittence
Shadows occasionally failed to render on initial load and could disappear
mid-session even though the `Lighting` configuration ran to completion. The
fix has two layers:
### Per-frame refresh (steady state)
The sun follows the camera, so its world matrix is dirty every frame. With
`shadow.autoUpdate` alone, three.js can skip the shadow map re-render on a
frame where the matrix update has happened but the renderer's internal dirty
tracking does not pick it up. To prevent that, `Lighting.useFrame` sets
`sun.shadow.needsUpdate = true` after the per-frame matrix updates. Shadow
config is centralized in `src/data/world/lightingConfig.ts` (`bias=0`,
`normalBias=0`, `cameraSize=95`).
### Mount-time shadow map reallocation (`useShadowMapWarmup`)
The merged static map and other GLTFs mount imperatively after `Lighting`,
so the shadow render target ends up linked to a renderer state that pre-dates
the final scene. Materials compiled at that point bake a "no shadow map"
permutation into their shader program and silently fail to render shadows
until a WebGL context-restore cycle (the kind triggered by Chrome DevTools
in `?debug` runs) reallocates everything.
`src/hooks/three/useShadowMapWarmup.ts` replays that cycle programmatically
without the cost of a full context loss. It runs a `useFrame` watchdog that
samples the scene mesh count every 6 frames; once the count has been stable
for ~1 s (or after a 5 s safety cap), it:
1. Disposes the directional light shadow map and nulls it. three.js
reallocates the render target on the next render at the configured
`mapSize`.
2. Marks every material's `needsUpdate = true`, forcing a shader recompile
that rebinds every program to the freshly created shadow sampler.
3. Forces a single shadow pass and invalidates the renderer.
The watchdog runs once per mount and adds a single traversal every 6 frames
during the warmup window, after which it self-terminates.
+140 -95
View File
@@ -1,75 +1,85 @@
# Zustand Game State
# Zustand Stores
This document explains how Zustand is used in the current project.
## Why Zustand Exists Here
The project needs one shared source of truth for the player's progression through the experience.
The project needs shared state that is durable enough to be read by multiple React and React Three Fiber systems.
The current progression is split into main states:
Zustand is used for:
- game progression
- settings
- subtitle display
It is not used for high-frequency frame values. Values such as player velocity, temporary vectors, object positions during a grab, raycasts, and animation-loop data stay in refs or manager-local state.
## Store Locations
Current Zustand stores:
```txt
src/managers/stores/useGameStore.ts
src/managers/stores/useSettingsStore.ts
src/managers/stores/useSubtitleStore.ts
```
They are under `src/managers/stores/` because they are shared runtime state, not state owned by one visual component.
## Store Responsibilities
| Store | Responsibility |
| ------------------ | ------------------------------------------------------------- |
| `useGameStore` | Durable game progression, mission steps, cinematic input lock |
| `useSettingsStore` | Menu visibility, volumes, and subtitle options |
| `useSubtitleStore` | Currently displayed subtitle cue |
## Managers vs Stores
Managers own imperative runtime objects and side effects.
Examples:
- `AudioManager` owns audio elements, music playback, sound pools, category volumes, and optional panner nodes.
- `InteractionManager` owns transient interaction handles and input-oriented focus/holding state.
Stores own durable shared state:
- current game phase
- mission sub-step
- progression flags
- settings values
- currently displayed subtitle cue
Rule of thumb:
- manager = runtime objects, side effects, frame-adjacent imperative logic
- store = shared state that UI, world, or gameplay components need to subscribe to
## Game Store Shape
`useGameStore` exposes the main game progression.
Main states:
| Main state | Role |
| ---------- | ------------------------------- |
| `intro` | Onboarding and opening sequence |
| `bike` | E-bike repair sequence |
| `pylone` | Power grid sequence |
| `ferme` | Vertical farm sequence |
| `ebike` | E-bike repair sequence |
| `pylon` | Power pylon repair sequence |
| `farm` | Vertical farm repair sequence |
| `outro` | Ending sequence |
Each main state can also own smaller sub state, such as the current mission step, dialogue audio, or completion flags.
Other important state:
Zustand is useful because React and React Three Fiber components can subscribe only to the state slice they need. When that slice changes, only the subscribed components re-render.
- `isCinematicPlaying`
- `intro`
- `ebike`
- `pylon`
- `farm`
- `outro`
## Store Location
The game progression store lives here:
```txt
src/managers/stores/useGameStore.ts
```
The store is placed under `src/managers/stores/` because it belongs to the gameplay orchestration layer, not to a specific visual component.
## Managers vs Store
Managers are responsible for local runtime objects and imperative behavior.
Examples:
- `AudioManager` owns audio elements and sound pools.
- `InteractionManager` owns transient interaction handles and input-oriented behavior.
Managers can read from or write to the Zustand store when their local behavior needs to affect global gameplay progression.
The Zustand store is responsible for durable global state:
- current main state
- mission sub state
- progression flags
- dialogue/audio references
- state transitions
Rule of thumb:
- manager = runtime objects, side effects, and local imperative logic
- store = global gameplay state that UI or world components can subscribe to
## Current Shape
The store exposes:
- `mainState`: the active game phase
- `missionFlow`: intro and mission 2 prototype state
- `intro`: intro-specific state
- `bike`: e-bike mission state
- `pylone`: power grid mission state
- `ferme`: farm mission state
- `outro`: ending state
- actions for direct updates and progression updates
The `missionFlow` slice contains the prototype step, player name, movement lock, city activity flag, and temporary dialog message. It is in the main game store because it is global gameplay state used by UI, world components, and the player controller.
The mission steps currently use this sequence:
Mission steps:
```ts
"locked" |
@@ -82,6 +92,8 @@ The mission steps currently use this sequence:
"done";
```
`isCinematicPlaying` is read by `PlayerController` to ignore player input while camera timelines are active.
## Reading State In Components
Use selectors to read only what the component needs.
@@ -98,7 +110,7 @@ export function Example(): React.JSX.Element {
This is better than reading the whole store, because the component re-renders only when `mainState` changes.
## Updating State
## Updating Game State
Prefer explicit actions from the store.
@@ -113,57 +125,89 @@ For development and debug tooling, direct setters also exist:
```ts
const setMainState = useGameStore((state) => state.setMainState);
setMainState("bike");
setMainState("ebike");
```
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as `advanceGameState`, `completeBike`, or `completePylone`.
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as:
Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer the generic mission actions:
- `advanceGameState`
- `completeEbike`
- `completePylon`
- `completeFarm`
- `completeMission`
Mission gameplay that can target `ebike`, `pylon`, or `farm` should prefer generic mission actions:
```ts
const setMissionStep = useGameStore((state) => state.setMissionStep);
const completeMission = useGameStore((state) => state.completeMission);
setMissionStep("bike", "inspected");
completeMission("bike");
setMissionStep("ebike", "inspected");
completeMission("ebike");
```
This keeps reusable gameplay components such as repair flows from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`.
This keeps reusable gameplay components such as `RepairGame` from duplicating mission-specific branches like `setEbikeState`, `setPylonState`, and `setFarmState`.
## Settings Store
`useSettingsStore` owns player-facing settings and forwards audio volume changes to `AudioManager`.
State:
- `isSettingsMenuOpen`
- `musicVolume`
- `sfxVolume`
- `dialogueVolume`
- `subtitlesEnabled`
- `subtitleLanguage`
Audio setters clamp values between `0` and `1`, then call:
```ts
AudioManager.getInstance().setCategoryVolume(category, nextVolume);
```
This keeps UI state and browser audio state synchronized.
## Subtitle Store
`useSubtitleStore` is intentionally tiny.
State/actions:
- `activeSubtitle`
- `setActiveSubtitle`
- `clearActiveSubtitle`
`playDialogueById()` writes to this store while dialogue audio plays. `Subtitles` reads from it and respects `useSettingsStore().subtitlesEnabled`.
## World Integration
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content.
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts the repair-game content.
For repair missions, it mounts the reusable `RepairGame` component with a mission id:
Current production repair placement:
```tsx
<RepairGame mission="bike" position={[8, 0, -6]} />
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
<RepairGame mission="pylon" position={[64, 0, -66]} />
<RepairGame mission="farm" position={[-24, 0, 42]} />
```
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. Shared repair ids, mission steps, and runtime guards live in `src/types/gameplay/repairMission.ts` so static mission config does not depend on the Zustand store. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions.
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`.
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
Shared repair ids, mission steps, and runtime guards live in:
The intro and mission 2 prototype flow is documented separately in `docs/technical/mission-flow.md`. It intentionally uses the same `useGameStore` source of truth instead of a dedicated `GameStepManager` or a second Zustand store.
That means the scene can progressively move toward this pattern:
```tsx
switch (mainState) {
case "intro":
return <IntroContent />;
case "bike":
return <BikeContent />;
case "pylone":
return <PyloneContent />;
case "ferme":
return <FarmContent />;
case "outro":
return <OutroContent />;
}
```txt
src/types/gameplay/repairMission.ts
```
In React Three Fiber, mounting and unmounting JSX controls what appears in the Three.js scene. When a state-specific component disappears from JSX, React removes it from the scene.
Mission-specific behavior stays in:
```txt
src/data/gameplay/repairMissions.ts
```
That lets the repair flow stay reusable while each mission defines its own model, broken parts, replacement parts, prompts, and timing.
## UI Integration
@@ -171,14 +215,14 @@ In React Three Fiber, mounting and unmounting JSX controls what appears in the T
Current overlays:
- `DebugOverlayLayout`: debug-only overlay shown with `?debug`, including the `GameStateDebugPanel` progression panel
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states, stepping backward or forward, and resetting the store
- `DebugOverlayLayout`: debug-only overlay shown with `?debug`
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states
- `Crosshair`: player aiming helper
- `InteractPrompt`: interaction prompt
- `RepairMovementLockIndicator`: player-facing indicator shown while repair steps temporarily disable movement
- Mission flow overlays such as `IntroUI`, `BienvenueDisplay`, and `DialogMessage` are mounted by `src/pages/page.tsx` because they are route-level HTML overlays rather than persistent game HUD elements.
`src/pages/page.tsx` should stay thin and mount the canvas, persistent `GameUI`, and route-level overlays.
- `RepairMovementLockIndicator`: indicator shown while repair steps lock movement
- `HandTrackingVisualizer`: hand tracking SVG fallback/debug visualization
- `Subtitles`: active dialogue subtitle overlay
- `GameSettingsMenu`: options menu and settings controls
## Regression Rules
@@ -188,7 +232,8 @@ Current overlays:
- Keep gameplay transitions inside store actions when possible.
- Keep debug-only controls behind `?debug`.
- Add new state only when a real runtime feature needs it.
- Keep settings side effects, such as audio category updates, inside settings actions rather than spreading them across UI components.
## Next Steps
Move repair validation into mission data once each mission has distinct broken module nodes, replacement assets, and completion events.
- Move broader mission orchestration into a clearer layer if intro, mission, dialogue, and cinematic branching grows.
+124 -17
View File
@@ -1,20 +1,26 @@
# Editor User Guide
The map editor is available at `/editor`. It is a browser-based tool for inspecting and adjusting the objects listed in `public/map.json`.
The map editor is available at `/editor`. It is a browser-based tool for editing the runtime map, cinematic manifest, dialogue manifest, and SRT subtitle files without manually jumping between JSON and subtitle files.
## Purpose
Use the editor when you need to move, rotate, or scale existing map objects without editing JSON by hand.
Use the editor when you need to:
The editor reads the same map data as the runtime scene:
- move, rotate, scale, add, or delete objects from `public/map.json`
- inspect the raw JSON generated by the editor
- preview and edit cinematics from `public/cinematics.json`
- create, preview, and validate dialogue entries from `public/sounds/dialogue/dialogues.json`
- edit FR/EN SRT subtitle files per voice
- `public/map.json` contains the object list.
The map editor reads the same map data as the runtime scene:
- `public/map.json` contains the current hierarchical runtime map.
- `public/models/{name}/model.glb` contains the matching 3D model for each object name. `model.gltf` is still supported as a fallback during migration.
- Missing models are displayed as gray fallback cubes, so incomplete maps remain editable.
## Map Node Format
Each entry in `public/map.json` represents one object:
`public/map.json` is hierarchical. Group nodes such as `Scene`, `blocking`, `vegetation`, or `agriculture` organize the map. Editable object nodes still use the same transform fields:
| Field | Description |
| ---------- | ------------------------------------------------- |
@@ -24,22 +30,51 @@ Each entry in `public/map.json` represents one object:
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
| `scale` | Object scale as `[x, y, z]` |
## Editing Workflow
## Panel Layout
The right panel is split into dropdown groups:
- `Editor`: map transform tools, shortcuts, selection, view mode, JSON preview, and file actions.
- `Cinematics`: editor for `public/cinematics.json`.
- `Dialogues`: editor for `public/sounds/dialogue/dialogues.json`.
- `SRT`: editor for subtitle files in `public/sounds/dialogue/subtitles/`.
Only the `Editor` group is open by default. Open the other groups when you need audio or cinematic tooling.
## Map Editing Workflow
1. Open `/editor` in the local app.
2. Click an object in the scene to select it.
3. Choose a transform mode: translate, rotate, or scale.
4. Drag the transform gizmo in the 3D view.
5. Check the JSON inspector if you need exact values.
6. Use undo or redo if the transform is not correct.
7. Export the JSON or save it to the dev server.
3. Use `Shift + right click` on other objects to add or remove them from the current multi-selection.
4. Choose a transform mode: translate, rotate, or scale.
5. Drag the transform gizmo in the 3D view. With multiple objects selected, the gizmo transforms the selected group and writes each object transform back to `map.json`.
6. Keep `Snap terrain on move` enabled when placing objects on the terrain.
7. Use `Center on object` or `Reset camera` from the `View` section when navigating large maps.
8. Adjust scale numerically from the `Selection` section if the gizmo is not precise enough.
9. Check the JSON inspector if you need exact values.
10. Use undo or redo if the transform is not correct.
11. Export the JSON or save it to the dev server.
## Adding And Deleting Nodes
Use `Add Node` to create a new editable object under the `blocking` group.
- The new object starts as a fallback cube at `[0, 0, 0]`.
- Name it with the model folder name you want, for example `maison1`.
- If `public/models/{name}/model.glb` or `model.gltf` exists, saving and reloading will display the matching model.
- If no matching model exists, the node stays editable as a gray cube.
Use the trash button in `Selection` to delete the selected node from the map tree.
## Controls
| Action | Input |
| -------------------- | -------------------------- |
| Select object | Click object |
| Deselect | `Esc` or click empty space |
| Toggle multi-select | `Shift` + right click |
| Deselect | `Esc` |
| Lock selection | `Lock` button in Selection |
| Clear selection | `X` button in Selection |
| Translate mode | `T` |
| Rotate mode | `R` |
| Scale mode | `S` |
@@ -49,39 +84,83 @@ Each entry in `public/map.json` represents one object:
| Move up | `Space` |
| Move down | `Shift` |
## Selection
The `Selection` section shows the selected object name and its index in `public/map.json`.
- Click an object to select it.
- Use `Shift + right click` on objects to add or remove them from a multi-selection.
- When several objects are selected, the gizmo appears on the selection group and applies translate, rotate, or scale to each selected node.
- Press `Esc` to clear the selection.
- Use the `X` button to clear the selection explicitly.
- Use the `Lock` button to protect the current selection while editing.
- Use the scale fields to edit X/Y/Z scale precisely.
- Use the trash button to remove the selected object.
## Terrain Snapping
`Snap terrain on move` is enabled by default. When you move an object, the editor samples the terrain height at the object's X/Z position and updates its Y position.
This is intended for map objects that should sit on the ground. Disable it when you intentionally need a floating object.
`Lock terrain` is also enabled by default. The terrain stays visible, but terrain clicks are ignored so normal objects remain easier to select. Disable it only when you need to select or transform the terrain node itself.
When selection is locked:
- clicking another object does not change the selection
- pressing `Esc` does not clear the selection
- the `X` button still clears the selection intentionally
## View Mode
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
The camera action switches between `Center on object` and `Reset camera`. Selecting an object also focuses the camera on that object automatically.
## JSON Inspector
The side panel includes a raw JSON inspector:
The `JSON` section shows the editable node data:
- When no object is selected, it shows the full map node list.
- When an object is selected, it highlights the JSON lines for that object.
This is useful for checking numeric transform values before saving or exporting.
Use it to verify exact numeric transform values before saving or exporting. The JSON inspector is read-only; transform values are changed through the gizmo in the scene.
## Saving Changes
### Export JSON
`Export JSON` downloads the current map node list as `map.json`. Use this when you want to manually replace `public/map.json`.
`Export JSON` downloads the current hierarchical map tree as `map.json`. Use this when you want to manually replace `public/map.json`.
### Save To Server
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
`Save to server` is available only during local development. It writes the edited hierarchical map back to `public/map.json` through the Vite dev-server endpoint.
The button is hidden in production builds because production persistence is not implemented.
## Editing Dialogue Subtitles
The side panel also includes dialogue tools for the dialogue manifest and SRT subtitles.
The side panel includes two separate audio text tools:
- `Dialogues` edits the dialogue manifest, which links dialogue IDs to audio files and SRT cue indexes.
- `SRT` edits the actual subtitle text and cue timings.
The important model is: one dialogue entry points to one cue inside one SRT file. The SRT file is grouped by voice and language, not by dialogue.
### Dialogue Manifest
Use the `Dialogues` panel to edit `public/sounds/dialogue/dialogues.json` without opening the JSON file manually.
Each dialogue entry contains:
| Field | Meaning |
| ------------------ | ----------------------------------------------------------------- |
| `id` | Unique dialogue ID used by cinematics and runtime triggers |
| `voice` | Voice file group: `narrateur`, `fermier`, or `electricienne` |
| `audio` | Runtime audio path, usually under `/sounds/dialogue/` |
| `subtitleCueIndex` | Cue number inside the selected voice/language SRT file |
| `timecode` | Optional global runtime trigger time, in seconds from scene start |
Available actions:
- `Reload` reloads the manifest from disk.
@@ -95,6 +174,19 @@ After using `Add`, save the manifest to keep the new dialogue entry. The generat
New dialogue audio paths start as placeholders such as `/sounds/dialogue/new_dialogue_24.mp3`. Replace them with real MP3 paths before validating the final asset set.
Recommended workflow for a new dialogue:
1. Open `Dialogues`.
2. Click `Add`.
3. Choose the correct `voice`.
4. Replace the generated `id` with a readable stable ID.
5. Replace the placeholder `audio` path with the real MP3 path.
6. Check the generated `subtitleCueIndex`.
7. Click `Create FR SRT cue` if the cue does not exist yet.
8. Click `Save`.
9. Open `SRT`, edit the cue text and timings, then save the SRT file.
10. Run `Validate` from the SRT panel.
### SRT Editor
Use the `SRT` panel to edit one subtitle file at a time.
@@ -108,6 +200,8 @@ Use the `SRT` panel to edit one subtitle file at a time.
Each SRT file belongs to one voice, not one dialogue. Cue indexes must match the `subtitleCueIndex` values referenced by the dialogue manifest.
SRT timings are relative to the dialogue audio file, not to the global game timeline and not to the cinematic timeline. For example, `00:00:01,000` means one second after that dialogue audio starts.
## Validating Dialogue Assets
Use `Validate` in the SRT panel to check the dialogue manifest and linked assets.
@@ -143,6 +237,17 @@ Dialogue cues define:
- `time`: seconds relative to the cinematic start
- `dialogueId`: an entry from `public/sounds/dialogue/dialogues.json`
Recommended workflow for a cinematic:
1. Open `Cinematics`.
2. Select an existing cinematic or click `Add`.
3. Set a stable `id`.
4. Add or adjust camera keyframes.
5. Keep keyframe `time` values increasing from start to end.
6. Add dialogue cues when a dialogue must start during the camera sequence.
7. Click `Preview cinematic` to test the camera path in the editor canvas.
8. Click `Save` when the manifest is correct.
Available actions:
- `Reload` reloads the cinematic manifest from disk.
@@ -155,6 +260,8 @@ Available actions:
Cinematic dialogue cues are the preferred way to synchronize a dialogue with a cinematic. Avoid also giving the same dialogue a global `timecode`, or it can be triggered twice.
Use `dialogueCues` when the dialogue belongs to a cinematic. Use a dialogue `timecode` only for simple global scene timing outside a cinematic.
## Current Limitations
- The editor only modifies existing nodes.
+214 -83
View File
@@ -1,112 +1,243 @@
# Implemented Features
This document lists features that are implemented in the current codebase.
This document lists the user-visible and developer-facing features implemented in the current `develop` branch.
## Scene
## Application And Routes
- Fullscreen React Three Fiber scene
- Main map scene loaded from `public/map.json` and matching `public/models/{name}/model.glb` or `model.gltf` assets
- Minimal fullscreen scene loading overlay for 3D scenes, with a global progress bar used by the production map, debug physics scene, and editor scene
- Debug physics test scene selectable from the debug panel, including grab/trigger tests, an animated model preview, and separate repair playground zones for `bike`, `pylone`, and `ferme`
- Rapier physics context available for production stage gameplay objects
- Ambient and directional lighting
- Environment background setup
- React 19 application bootstrapped by Vite and TypeScript
- TanStack Router route tree
- `/` playable 3D experience
- `/editor` local content editor
- `/docs` in-app documentation browser
- Lazy-loaded docs pages rendered from repository Markdown files
## 3D World
- Fullscreen React Three Fiber canvas
- Production world composition in `src/world/World.tsx`
- Environment model/background through `Environment` and `SkyModel`
- Shared lighting setup
- Production map loaded from `public/map.json`
- Model resolution from `public/models/{name}/model.glb`, then `model.gltf`
- Fallback cubes when a map node has no available model
- Progressive scene loading overlay for game, debug physics scene, and editor
- Stabilized game scene loading gates for map, model, collision, octree, and gameplay stage readiness
- Game stage content mounted only after the map has loaded
- Player, music, dialogues, and gameplay-dependent systems mounted only after gameplay is ready
## Player
- Player camera mode
- Pointer lock mouse look
- Movement with `ZQSD`
- Jumping
- Movement lock during active repair steps, with an on-screen indicator while keeping trigger interactions available
- Octree-based collision against dedicated map collision nodes, currently scoped to `terrain`
- Pointer-lock mouse look
- `ZQSD` movement
- Jump with `Space`
- Trigger interaction with `E`
- Grab interaction with primary mouse button
- Spawn reset based on scene mode
- Input lock while the settings menu is open
- Input lock while a cinematic is playing
- Octree collision against dedicated map collision nodes, currently scoped to the `terrain` node
- Repair movement lock during focused repair steps, with a matching UI indicator
## Interactions
## Physics And Collision
- Focus detection by distance and raycast
- Trigger interactions activated with `E`
- Grab interactions activated with the primary mouse button
- Physics-backed gameplay objects can be mounted inside stage content without replacing player octree collision
- Interaction prompt shown for trigger interactions
- Separate collision responsibility between player and gameplay objects
- Player collision uses a Three.js capsule plus octree
- Gameplay objects use Rapier rigid bodies and colliders
- Production `GameStageContent` is mounted inside a Rapier `Physics` provider
- Debug physics scene owns its own Rapier playground
- Map collision octree is built from explicit collision nodes instead of the full visible map
## Interaction System
- Shared `InteractionManager` singleton for focused object, nearby object, holding state, and hand-holding state
- React subscription through `useSyncExternalStore`
- Distance and camera-ray focus detection in `InteractableObject`
- Trigger interactions through `TriggerObject`
- Grab interactions through `GrabbableObject`
- Trigger prompt shown by `InteractPrompt`
- Optional trigger SFX and optional spawned model support
- Debug interaction sphere visibility through the `Interaction` lil-gui folder
- Hand-controlled grab support for grabbable objects
- Snap-to-target behavior after releasing grabbable objects
## Repair Gameplay
- Reusable production `RepairGame` mounted for `bike`, `pylone`, and `ferme` mission states
- Debug physics playground mounts the same reusable `RepairGame` in `Bike`, `Pylone`, and `Farm` zones so each state can be tuned with isolated positioning before moving placement into the production map
- Repair mission config shared through `src/data/gameplay/repairMissions.ts`, including per-mission broken nodes, placeholder targets, scan timing, and reassembly timing
- Repair-game flow supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` with `.webm` prompts, repair case spawn/opening/exit, focused repair-case view, movement lock indicator during active repair, repair-case trigger interaction, case placeholder traversal, snap-to-placeholder placement, broken-part deposit feedback, `E`, two-fists hold input, exploded and inverse reassembly transitions, completion particles, per-part scan visuals, persistent red broken-part markers, centered broken-part UI videos, multiple grabbable replacement choices, correct-part install validation feedback, and mission completion
- Reusable `RepairGame` mounted for `ebike`, `pylon`, and `farm`
- Mission progression driven by Zustand and shared `MissionStep` types
- Production repair positions:
- `ebike` at `[42.2399, 4.5484, 34.6468]`
- `pylon` at `[64, 0, -66]`
- `farm` at `[-24, 0, 42]`
- Debug physics repair playground zones for all three missions
- Data-driven mission config in `src/data/gameplay/repairMissions.ts`
- Mission flow: `locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done`
- `.webm` 3D prompts for mission object, interaction, and broken parts
- Repair object inspection
- Repair case spawn, pop animation, proximity float, wobble, open/close lid animation, exit animation, and open/close sounds
- Repair case placeholder traversal from GLTF nodes named `placeholder_*`
- Fallback placeholder positions when a case asset has no placeholder nodes
- Fragmentation through repair-case trigger or two-fists hand gesture
- Exploded model visualization through `ExplodableModel`
- Scan visual that steps through exploded parts
- Broken-part detection by configured `nodeName`, with diagnostics when configured parts are missing
- Persistent broken-part highlight and broken-part prompt after discovery
- Grabbable replacement part choices, including distractor parts
- Grabbable broken parts that must be deposited back into the case
- Snap-to-placeholder placement
- Correct-part, wrong-part, and stored-part visual feedback
- Blocked install feedback when validation is attempted too early
- Install target that validates only when the correct replacement is placed and all broken parts are stored
- Inverse reassembly animation
- Completion particles
- Completion target that closes/exits the repair case before calling `completeMission`
## Audio
## Game Progression Store
- Category-based volumes for music, SFX, and dialogue
- Looped background music playback through `AudioManager`
- One-shot sound playback for SFX and dialogue, with simple per-sound pooling
- Optional stereo pan for one-shot sounds
- Zustand `useGameStore` for durable gameplay progression
- Main states: `intro`, `ebike`, `pylon`, `farm`, `outro`
- Per-mission repair step state
- Per-mission completion flags
- Generic mission helpers: `setMissionStep`, `completeMission`, `advanceGameState`, `rewindGameState`, `resetGame`
- `isCinematicPlaying` flag used by the player input lock
- Debug game-state panel that can jump between main states and sub-states
## Dialogue And Subtitles
## Settings And UI Overlays
- Dialogue manifest in `public/sounds/dialogue/dialogues.json`
- Dialogue audio loaded from `public/sounds/dialogue/`
- One SRT subtitle file per voice and language
- French subtitle fallback when the selected language file is missing
- Runtime subtitle overlay with speaker-specific colors
- Timecoded dialogue trigger support for dialogue entries that define `timecode`
- Dialogue queueing to avoid overlapping dialogue playback
## Cinematics
- Cinematic manifest in `public/cinematics.json`
- Timecoded cinematic trigger support
- GSAP camera keyframe playback
- Optional dialogue cues synchronized to cinematic timelines
- Player input lock while a cinematic is active
## Game Options Menu
- `Esc` opens and closes the in-game options menu
- `Esc` opens and closes the settings menu
- Music, SFX, and dialogue volume sliders
- Subtitle visibility toggle
- Subtitle language choice between French and English
- Repair runtime choice between local JavaScript and Python server mode
- Quit action that clears browser-accessible cookies and returns to `/`
- Crosshair overlay
- Interaction prompt
- Subtitle overlay
- Repair movement-lock indicator
- Debug overlay layout
- Scene loading overlay
## Audio
- Singleton `AudioManager`
- Looped music playback
- One-shot SFX/dialogue playback
- Per-path one-shot audio pools
- Category volumes for `music`, `sfx`, and `dialogue`
- Optional stereo panning for one-shot sounds
- Playback-rate option for one-shot sounds
- Browser autoplay fallback for music: retry after user `pointerdown` or `keydown`
- Game music mounted through `GameMusic`
- Current game music path: `/sounds/musique/test.mp3`
- Current base music volume: `0.33`
- Repair case open/close sounds
- Trigger-object SFX support
## Dialogue And Subtitles
- Runtime dialogue manifest in `public/sounds/dialogue/dialogues.json`
- Dialogue audio under `public/sounds/dialogue/`
- One SRT file per voice and language
- French and English subtitle folders
- Runtime SRT parsing
- Subtitle cue lookup by voice, selected language, and `subtitleCueIndex`
- French fallback when the selected language file is unavailable
- Dialogue playback through the `dialogue` audio category
- Runtime subtitle synchronization from audio `timeupdate`
- Speaker-aware subtitle overlay
- Dialogue queueing to avoid overlapping dialogue playback
- Global timecode dialogue triggering through `GameDialogues`
## Cinematics
- Runtime cinematic manifest in `public/cinematics.json`
- Cinematic manifest validation
- GSAP camera keyframe playback
- Camera position and look target interpolation
- Optional dialogue cues relative to cinematic start time
- Player input lock while a cinematic is active
- Current world integration only mounts `GameCinematics` during `mainState === "outro"`
## Hand Tracking
- Optional webcam hand tracking provider around the playable scene
- Source switch in debug GUI: local Python backend or browser-side MediaPipe
- Backend WebSocket endpoint at `ws://localhost:8000/ws`
- Backend health endpoint at `http://localhost:8000/health`
- Browser-side MediaPipe through `@mediapipe/tasks-vision`
- Lazy activation so camera/tracking is not always active
- Production activation during repair steps that need hand input
- Debug activation in physics mode while near, holding, or hand-holding interactions
- Hand snapshot context for R3F and UI consumers
- Fist detection
- Two-fists hold gesture for repair fragmentation
- Hand grab support for `GrabbableObject`
- Hand-tracking debug panel with status, source/server state, hand count, fist state, and glove model status
- SVG hand visualizer fallback
- `gant_l` and `gant_r` R3F glove overlays when tracking is active
## Debug Tooling
- `?debug` query param enables the debug panel
- `lil-gui` controls for camera mode, scene mode, `R3F Perf`, `Debug Overlay`, and interaction tuning
- Compact debug overlay for game state controls and hand tracking status
- Debug game-state mission switching unlocks locked repair missions at `waiting` for faster testing
- Debug scene helpers
- Free debug camera
- `r3f-perf` overlay
- `?debug` query param enables debug systems
- `lil-gui` root debug folder
- Camera mode switch between player and debug camera
- Scene mode switch between production game and physics test scene
- R3F perf toggle
- Debug overlay toggle
- Hand-tracking source switch
- Interaction sphere debug toggle
- Grabbable tuning controls for stiffness, throw boost, and hold distance
- Debug helpers: grid and axes
- Debug camera controls
- Debug game-state panel
- Debug hand-tracking panel
- Physics test scene with floor, grabbable object, trigger object, repair zones, and animated model preview
- Animated `electricienne-animated` model preview restored in the debug physics scene
## Map Editor
## Map And Content Editor
- `/editor` route for inspecting and editing `public/map.json`
- Automatic loading of `public/map.json` when available
- Folder upload fallback when `map.json` is missing
- Rendering of available `public/models/{name}/model.glb` or `model.gltf` assets
- Fallback cubes for nodes whose model is missing
- `/editor` route
- Automatic loading of `public/map.json`
- Folder upload fallback when map data is not available
- Shared `MapNode` format with runtime map loading
- Render available map models
- Fallback cubes for missing models
- Object selection by click
- Transform modes for translate, rotate, and scale
- Keyboard shortcuts for `T`, `R`, `S`, `Esc`, undo, and redo
- Player-style navigation mode with `WASD`, `ZQSD`, arrow keys, `Space`, and `Shift`
- JSON export for downloading the edited map
- Dev-server save endpoint for writing changes back to `public/map.json`
- SRT editor for dialogue subtitles
- Audio preview and timing helpers for SRT cues
- Dev-server save endpoint for SRT files
- Dialogue manifest editor with preview and assisted French SRT cue creation
- Cinematic manifest editor with camera keyframes, dialogue cues, and canvas preview
- Dialogue manifest validation from the editor UI
- Transform modes: translate, rotate, scale
- Transform keyboard shortcuts: `T`, `R`, `S`
- Selection lock
- Explicit selection clear
- Undo and redo
- Player-style editor navigation with `WASD`, `ZQSD`, arrows, `Space`, and `Shift`
- JSON inspector
- JSON export
- Dev-server save endpoint for `public/map.json`
- Dialogue manifest editor
- SRT subtitle editor
- Audio preview and cue timing helpers
- French SRT cue creation helper
- Dialogue asset validation endpoint
- Cinematic manifest editor
- Cinematic camera keyframe editor
- Cinematic dialogue cue editor
- Cinematic preview in the editor canvas
- Dev-server endpoints for dialogue, SRT, and cinematic saves
## Not Implemented Yet
## In-App Documentation
- complete mission system
- zone system
- full cinematic system beyond current timecode prototype
- gameplay-triggered dialogue branches beyond current prototype triggers
- loading flow
- minimap and mission HUD
- full production separation between gameplay and debug scenes
- production backend persistence for editor saves
- `/docs` documentation layout
- Markdown rendered through `react-markdown` and `remark-gfm`
- Technical docs for architecture, scene runtime, repair game, interaction, editor, audio, hand tracking, Zustand, Three debugging, animation, and target architecture
- User docs for implemented features, main feature, editor usage, and code-review preparation
## Known Gaps
- Complete production mission manager/orchestrator
- Full mission HUD or minimap
- Full zone system
- Dialogue branching
- Production persistence for editor saves
- Production backend for repair-game runtime selection
- Production save/load of player progression
- Full migration of player movement to Rapier
- Advanced hand smoothing and calibrated glove finger animation
- Snap-to-grid, object creation, object deletion, material editing, or model editing in the map editor
+46
View File
@@ -0,0 +1,46 @@
# Galerie des modèles
La galerie est disponible sur `/gallery`. Elle permet de parcourir les modèles 3D présents dans `public/models/` sans lancer la boucle de gameplay principale.
## Objectif
Cette page sert à remercier et valoriser le travail des designers du projet La Fabrik. Chaque modèle est affiché dans un canvas dédié, avec la même skybox et le même lighting que l'expérience principale.
## Utilisation
1. Ouvrir `/gallery`.
2. Utiliser les flèches en bas de l'écran pour passer au modèle précédent ou suivant.
3. Tourner autour du modèle avec la souris ou le doigt.
4. Utiliser le bouton de réglages à droite pour ouvrir ou fermer le panneau lumière.
5. Lire le diagnostic texture discret pour savoir si le modèle chargé semble correct côté textures.
## Fonctionnement
- La liste des modèles est déclarée dans `src/data/galleryModels.ts`.
- Le viewer utilise `@react-three/fiber` et `@react-three/drei`.
- `OrbitControls` permet de manipuler la caméra autour du modèle.
- `Bounds` et `Center` recadrent automatiquement le modèle actif.
- `SkyModel` réutilise la skybox du jeu, avec un matériau non éclairé uniquement dans la galerie pour éviter que certaines faces deviennent noires avec une caméra orbitale libre.
- Les lumières reprennent les valeurs par défaut du jeu, puis peuvent être ajustées dans le panneau latéral.
- `OrbitControls` autorise une orbite verticale complète pour inspecter le dessous des modèles.
- Le viewer désactive les normal maps dans la preview pour limiter les coutures visibles sur certains exports découpés en plusieurs meshes.
- Les animations GLTF présentes dans un modèle sont lancées automatiquement.
- Un diagnostic simple inspecte les matériaux chargés pour signaler les textures absentes ou non exploitables.
## Ajouter un modèle
1. Ajouter le dossier du modèle dans `public/models/{nom}`.
2. Vérifier que le modèle possède un fichier chargeable, par exemple `model.gltf`, `model.glb` ou un nom explicite comme `potager.gltf`.
3. Ajouter une entrée dans `src/data/galleryModels.ts` avec un `id`, un `name` et un `path`.
Exemple :
```ts
{ id: "nouveau-modele", name: "Nouveau modèle", path: "/models/nouveau-modele/model.gltf" }
```
## Limites connues
- Le navigateur ne liste pas automatiquement les dossiers de `public/models/`, donc la liste reste déclarative.
- Les modèles très lourds peuvent prendre du temps à charger.
- La galerie est un viewer simple : elle ne remplace pas les outils d'inspection avancée comme Blender ou le viewer d'upload.
+14 -11
View File
@@ -8,11 +8,11 @@ The main feature is a reusable repair flow mounted in the production game scene.
The current user flow is:
1. Enter a mission state such as `bike`, `pylone`, or `ferme`.
1. Enter a mission state such as `ebike`, `pylon`, or `farm`.
2. Move close to the active repair object in the game scene.
3. Aim at the object and press the interaction key when prompted.
4. The mission step moves from `waiting` to `inspected`.
5. The repair case appears near the mission object, the player movement controls are locked, and the case can float when the player approaches it.
5. The repair case appears near the mission object and can float when the player approaches it. A repair movement-lock rule exists in code, but it is currently disabled by the hook on `develop`.
6. Aim at the repair case and press `E`, or hold both fists closed for one second, to move from `inspected` to `fragmented`.
7. The mission object uses an exploded-model transition, then moves to `scanning`.
8. The scan visual moves across the fragmented model one part at a time and keeps a red marker plus the `cassé.webm` prompt centered on any configured broken part once it has been found.
@@ -20,28 +20,30 @@ The current user flow is:
10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation.
11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
12. Press `E` on the green install target to move to `reassembling`. Wrong parts turn the target red and cannot finish the repair.
13. The exploded object animates back into its assembled form with completion particles, then moves to `done` and restores player movement controls.
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `ferme`.
13. The exploded object animates back into its assembled form with completion particles, then moves to `done`.
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `farm`.
## Why It Matters
This feature validates the repair loop before a full mission system exists. It tests whether repair objects, physical proximity, model selection, audio feedback, and exploded model visualization can work together in the 3D scene.
For implementation details, see `docs/technical/repair-game.md`.
## Current Behavior
In `waiting`, the active mission renders its repair object and the `interagir.webm` prompt in the game scene. The interaction uses the shared focus/raycast interaction system, so the player still gets the normal `E` prompt.
When the player inspects the object, `RepairGame` writes `inspected` through the generic mission store action. The repair case then appears from the mission config with a small pop animation, player movement is locked while the repair sequence is active, and a small HTML indicator confirms that movement is temporarily unavailable. When the player is close enough, the existing case model floats upward and rotates gently to signal interactivity.
When the player inspects the object, `RepairGame` writes `inspected` through the generic mission store action. The repair case then appears from the mission config with a small pop animation. When the player is close enough, the case model floats upward and rotates gently to signal interactivity. The shared repair movement-lock hook and HTML indicator keep movement disabled during active repair steps.
In `inspected`, `RepairGame` can also move to `fragmented`. Keyboard input goes through the shared focus/raycast interaction system on the repair case, so the player must be close enough and aim at the case before pressing `E`. The hand-tracking path still uses a two-fists hold gesture and is state-based, so it does not depend on being inside a local object interaction radius.
In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan can match a specific `nodeName` when mission data provides one, otherwise it falls back to the first scanned parts as placeholder broken parts. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible placeholder before the final install target validates. If `brokenParts[].placeholderName` is configured, that broken part snaps only to the matching placeholder; otherwise it can use any available placeholder. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. Replacement parts show green or red placement feedback after snapping, broken parts show stored feedback after deposit, and the install target gives a short blocked feedback if the player tries to validate too early. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. Player movement stays locked through `inspected`, `fragmented`, `scanning`, `repairing`, and `reassembling`, while trigger interactions remain available. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. In `done`, player movement is available again and the repaired object remains visible with a completion target; validating closes the repair case first, then plays the case exit animation before advancing the global mission progression.
In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan matches configured broken parts by `nodeName` and reports diagnostics when a configured node is missing. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those slot positions, and releasing a part near a slot snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible slot before the final install target validates. If `brokenParts[].caseSlotName` is configured, that broken part snaps only to the matching slot; otherwise it can use any available slot. If the current case asset has no slot nodes, the flow keeps using fallback focus positions and logs the fallback. Replacement parts show green or red placement feedback after snapping, broken parts show stored feedback after deposit, and the install target gives a short blocked feedback if the player tries to validate too early. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. In `done`, the repaired object remains visible with a completion target; validating closes the repair case first, then plays the case exit animation before advancing the global mission progression.
The mission config now carries the mission-specific variations. `bike` repairs one cooling core, `pylone` scans and stores both the lamp relay and a damaged panel with slower scan/reassembly timing, and `ferme` scans and stores an irrigation pump plus humidity sensor with faster scan/reassembly timing.
The mission config now carries the mission-specific variations. `ebike` repairs one cooling core, `pylon` scans and stores both the lamp relay and a damaged panel with slower scan/reassembly timing, and `farm` scans and stores an irrigation pump plus humidity sensor with faster scan/reassembly timing.
## Key Files
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`.
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `ebike`, `pylon`, and `farm`.
- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, case exit animation, and mission UI prompt.
- `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow.
- `src/components/three/gameplay/RepairBrokenPartHighlight.tsx` renders the red halo and wire marker around detected broken parts during scanning.
@@ -54,7 +56,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part.
- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
- `src/components/ui/RepairMovementLockIndicator.tsx` renders the HTML indicator shown while repair movement is locked.
- `src/components/ui/RepairMovementLockIndicator.tsx` renders the HTML repair movement-lock indicator.
- `src/hooks/gameplay/useRepairFragmentationInput.ts` handles the `inspected -> fragmented` two-fists input and can optionally bind keyboard input for non-trigger flows.
- `src/hooks/gameplay/useRepairMissionStep.ts` reads the active mission step from the game store.
- `src/hooks/gameplay/useRepairMovementLocked.ts` exposes the shared repair movement-lock rule used by the player controller and UI indicator.
@@ -63,7 +65,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
- `src/data/gameplay/repairCaseConfig.ts` stores repair case model, sound, and animation constants.
- `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants.
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`.
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `ebike`, `pylon`, and `farm`.
- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers.
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used by the store, data config, debug UI, and gameplay components.
@@ -71,7 +73,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
The production repair flow currently requires:
- the active `mainState` to be one of `bike`, `pylone`, or `ferme`
- the active `mainState` to be one of `ebike`, `pylon`, or `farm`
- `GameStageContent` mounted inside the game scene Rapier `Physics` boundary
- model assets available under `public/models/`
- sound assets available under `public/sounds/`
@@ -106,5 +108,6 @@ python -m backend.main
- The reusable production `RepairGame` currently covers `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission`.
- Mission progression is wired through Zustand using `completeMission` at the end of each repair.
- There is no central `GameManager` in this branch.
- Repair movement lock is currently disabled by `useRepairMovementLocked()`.
- Hand tracking is available for the two-fists input and grabbable repair parts; case interaction and final installation still use the shared `E` trigger path.
- The repair-game content is configured statically in `src/data/gameplay/`.
+258 -230
View File
@@ -12,7 +12,7 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/rapier": "^2.2.0",
"@tanstack/react-router": "^1.168.25",
"@tanstack/react-router": "^1.169.2",
"gsap": "^3.15.0",
"lil-gui": "^0.21.0",
"lucide-react": "^1.11.0",
@@ -22,6 +22,7 @@
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"three": "0.182.0",
"three-stdlib": "^2.36.1",
"zustand": "^5.0.12"
},
"devDependencies": {
@@ -61,9 +62,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -222,9 +223,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -294,6 +295,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.19.2.tgz",
"integrity": "sha512-AZHL1jqUF55QJkJyU1yKeh4ImX2J93bVLIezT1+o0FZqTix6O06MOaqpKoJ4MmbDCsoZmwO+qc471/SDMDm2AA==",
"license": "Apache-2.0"
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -639,9 +646,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.127.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
"version": "0.129.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -779,16 +786,10 @@
"three": ">=0.159.0"
}
},
"node_modules/@react-three/rapier/node_modules/@dimforge/rapier3d-compat": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.19.1.tgz",
"integrity": "sha512-xvFNtb/9xILxfvdFOa7NCnYUEF6cfn51R44C1xnKXtk5DpyAARqsC4sxZwiJAHRSzYT5FFe889t36iFnzb3vxg==",
"license": "Apache-2.0"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
"cpu": [
"arm64"
],
@@ -803,9 +804,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
"cpu": [
"arm64"
],
@@ -820,9 +821,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
"cpu": [
"x64"
],
@@ -837,9 +838,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
"cpu": [
"x64"
],
@@ -854,9 +855,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
"cpu": [
"arm"
],
@@ -871,13 +872,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -888,13 +892,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -905,13 +912,16 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -922,13 +932,16 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -939,13 +952,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -956,13 +972,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -973,9 +992,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
"cpu": [
"arm64"
],
@@ -990,9 +1009,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
"cpu": [
"wasm32"
],
@@ -1009,9 +1028,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
"cpu": [
"arm64"
],
@@ -1026,9 +1045,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
"cpu": [
"x64"
],
@@ -1072,14 +1091,14 @@
}
},
"node_modules/@tanstack/react-router": {
"version": "1.168.25",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.25.tgz",
"integrity": "sha512-4U/E76dc+fYuLixjV1RLNfqrkQoexSL8MqGNpIHOodtvY3fMPGaALrvDVtBDQYBEU4z5r5fHaV6+kclWAVFP9A==",
"version": "1.169.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.169.2.tgz",
"integrity": "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.161.6",
"@tanstack/react-store": "^0.9.3",
"@tanstack/router-core": "1.168.17",
"@tanstack/router-core": "1.169.2",
"isbot": "^5.1.22"
},
"engines": {
@@ -1113,18 +1132,15 @@
}
},
"node_modules/@tanstack/router-core": {
"version": "1.168.17",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.17.tgz",
"integrity": "sha512-VDq7HCqRK3sdpxoETwYoTXTaYi+OVQC197g1fdzaiZBUmhntfjn+PQc15OzTqNNhf8Menk6r6ftmuphybMKdig==",
"version": "1.169.2",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.169.2.tgz",
"integrity": "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.161.6",
"cookie-es": "^3.0.0",
"seroval": "^1.5.0",
"seroval-plugins": "^1.5.0"
},
"bin": {
"intent": "bin/intent.js"
"seroval": "^1.5.4",
"seroval-plugins": "^1.5.4"
},
"engines": {
"node": ">=20.19"
@@ -1151,9 +1167,9 @@
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1177,9 +1193,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/@types/estree-jsx": {
@@ -1223,9 +1239,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"version": "24.12.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1273,9 +1289,9 @@
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.184.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz",
"integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==",
"version": "0.184.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
"integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
@@ -1305,17 +1321,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/type-utils": "8.59.1",
"@typescript-eslint/utils": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1328,7 +1344,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1344,16 +1360,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1369,14 +1385,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.1",
"@typescript-eslint/types": "^8.59.1",
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
@@ -1391,14 +1407,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1409,9 +1425,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1426,15 +1442,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1",
"@typescript-eslint/utils": "8.59.1",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1451,9 +1467,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1465,16 +1481,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.1",
"@typescript-eslint/tsconfig-utils": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1503,9 +1519,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1532,9 +1548,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1545,16 +1561,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1569,13 +1585,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1600,9 +1616,9 @@
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
"license": "ISC"
},
"node_modules/@use-gesture/core": {
@@ -1767,9 +1783,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.23",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
"integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
"version": "2.10.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1881,9 +1897,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001791",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
"integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
"version": "1.0.30001792",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
"dev": true,
"funding": [
{
@@ -2141,9 +2157,9 @@
"license": "Apache-2.0"
},
"node_modules/electron-to-chromium": {
"version": "1.5.344",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
"version": "1.5.353",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
"integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
"dev": true,
"license": "ISC"
},
@@ -2565,9 +2581,9 @@
}
},
"node_modules/globals": {
"version": "17.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
"integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2827,9 +2843,9 @@
"license": "MIT"
},
"node_modules/isbot": {
"version": "5.1.39",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.39.tgz",
"integrity": "sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==",
"version": "5.1.40",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz",
"integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==",
"license": "Unlicense",
"engines": {
"node": ">=18"
@@ -3096,6 +3112,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3117,6 +3136,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3138,6 +3160,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3159,6 +3184,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3264,9 +3292,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz",
"integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
"integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -4172,9 +4200,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -4198,9 +4226,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.38",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"version": "2.0.44",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
"integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
"dev": true,
"license": "MIT"
},
@@ -4332,9 +4360,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
@@ -4494,24 +4522,24 @@
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
"react": "^19.2.6"
}
},
"node_modules/react-markdown": {
@@ -4642,14 +4670,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
"@oxc-project/types": "=0.129.0",
"@rolldown/pluginutils": "1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -4658,27 +4686,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
"@rolldown/binding-android-arm64": "1.0.0",
"@rolldown/binding-darwin-arm64": "1.0.0",
"@rolldown/binding-darwin-x64": "1.0.0",
"@rolldown/binding-freebsd-x64": "1.0.0",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
"@rolldown/binding-linux-arm64-musl": "1.0.0",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
"@rolldown/binding-linux-x64-gnu": "1.0.0",
"@rolldown/binding-linux-x64-musl": "1.0.0",
"@rolldown/binding-openharmony-arm64": "1.0.0",
"@rolldown/binding-wasm32-wasi": "1.0.0",
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
"@rolldown/binding-win32-x64-msvc": "1.0.0"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
"dev": true,
"license": "MIT"
},
@@ -4699,18 +4727,18 @@
}
},
"node_modules/seroval": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz",
"integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz",
"integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.2.tgz",
"integrity": "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz",
"integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5060,16 +5088,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
"integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.59.1",
"@typescript-eslint/parser": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1",
"@typescript-eslint/utils": "8.59.1"
"@typescript-eslint/eslint-plugin": "8.59.3",
"@typescript-eslint/parser": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5265,16 +5293,16 @@
}
},
"node_modules/vite": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"version": "8.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"postcss": "^8.5.14",
"rolldown": "1.0.0",
"tinyglobby": "^0.2.16"
},
"bin": {
@@ -5291,7 +5319,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -5399,9 +5427,9 @@
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -5422,9 +5450,9 @@
}
},
"node_modules/zustand": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
"version": "5.0.13",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz",
"integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
+4 -4
View File
@@ -8,11 +8,13 @@
},
"scripts": {
"dev": "vite",
"dev:three-debug": "vite --mode three-debug --host 0.0.0.0",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"map:transform": "node scripts/transformMap.cjs",
"preview": "vite preview",
"typecheck": "tsc -b"
},
@@ -21,7 +23,7 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@react-three/rapier": "^2.2.0",
"@tanstack/react-router": "^1.168.25",
"@tanstack/react-router": "^1.169.2",
"gsap": "^3.15.0",
"lil-gui": "^0.21.0",
"lucide-react": "^1.11.0",
@@ -31,6 +33,7 @@
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"three": "0.182.0",
"three-stdlib": "^2.36.1",
"zustand": "^5.0.12"
},
"devDependencies": {
@@ -51,9 +54,6 @@
"vite": "^8.0.4"
},
"overrides": {
"@react-three/rapier": {
"@dimforge/rapier3d-compat": "0.19.1"
},
"r3f-perf": {
"@react-three/drei": "$@react-three/drei"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -12
View File
@@ -2,24 +2,18 @@
"version": 1,
"cinematics": [
{
"id": "intro_overview",
"id": "outro_farm_drone",
"timecode": 0,
"dialogueCues": [
{
"time": 0,
"dialogueId": "narrateur_bienvenueaaltera"
}
],
"cameraKeyframes": [
{
"time": 0,
"position": [8, 5, 12],
"target": [0, 2, 0]
"position": [-24, 5, 65],
"target": [-24, 2, 42]
},
{
"time": 4,
"position": [12, 4, -6],
"target": [10, 1.4, -8]
"time": 10,
"position": [-24, 90, 200],
"target": [-24, 0, 42]
}
]
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.
Binary file not shown.
+40324 -4587
View File
File diff suppressed because it is too large Load Diff
+35051
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More