connect game progression state to world
This commit is contained in:
@@ -151,7 +151,7 @@ export function AnimatedModel({
|
|||||||
defaultAction.play();
|
defaultAction.play();
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setCurrentAnim(defaultAction.getClip().name);
|
setCurrentAnim(defaultAction.getClip().name);
|
||||||
onLoaded?.();
|
onLoaded?.();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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">
|
||||||
|
{MAIN_STATES.map((state) => (
|
||||||
|
<button
|
||||||
|
key={state}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -391,6 +391,77 @@ canvas {
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 page */
|
||||||
.editor-container {
|
.editor-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
||||||
export type MissionStep = "locked" | "inspect" | "repair" | "done";
|
export type MissionStep =
|
||||||
|
| "locked"
|
||||||
|
| "waiting"
|
||||||
|
| "inspected"
|
||||||
|
| "fragmented"
|
||||||
|
| "scanning"
|
||||||
|
| "repairing"
|
||||||
|
| "done";
|
||||||
|
|
||||||
export interface IntroState {
|
export interface IntroState {
|
||||||
dialogueAudio: string | null;
|
dialogueAudio: string | null;
|
||||||
@@ -39,31 +46,54 @@ interface GameActions {
|
|||||||
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
||||||
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
||||||
setOutroState: (outro: Partial<GameState["outro"]>) => void;
|
setOutroState: (outro: Partial<GameState["outro"]>) => void;
|
||||||
|
completeIntro: () => void;
|
||||||
|
completeBike: () => void;
|
||||||
|
completePylone: () => void;
|
||||||
|
completeFerme: () => void;
|
||||||
|
startOutro: () => void;
|
||||||
|
advanceGameState: () => void;
|
||||||
resetGame: () => void;
|
resetGame: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameStore = GameState & GameActions;
|
export type GameStore = GameState & GameActions;
|
||||||
|
|
||||||
|
function getNextMissionStep(step: MissionStep): MissionStep {
|
||||||
|
switch (step) {
|
||||||
|
case "locked":
|
||||||
|
case "waiting":
|
||||||
|
return "inspected";
|
||||||
|
case "inspected":
|
||||||
|
return "fragmented";
|
||||||
|
case "fragmented":
|
||||||
|
return "scanning";
|
||||||
|
case "scanning":
|
||||||
|
return "repairing";
|
||||||
|
case "repairing":
|
||||||
|
case "done":
|
||||||
|
return "done";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createInitialGameState(): GameState {
|
function createInitialGameState(): GameState {
|
||||||
return {
|
return {
|
||||||
mainState: "intro",
|
mainState: "intro",
|
||||||
intro: {
|
intro: {
|
||||||
dialogueAudio: ""null,
|
dialogueAudio: null,
|
||||||
hasCompleted: false,
|
hasCompleted: false,
|
||||||
isBikeUnlocked: false,
|
isBikeUnlocked: false,
|
||||||
},
|
},
|
||||||
bike: {
|
bike: {
|
||||||
currentStep: "locked",
|
currentStep: "waiting",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
isRepaired: false,
|
isRepaired: false,
|
||||||
},
|
},
|
||||||
pylone: {
|
pylone: {
|
||||||
currentStep: "locked",
|
currentStep: "waiting",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
isPowered: false,
|
isPowered: false,
|
||||||
},
|
},
|
||||||
ferme: {
|
ferme: {
|
||||||
currentStep: "locked",
|
currentStep: "waiting",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
irrigationFixed: false,
|
irrigationFixed: false,
|
||||||
},
|
},
|
||||||
@@ -87,5 +117,149 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
|
set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
|
||||||
setOutroState: (outro) =>
|
setOutroState: (outro) =>
|
||||||
set((state) => ({ outro: { ...state.outro, ...outro } })),
|
set((state) => ({ outro: { ...state.outro, ...outro } })),
|
||||||
|
completeIntro: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mainState: "bike",
|
||||||
|
intro: {
|
||||||
|
...state.intro,
|
||||||
|
hasCompleted: true,
|
||||||
|
isBikeUnlocked: true,
|
||||||
|
},
|
||||||
|
bike: {
|
||||||
|
...state.bike,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
completeBike: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mainState: "pylone",
|
||||||
|
bike: {
|
||||||
|
...state.bike,
|
||||||
|
currentStep: "done",
|
||||||
|
isRepaired: true,
|
||||||
|
},
|
||||||
|
pylone: {
|
||||||
|
...state.pylone,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
completePylone: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mainState: "ferme",
|
||||||
|
pylone: {
|
||||||
|
...state.pylone,
|
||||||
|
currentStep: "done",
|
||||||
|
isPowered: true,
|
||||||
|
},
|
||||||
|
ferme: {
|
||||||
|
...state.ferme,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
completeFerme: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mainState: "outro",
|
||||||
|
ferme: {
|
||||||
|
...state.ferme,
|
||||||
|
currentStep: "done",
|
||||||
|
irrigationFixed: true,
|
||||||
|
},
|
||||||
|
outro: {
|
||||||
|
...state.outro,
|
||||||
|
hasStarted: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
startOutro: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mainState: "outro",
|
||||||
|
outro: {
|
||||||
|
...state.outro,
|
||||||
|
hasStarted: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
advanceGameState: () =>
|
||||||
|
set((state) => {
|
||||||
|
if (state.mainState === "intro") {
|
||||||
|
return {
|
||||||
|
mainState: "bike",
|
||||||
|
intro: {
|
||||||
|
...state.intro,
|
||||||
|
hasCompleted: true,
|
||||||
|
isBikeUnlocked: true,
|
||||||
|
},
|
||||||
|
bike: {
|
||||||
|
...state.bike,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.mainState === "bike") {
|
||||||
|
const nextStep = getNextMissionStep(state.bike.currentStep);
|
||||||
|
if (nextStep === "done") {
|
||||||
|
return {
|
||||||
|
mainState: "pylone",
|
||||||
|
bike: {
|
||||||
|
...state.bike,
|
||||||
|
currentStep: "done",
|
||||||
|
isRepaired: true,
|
||||||
|
},
|
||||||
|
pylone: {
|
||||||
|
...state.pylone,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bike: { ...state.bike, currentStep: nextStep } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.mainState === "pylone") {
|
||||||
|
const nextStep = getNextMissionStep(state.pylone.currentStep);
|
||||||
|
if (nextStep === "done") {
|
||||||
|
return {
|
||||||
|
mainState: "ferme",
|
||||||
|
pylone: {
|
||||||
|
...state.pylone,
|
||||||
|
currentStep: "done",
|
||||||
|
isPowered: true,
|
||||||
|
},
|
||||||
|
ferme: {
|
||||||
|
...state.ferme,
|
||||||
|
currentStep: "inspected",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pylone: { ...state.pylone, currentStep: nextStep } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.mainState === "ferme") {
|
||||||
|
const nextStep = getNextMissionStep(state.ferme.currentStep);
|
||||||
|
if (nextStep === "done") {
|
||||||
|
return {
|
||||||
|
mainState: "outro",
|
||||||
|
ferme: {
|
||||||
|
...state.ferme,
|
||||||
|
currentStep: "done",
|
||||||
|
irrigationFixed: true,
|
||||||
|
},
|
||||||
|
outro: {
|
||||||
|
...state.outro,
|
||||||
|
hasStarted: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ferme: { ...state.ferme, currentStep: nextStep } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
outro: {
|
||||||
|
...state.outro,
|
||||||
|
hasStarted: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
resetGame: () => set(createInitialGameState()),
|
resetGame: () => set(createInitialGameState()),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Crosshair } from "@/components/ui/Crosshair";
|
import { Crosshair } from "@/components/ui/Crosshair";
|
||||||
|
import { GameStateHUD } from "@/components/ui/GameStateHUD";
|
||||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||||
import { DebugPerf } from "@/components/debug/DebugPerf";
|
import { DebugPerf } from "@/components/debug/DebugPerf";
|
||||||
import { World } from "@/world/World";
|
import { World } from "@/world/World";
|
||||||
@@ -14,6 +15,7 @@ export function HomePage(): React.JSX.Element {
|
|||||||
<DebugPerf />
|
<DebugPerf />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
<GameStateHUD />
|
||||||
<Crosshair />
|
<Crosshair />
|
||||||
<InteractPrompt />
|
<InteractPrompt />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
import type { Vector3Tuple } from "@/types/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} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -11,6 +11,7 @@ import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
|
|||||||
import { Environment } from "@/world/Environment";
|
import { Environment } from "@/world/Environment";
|
||||||
import { Lighting } from "@/world/Lighting";
|
import { Lighting } from "@/world/Lighting";
|
||||||
import { GameMap } from "@/world/GameMap";
|
import { GameMap } from "@/world/GameMap";
|
||||||
|
import { GameStageContent } from "@/world/GameStageContent";
|
||||||
import { Player } from "@/world/player/Player";
|
import { Player } from "@/world/player/Player";
|
||||||
import { TestScene } from "@/world/debug/TestScene";
|
import { TestScene } from "@/world/debug/TestScene";
|
||||||
|
|
||||||
@@ -31,7 +32,10 @@ export function World(): React.JSX.Element {
|
|||||||
{cameraMode === "debug" ? <DebugCameraControls /> : null}
|
{cameraMode === "debug" ? <DebugCameraControls /> : null}
|
||||||
|
|
||||||
{sceneMode === "game" ? (
|
{sceneMode === "game" ? (
|
||||||
<GameMap onOctreeReady={setOctree} />
|
<>
|
||||||
|
<GameMap onOctreeReady={setOctree} />
|
||||||
|
<GameStageContent />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TestScene onOctreeReady={setOctree} />
|
<TestScene onOctreeReady={setOctree} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user