connect repair gameplay to zustand progression
This commit is contained in:
Binary file not shown.
Binary file not shown.
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 { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||||
import { RepairCaseModel } from "@/components/three/gameplay/RepairCaseModel";
|
import { RepairCaseModel } from "@/components/three/gameplay/RepairCaseModel";
|
||||||
import {
|
import {
|
||||||
REPAIR_CASE_CLOSE_SOUND_PATH,
|
|
||||||
REPAIR_CASE_MODEL_PATH,
|
REPAIR_CASE_MODEL_PATH,
|
||||||
REPAIR_CASE_OPEN_SOUND_PATH,
|
REPAIR_CASE_OPEN_SOUND_PATH,
|
||||||
} from "@/data/gameplay/repairCaseConfig";
|
} from "@/data/gameplay/repairCaseConfig";
|
||||||
import { AudioManager } from "@/managers/AudioManager";
|
import { AudioManager } from "@/managers/AudioManager";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
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 {
|
interface RepairCaseObjectProps {
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onToggle: () => void;
|
onInspect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepairCaseObject({
|
export function RepairCaseObject({
|
||||||
position,
|
position,
|
||||||
open,
|
open,
|
||||||
onToggle,
|
onInspect,
|
||||||
}: RepairCaseObjectProps): React.JSX.Element {
|
}: RepairCaseObjectProps): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<TriggerObject
|
<TriggerObject
|
||||||
position={position}
|
position={position}
|
||||||
colliders="cuboid"
|
colliders="cuboid"
|
||||||
label={open ? "Fermer la mallette" : "Ouvrir la mallette"}
|
label={open ? "Mallette inspectée" : "Inspecter la mallette"}
|
||||||
onTrigger={() => {
|
onTrigger={() => {
|
||||||
AudioManager.getInstance().playSound(
|
if (open) return;
|
||||||
open ? REPAIR_CASE_CLOSE_SOUND_PATH : REPAIR_CASE_OPEN_SOUND_PATH,
|
AudioManager.getInstance().playSound(REPAIR_CASE_OPEN_SOUND_PATH);
|
||||||
);
|
onInspect();
|
||||||
onToggle();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RepairCaseModel
|
<RepairCaseErrorBoundary>
|
||||||
modelPath={REPAIR_CASE_MODEL_PATH}
|
<RepairCaseModel
|
||||||
open={open}
|
modelPath={REPAIR_CASE_MODEL_PATH}
|
||||||
position={[0, -0.45, 0]}
|
open={open}
|
||||||
scale={1.5}
|
position={[0, -0.45, 0]}
|
||||||
/>
|
scale={1.5}
|
||||||
|
/>
|
||||||
|
</RepairCaseErrorBoundary>
|
||||||
</TriggerObject>
|
</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 { Text } from "@react-three/drei";
|
||||||
import { RepairCaseObject } from "@/components/three/gameplay/RepairCaseObject";
|
import { RepairCaseObject } from "@/components/three/gameplay/RepairCaseObject";
|
||||||
import { RepairModuleSlot } from "@/components/three/gameplay/RepairModuleSlot";
|
import { RepairModuleSlot } from "@/components/three/gameplay/RepairModuleSlot";
|
||||||
@@ -8,9 +7,47 @@ import {
|
|||||||
REPAIR_GAME_ZONE_ORIGIN,
|
REPAIR_GAME_ZONE_ORIGIN,
|
||||||
REPAIR_GAME_ZONE_RADIUS,
|
REPAIR_GAME_ZONE_RADIUS,
|
||||||
} from "@/data/gameplay/repairGameConfig";
|
} from "@/data/gameplay/repairGameConfig";
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
|
const CASE_CLOSED_STEPS = new Set(["locked", "waiting"]);
|
||||||
|
|
||||||
export function RepairGameZone(): React.JSX.Element {
|
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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -62,7 +99,7 @@ export function RepairGameZone(): React.JSX.Element {
|
|||||||
<RepairCaseObject
|
<RepairCaseObject
|
||||||
position={REPAIR_GAME_ZONE_ORIGIN}
|
position={REPAIR_GAME_ZONE_ORIGIN}
|
||||||
open={caseOpen}
|
open={caseOpen}
|
||||||
onToggle={() => setCaseOpen((value) => !value)}
|
onInspect={inspectRepairCase}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{REPAIR_GAME_MODULE_SLOTS.map((slot) => (
|
{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[1] + slot.offset[1],
|
||||||
REPAIR_GAME_ZONE_ORIGIN[2] + slot.offset[2],
|
REPAIR_GAME_ZONE_ORIGIN[2] + slot.offset[2],
|
||||||
]}
|
]}
|
||||||
|
disabled={slotsDisabled}
|
||||||
|
onModelSelected={markModelSelected}
|
||||||
|
onSplit={markModuleSplit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -10,26 +10,38 @@ import type { Vector3Tuple } from "@/types/three/three";
|
|||||||
interface RepairModuleSlotProps {
|
interface RepairModuleSlotProps {
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onModelSelected?: () => void;
|
||||||
|
onSplit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepairModuleSlot({
|
export function RepairModuleSlot({
|
||||||
position,
|
position,
|
||||||
label,
|
label,
|
||||||
|
disabled = false,
|
||||||
|
onModelSelected,
|
||||||
|
onSplit,
|
||||||
}: RepairModuleSlotProps): React.JSX.Element {
|
}: RepairModuleSlotProps): React.JSX.Element {
|
||||||
const [selectedModel, setSelectedModel] = useState<ModelCatalogItem | null>(
|
const [selectedModel, setSelectedModel] = useState<ModelCatalogItem | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [split, setSplit] = useState(false);
|
const [split, setSplit] = useState(false);
|
||||||
const handleSelect = useCallback((model: ModelCatalogItem) => {
|
const handleSelect = useCallback(
|
||||||
setSelectedModel(model);
|
(model: ModelCatalogItem) => {
|
||||||
setSplit(false);
|
setSelectedModel(model);
|
||||||
}, []);
|
setSplit(false);
|
||||||
|
onModelSelected?.();
|
||||||
|
},
|
||||||
|
[onModelSelected],
|
||||||
|
);
|
||||||
const selection = useModelSelection(REPAIR_GAME_MODEL_CATALOG, handleSelect);
|
const selection = useModelSelection(REPAIR_GAME_MODEL_CATALOG, handleSelect);
|
||||||
const triggerLabel = selectedModel
|
const triggerLabel = disabled
|
||||||
? split
|
? "Ouvrir la mallette d'abord"
|
||||||
? `Réassembler ${label}`
|
: selectedModel
|
||||||
: `Démonter ${label}`
|
? split
|
||||||
: `Choisir ${label}`;
|
? `Réassembler ${label}`
|
||||||
|
: `Démonter ${label}`
|
||||||
|
: `Choisir ${label}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -38,8 +50,16 @@ export function RepairModuleSlot({
|
|||||||
colliders="cuboid"
|
colliders="cuboid"
|
||||||
label={triggerLabel}
|
label={triggerLabel}
|
||||||
onTrigger={() => {
|
onTrigger={() => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
setSplit((value) => !value);
|
setSplit((value) => {
|
||||||
|
const nextSplit = !value;
|
||||||
|
if (nextSplit) {
|
||||||
|
onSplit?.();
|
||||||
|
}
|
||||||
|
return nextSplit;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as THREE from "three";
|
|||||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||||
import { RepairGameZone } from "@/components/three/gameplay/RepairGameZone";
|
import { RepairGameZone } from "@/components/three/gameplay/RepairGameZone";
|
||||||
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
||||||
|
import { AnimatedModel } from "@/components/three/models/AnimatedModel";
|
||||||
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||||
import {
|
import {
|
||||||
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
||||||
@@ -88,13 +89,12 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
|||||||
<RepairGameZone />
|
<RepairGameZone />
|
||||||
</Physics>
|
</Physics>
|
||||||
|
|
||||||
{/* Temporary: re-enable when Git LFS downloads are available again.
|
|
||||||
<AnimatedModel
|
<AnimatedModel
|
||||||
modelPath="/models/elec/model.gltf"
|
modelPath="/models/elec/model.gltf"
|
||||||
defaultAnimation="Idle"
|
defaultAnimation="Idle"
|
||||||
position={[0, 0, -5]}
|
position={[0, 0, -5]}
|
||||||
scale={1}
|
scale={1}
|
||||||
/> */}
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user