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