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. */} + /> ); }