From 118e5f3b4a793793b4cf116b7a0f6a8a49408824 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 01:09:42 +0100 Subject: [PATCH 01/45] update: add generic repair mission store helpers --- .agent/AGENT.md | 2 +- docs/technical/zustand.md | 12 +++ src/data/docs/docsTranslations.ts | 12 +++ src/managers/stores/useGameStore.ts | 123 ++++++++++++++++------------ 4 files changed, 95 insertions(+), 54 deletions(-) diff --git a/.agent/AGENT.md b/.agent/AGENT.md index aa8477b..deaf3c4 100644 --- a/.agent/AGENT.md +++ b/.agent/AGENT.md @@ -11,7 +11,7 @@ You are working on **La Fabrik**, an interactive 3D web experience built with Re ## Current Implementation - Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite -- No external global state library is used. +- Zustand is used for shared game progression state. - Current singleton-style services are limited to: - `InteractionManager` - `AudioManager` diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md index 80b6f5c..27e0f9a 100644 --- a/docs/technical/zustand.md +++ b/docs/technical/zustand.md @@ -114,6 +114,18 @@ setMainState("bike"); Direct setters are useful for debug panels, but production gameplay should prefer business actions such as `advanceGameState`, `completeBike`, or `completePylone`. +Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer the generic mission actions: + +```ts +const setMissionStep = useGameStore((state) => state.setMissionStep); +const completeMission = useGameStore((state) => state.completeMission); + +setMissionStep("bike", "inspected"); +completeMission("bike"); +``` + +This keeps reusable gameplay components such as repair flows from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`. + ## World Integration `src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content. diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts index 166cfc0..9eac4ed 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -321,6 +321,18 @@ setMainState("bike"); Les setters directs sont pratiques pour les panneaux debug, mais le gameplay de production devrait préférer les actions métier comme \`advanceGameState\`, \`completeBike\` ou \`completePylone\`. +Le gameplay de mission qui peut cibler \`bike\`, \`pylone\` ou \`ferme\` doit préférer les actions génériques de mission : + +\`\`\`ts +const setMissionStep = useGameStore((state) => state.setMissionStep); +const completeMission = useGameStore((state) => state.completeMission); + +setMissionStep("bike", "inspected"); +completeMission("bike"); +\`\`\` + +Cela évite aux composants gameplay réutilisables, comme les flows de réparation, de dupliquer des branches spécifiques à chaque mission avec \`setBikeState\`, \`setPyloneState\` et \`setFermeState\`. + ## Intégration avec le World \`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant. diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index c58bc89..d49865e 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; +export type RepairMissionId = "bike" | "pylone" | "ferme"; export type MissionStep = | "locked" | "waiting" @@ -46,10 +47,12 @@ interface GameActions { setPyloneState: (pylone: Partial) => void; setFermeState: (ferme: Partial) => void; setOutroState: (outro: Partial) => void; + setMissionStep: (mission: RepairMissionId, step: MissionStep) => void; completeIntro: () => void; completeBike: () => void; completePylone: () => void; completeFerme: () => void; + completeMission: (mission: RepairMissionId) => void; startOutro: () => void; advanceGameState: () => void; rewindGameState: () => void; @@ -59,6 +62,12 @@ interface GameActions { type GameStore = GameState & GameActions; type GameStateUpdate = Partial; +export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const; + +function isRepairMissionId(value: MainGameState): value is RepairMissionId { + return REPAIR_MISSION_IDS.includes(value as RepairMissionId); +} + function getNextMissionStep(step: MissionStep): MissionStep { switch (step) { case "locked": @@ -155,6 +164,56 @@ function completeFermeState(state: GameState): GameStateUpdate { }; } +function setMissionStepState( + state: GameState, + mission: RepairMissionId, + step: MissionStep, +): GameStateUpdate { + return { + [mission]: { + ...state[mission], + currentStep: step, + }, + }; +} + +function completeMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + switch (mission) { + case "bike": + return completeBikeState(state); + case "pylone": + return completePyloneState(state); + case "ferme": + return completeFermeState(state); + } +} + +function advanceRepairMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + const nextStep = getNextMissionStep(state[mission].currentStep); + if (nextStep === "done") { + return completeMissionState(state, mission); + } + + return setMissionStepState(state, mission, nextStep); +} + +function rewindRepairMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + return setMissionStepState( + state, + mission, + getPreviousMissionStep(state[mission].currentStep), + ); +} + function startOutroState(state: GameState): GameStateUpdate { return { mainState: "outro", @@ -208,10 +267,14 @@ export const useGameStore = create()((set) => ({ set((state) => ({ ferme: { ...state.ferme, ...ferme } })), setOutroState: (outro) => set((state) => ({ outro: { ...state.outro, ...outro } })), + setMissionStep: (mission, step) => + set((state) => setMissionStepState(state, mission, step)), completeIntro: () => set(completeIntroState), - completeBike: () => set(completeBikeState), - completePylone: () => set(completePyloneState), - completeFerme: () => set(completeFermeState), + completeBike: () => set((state) => completeMissionState(state, "bike")), + completePylone: () => set((state) => completeMissionState(state, "pylone")), + completeFerme: () => set((state) => completeMissionState(state, "ferme")), + completeMission: (mission) => + set((state) => completeMissionState(state, mission)), startOutro: () => set(startOutroState), advanceGameState: () => set((state) => { @@ -219,31 +282,8 @@ export const useGameStore = create()((set) => ({ return completeIntroState(state); } - if (state.mainState === "bike") { - const nextStep = getNextMissionStep(state.bike.currentStep); - if (nextStep === "done") { - return completeBikeState(state); - } - - return { bike: { ...state.bike, currentStep: nextStep } }; - } - - if (state.mainState === "pylone") { - const nextStep = getNextMissionStep(state.pylone.currentStep); - if (nextStep === "done") { - return completePyloneState(state); - } - - return { pylone: { ...state.pylone, currentStep: nextStep } }; - } - - if (state.mainState === "ferme") { - const nextStep = getNextMissionStep(state.ferme.currentStep); - if (nextStep === "done") { - return completeFermeState(state); - } - - return { ferme: { ...state.ferme, currentStep: nextStep } }; + if (isRepairMissionId(state.mainState)) { + return advanceRepairMissionState(state, state.mainState); } return startOutroState(state); @@ -254,31 +294,8 @@ export const useGameStore = create()((set) => ({ return { intro: { ...state.intro, hasCompleted: false } }; } - if (state.mainState === "bike") { - return { - bike: { - ...state.bike, - currentStep: getPreviousMissionStep(state.bike.currentStep), - }, - }; - } - - if (state.mainState === "pylone") { - return { - pylone: { - ...state.pylone, - currentStep: getPreviousMissionStep(state.pylone.currentStep), - }, - }; - } - - if (state.mainState === "ferme") { - return { - ferme: { - ...state.ferme, - currentStep: getPreviousMissionStep(state.ferme.currentStep), - }, - }; + if (isRepairMissionId(state.mainState)) { + return rewindRepairMissionState(state, state.mainState); } return { outro: { ...state.outro, hasStarted: false } }; -- 2.52.0 From 1dfbdd1d652f1a5d5f8a9d26aed787718f893e7f Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 01:14:30 +0100 Subject: [PATCH 02/45] add: repair mission config --- docs/user/main-feature.md | 9 ++- src/data/gameplay/repairMissions.ts | 88 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/data/gameplay/repairMissions.ts diff --git a/docs/user/main-feature.md b/docs/user/main-feature.md index 7133196..4325450 100644 --- a/docs/user/main-feature.md +++ b/docs/user/main-feature.md @@ -15,6 +15,8 @@ The current user flow is: 5. Watch the case open or close with sound feedback. 6. Interact with repair module slots to cycle/select repair models. +The production repair flow is now being moved toward reusable mission data for `bike`, `pylone`, and `ferme`. This lets the same future `RepairGame` component read one mission config instead of duplicating per-mission setup. + ## Why It Matters This feature validates the core repair fantasy before a full mission system exists. It tests whether repair objects, physical proximity, model selection, audio feedback, and exploded model visualization can work together in the 3D scene. @@ -38,6 +40,8 @@ Repair module slots are configured from static gameplay data. They render select - `src/data/gameplay/repairCaseConfig.ts` stores repair case model, sound, and animation constants. - `src/data/gameplay/repairGameConfig.ts` stores repair zone and slot positions. - `src/data/gameplay/repairGameModelCatalog.ts` stores selectable repair models. +- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`. +- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers. ## Debug Requirements @@ -74,7 +78,8 @@ python -m backend.main ## Current Limitations - It is mounted only in the debug physics scene. -- There is no mission progression system yet. -- There is no central `GameManager` or Zustand store in this branch. +- The production `RepairGame` component is not mounted in the main game scene yet. +- Mission progression exists in Zustand, but the full repair mission flow is still being integrated. +- There is no central `GameManager` in this branch. - Hand tracking is available as debug interaction input, not as final repair gameplay. - The repair-game content is configured statically in `src/data/gameplay/`. diff --git a/src/data/gameplay/repairMissions.ts b/src/data/gameplay/repairMissions.ts new file mode 100644 index 0000000..dd0135c --- /dev/null +++ b/src/data/gameplay/repairMissions.ts @@ -0,0 +1,88 @@ +import type { RepairMissionId } from "@/managers/stores/useGameStore"; +import type { Vector3Scale, Vector3Tuple } from "@/types/three/three"; + +export interface RepairMissionCaseConfig { + position: Vector3Tuple; + rotation: Vector3Tuple; + scale: Vector3Scale; +} + +export interface RepairMissionPartConfig { + id: string; + label: string; + nodeName?: string; + modelPath?: string; +} + +export interface RepairMissionConfig { + id: RepairMissionId; + label: string; + description: string; + modelPath: string; + stageUiPath: string; + interactUiPath: string; + brokenUiPath: string; + case: RepairMissionCaseConfig; + brokenParts: readonly RepairMissionPartConfig[]; + replacementParts: readonly RepairMissionPartConfig[]; +} + +const REPAIR_INTERACT_UI_PATH = "/assets/UI/interagir.webm"; +const REPAIR_BROKEN_UI_PATH = "/assets/UI/cassé.webm"; + +const DEFAULT_REPAIR_CASE = { + position: [0, 0.4, 1.8], + rotation: [0, 0, 0], + scale: 1.5, +} satisfies RepairMissionCaseConfig; + +export const REPAIR_MISSIONS = { + bike: { + id: "bike", + label: "E-bike", + description: + "Repair the damaged cooling module before relaunching the bike", + modelPath: "/models/refroidisseur/model.gltf", + stageUiPath: "/assets/UI/ebike.webm", + interactUiPath: REPAIR_INTERACT_UI_PATH, + brokenUiPath: REPAIR_BROKEN_UI_PATH, + case: DEFAULT_REPAIR_CASE, + brokenParts: [ + { + id: "bike-cooling-core", + label: "Cooling core", + }, + ], + replacementParts: [ + { + id: "bike-cooling-core-replacement", + label: "Replacement cooling core", + modelPath: "/models/refroidisseur/model.gltf", + }, + ], + }, + pylone: { + id: "pylone", + label: "Power pylon", + description: "Generic description", + modelPath: "/models/pylone/model.gltf", + stageUiPath: "/assets/UI/centrale.webm", + interactUiPath: REPAIR_INTERACT_UI_PATH, + brokenUiPath: REPAIR_BROKEN_UI_PATH, + case: DEFAULT_REPAIR_CASE, + brokenParts: [], + replacementParts: [], + }, + ferme: { + id: "ferme", + label: "Vertical farm", + description: "Genreric 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: [], + }, +} satisfies Record; -- 2.52.0 From f15d08de955f9b395d175b46c21c76f93b836ca2 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 01:17:35 +0100 Subject: [PATCH 03/45] add: physics in game scene --- docs/technical/architecture.md | 10 ++++++++++ docs/user/features.md | 2 ++ src/data/docs/docsTranslations.ts | 12 ++++++++++++ src/world/World.tsx | 5 ++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index f8fdbda..6b4c04f 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -15,10 +15,20 @@ This document describes the code that exists today in the repository. - either the map scene or the debug physics test scene - the player rig when the active camera mode is `player` - `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, and builds the collision octree. +- `src/world/GameStageContent.tsx` is wrapped in Rapier `Physics` in the production game scene so stage gameplay objects can use physics without moving the map or player to Rapier. - `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map. - `src/world/player/Player.tsx` mounts the camera and controller. - `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input. +## Physics Boundaries + +The project currently uses two collision layers with separate responsibilities: + +- `GameMap` builds an octree used by the player controller for map collision. +- `GameStageContent` is wrapped in Rapier `Physics` for gameplay objects such as repair triggers, cases, grabbables, and future mission-specific objects. + +Keep the player and map octree outside the Rapier provider until there is a deliberate migration plan. This avoids mixing player movement rules with object physics before the gameplay systems need it. + ## Interaction Model - `src/managers/InteractionManager.ts` is the current interaction state source. diff --git a/docs/user/features.md b/docs/user/features.md index 1a3a210..d6c0a02 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -7,6 +7,7 @@ This document lists features that are implemented in the current codebase. - Fullscreen React Three Fiber scene - Main map scene loaded from `public/map.json` and matching `public/models/{name}/model.glb` or `model.gltf` assets - Debug physics test scene selectable from the debug panel +- Rapier physics context available for production stage gameplay objects - Ambient and directional lighting - Environment background setup @@ -23,6 +24,7 @@ This document lists features that are implemented in the current codebase. - Focus detection by distance and raycast - Trigger interactions activated with `E` - Grab interactions activated with the primary mouse button +- Physics-backed gameplay objects can be mounted inside stage content without replacing player octree collision - Interaction prompt shown for trigger interactions ## Audio diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts index 9eac4ed..28d7829 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -98,10 +98,20 @@ Ce document décrit le code réellement présent aujourd'hui dans le dépôt. - soit la carte principale, soit la scène de test physique debug - le rig joueur quand le mode caméra actif est \`player\` - \`src/world/GameMap.tsx\` charge les modèles de carte disponibles et construit l'octree de collision. +- \`src/world/GameStageContent.tsx\` est enveloppé dans le contexte Rapier \`Physics\` dans la scène de jeu de production afin que les objets gameplay de stage puissent utiliser la physique sans migrer la carte ou le joueur vers Rapier. - \`src/world/debug/TestMap.tsx\` fournit une carte orientée debug pour les interactions et la physique. - \`src/world/player/Player.tsx\` monte la caméra et le contrôleur. - \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut et les inputs d'interaction. +## Frontières physiques + +Le projet utilise actuellement deux couches de collision avec des responsabilités séparées : + +- \`GameMap\` construit une octree utilisée par le contrôleur joueur pour les collisions avec la carte. +- \`GameStageContent\` est enveloppé dans Rapier \`Physics\` pour les objets gameplay comme les triggers de réparation, les mallettes, les objets saisissables et les futurs objets spécifiques aux missions. + +Le joueur et l'octree de carte doivent rester hors du provider Rapier tant qu'il n'existe pas de plan de migration volontaire. Cela évite de mélanger les règles de déplacement joueur avec la physique d'objets avant que les systèmes gameplay en aient besoin. + ## Modèle d'interaction - \`src/managers/InteractionManager.ts\` est la source d'état actuelle des interactions. @@ -392,6 +402,7 @@ Ce document liste les fonctionnalités présentes dans le code actuel. - Scène React Three Fiber plein écran - Carte principale chargée depuis \`public/models/{name}/model.glb\`, avec fallback vers \`model.gltf\` - Scène de test physique debug sélectionnable depuis le panneau debug +- Contexte physique Rapier disponible pour les objets gameplay de stage en production - Éclairage ambiant et directionnel - Configuration de l'environnement de fond @@ -408,6 +419,7 @@ Ce document liste les fonctionnalités présentes dans le code actuel. - Détection de focus par distance et raycast - Interactions trigger activées avec \`E\` - Interactions grab activées avec le bouton principal de la souris +- Les objets gameplay avec physique peuvent être montés dans le contenu de stage sans remplacer la collision octree du joueur - Prompt d'interaction affiché pour les interactions trigger ## Audio diff --git a/src/world/World.tsx b/src/world/World.tsx index 4737abb..27be858 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { Physics } from "@react-three/rapier"; import type { Octree } from "three/addons/math/Octree.js"; import { PLAYER_SPAWN_POSITION_GAME, @@ -43,7 +44,9 @@ export function World(): React.JSX.Element { <> - + + + ) : ( -- 2.52.0 From c9db2637a603d62dbfa4a986c56215f38bf255b0 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 01:27:32 +0100 Subject: [PATCH 04/45] add: repair game inspection sub state --- docs/technical/architecture.md | 6 +- docs/technical/zustand.md | 10 +- docs/user/features.md | 8 +- docs/user/main-feature.md | 42 ++++---- src/components/three/gameplay/RepairGame.tsx | 45 +++++++++ .../three/gameplay/RepairInspectionObject.tsx | 33 +++++++ .../three/gameplay/RepairMissionCase.tsx | 21 ++++ .../three/gameplay/RepairObjectModel.tsx | 96 +++++++++++++++++++ .../three/gameplay/RepairPromptVideo.tsx | 34 +++++++ src/data/docs/docsTranslations.ts | 30 +++++- src/hooks/gameplay/useRepairMissionStep.ts | 9 ++ src/world/GameStageContent.tsx | 7 +- 12 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 src/components/three/gameplay/RepairGame.tsx create mode 100644 src/components/three/gameplay/RepairInspectionObject.tsx create mode 100644 src/components/three/gameplay/RepairMissionCase.tsx create mode 100644 src/components/three/gameplay/RepairObjectModel.tsx create mode 100644 src/components/three/gameplay/RepairPromptVideo.tsx create mode 100644 src/hooks/gameplay/useRepairMissionStep.ts diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index 6b4c04f..c291e5b 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -15,7 +15,7 @@ This document describes the code that exists today in the repository. - either the map scene or the debug physics test scene - the player rig when the active camera mode is `player` - `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, and builds the collision octree. -- `src/world/GameStageContent.tsx` is wrapped in Rapier `Physics` in the production game scene so stage gameplay objects can use physics without moving the map or player to Rapier. +- `src/world/GameStageContent.tsx` is wrapped in Rapier `Physics` in the production game scene so stage gameplay objects can use physics without moving the map or player to Rapier. It now mounts reusable `RepairGame` instances for `bike`, `pylone`, and `ferme` mission states. - `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map. - `src/world/player/Player.tsx` mounts the camera and controller. - `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input. @@ -62,7 +62,7 @@ Keep the player and map octree outside the Rapier provider until there is a deli - `src/components/three/models/` contains reusable model helpers such as `ExplodableModel`. - `src/components/three/interaction/` contains reusable interaction wrappers such as `InteractableObject`, `TriggerObject`, and `GrabbableObject`. - `src/components/three/handTracking/` contains R3F hand tracking debug models such as the glove overlays. -- `src/components/three/gameplay/` contains the current core repair gameplay prototype: the repair case, repair game zone, and module slots. +- `src/components/three/gameplay/` contains the repair gameplay components: the reusable production `RepairGame` flow, the repair case, the debug repair game zone, and module slots. - `src/components/three/world/` contains reusable world/environment objects such as `SkyModel`. ## Editor System @@ -90,6 +90,6 @@ Keep the player and map octree outside the Rapier provider until there is a deli - The repository is a prototype, not the full intended game runtime. - `src/world/debug/TestMap.tsx` is part of the active scene composition. - There is no central gameplay orchestrator such as `GameManager`. -- Missions, zones, cinematics, and dialogue systems are not implemented. +- The mission state exists in Zustand, but zones, cinematics, dialogue, and the full repair sequence are not implemented. - The player uses octree collision and simple movement rules, not a complete gameplay physics stack. - Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API. diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md index 27e0f9a..1e69db0 100644 --- a/docs/technical/zustand.md +++ b/docs/technical/zustand.md @@ -130,6 +130,14 @@ This keeps reusable gameplay components such as repair flows from duplicating mi `src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content. +For repair missions, it mounts the reusable `RepairGame` component with a mission id: + +```tsx + +``` + +`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep`. This keeps the scene component small and avoids mission-specific branching inside the repair flow. + That means the scene can progressively move toward this pattern: ```tsx @@ -173,4 +181,4 @@ Current overlays: ## Next Steps -The next natural step is to replace the temporary stage anchors in `GameStageContent` with real stage components, for example `IntroContent`, `BikeContent`, `PyloneContent`, `FermeContent`, and `OutroContent`. +The next natural step is to extend `RepairGame` beyond `waiting -> inspected` with fragmentation, scanning, repairing, and completion behavior. diff --git a/docs/user/features.md b/docs/user/features.md index d6c0a02..03659cd 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -27,6 +27,12 @@ This document lists features that are implemented in the current codebase. - Physics-backed gameplay objects can be mounted inside stage content without replacing player octree collision - Interaction prompt shown for trigger interactions +## Repair Gameplay + +- Reusable production `RepairGame` mounted for `bike`, `pylone`, and `ferme` mission states +- Repair mission config shared through `src/data/gameplay/repairMissions.ts` +- First repair-game slice supports `waiting -> inspected` with `.webm` interaction prompts and repair case spawn + ## Audio - One-shot sound playback for trigger interactions @@ -57,7 +63,7 @@ This document lists features that are implemented in the current codebase. ## Not Implemented Yet -- mission system +- complete mission system - zone system - cinematic system - dialogue system diff --git a/docs/user/main-feature.md b/docs/user/main-feature.md index 4325450..37cbf98 100644 --- a/docs/user/main-feature.md +++ b/docs/user/main-feature.md @@ -1,21 +1,20 @@ # Main Feature -This document explains the current repair-game prototype in La-Fabrik. +This document explains the current repair-game flow in La-Fabrik. ## What It Does -The main feature is a repair interaction sandbox mounted in the debug physics scene. It lets the player approach a repair case, open it, and interact with module slots that can show selectable models and exploded-model states. +The main feature is becoming a reusable repair flow mounted in the production game scene. It lets the player approach the active mission object, inspect it, and bring in the repair case before later repair steps take over. The current user flow is: -1. Open the app with `?debug`. -2. Switch the scene to `Physics` in the debug panel. -3. Move close to the repair case. -4. Press the interaction key when prompted. -5. Watch the case open or close with sound feedback. -6. Interact with repair module slots to cycle/select repair models. +1. Enter a mission state such as `bike`, `pylone`, or `ferme`. +2. Move close to the active repair object in the game scene. +3. Aim at the object and press the interaction key when prompted. +4. The mission step moves from `waiting` to `inspected`. +5. The repair case appears near the mission object and can float when the player approaches it. -The production repair flow is now being moved toward reusable mission data for `bike`, `pylone`, and `ferme`. This lets the same future `RepairGame` component read one mission config instead of duplicating per-mission setup. +The older debug repair sandbox still exists in the physics test scene, but the production path now starts from the reusable `RepairGame` component. ## Why It Matters @@ -23,15 +22,21 @@ This feature validates the core repair fantasy before a full mission system exis ## Current Behavior -The repair case reacts to player proximity. When the player is close enough, it floats upward and rotates gently to signal interactivity. When the player moves away, it returns to its resting transform. +In `waiting`, the active mission renders its repair object and the `interagir.webm` prompt in the game scene. The interaction uses the shared focus/raycast interaction system, so the player still gets the normal `E` prompt. -Interacting with the case toggles its open state. The lid animation is handled with GSAP because it is a discrete interaction animation, not a continuous per-frame loop. +When the player inspects the object, `RepairGame` writes `inspected` through the generic mission store action. The repair case then appears from the mission config. When the player is close enough, the existing case model floats upward and rotates gently to signal interactivity. -Repair module slots are configured from static gameplay data. They render selectable repair models and can use exploded model visualization to show parts separated from their original positions. +Repair module slots and exploded-model behavior still exist in the debug prototype. They will be migrated into the reusable repair flow in later steps. ## Key Files - `src/world/debug/TestMap.tsx` mounts the repair-game prototype in the debug physics scene. +- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`. +- `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow. +- `src/components/three/gameplay/RepairInspectionObject.tsx` handles the `waiting` inspection interaction. +- `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection. +- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene. +- `src/hooks/gameplay/useRepairMissionStep.ts` reads the active mission step from the game store. - `src/components/three/gameplay/RepairGameZone.tsx` composes the repair-game zone. - `src/components/three/gameplay/RepairCaseObject.tsx` connects the repair case to trigger interaction and audio. - `src/components/three/gameplay/RepairCaseModel.tsx` renders and animates the case model. @@ -43,12 +48,12 @@ Repair module slots are configured from static gameplay data. They render select - `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`. - `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers. -## Debug Requirements +## Runtime Requirements -The repair-game prototype currently requires: +The production repair flow currently requires: -- the app opened with `?debug` -- the debug scene set to `Physics` +- the active `mainState` to be one of `bike`, `pylone`, or `ferme` +- `GameStageContent` mounted inside the game scene Rapier `Physics` boundary - model assets available under `public/models/` - sound assets available under `public/sounds/` @@ -58,7 +63,7 @@ Frontend command: npm run dev ``` -Debug URL: +Debug URL for state switching and inspection: ```txt http://localhost:5173/?debug @@ -77,8 +82,7 @@ python -m backend.main ## Current Limitations -- It is mounted only in the debug physics scene. -- The production `RepairGame` component is not mounted in the main game scene yet. +- The reusable production `RepairGame` currently covers only `waiting -> inspected`. - Mission progression exists in Zustand, but the full repair mission flow is still being integrated. - There is no central `GameManager` in this branch. - Hand tracking is available as debug interaction input, not as final repair gameplay. diff --git a/src/components/three/gameplay/RepairGame.tsx b/src/components/three/gameplay/RepairGame.tsx new file mode 100644 index 0000000..ae10392 --- /dev/null +++ b/src/components/three/gameplay/RepairGame.tsx @@ -0,0 +1,45 @@ +import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject"; +import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase"; +import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions"; +import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep"; +import type { RepairMissionId } from "@/managers/stores/useGameStore"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three"; +import { toVector3Scale } from "@/utils/three/scale"; + +interface RepairGameProps extends Required< + Pick +> { + mission: RepairMissionId; + rotation?: Vector3Tuple; + scale?: ModelTransformProps["scale"]; +} + +export function RepairGame({ + mission, + position, + rotation = [0, 0, 0], + scale = 1, +}: RepairGameProps): React.JSX.Element | null { + const config = REPAIR_MISSIONS[mission]; + const mainState = useGameStore((state) => state.mainState); + const setMissionStep = useGameStore((state) => state.setMissionStep); + const step = useRepairMissionStep(mission); + const parsedScale = toVector3Scale(scale); + + if (mainState !== mission) return null; + if (step === "locked") return null; + + return ( + + {step === "waiting" ? ( + setMissionStep(mission, "inspected")} + /> + ) : null} + {step !== "waiting" ? : null} + + ); +} diff --git a/src/components/three/gameplay/RepairInspectionObject.tsx b/src/components/three/gameplay/RepairInspectionObject.tsx new file mode 100644 index 0000000..8dfd5b0 --- /dev/null +++ b/src/components/three/gameplay/RepairInspectionObject.tsx @@ -0,0 +1,33 @@ +import { InteractableObject } from "@/components/three/interaction/InteractableObject"; +import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel"; +import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo"; +import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; +import type { Vector3Tuple } from "@/types/three/three"; + +interface RepairInspectionObjectProps { + config: RepairMissionConfig; + worldPosition: Vector3Tuple; + onInspect: () => void; +} + +export function RepairInspectionObject({ + config, + worldPosition, + onInspect, +}: RepairInspectionObjectProps): React.JSX.Element { + return ( + + + + + ); +} diff --git a/src/components/three/gameplay/RepairMissionCase.tsx b/src/components/three/gameplay/RepairMissionCase.tsx new file mode 100644 index 0000000..f484f87 --- /dev/null +++ b/src/components/three/gameplay/RepairMissionCase.tsx @@ -0,0 +1,21 @@ +import { RepairCaseModel } from "@/components/three/gameplay/RepairCaseModel"; +import { REPAIR_CASE_MODEL_PATH } from "@/data/gameplay/repairCaseConfig"; +import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; + +interface RepairMissionCaseProps { + config: RepairMissionConfig; +} + +export function RepairMissionCase({ + config, +}: RepairMissionCaseProps): React.JSX.Element { + return ( + + ); +} diff --git a/src/components/three/gameplay/RepairObjectModel.tsx b/src/components/three/gameplay/RepairObjectModel.tsx new file mode 100644 index 0000000..c6a8e15 --- /dev/null +++ b/src/components/three/gameplay/RepairObjectModel.tsx @@ -0,0 +1,96 @@ +import type { ReactNode } from "react"; +import { Component } from "react"; +import { SimpleModel } from "@/components/three/models/SimpleModel"; +import type { Vector3Scale, Vector3Tuple } from "@/types/three/three"; +import { logModelLoadError } from "@/utils/three/modelLoadLogger"; + +interface RepairObjectModelProps { + label: string; + modelPath: string; + position?: Vector3Tuple; + rotation?: Vector3Tuple; + scale?: Vector3Scale; +} + +interface RepairObjectModelBoundaryProps extends RepairObjectModelProps { + children: ReactNode; +} + +interface RepairObjectModelBoundaryState { + hasError: boolean; +} + +class RepairObjectModelBoundary extends Component< + RepairObjectModelBoundaryProps, + RepairObjectModelBoundaryState +> { + constructor(props: RepairObjectModelBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(): RepairObjectModelBoundaryState { + return { hasError: true }; + } + + componentDidCatch(error: Error): void { + logModelLoadError( + { + modelPath: this.props.modelPath, + position: this.props.position, + rotation: this.props.rotation, + scale: this.props.scale, + scope: `RepairObjectModel.${this.props.label}`, + }, + error, + ); + } + + render(): ReactNode { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +} + +export function RepairObjectModel({ + label, + modelPath, + position = [0, 0, 0], + rotation = [0, 0, 0], + scale = 1, +}: RepairObjectModelProps): React.JSX.Element { + return ( + + + + ); +} + +function RepairObjectFallback({ label }: { label: string }): React.JSX.Element { + return ( + + + + + + + + + + + ); +} diff --git a/src/components/three/gameplay/RepairPromptVideo.tsx b/src/components/three/gameplay/RepairPromptVideo.tsx new file mode 100644 index 0000000..a3ca7e1 --- /dev/null +++ b/src/components/three/gameplay/RepairPromptVideo.tsx @@ -0,0 +1,34 @@ +import { Html } from "@react-three/drei"; +import type { Vector3Tuple } from "@/types/three/three"; + +interface RepairPromptVideoProps { + src: string; + position?: Vector3Tuple; + size?: number; +} + +export function RepairPromptVideo({ + src, + position = [0, 1.8, 0], + size = 96, +}: RepairPromptVideoProps): React.JSX.Element { + return ( + +