diff --git a/public/assets/PDF/Gilbert Le Fermier - Doublage Altera.pdf b/public/assets/PDF/Gilbert Le Fermier - Doublage Altera.pdf
new file mode 100644
index 0000000..a1a1ccc
Binary files /dev/null and b/public/assets/PDF/Gilbert Le Fermier - Doublage Altera.pdf differ
diff --git a/public/assets/PDF/Le Gérant - Doublage Altera.pdf b/public/assets/PDF/Le Gérant - Doublage Altera.pdf
new file mode 100644
index 0000000..9ec89d7
Binary files /dev/null and b/public/assets/PDF/Le Gérant - Doublage Altera.pdf differ
diff --git a/public/assets/PDF/Leonie Electricienne - Doublage Altera.pdf b/public/assets/PDF/Leonie Electricienne - Doublage Altera.pdf
new file mode 100644
index 0000000..226a9bd
Binary files /dev/null and b/public/assets/PDF/Leonie Electricienne - Doublage Altera.pdf differ
diff --git a/public/assets/world/dashboard.webm b/public/assets/world/dashboard.webm
new file mode 100644
index 0000000..240a1e9
--- /dev/null
+++ b/public/assets/world/dashboard.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1a66ac7a200090365d7cb2623ebf9f1dedc6e052628c15b1701086786b3772bd
+size 485140
diff --git a/src/components/three/gameplay/RepairCaseObject.tsx b/src/components/three/gameplay/RepairCaseObject.tsx
index 1371f3c..1b4da23 100644
--- a/src/components/three/gameplay/RepairCaseObject.tsx
+++ b/src/components/three/gameplay/RepairCaseObject.tsx
@@ -1,42 +1,93 @@
+import type { ReactNode } from "react";
+import { Component } from "react";
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
import { RepairCaseModel } from "@/components/three/gameplay/RepairCaseModel";
import {
- REPAIR_CASE_CLOSE_SOUND_PATH,
REPAIR_CASE_MODEL_PATH,
REPAIR_CASE_OPEN_SOUND_PATH,
} from "@/data/gameplay/repairCaseConfig";
import { AudioManager } from "@/managers/AudioManager";
import type { Vector3Tuple } from "@/types/three/three";
+interface RepairCaseErrorBoundaryProps {
+ children: ReactNode;
+}
+
+interface RepairCaseErrorBoundaryState {
+ hasError: boolean;
+}
+
+class RepairCaseErrorBoundary extends Component<
+ RepairCaseErrorBoundaryProps,
+ RepairCaseErrorBoundaryState
+> {
+ constructor(props: RepairCaseErrorBoundaryProps) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(): RepairCaseErrorBoundaryState {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error): void {
+ console.warn("Failed to load repair case model", error);
+ }
+
+ render(): ReactNode {
+ if (this.state.hasError) {
+ return ;
+ }
+
+ return this.props.children;
+ }
+}
+
interface RepairCaseObjectProps {
position: Vector3Tuple;
open: boolean;
- onToggle: () => void;
+ onInspect: () => void;
}
export function RepairCaseObject({
position,
open,
- onToggle,
+ onInspect,
}: RepairCaseObjectProps): React.JSX.Element {
return (
{
- AudioManager.getInstance().playSound(
- open ? REPAIR_CASE_CLOSE_SOUND_PATH : REPAIR_CASE_OPEN_SOUND_PATH,
- );
- onToggle();
+ if (open) return;
+ AudioManager.getInstance().playSound(REPAIR_CASE_OPEN_SOUND_PATH);
+ onInspect();
}}
>
-
+
+
+
);
}
+
+function RepairCaseFallback(): React.JSX.Element {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/three/gameplay/RepairGameZone.tsx b/src/components/three/gameplay/RepairGameZone.tsx
index 31dfeb5..cf0aaed 100644
--- a/src/components/three/gameplay/RepairGameZone.tsx
+++ b/src/components/three/gameplay/RepairGameZone.tsx
@@ -1,4 +1,3 @@
-import { useState } from "react";
import { Text } from "@react-three/drei";
import { RepairCaseObject } from "@/components/three/gameplay/RepairCaseObject";
import { RepairModuleSlot } from "@/components/three/gameplay/RepairModuleSlot";
@@ -8,9 +7,47 @@ import {
REPAIR_GAME_ZONE_ORIGIN,
REPAIR_GAME_ZONE_RADIUS,
} from "@/data/gameplay/repairGameConfig";
+import { useGameStore } from "@/managers/stores/useGameStore";
+
+const CASE_CLOSED_STEPS = new Set(["locked", "waiting"]);
export function RepairGameZone(): React.JSX.Element {
- const [caseOpen, setCaseOpen] = useState(false);
+ const mainState = useGameStore((state) => state.mainState);
+ const bikeStep = useGameStore((state) => state.bike.currentStep);
+ const setMainState = useGameStore((state) => state.setMainState);
+ const setBikeState = useGameStore((state) => state.setBikeState);
+ const caseOpen = !CASE_CLOSED_STEPS.has(bikeStep);
+ const slotsDisabled = !caseOpen;
+
+ const inspectRepairCase = (): void => {
+ if (mainState !== "bike") {
+ setMainState("bike");
+ }
+
+ if (CASE_CLOSED_STEPS.has(bikeStep)) {
+ setBikeState({ currentStep: "inspected" });
+ }
+ };
+
+ const markModelSelected = (): void => {
+ if (mainState !== "bike") {
+ setMainState("bike");
+ }
+
+ if (bikeStep === "inspected") {
+ setBikeState({ currentStep: "fragmented" });
+ }
+ };
+
+ const markModuleSplit = (): void => {
+ if (mainState !== "bike") {
+ setMainState("bike");
+ }
+
+ if (bikeStep === "fragmented") {
+ setBikeState({ currentStep: "scanning" });
+ }
+ };
return (
@@ -62,7 +99,7 @@ export function RepairGameZone(): React.JSX.Element {
setCaseOpen((value) => !value)}
+ onInspect={inspectRepairCase}
/>
{REPAIR_GAME_MODULE_SLOTS.map((slot) => (
@@ -74,6 +111,9 @@ export function RepairGameZone(): React.JSX.Element {
REPAIR_GAME_ZONE_ORIGIN[1] + slot.offset[1],
REPAIR_GAME_ZONE_ORIGIN[2] + slot.offset[2],
]}
+ disabled={slotsDisabled}
+ onModelSelected={markModelSelected}
+ onSplit={markModuleSplit}
/>
))}
diff --git a/src/components/three/gameplay/RepairModuleSlot.tsx b/src/components/three/gameplay/RepairModuleSlot.tsx
index 630fdca..6ffed47 100644
--- a/src/components/three/gameplay/RepairModuleSlot.tsx
+++ b/src/components/three/gameplay/RepairModuleSlot.tsx
@@ -10,26 +10,38 @@ import type { Vector3Tuple } from "@/types/three/three";
interface RepairModuleSlotProps {
position: Vector3Tuple;
label: string;
+ disabled?: boolean;
+ onModelSelected?: () => void;
+ onSplit?: () => void;
}
export function RepairModuleSlot({
position,
label,
+ disabled = false,
+ onModelSelected,
+ onSplit,
}: RepairModuleSlotProps): React.JSX.Element {
const [selectedModel, setSelectedModel] = useState(
null,
);
const [split, setSplit] = useState(false);
- const handleSelect = useCallback((model: ModelCatalogItem) => {
- setSelectedModel(model);
- setSplit(false);
- }, []);
+ const handleSelect = useCallback(
+ (model: ModelCatalogItem) => {
+ setSelectedModel(model);
+ setSplit(false);
+ onModelSelected?.();
+ },
+ [onModelSelected],
+ );
const selection = useModelSelection(REPAIR_GAME_MODEL_CATALOG, handleSelect);
- const triggerLabel = selectedModel
- ? split
- ? `Réassembler ${label}`
- : `Démonter ${label}`
- : `Choisir ${label}`;
+ const triggerLabel = disabled
+ ? "Ouvrir la mallette d'abord"
+ : selectedModel
+ ? split
+ ? `Réassembler ${label}`
+ : `Démonter ${label}`
+ : `Choisir ${label}`;
return (
@@ -38,8 +50,16 @@ export function RepairModuleSlot({
colliders="cuboid"
label={triggerLabel}
onTrigger={() => {
+ if (disabled) return;
+
if (selectedModel) {
- setSplit((value) => !value);
+ setSplit((value) => {
+ const nextSplit = !value;
+ if (nextSplit) {
+ onSplit?.();
+ }
+ return nextSplit;
+ });
return;
}
diff --git a/src/world/debug/TestMap.tsx b/src/world/debug/TestMap.tsx
index 9401b2e..3b0af49 100644
--- a/src/world/debug/TestMap.tsx
+++ b/src/world/debug/TestMap.tsx
@@ -3,6 +3,7 @@ import * as THREE from "three";
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
import { RepairGameZone } from "@/components/three/gameplay/RepairGameZone";
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
+import { AnimatedModel } from "@/components/three/models/AnimatedModel";
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
import {
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
@@ -88,13 +89,12 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
- {/* Temporary: re-enable when Git LFS downloads are available again.
*/}
+ />
>
);
}