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:
@@ -33,9 +33,19 @@ const _up = new THREE.Vector3(0, 1, 0);
|
|||||||
|
|
||||||
interface EbikeProps {
|
interface EbikeProps {
|
||||||
position: Vector3Tuple;
|
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 groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useLoggedGLTF(EBIKE_MODEL_PATH, {
|
const { scene } = useLoggedGLTF(EBIKE_MODEL_PATH, {
|
||||||
scope: "Ebike",
|
scope: "Ebike",
|
||||||
@@ -45,7 +55,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
const terrainHeight = useTerrainHeightSampler();
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
const parkedPosition = useMemo<Vector3Tuple>(() => {
|
const parkedPosition = useMemo<Vector3Tuple>(() => {
|
||||||
const [x, y, z] = position;
|
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, [
|
const bottomOffset = getObjectBottomOffset(model, [
|
||||||
EBIKE_WORLD_SCALE,
|
EBIKE_WORLD_SCALE,
|
||||||
EBIKE_WORLD_SCALE,
|
EBIKE_WORLD_SCALE,
|
||||||
@@ -53,7 +63,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return [x, height + bottomOffset, z];
|
return [x, height + bottomOffset, z];
|
||||||
}, [model, position, terrainHeight]);
|
}, [model, position, snapToTerrain, terrainHeight]);
|
||||||
const movementMode = useGameStore((state) => state.player.movementMode);
|
const movementMode = useGameStore((state) => state.player.movementMode);
|
||||||
const mainState = useGameStore((state) => state.mainState);
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
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.
|
// SpotLight target must be in the scene to define the cone direction.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
threeScene.add(headlightTarget);
|
threeScene.add(headlightTarget);
|
||||||
return () => { threeScene.remove(headlightTarget); };
|
return () => {
|
||||||
|
threeScene.remove(headlightTarget);
|
||||||
|
};
|
||||||
}, [threeScene, headlightTarget]);
|
}, [threeScene, headlightTarget]);
|
||||||
|
|
||||||
// Link the target to the SpotLight once it mounts.
|
// 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);
|
console.log("[Ebike] Fork found:", (forkNode as THREE.Object3D).name);
|
||||||
} else {
|
} else {
|
||||||
const names: string[] = [];
|
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);
|
console.warn("[Ebike] Fork not found. All nodes:", names);
|
||||||
}
|
}
|
||||||
}, [model]);
|
}, [model]);
|
||||||
@@ -222,11 +236,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
// ── SpotLight headlight — tune the constants below ────────────────────────
|
// ── SpotLight headlight — tune the constants below ────────────────────────
|
||||||
// ── SpotLight headlight — tune these four constants ───────────────────────
|
// ── SpotLight headlight — tune these four constants ───────────────────────
|
||||||
const LIGHT_OFFSET_X = -0.7; // position : left(-) / right(+)
|
const LIGHT_OFFSET_X = -0.7; // position : left(-) / right(+)
|
||||||
const LIGHT_OFFSET_Y = 1.5; // position : down(-) / up(+)
|
const LIGHT_OFFSET_Y = 1.5; // position : down(-) / up(+)
|
||||||
const LIGHT_OFFSET_Z = 0; // position : backward(-) / forward(+)
|
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_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_TARGET_DIST = 20; // metres devant la position de la lumière
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
if (headlightRef.current && phareRef.current && groupRef.current) {
|
if (headlightRef.current && phareRef.current && groupRef.current) {
|
||||||
phareRef.current.getWorldPosition(_phareWorldPos);
|
phareRef.current.getWorldPosition(_phareWorldPos);
|
||||||
@@ -460,7 +474,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
<mesh>
|
<mesh>
|
||||||
<sphereGeometry args={[8, 15, 12]} />
|
<sphereGeometry args={[8, 15, 12]} />
|
||||||
<meshBasicMaterial colorWrite={false} color={"red"} depthWrite={false} />
|
<meshBasicMaterial
|
||||||
|
colorWrite={false}
|
||||||
|
color={"red"}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
</InteractableObject>
|
</InteractableObject>
|
||||||
|
|
||||||
@@ -469,7 +487,12 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
Speedmeter: upper-half arc overlay, renderOrder 10 001
|
Speedmeter: upper-half arc overlay, renderOrder 10 001
|
||||||
rotation: Math.PI/2 radians = 90° (NOT the number 90 which = ~116.6°) */}
|
rotation: Math.PI/2 radians = 90° (NOT the number 90 which = ~116.6°) */}
|
||||||
<group position={[2, 6, 0]} rotation={[0, -80, 0]}>
|
<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}
|
gaugeWidth={2.5}
|
||||||
gaugeHeight={2.1}
|
gaugeHeight={2.1}
|
||||||
gaugeOffsetX={0}
|
gaugeOffsetX={0}
|
||||||
@@ -499,8 +522,8 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
ref={headlightRef}
|
ref={headlightRef}
|
||||||
intensity={100}
|
intensity={100}
|
||||||
color="#ffca60"
|
color="#ffca60"
|
||||||
angle={Math.PI / 5} // 22.5° demi-angle — cone étroit comme une torche
|
angle={Math.PI / 5} // 22.5° demi-angle — cone étroit comme une torche
|
||||||
penumbra={0.5} // bord doux (0 = dur, 1 = très doux)
|
penumbra={0.5} // bord doux (0 = dur, 1 = très doux)
|
||||||
distance={50}
|
distance={50}
|
||||||
decay={2.5}
|
decay={2.5}
|
||||||
castShadow={false}
|
castShadow={false}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
|||||||
<RepairPlaygroundZoneMarker color={zone.color} />
|
<RepairPlaygroundZoneMarker color={zone.color} />
|
||||||
</group>
|
</group>
|
||||||
{zone.mission === "ebike" ? (
|
{zone.mission === "ebike" ? (
|
||||||
<Ebike position={zone.position} />
|
<Ebike position={zone.position} snapToTerrain={false} />
|
||||||
) : null}
|
) : null}
|
||||||
<RepairGame mission={zone.mission} position={zone.position} />
|
<RepairGame mission={zone.mission} position={zone.position} />
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
Reference in New Issue
Block a user