update: require replacement placement before repair completion

This commit is contained in:
Tom Boullay
2026-05-08 01:45:00 +01:00
parent 7bbcf4359e
commit d4f215a948
6 changed files with 59 additions and 19 deletions
@@ -1,7 +1,16 @@
import { useCallback, useState } from "react";
import * as THREE from "three";
import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
import type { Vector3Tuple } from "@/types/three/three";
const INSTALL_TARGET_POSITION: Vector3Tuple = [0, 0.8, 0];
const INSTALL_TARGET_VECTOR = new THREE.Vector3(...INSTALL_TARGET_POSITION);
const REPLACEMENT_START_POSITION: Vector3Tuple = [0, 1.35, 1.8];
const REPAIR_INSTALL_RADIUS = 1.1;
interface RepairRepairingStepProps {
config: RepairMissionConfig;
@@ -12,41 +21,68 @@ export function RepairRepairingStep({
config,
onRepair,
}: RepairRepairingStepProps): React.JSX.Element {
const [isReplacementPlaced, setIsReplacementPlaced] = useState(false);
const replacementPart = config.replacementParts[0];
const replacementModelPath = replacementPart?.modelPath ?? config.modelPath;
const replacementLabel = replacementPart?.label ?? config.label;
const installColor = isReplacementPlaced ? "#22c55e" : "#f97316";
const installFillColor = isReplacementPlaced ? "#86efac" : "#fed7aa";
const handleReplacementPosition = useCallback((position: THREE.Vector3) => {
const isPlaced =
position.distanceTo(INSTALL_TARGET_VECTOR) <= REPAIR_INSTALL_RADIUS;
setIsReplacementPlaced((current) =>
current === isPlaced ? current : isPlaced,
);
}, []);
return (
<group>
<TriggerObject
position={[0, 0.8, 0]}
position={INSTALL_TARGET_POSITION}
colliders="ball"
label={`Installer ${replacementLabel}`}
onTrigger={onRepair}
label={
isReplacementPlaced
? `Installer ${replacementLabel}`
: `Approcher ${replacementLabel}`
}
onTrigger={() => {
if (!isReplacementPlaced) return;
onRepair();
}}
>
<mesh>
<torusGeometry args={[0.95, 0.045, 12, 96]} />
<meshBasicMaterial color="#22c55e" transparent opacity={0.85} />
<meshBasicMaterial color={installColor} transparent opacity={0.85} />
</mesh>
<mesh position={[0, 0.02, 0]} rotation={[Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.15, 0.9, 96]} />
<meshBasicMaterial color="#86efac" transparent opacity={0.35} />
<meshBasicMaterial
color={installFillColor}
transparent
opacity={0.35}
/>
</mesh>
</TriggerObject>
<group
<GrabbableObject
position={[
config.case.position[0],
config.case.position[1] + 1.2,
config.case.position[2],
config.case.position[0] + REPLACEMENT_START_POSITION[0],
config.case.position[1] + REPLACEMENT_START_POSITION[1],
config.case.position[2] + REPLACEMENT_START_POSITION[2],
]}
colliders="ball"
handControlled
label={`Prendre ${replacementLabel}`}
onPositionChange={handleReplacementPosition}
>
<RepairObjectModel
label={replacementLabel}
modelPath={replacementModelPath}
scale={0.35}
/>
</group>
</GrabbableObject>
<RepairPromptVideo src={config.interactUiPath} position={[0, 2.3, 0]} />
</group>
@@ -36,6 +36,7 @@ interface GrabbableObjectProps {
colliders?: ColliderShape;
label?: string;
handControlled?: boolean;
onPositionChange?: (position: THREE.Vector3) => void;
}
const grabDebugParams = {
@@ -123,6 +124,7 @@ export function GrabbableObject({
colliders = GRAB_DEFAULT_COLLIDERS,
label = GRAB_DEFAULT_LABEL,
handControlled = false,
onPositionChange,
}: GrabbableObjectProps): React.JSX.Element {
const camera = useThree((state) => state.camera);
const { hands } = useHandTrackingSnapshot();
@@ -172,6 +174,7 @@ export function GrabbableObject({
const t = rbRef.current.translation();
_currentPos.set(t.x, t.y, t.z);
onPositionChange?.(_currentPos);
if (fistHand) {
const handCenter = getHandCenterPoint(fistHand);
+2 -2
View File
@@ -406,7 +406,7 @@ Overlays actuels :
## Prochaines étapes
La prochaine étape naturelle est de remplacer le trigger simple d'installation par des interactions de réparation plus profondes, comme choisir des pièces dans la mallette, les saisir et valider le bon remplacement sur un module cassé.
La prochaine étape naturelle est d'étendre l'interaction de réparation avec une sélection de pièces depuis la mallette et une validation plus stricte du bon remplacement pour chaque module cassé.
`;
export const featuresFr = `# Fonctionnalités implémentées
@@ -442,7 +442,7 @@ Ce document liste les fonctionnalités présentes dans le code actuel.
- \`RepairGame\` de production réutilisable monté pour les états de mission \`bike\`, \`pylone\` et \`ferme\`
- Configuration de mission partagée via \`src/data/gameplay/repairMissions.ts\`
- Flow repair-game avec \`waiting -> inspected -> fragmented -> scanning -> repairing -> done\`, prompts \`.webm\`, apparition/ouverture de la mallette, touche \`E\`, hold deux poings, transition de modèle explosé, visuels de scan, pièce de remplacement et trigger d'installation
- Flow repair-game avec \`waiting -> inspected -> fragmented -> scanning -> repairing -> done\`, prompts \`.webm\`, apparition/ouverture de la mallette, touche \`E\`, hold deux poings, transition de modèle explosé, visuels de scan, placement d'une pièce de remplacement grabbable et validation d'installation
## Audio