diff --git a/public/map.json b/public/map.json
index a50a559..0ad8b92 100644
--- a/public/map.json
+++ b/public/map.json
@@ -37602,6 +37602,23 @@
"rotation": [0, 0, 0],
"scale": [1, 1, 1],
"children": [
+ {
+ "name": "ebike",
+ "type": "Object3D",
+ "role": "group",
+ "position": [0, 0, 0],
+ "rotation": [0, 0, 0],
+ "scale": [1, 1, 1],
+ "children": [
+ {
+ "name": "ebike",
+ "type": "Object3D",
+ "position": [42.2399, 4.5484, 34.6468],
+ "rotation": [0, 0, 0],
+ "scale": [1, 1, 1]
+ }
+ ]
+ },
{
"name": "zone1_residence",
"type": "Object3D",
diff --git a/scripts/transformMap.cjs b/scripts/transformMap.cjs
index 798a91e..7e985a5 100644
--- a/scripts/transformMap.cjs
+++ b/scripts/transformMap.cjs
@@ -105,6 +105,16 @@ function addRenderable(parent, objectNode, meshNode) {
getOrCreateModelGroup(parent, renderable.name).children.push(renderable);
}
+function addStandaloneObject(rawData, parent, name) {
+ const node = rawData.find(
+ (rawNode) => rawNode?.type === "Object3D" && rawNode.name === name,
+ );
+
+ if (!node) return;
+
+ getOrCreateModelGroup(parent, name).children.push(cloneNode(node));
+}
+
function addObjectsByRange(rawData, parent, start, end, allowedNames) {
let currentObject = null;
@@ -279,6 +289,7 @@ function transformMap() {
agriculture.children.push(champs, ferme);
addObjectsByRange(rawData, direction, 6, 12, DIRECTION_MESH_NAMES);
+ addStandaloneObject(rawData, residence, "ebike");
createResidenceZones(rawData, residence);
addObjectsByRange(rawData, energie, 61, 96, new Set(["pyloneelectrique"]));
addObjectsByRange(rawData, vegetation, 98, 829, VEGETATION_MESH_NAMES);
diff --git a/src/data/gameplay/repairMissionAnchors.ts b/src/data/gameplay/repairMissionAnchors.ts
new file mode 100644
index 0000000..3adc442
--- /dev/null
+++ b/src/data/gameplay/repairMissionAnchors.ts
@@ -0,0 +1,5 @@
+import type { Vector3Tuple } from "@/types/three/three";
+
+export const EBIKE_REPAIR_POSITION = [
+ 42.2399, 4.5484, 34.6468,
+] as const satisfies Vector3Tuple;
diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts
index 3a9a0f1..bbff26e 100644
--- a/src/managers/stores/useGameStore.ts
+++ b/src/managers/stores/useGameStore.ts
@@ -124,7 +124,7 @@ function completeIntroState(state: GameState): GameStateUpdate {
},
bike: {
...state.bike,
- currentStep: "waiting",
+ currentStep: "locked",
},
};
}
diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx
index 9b85441..7b498f0 100644
--- a/src/world/GameMap.tsx
+++ b/src/world/GameMap.tsx
@@ -19,6 +19,7 @@ import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
+import { useGameStore } from "@/managers/stores/useGameStore";
import { GameMapCollision } from "@/world/GameMapCollision";
import { CloudSystem } from "@/world/clouds/CloudSystem";
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
@@ -302,8 +303,12 @@ function MapNodeInstance({
node: MapNode;
modelUrl: string | null;
onSettled: () => void;
-}): React.JSX.Element {
+}): React.JSX.Element | null {
const isGeneratedModel = isGeneratedMapModelName(node.name);
+ const mainState = useGameStore((state) => state.mainState);
+ const bikeStep = useGameStore((state) => state.bike.currentStep);
+ const hideEbikeMapModel =
+ node.name === "ebike" && mainState === "bike" && bikeStep !== "locked";
useEffect(() => {
if (modelUrl !== null || isGeneratedModel) return;
@@ -311,6 +316,16 @@ function MapNodeInstance({
onSettled();
}, [isGeneratedModel, modelUrl, onSettled]);
+ useEffect(() => {
+ if (!hideEbikeMapModel) return;
+
+ onSettled();
+ }, [hideEbikeMapModel, onSettled]);
+
+ if (hideEbikeMapModel) {
+ return null;
+ }
+
if (isGeneratedModel) {
return (
}>
diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx
index e18be89..6b67d73 100644
--- a/src/world/GameStageContent.tsx
+++ b/src/world/GameStageContent.tsx
@@ -1,4 +1,6 @@
+import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { RepairGame } from "@/components/three/gameplay/RepairGame";
+import { EBIKE_REPAIR_POSITION } from "@/data/gameplay/repairMissionAnchors";
import { useGameStore } from "@/managers/stores/useGameStore";
import type { RepairMissionId } from "@/types/gameplay/repairMission";
import type { Vector3Tuple } from "@/types/three/three";
@@ -17,7 +19,7 @@ interface GameRepairZone {
const GAME_REPAIR_ZONES = [
{
mission: "bike",
- position: [8, 0, -6],
+ position: EBIKE_REPAIR_POSITION,
},
{
mission: "pylone",
@@ -48,6 +50,31 @@ function StageAnchor({
);
}
+function EbikeMissionTrigger(): React.JSX.Element | null {
+ const mainState = useGameStore((state) => state.mainState);
+ const bikeStep = useGameStore((state) => state.bike.currentStep);
+ const setMissionStep = useGameStore((state) => state.setMissionStep);
+
+ if (mainState !== "bike" || bikeStep !== "locked") return null;
+
+ return (
+
+ setMissionStep("bike", "waiting")}
+ >
+
+
+
+
+
+
+ );
+}
+
export function GameStageContent(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState);
@@ -63,6 +90,7 @@ export function GameStageContent(): React.JSX.Element {
position={zone.position}
/>
))}
+
{mainState === "outro" ? (
) : null}