Merge branch 'develop' into feat/main-feature
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { Debug } from "@/utils/debug/Debug";
|
||||
import {
|
||||
type MainGameState,
|
||||
useGameStore,
|
||||
} from "@/managers/stores/useGameStore";
|
||||
|
||||
const MAIN_STATES: MainGameState[] = [
|
||||
"intro",
|
||||
"bike",
|
||||
"pylone",
|
||||
"ferme",
|
||||
"outro",
|
||||
];
|
||||
|
||||
export function GameStateHUD(): React.JSX.Element | null {
|
||||
const debug = Debug.getInstance();
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
const detail = useGameStore((state) => {
|
||||
switch (state.mainState) {
|
||||
case "intro":
|
||||
return state.intro.hasCompleted ? "completed" : "waiting";
|
||||
case "bike":
|
||||
return state.bike.currentStep;
|
||||
case "pylone":
|
||||
return state.pylone.currentStep;
|
||||
case "ferme":
|
||||
return state.ferme.currentStep;
|
||||
case "outro":
|
||||
return state.outro.hasStarted ? "started" : "waiting";
|
||||
}
|
||||
});
|
||||
const setMainState = useGameStore((state) => state.setMainState);
|
||||
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||
const resetGame = useGameStore((state) => state.resetGame);
|
||||
|
||||
if (!debug.active) return null;
|
||||
|
||||
return (
|
||||
<aside className="game-state-hud" aria-label="Game state debug panel">
|
||||
<div className="game-state-hud__header">
|
||||
<span>Game State</span>
|
||||
<strong>{mainState}</strong>
|
||||
</div>
|
||||
|
||||
<p className="game-state-hud__detail">Sub state: {detail}</p>
|
||||
|
||||
<div
|
||||
className="game-state-hud__states"
|
||||
aria-label="Main states"
|
||||
role="group"
|
||||
>
|
||||
{MAIN_STATES.map((state) => (
|
||||
<button
|
||||
key={state}
|
||||
aria-pressed={state === mainState}
|
||||
className={state === mainState ? "is-active" : undefined}
|
||||
type="button"
|
||||
onClick={() => setMainState(state)}
|
||||
>
|
||||
{state}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="game-state-hud__actions">
|
||||
<button type="button" onClick={advanceGameState}>
|
||||
Next step
|
||||
</button>
|
||||
<button type="button" onClick={resetGame}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Crosshair } from "@/components/ui/Crosshair";
|
||||
import { GameStateHUD } from "@/components/ui/GameStateHUD";
|
||||
import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
|
||||
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
|
||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||
|
||||
export function GameUI(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<GameStateHUD />
|
||||
<Crosshair />
|
||||
<InteractPrompt />
|
||||
<HandTrackingVisualizer />
|
||||
<HandTrackingOverlay />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,12 @@ export const docGroups: DocGroup[] = [
|
||||
subtitle: "Webcam interaction pipeline",
|
||||
meta: "05",
|
||||
},
|
||||
{
|
||||
path: "/docs/zustand",
|
||||
title: "Zustand Game State",
|
||||
subtitle: "Progression store",
|
||||
meta: "06",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -53,25 +59,25 @@ export const docGroups: DocGroup[] = [
|
||||
path: "/docs/features",
|
||||
title: "Features",
|
||||
subtitle: "Implemented scope",
|
||||
meta: "06",
|
||||
meta: "07",
|
||||
},
|
||||
{
|
||||
path: "/docs/main-feature",
|
||||
title: "Main Feature",
|
||||
subtitle: "Hand grab prototype",
|
||||
meta: "07",
|
||||
subtitle: "Repair-game prototype",
|
||||
meta: "08",
|
||||
},
|
||||
{
|
||||
path: "/docs/editor",
|
||||
title: "Editor User Guide",
|
||||
subtitle: "Editing workflow",
|
||||
meta: "08",
|
||||
meta: "09",
|
||||
},
|
||||
{
|
||||
path: "/docs/animation",
|
||||
title: "Animation & 3D Model System",
|
||||
subtitle: "Components and usage",
|
||||
meta: "07",
|
||||
meta: "010",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -206,6 +206,165 @@ Ce document décrit l'architecture visée à moyen terme pour le projet.
|
||||
- Les chemins runtime uniquement debug doivent être clairement marqués et faciles à retirer plus tard.
|
||||
`;
|
||||
|
||||
export const zustandFr = `# État de jeu Zustand
|
||||
|
||||
Ce document explique comment Zustand est utilisé dans le projet actuel.
|
||||
|
||||
## Pourquoi Zustand existe ici
|
||||
|
||||
Le projet a besoin d'une source de vérité partagée pour suivre la progression du joueur dans l'expérience.
|
||||
|
||||
La progression actuelle est découpée en main states :
|
||||
|
||||
| Main state | Rôle |
|
||||
| --- | --- |
|
||||
| \`intro\` | Onboarding et séquence d'ouverture |
|
||||
| \`bike\` | Séquence de réparation du vélo électrique |
|
||||
| \`pylone\` | Séquence du réseau électrique |
|
||||
| \`ferme\` | Séquence de la ferme verticale |
|
||||
| \`outro\` | Séquence de fin |
|
||||
|
||||
Chaque main state peut aussi posséder un sous-état plus fin, comme l'étape de mission courante, l'audio de dialogue ou des flags de complétion.
|
||||
|
||||
Zustand est utile parce que les composants React et React Three Fiber peuvent s'abonner uniquement à la partie de state dont ils ont besoin. Quand cette partie change, seuls les composants abonnés se mettent à jour.
|
||||
|
||||
## Emplacement du store
|
||||
|
||||
Le store de progression du jeu vit ici :
|
||||
|
||||
\`\`\`txt
|
||||
src/managers/stores/useGameStore.ts
|
||||
\`\`\`
|
||||
|
||||
Le store est placé dans \`src/managers/stores/\` parce qu'il appartient à la couche d'orchestration gameplay, pas à un composant visuel précis.
|
||||
|
||||
## Managers vs Store
|
||||
|
||||
Les managers sont responsables des objets runtime locaux et des comportements impératifs.
|
||||
|
||||
Exemples :
|
||||
|
||||
- \`AudioManager\` possède les éléments audio et les pools de sons.
|
||||
- \`InteractionManager\` possède les handles d'interaction transitoires et la logique orientée input.
|
||||
|
||||
Un manager peut lire ou mettre à jour le store Zustand quand son comportement local doit impacter la progression globale du jeu.
|
||||
|
||||
Le store Zustand est responsable de l'état global durable :
|
||||
|
||||
- main state courant
|
||||
- sous-état de mission
|
||||
- flags de progression
|
||||
- références de dialogue/audio
|
||||
- transitions de state
|
||||
|
||||
Règle simple :
|
||||
|
||||
- manager = objets runtime, effets de bord et logique impérative locale
|
||||
- store = état gameplay global auquel l'UI ou le world peuvent s'abonner
|
||||
|
||||
## Forme actuelle
|
||||
|
||||
Le store expose :
|
||||
|
||||
- \`mainState\` : phase active du jeu
|
||||
- \`intro\` : état spécifique à l'intro
|
||||
- \`bike\` : état de la mission vélo
|
||||
- \`pylone\` : état de la mission réseau électrique
|
||||
- \`ferme\` : état de la mission ferme
|
||||
- \`outro\` : état de fin
|
||||
- des actions de mise à jour directe et des actions de progression
|
||||
|
||||
Les étapes de mission utilisent actuellement cette séquence :
|
||||
|
||||
\`\`\`ts
|
||||
"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "done"
|
||||
\`\`\`
|
||||
|
||||
## Lire le state dans un composant
|
||||
|
||||
Utilise des selectors pour lire uniquement ce dont le composant a besoin.
|
||||
|
||||
\`\`\`tsx
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
|
||||
export function Example(): React.JSX.Element {
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
|
||||
return <p>State courant : {mainState}</p>;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
C'est mieux que de lire tout le store, car le composant se re-render uniquement quand \`mainState\` change.
|
||||
|
||||
## Mettre à jour le state
|
||||
|
||||
Préfère les actions explicites du store.
|
||||
|
||||
\`\`\`ts
|
||||
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||
|
||||
advanceGameState();
|
||||
\`\`\`
|
||||
|
||||
Pour le développement et le debug, des setters directs existent aussi :
|
||||
|
||||
\`\`\`ts
|
||||
const setMainState = useGameStore((state) => state.setMainState);
|
||||
|
||||
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\`.
|
||||
|
||||
## Intégration avec le World
|
||||
|
||||
\`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant.
|
||||
|
||||
La scène peut donc évoluer progressivement vers ce pattern :
|
||||
|
||||
\`\`\`tsx
|
||||
switch (mainState) {
|
||||
case "intro":
|
||||
return <IntroContent />;
|
||||
case "bike":
|
||||
return <BikeContent />;
|
||||
case "pylone":
|
||||
return <PyloneContent />;
|
||||
case "ferme":
|
||||
return <FarmContent />;
|
||||
case "outro":
|
||||
return <OutroContent />;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Dans React Three Fiber, monter ou démonter du JSX contrôle ce qui apparaît dans la scène Three.js. Quand un composant lié à un state disparaît du JSX, React le retire de la scène.
|
||||
|
||||
## Intégration UI
|
||||
|
||||
\`src/components/ui/GameUI.tsx\` regroupe les overlays HTML utilisés par la route jouable.
|
||||
|
||||
Overlays actuels :
|
||||
|
||||
- \`GameStateHUD\` : panneau de progression debug visible avec \`?debug\`
|
||||
- \`Crosshair\` : aide de visée joueur
|
||||
- \`InteractPrompt\` : prompt d'interaction
|
||||
|
||||
\`src/pages/page.tsx\` doit rester fin et monter seulement le canvas et \`GameUI\`.
|
||||
|
||||
## Règles anti-régression
|
||||
|
||||
- Ne pas stocker les valeurs mises à jour à chaque frame dans Zustand.
|
||||
- Utiliser \`useRef\` pour les valeurs mutables fréquentes comme la vélocité joueur, les vecteurs temporaires ou les données de boucle d'animation.
|
||||
- Utiliser des selectors au lieu de lire tout le store dans les composants.
|
||||
- Garder les transitions gameplay dans les actions du store quand possible.
|
||||
- Garder les contrôles debug derrière \`?debug\`.
|
||||
- Ajouter du state uniquement quand une vraie fonctionnalité runtime en a besoin.
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
La prochaine étape naturelle est de remplacer les ancres temporaires de \`GameStageContent\` par de vrais composants de phase, par exemple \`IntroContent\`, \`BikeContent\`, \`PyloneContent\`, \`FermeContent\` et \`OutroContent\`.
|
||||
`;
|
||||
|
||||
export const featuresFr = `# Fonctionnalités implémentées
|
||||
|
||||
Ce document liste les fonctionnalités présentes dans le code actuel.
|
||||
|
||||
@@ -467,6 +467,77 @@ canvas {
|
||||
background: #38bdf8;
|
||||
}
|
||||
|
||||
.game-state-hud {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(320px, calc(100vw - 36px));
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 18px;
|
||||
background: rgba(4, 7, 13, 0.78);
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
|
||||
color: #f8fafc;
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.game-state-hud__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.game-state-hud__header span,
|
||||
.game-state-hud__detail {
|
||||
color: rgba(248, 250, 252, 0.68);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-state-hud__header strong {
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-state-hud__detail {
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.game-state-hud__states,
|
||||
.game-state-hud__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.game-state-hud button {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.game-state-hud button:hover,
|
||||
.game-state-hud button:focus-visible,
|
||||
.game-state-hud button.is-active {
|
||||
border-color: rgba(125, 211, 252, 0.75);
|
||||
background: rgba(125, 211, 252, 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Editor page */
|
||||
.editor-container {
|
||||
position: fixed;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
||||
type MissionStep =
|
||||
| "locked"
|
||||
| "waiting"
|
||||
| "inspected"
|
||||
| "fragmented"
|
||||
| "scanning"
|
||||
| "repairing"
|
||||
| "done";
|
||||
|
||||
interface IntroState {
|
||||
dialogueAudio: string | null;
|
||||
hasCompleted: boolean;
|
||||
isBikeUnlocked: boolean;
|
||||
}
|
||||
|
||||
interface MissionState {
|
||||
currentStep: MissionStep;
|
||||
dialogueAudio: string | null;
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
mainState: MainGameState;
|
||||
intro: IntroState;
|
||||
bike: MissionState & {
|
||||
isRepaired: boolean;
|
||||
};
|
||||
pylone: MissionState & {
|
||||
isPowered: boolean;
|
||||
};
|
||||
ferme: MissionState & {
|
||||
irrigationFixed: boolean;
|
||||
};
|
||||
outro: {
|
||||
dialogueAudio: string | null;
|
||||
hasStarted: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GameActions {
|
||||
setMainState: (mainState: MainGameState) => void;
|
||||
setIntroState: (intro: Partial<IntroState>) => void;
|
||||
setBikeState: (bike: Partial<GameState["bike"]>) => void;
|
||||
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
||||
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
||||
setOutroState: (outro: Partial<GameState["outro"]>) => void;
|
||||
completeIntro: () => void;
|
||||
completeBike: () => void;
|
||||
completePylone: () => void;
|
||||
completeFerme: () => void;
|
||||
startOutro: () => void;
|
||||
advanceGameState: () => void;
|
||||
resetGame: () => void;
|
||||
}
|
||||
|
||||
type GameStore = GameState & GameActions;
|
||||
type GameStateUpdate = Partial<GameState>;
|
||||
|
||||
function getNextMissionStep(step: MissionStep): MissionStep {
|
||||
switch (step) {
|
||||
case "locked":
|
||||
return "waiting";
|
||||
case "waiting":
|
||||
return "inspected";
|
||||
case "inspected":
|
||||
return "fragmented";
|
||||
case "fragmented":
|
||||
return "scanning";
|
||||
case "scanning":
|
||||
return "repairing";
|
||||
case "repairing":
|
||||
case "done":
|
||||
return "done";
|
||||
}
|
||||
}
|
||||
|
||||
function completeIntroState(state: GameState): GameStateUpdate {
|
||||
return {
|
||||
mainState: "bike",
|
||||
intro: {
|
||||
...state.intro,
|
||||
hasCompleted: true,
|
||||
isBikeUnlocked: true,
|
||||
},
|
||||
bike: {
|
||||
...state.bike,
|
||||
currentStep: "waiting",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function completeBikeState(state: GameState): GameStateUpdate {
|
||||
return {
|
||||
mainState: "pylone",
|
||||
bike: {
|
||||
...state.bike,
|
||||
currentStep: "done",
|
||||
isRepaired: true,
|
||||
},
|
||||
pylone: {
|
||||
...state.pylone,
|
||||
currentStep: "waiting",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function completePyloneState(state: GameState): GameStateUpdate {
|
||||
return {
|
||||
mainState: "ferme",
|
||||
pylone: {
|
||||
...state.pylone,
|
||||
currentStep: "done",
|
||||
isPowered: true,
|
||||
},
|
||||
ferme: {
|
||||
...state.ferme,
|
||||
currentStep: "waiting",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function completeFermeState(state: GameState): GameStateUpdate {
|
||||
return {
|
||||
mainState: "outro",
|
||||
ferme: {
|
||||
...state.ferme,
|
||||
currentStep: "done",
|
||||
irrigationFixed: true,
|
||||
},
|
||||
outro: {
|
||||
...state.outro,
|
||||
hasStarted: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function startOutroState(state: GameState): GameStateUpdate {
|
||||
return {
|
||||
mainState: "outro",
|
||||
outro: {
|
||||
...state.outro,
|
||||
hasStarted: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInitialGameState(): GameState {
|
||||
return {
|
||||
mainState: "intro",
|
||||
intro: {
|
||||
dialogueAudio: null,
|
||||
hasCompleted: false,
|
||||
isBikeUnlocked: false,
|
||||
},
|
||||
bike: {
|
||||
currentStep: "waiting",
|
||||
dialogueAudio: null,
|
||||
isRepaired: false,
|
||||
},
|
||||
pylone: {
|
||||
currentStep: "waiting",
|
||||
dialogueAudio: null,
|
||||
isPowered: false,
|
||||
},
|
||||
ferme: {
|
||||
currentStep: "waiting",
|
||||
dialogueAudio: null,
|
||||
irrigationFixed: false,
|
||||
},
|
||||
outro: {
|
||||
dialogueAudio: null,
|
||||
hasStarted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useGameStore = create<GameStore>()((set) => ({
|
||||
...createInitialGameState(),
|
||||
setMainState: (mainState) => set({ mainState }),
|
||||
setIntroState: (intro) =>
|
||||
set((state) => ({ intro: { ...state.intro, ...intro } })),
|
||||
setBikeState: (bike) =>
|
||||
set((state) => ({ bike: { ...state.bike, ...bike } })),
|
||||
setPyloneState: (pylone) =>
|
||||
set((state) => ({ pylone: { ...state.pylone, ...pylone } })),
|
||||
setFermeState: (ferme) =>
|
||||
set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
|
||||
setOutroState: (outro) =>
|
||||
set((state) => ({ outro: { ...state.outro, ...outro } })),
|
||||
completeIntro: () => set(completeIntroState),
|
||||
completeBike: () => set(completeBikeState),
|
||||
completePylone: () => set(completePyloneState),
|
||||
completeFerme: () => set(completeFermeState),
|
||||
startOutro: () => set(startOutroState),
|
||||
advanceGameState: () =>
|
||||
set((state) => {
|
||||
if (state.mainState === "intro") {
|
||||
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 } };
|
||||
}
|
||||
|
||||
return startOutroState(state);
|
||||
}),
|
||||
resetGame: () => set(createInitialGameState()),
|
||||
}));
|
||||
@@ -6,7 +6,7 @@ export function DocsAnimationPage(): React.JSX.Element {
|
||||
<DocsDocument
|
||||
content={animation}
|
||||
frContent={animation}
|
||||
meta="07"
|
||||
meta="08"
|
||||
title="Animation & 3D Model System"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ export function DocsEditorPage(): React.JSX.Element {
|
||||
<DocsDocument
|
||||
content={editor}
|
||||
frContent={editorFr}
|
||||
meta="08"
|
||||
meta="09"
|
||||
title="Editor User Guide"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import zustand from "../../../../docs/technical/zustand.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
import { zustandFr } from "@/data/docs/docsTranslations";
|
||||
|
||||
export function DocsZustandPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={zustand}
|
||||
frContent={zustandFr}
|
||||
meta="05"
|
||||
title="Zustand Game State"
|
||||
/>
|
||||
);
|
||||
}
|
||||
+3
-9
@@ -1,11 +1,8 @@
|
||||
import { Suspense } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Crosshair } from "@/components/ui/Crosshair";
|
||||
import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
|
||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
|
||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||
import { DebugPerf } from "@/components/debug/DebugPerf";
|
||||
import { GameUI } from "@/components/ui/GameUI";
|
||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||
import { World } from "@/world/World";
|
||||
|
||||
export function HomePage(): React.JSX.Element {
|
||||
@@ -17,10 +14,7 @@ export function HomePage(): React.JSX.Element {
|
||||
<DebugPerf />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
<Crosshair />
|
||||
<InteractPrompt />
|
||||
<HandTrackingVisualizer />
|
||||
<HandTrackingOverlay />
|
||||
<GameUI />
|
||||
</HandTrackingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DocsReadmeRoute,
|
||||
DocsTargetArchitectureRoute,
|
||||
DocsTechnicalEditorRoute,
|
||||
DocsZustandRoute,
|
||||
} from "@/routes/DocsRoute";
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
@@ -47,6 +48,7 @@ const docsChildRoutes = [
|
||||
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
|
||||
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
||||
{ path: "hand-tracking", component: DocsHandTrackingRoute },
|
||||
{ path: "zustand", component: DocsZustandRoute },
|
||||
{ path: "features", component: DocsFeaturesRoute },
|
||||
{ path: "main-feature", component: DocsMainFeatureRoute },
|
||||
{ path: "editor", component: DocsEditorRoute },
|
||||
|
||||
@@ -43,6 +43,10 @@ const LazyDocsHandTrackingPage = lazyNamed(
|
||||
() => import("@/pages/docs/hand-tracking/page"),
|
||||
"DocsHandTrackingPage",
|
||||
);
|
||||
const LazyDocsZustandPage = lazyNamed(
|
||||
() => import("@/pages/docs/zustand/page"),
|
||||
"DocsZustandPage",
|
||||
);
|
||||
const LazyDocsFeaturesPage = lazyNamed(
|
||||
() => import("@/pages/docs/features/page"),
|
||||
"DocsFeaturesPage",
|
||||
@@ -84,6 +88,10 @@ export function DocsHandTrackingRoute(): React.JSX.Element {
|
||||
return withDocsSuspense(LazyDocsHandTrackingPage);
|
||||
}
|
||||
|
||||
export function DocsZustandRoute(): React.JSX.Element {
|
||||
return withDocsSuspense(LazyDocsZustandPage);
|
||||
}
|
||||
|
||||
export function DocsFeaturesRoute(): React.JSX.Element {
|
||||
return withDocsSuspense(LazyDocsFeaturesPage);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
|
||||
interface StageAnchorProps {
|
||||
color: string;
|
||||
position: Vector3Tuple;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
function StageAnchor({
|
||||
color,
|
||||
position,
|
||||
scale = 1,
|
||||
}: StageAnchorProps): React.JSX.Element {
|
||||
return (
|
||||
<group position={position} scale={scale}>
|
||||
<mesh>
|
||||
<octahedronGeometry args={[1.2, 0]} />
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={0.25}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function GameStageContent(): React.JSX.Element {
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
|
||||
switch (mainState) {
|
||||
case "intro":
|
||||
return <StageAnchor color="#7dd3fc" position={[0, 4, 0]} />;
|
||||
case "bike":
|
||||
return <StageAnchor color="#facc15" position={[8, 3, -6]} />;
|
||||
case "pylone":
|
||||
return <StageAnchor color="#a78bfa" position={[64, 6, -66]} />;
|
||||
case "ferme":
|
||||
return <StageAnchor color="#86efac" position={[-24, 5, 42]} />;
|
||||
case "outro":
|
||||
return <StageAnchor color="#fb7185" position={[0, 6, 10]} scale={1.25} />;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { Environment } from "@/world/Environment";
|
||||
import { GameMusic } from "@/world/GameMusic";
|
||||
import { Lighting } from "@/world/Lighting";
|
||||
import { GameMap } from "@/world/GameMap";
|
||||
import { GameStageContent } from "@/world/GameStageContent";
|
||||
import { Player } from "@/world/player/Player";
|
||||
import { TestMap } from "@/world/debug/TestMap";
|
||||
|
||||
@@ -35,6 +36,7 @@ export function World(): React.JSX.Element {
|
||||
<>
|
||||
<GameMusic />
|
||||
<GameMap onOctreeReady={setOctree} />
|
||||
<GameStageContent />
|
||||
</>
|
||||
) : (
|
||||
<TestMap onOctreeReady={setOctree} />
|
||||
|
||||
Reference in New Issue
Block a user