connect repair gameplay to zustand progression

This commit is contained in:
Tom Boullay
2026-04-30 16:25:54 +02:00
parent 1625895708
commit bb08054722
8 changed files with 143 additions and 29 deletions
Binary file not shown.
Binary file not shown.
@@ -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 <RepairCaseFallback />;
}
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 (
<TriggerObject
position={position}
colliders="cuboid"
label={open ? "Fermer la mallette" : "Ouvrir la mallette"}
label={open ? "Mallette inspectée" : "Inspecter la mallette"}
onTrigger={() => {
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();
}}
>
<RepairCaseModel
modelPath={REPAIR_CASE_MODEL_PATH}
open={open}
position={[0, -0.45, 0]}
scale={1.5}
/>
<RepairCaseErrorBoundary>
<RepairCaseModel
modelPath={REPAIR_CASE_MODEL_PATH}
open={open}
position={[0, -0.45, 0]}
scale={1.5}
/>
</RepairCaseErrorBoundary>
</TriggerObject>
);
}
function RepairCaseFallback(): React.JSX.Element {
return (
<group position={[0, -0.25, 0]}>
<mesh castShadow receiveShadow>
<boxGeometry args={[1.5, 0.5, 1]} />
<meshStandardMaterial color="#2563eb" roughness={0.55} />
</mesh>
<mesh position={[0, 0.35, -0.25]} castShadow receiveShadow>
<boxGeometry args={[1.5, 0.12, 0.65]} />
<meshStandardMaterial color="#1d4ed8" roughness={0.55} />
</mesh>
</group>
);
}
@@ -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 (
<group>
@@ -62,7 +99,7 @@ export function RepairGameZone(): React.JSX.Element {
<RepairCaseObject
position={REPAIR_GAME_ZONE_ORIGIN}
open={caseOpen}
onToggle={() => 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}
/>
))}
</group>
@@ -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<ModelCatalogItem | null>(
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 (
<group>
@@ -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;
}
+2 -2
View File
@@ -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 {
<RepairGameZone />
</Physics>
{/* Temporary: re-enable when Git LFS downloads are available again.
<AnimatedModel
modelPath="/models/elec/model.gltf"
defaultAnimation="Idle"
position={[0, 0, -5]}
scale={1}
/> */}
/>
</>
);
}