update: validate correct repair replacement part

This commit is contained in:
Tom Boullay
2026-05-08 01:47:07 +01:00
parent d4f215a948
commit 7ee842c535
6 changed files with 166 additions and 51 deletions
@@ -4,12 +4,19 @@ 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 {
RepairMissionConfig,
RepairMissionPartConfig,
} 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 REPLACEMENT_START_OFFSETS: Vector3Tuple[] = [
[-0.9, 1.35, 1.8],
[0, 1.35, 2.15],
[0.9, 1.35, 1.8],
];
const REPAIR_INSTALL_RADIUS = 1.1;
interface RepairRepairingStepProps {
@@ -21,20 +28,45 @@ 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 [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
{},
);
const replacementParts = getReplacementParts(config);
const requiredReplacementPart = replacementParts.find(
(part) => part.id === config.requiredReplacementPartId,
);
const requiredReplacementLabel =
requiredReplacementPart?.label ?? config.label;
const hasCorrectPartPlaced = Boolean(
placedPartIds[config.requiredReplacementPartId],
);
const hasWrongPartPlaced = replacementParts.some(
(part) =>
part.id !== config.requiredReplacementPartId && placedPartIds[part.id],
);
const installColor = hasCorrectPartPlaced
? "#22c55e"
: hasWrongPartPlaced
? "#ef4444"
: "#f97316";
const installFillColor = hasCorrectPartPlaced
? "#86efac"
: hasWrongPartPlaced
? "#fecaca"
: "#fed7aa";
const handleReplacementPosition = useCallback((position: THREE.Vector3) => {
const isPlaced =
position.distanceTo(INSTALL_TARGET_VECTOR) <= REPAIR_INSTALL_RADIUS;
setIsReplacementPlaced((current) =>
current === isPlaced ? current : isPlaced,
);
}, []);
const handleReplacementPosition = useCallback(
(partId: string, position: THREE.Vector3) => {
const isPlaced =
position.distanceTo(INSTALL_TARGET_VECTOR) <= REPAIR_INSTALL_RADIUS;
setPlacedPartIds((current) => {
if (current[partId] === isPlaced) return current;
return { ...current, [partId]: isPlaced };
});
},
[],
);
return (
<group>
@@ -42,12 +74,14 @@ export function RepairRepairingStep({
position={INSTALL_TARGET_POSITION}
colliders="ball"
label={
isReplacementPlaced
? `Installer ${replacementLabel}`
: `Approcher ${replacementLabel}`
hasCorrectPartPlaced
? `Installer ${requiredReplacementLabel}`
: hasWrongPartPlaced
? `Mauvaise piece`
: `Approcher ${requiredReplacementLabel}`
}
onTrigger={() => {
if (!isReplacementPlaced) return;
if (!hasCorrectPartPlaced) return;
onRepair();
}}
@@ -66,25 +100,50 @@ export function RepairRepairingStep({
</mesh>
</TriggerObject>
<GrabbableObject
position={[
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}
/>
</GrabbableObject>
{replacementParts.map((part, index) => {
const offset =
REPLACEMENT_START_OFFSETS[index % REPLACEMENT_START_OFFSETS.length] ??
REPLACEMENT_START_OFFSETS[0]!;
return (
<GrabbableObject
key={part.id}
position={[
config.case.position[0] + offset[0],
config.case.position[1] + offset[1],
config.case.position[2] + offset[2],
]}
colliders="ball"
handControlled
label={`Prendre ${part.label}`}
onPositionChange={(position) => {
handleReplacementPosition(part.id, position);
}}
>
<RepairObjectModel
label={part.label}
modelPath={part.modelPath ?? config.modelPath}
scale={0.28}
/>
</GrabbableObject>
);
})}
<RepairPromptVideo src={config.interactUiPath} position={[0, 2.3, 0]} />
</group>
);
}
function getReplacementParts(
config: RepairMissionConfig,
): readonly RepairMissionPartConfig[] {
if (config.replacementParts.length > 0) return config.replacementParts;
return [
{
id: config.requiredReplacementPartId,
label: config.label,
modelPath: config.modelPath,
},
];
}
+2 -2
View File
@@ -406,7 +406,7 @@ Overlays actuels :
## Prochaines étapes
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é.
La prochaine étape naturelle est de déplacer la validation de réparation depuis cette interaction locale vers des données de mission plus riches quand chaque mission aura des nodes de modules cassés et des assets de remplacement dédiés.
`;
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, placement d'une pièce de remplacement grabbable et validation 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, plusieurs choix de pièces grabbables et validation de la bonne pièce
## Audio
+61 -5
View File
@@ -23,6 +23,7 @@ export interface RepairMissionConfig {
interactUiPath: string;
brokenUiPath: string;
case: RepairMissionCaseConfig;
requiredReplacementPartId: string;
brokenParts: readonly RepairMissionPartConfig[];
replacementParts: readonly RepairMissionPartConfig[];
}
@@ -47,6 +48,7 @@ export const REPAIR_MISSIONS = {
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
requiredReplacementPartId: "bike-cooling-core-replacement",
brokenParts: [
{
id: "bike-cooling-core",
@@ -59,6 +61,16 @@ export const REPAIR_MISSIONS = {
label: "Replacement cooling core",
modelPath: "/models/refroidisseur/model.gltf",
},
{
id: "bike-radio-decoy",
label: "Radio module",
modelPath: "/models/talkie/model.gltf",
},
{
id: "bike-glove-decoy",
label: "Insulation glove",
modelPath: "/models/gant/model.gltf",
},
],
},
pylone: {
@@ -70,19 +82,63 @@ export const REPAIR_MISSIONS = {
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
brokenParts: [],
replacementParts: [],
requiredReplacementPartId: "pylone-grid-relay-replacement",
brokenParts: [
{
id: "pylone-grid-relay",
label: "Grid relay",
},
],
replacementParts: [
{
id: "pylone-grid-relay-replacement",
label: "Replacement grid relay",
modelPath: "/models/pylone/model.gltf",
},
{
id: "pylone-stone-decoy",
label: "Stone counterweight",
modelPath: "/models/galet/model.gltf",
},
{
id: "pylone-cooling-decoy",
label: "Cooling core",
modelPath: "/models/refroidisseur/model.gltf",
},
],
},
ferme: {
id: "ferme",
label: "Vertical farm",
description: "Genreric description",
description: "Generic description",
modelPath: "/models/fermeverticale/model.gltf",
stageUiPath: "/assets/UI/laferme.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
case: DEFAULT_REPAIR_CASE,
brokenParts: [],
replacementParts: [],
requiredReplacementPartId: "ferme-irrigation-pump-replacement",
brokenParts: [
{
id: "ferme-irrigation-pump",
label: "Irrigation pump",
},
],
replacementParts: [
{
id: "ferme-irrigation-pump-replacement",
label: "Replacement irrigation pump",
modelPath: "/models/fermeverticale/model.gltf",
},
{
id: "ferme-tree-decoy",
label: "Tree sensor",
modelPath: "/models/sapin/model.gltf",
},
{
id: "ferme-radio-decoy",
label: "Radio module",
modelPath: "/models/talkie/model.gltf",
},
],
},
} satisfies Record<RepairMissionId, RepairMissionConfig>;