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.
This commit is contained in:
Tom Boullay
2026-06-02 22:10:31 +02:00
parent 2a6a028e1d
commit 6a0215d1a6
2 changed files with 38 additions and 15 deletions
+37 -14
View File
@@ -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<THREE.Group>(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<Vector3Tuple>(() => {
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 {
>
<mesh>
<sphereGeometry args={[8, 15, 12]} />
<meshBasicMaterial colorWrite={false} color={"red"} depthWrite={false} />
<meshBasicMaterial
colorWrite={false}
color={"red"}
depthWrite={false}
/>
</mesh>
</InteractableObject>
@@ -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°) */}
<group position={[2, 6, 0]} rotation={[0, -80, 0]}>
<EbikeSpeedmeter width={3} height={1.5} position={[0, 0.4, 0]} gaugeInnerR={0.33} gaugeOuterR={0.445}
<EbikeSpeedmeter
width={3}
height={1.5}
position={[0, 0.4, 0]}
gaugeInnerR={0.33}
gaugeOuterR={0.445}
gaugeWidth={2.5}
gaugeHeight={2.1}
gaugeOffsetX={0}
@@ -499,8 +522,8 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
ref={headlightRef}
intensity={100}
color="#ffca60"
angle={Math.PI / 5} // 22.5° demi-angle — cone étroit comme une torche
penumbra={0.5} // bord doux (0 = dur, 1 = très doux)
angle={Math.PI / 5} // 22.5° demi-angle — cone étroit comme une torche
penumbra={0.5} // bord doux (0 = dur, 1 = très doux)
distance={50}
decay={2.5}
castShadow={false}