From 6a0215d1a6158727e1c94de294793a830fcc14c7 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 2 Jun 2026 22:10:31 +0200 Subject: [PATCH] fix(repair): keep ebike at zone Y in test scene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/ebike/Ebike.tsx | 51 ++++++++++++++++++++++++---------- src/world/debug/TestMap.tsx | 2 +- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx index a4c6215..dfa8c50 100644 --- a/src/components/ebike/Ebike.tsx +++ b/src/components/ebike/Ebike.tsx @@ -33,9 +33,19 @@ const _up = new THREE.Vector3(0, 1, 0); interface EbikeProps { position: Vector3Tuple; + /** + * When true (default), the parked position is snapped to the world terrain + * height. Pass false in test scenes that don't render the world terrain so + * the bike stays at the explicit Y of {@link position} instead of floating + * at the (invisible) terrain height. + */ + snapToTerrain?: boolean; } -export function Ebike({ position }: EbikeProps): React.JSX.Element { +export function Ebike({ + position, + snapToTerrain = true, +}: EbikeProps): React.JSX.Element { const groupRef = useRef(null); const { scene } = useLoggedGLTF(EBIKE_MODEL_PATH, { scope: "Ebike", @@ -45,7 +55,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { const terrainHeight = useTerrainHeightSampler(); const parkedPosition = useMemo(() => { const [x, y, z] = position; - const height = terrainHeight.getHeight(x, z) ?? y; + const height = snapToTerrain ? (terrainHeight.getHeight(x, z) ?? y) : y; const bottomOffset = getObjectBottomOffset(model, [ EBIKE_WORLD_SCALE, EBIKE_WORLD_SCALE, @@ -53,7 +63,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { ]); return [x, height + bottomOffset, z]; - }, [model, position, terrainHeight]); + }, [model, position, snapToTerrain, terrainHeight]); const movementMode = useGameStore((state) => state.player.movementMode); const mainState = useGameStore((state) => state.mainState); const ebikeStep = useGameStore((state) => state.ebike.currentStep); @@ -135,7 +145,9 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { // SpotLight target must be in the scene to define the cone direction. useEffect(() => { threeScene.add(headlightTarget); - return () => { threeScene.remove(headlightTarget); }; + return () => { + threeScene.remove(headlightTarget); + }; }, [threeScene, headlightTarget]); // Link the target to the SpotLight once it mounts. @@ -192,7 +204,9 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { console.log("[Ebike] Fork found:", (forkNode as THREE.Object3D).name); } else { const names: string[] = []; - model.traverse((c) => { if (c.name) names.push(c.name); }); + model.traverse((c) => { + if (c.name) names.push(c.name); + }); console.warn("[Ebike] Fork not found. All nodes:", names); } }, [model]); @@ -222,11 +236,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { useFrame((_, delta) => { // ── SpotLight headlight — tune the constants below ──────────────────────── // ── SpotLight headlight — tune these four constants ─────────────────────── - const LIGHT_OFFSET_X = -0.7; // position : left(-) / right(+) - const LIGHT_OFFSET_Y = 1.5; // position : down(-) / up(+) - const LIGHT_OFFSET_Z = 0; // position : backward(-) / forward(+) - const LIGHT_AIM_DEG = 90; // aim rotation around Y : 0=forward, -90=left, +90=right - const LIGHT_TARGET_DIST = 20; // metres devant la position de la lumière + const LIGHT_OFFSET_X = -0.7; // position : left(-) / right(+) + const LIGHT_OFFSET_Y = 1.5; // position : down(-) / up(+) + const LIGHT_OFFSET_Z = 0; // position : backward(-) / forward(+) + const LIGHT_AIM_DEG = 90; // aim rotation around Y : 0=forward, -90=left, +90=right + const LIGHT_TARGET_DIST = 20; // metres devant la position de la lumière // ───────────────────────────────────────────────────────────────────────── if (headlightRef.current && phareRef.current && groupRef.current) { phareRef.current.getWorldPosition(_phareWorldPos); @@ -460,7 +474,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { > - + @@ -469,7 +487,12 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { Speedmeter: upper-half arc overlay, renderOrder 10 001 rotation: Math.PI/2 radians = 90° (NOT the number 90 which = ~116.6°) */} - {zone.mission === "ebike" ? ( - + ) : null}