Mechanical formatting cleanup carried over from the develop merge:
inline single-line tuples and break long lines per project prettier
config. No behavior change.
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.
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.
- 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.
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).
- 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.
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.
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.
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.
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.
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.
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.
- 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.
- 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.
- 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.
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>
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.
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.
- 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.
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).
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.
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.
- 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.
- 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.