Merge remote-tracking branch 'origin/develop' into feat/mission-2
# Conflicts: # package-lock.json # package.json # src/App.tsx # src/components/three/interaction/CentralObject.tsx # src/components/three/interaction/VillageoisHelperObject.tsx # src/managers/GameStepManager.ts # src/stateManager/AudioManager.ts # src/world/World.tsx # src/world/player/PlayerController.tsx
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
isRepairMissionId,
|
||||
type MissionStep,
|
||||
type RepairMissionId,
|
||||
} from "@/types/gameplay/repairMission";
|
||||
|
||||
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
||||
export type { MissionStep, RepairMissionId };
|
||||
|
||||
interface IntroState {
|
||||
dialogueAudio: string | null;
|
||||
hasCompleted: boolean;
|
||||
isBikeUnlocked: boolean;
|
||||
}
|
||||
|
||||
interface MissionState {
|
||||
currentStep: MissionStep;
|
||||
dialogueAudio: string | null;
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
mainState: MainGameState;
|
||||
isCinematicPlaying: boolean;
|
||||
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;
|
||||
setCinematicPlaying: (isCinematicPlaying: boolean) => 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;
|
||||
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
|
||||
completeIntro: () => void;
|
||||
completeBike: () => void;
|
||||
completePylone: () => void;
|
||||
completeFerme: () => void;
|
||||
completeMission: (mission: RepairMissionId) => void;
|
||||
startOutro: () => void;
|
||||
advanceGameState: () => void;
|
||||
rewindGameState: () => 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":
|
||||
return "reassembling";
|
||||
case "reassembling":
|
||||
case "done":
|
||||
return "done";
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMissionStep(step: MissionStep): MissionStep {
|
||||
switch (step) {
|
||||
case "locked":
|
||||
case "waiting":
|
||||
return "locked";
|
||||
case "inspected":
|
||||
return "waiting";
|
||||
case "fragmented":
|
||||
return "inspected";
|
||||
case "scanning":
|
||||
return "fragmented";
|
||||
case "repairing":
|
||||
return "scanning";
|
||||
case "reassembling":
|
||||
return "repairing";
|
||||
case "done":
|
||||
return "reassembling";
|
||||
}
|
||||
}
|
||||
|
||||
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 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",
|
||||
outro: {
|
||||
...state.outro,
|
||||
hasStarted: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInitialGameState(): GameState {
|
||||
return {
|
||||
mainState: "intro",
|
||||
isCinematicPlaying: false,
|
||||
intro: {
|
||||
dialogueAudio: null,
|
||||
hasCompleted: false,
|
||||
isBikeUnlocked: false,
|
||||
},
|
||||
bike: {
|
||||
currentStep: "locked",
|
||||
dialogueAudio: null,
|
||||
isRepaired: false,
|
||||
},
|
||||
pylone: {
|
||||
currentStep: "locked",
|
||||
dialogueAudio: null,
|
||||
isPowered: false,
|
||||
},
|
||||
ferme: {
|
||||
currentStep: "locked",
|
||||
dialogueAudio: null,
|
||||
irrigationFixed: false,
|
||||
},
|
||||
outro: {
|
||||
dialogueAudio: null,
|
||||
hasStarted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useGameStore = create<GameStore>()((set) => ({
|
||||
...createInitialGameState(),
|
||||
setMainState: (mainState) => set({ mainState }),
|
||||
setCinematicPlaying: (isCinematicPlaying) => set({ isCinematicPlaying }),
|
||||
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 } })),
|
||||
setMissionStep: (mission, step) =>
|
||||
set((state) => setMissionStepState(state, mission, step)),
|
||||
completeIntro: () => set(completeIntroState),
|
||||
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) => {
|
||||
if (state.mainState === "intro") {
|
||||
return completeIntroState(state);
|
||||
}
|
||||
|
||||
if (isRepairMissionId(state.mainState)) {
|
||||
return advanceRepairMissionState(state, state.mainState);
|
||||
}
|
||||
|
||||
return startOutroState(state);
|
||||
}),
|
||||
rewindGameState: () =>
|
||||
set((state) => {
|
||||
if (state.mainState === "intro") {
|
||||
return { intro: { ...state.intro, hasCompleted: false } };
|
||||
}
|
||||
|
||||
if (isRepairMissionId(state.mainState)) {
|
||||
return rewindRepairMissionState(state, state.mainState);
|
||||
}
|
||||
|
||||
return { outro: { ...state.outro, hasStarted: false } };
|
||||
}),
|
||||
resetGame: () => set(createInitialGameState()),
|
||||
}));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { create } from "zustand";
|
||||
import type { GameStep } from "@/types/game";
|
||||
|
||||
interface MissionFlowState {
|
||||
activityCity: boolean;
|
||||
canMove: boolean;
|
||||
dialogMessage: string | null;
|
||||
playerName: string;
|
||||
step: GameStep;
|
||||
}
|
||||
|
||||
interface MissionFlowActions {
|
||||
hideDialog: () => void;
|
||||
setActivityCity: (value: boolean) => void;
|
||||
setCanMove: (canMove: boolean) => void;
|
||||
setPlayerName: (name: string) => void;
|
||||
setStep: (step: GameStep) => void;
|
||||
showDialog: (message: string) => void;
|
||||
}
|
||||
|
||||
export const useMissionFlowStore = create<
|
||||
MissionFlowState & MissionFlowActions
|
||||
>((set) => ({
|
||||
activityCity: true,
|
||||
canMove: false,
|
||||
dialogMessage: null,
|
||||
playerName: "",
|
||||
step: "intro",
|
||||
hideDialog: () => set({ dialogMessage: null }),
|
||||
setActivityCity: (activityCity) => set({ activityCity }),
|
||||
setCanMove: (canMove) => set({ canMove }),
|
||||
setPlayerName: (playerName) => set({ playerName }),
|
||||
setStep: (step) => set({ step }),
|
||||
showDialog: (dialogMessage) => set({ dialogMessage }),
|
||||
}));
|
||||
@@ -0,0 +1,87 @@
|
||||
import { create } from "zustand";
|
||||
import { AudioManager } from "@/managers/AudioManager";
|
||||
import type { AudioCategory } from "@/managers/AudioManager";
|
||||
|
||||
export type SubtitleLanguage = "fr" | "en";
|
||||
export type RepairRuntime = "js" | "python";
|
||||
|
||||
interface SettingsState {
|
||||
isSettingsMenuOpen: boolean;
|
||||
musicVolume: number;
|
||||
sfxVolume: number;
|
||||
dialogueVolume: number;
|
||||
subtitlesEnabled: boolean;
|
||||
subtitleLanguage: SubtitleLanguage;
|
||||
repairRuntime: RepairRuntime;
|
||||
}
|
||||
|
||||
interface SettingsActions {
|
||||
setSettingsMenuOpen: (open: boolean) => void;
|
||||
setMusicVolume: (volume: number) => void;
|
||||
setSfxVolume: (volume: number) => void;
|
||||
setDialogueVolume: (volume: number) => void;
|
||||
setSubtitlesEnabled: (enabled: boolean) => void;
|
||||
setSubtitleLanguage: (language: SubtitleLanguage) => void;
|
||||
setRepairRuntime: (runtime: RepairRuntime) => void;
|
||||
resetSettings: () => void;
|
||||
}
|
||||
|
||||
type SettingsStore = SettingsState & SettingsActions;
|
||||
|
||||
const DEFAULT_SETTINGS: SettingsState = {
|
||||
isSettingsMenuOpen: false,
|
||||
musicVolume: 1,
|
||||
sfxVolume: 1,
|
||||
dialogueVolume: 1,
|
||||
subtitlesEnabled: true,
|
||||
subtitleLanguage: "fr",
|
||||
repairRuntime: "js",
|
||||
};
|
||||
|
||||
function clampVolume(volume: number): number {
|
||||
return Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
function setAudioCategoryVolume(
|
||||
category: AudioCategory,
|
||||
volume: number,
|
||||
): number {
|
||||
const nextVolume = clampVolume(volume);
|
||||
AudioManager.getInstance().setCategoryVolume(category, nextVolume);
|
||||
return nextVolume;
|
||||
}
|
||||
|
||||
function applyDefaultAudioSettings(): void {
|
||||
AudioManager.getInstance().setCategoryVolume(
|
||||
"music",
|
||||
DEFAULT_SETTINGS.musicVolume,
|
||||
);
|
||||
AudioManager.getInstance().setCategoryVolume(
|
||||
"sfx",
|
||||
DEFAULT_SETTINGS.sfxVolume,
|
||||
);
|
||||
AudioManager.getInstance().setCategoryVolume(
|
||||
"dialogue",
|
||||
DEFAULT_SETTINGS.dialogueVolume,
|
||||
);
|
||||
}
|
||||
|
||||
applyDefaultAudioSettings();
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>()((set) => ({
|
||||
...DEFAULT_SETTINGS,
|
||||
setSettingsMenuOpen: (isSettingsMenuOpen) => set({ isSettingsMenuOpen }),
|
||||
setMusicVolume: (volume) =>
|
||||
set({ musicVolume: setAudioCategoryVolume("music", volume) }),
|
||||
setSfxVolume: (volume) =>
|
||||
set({ sfxVolume: setAudioCategoryVolume("sfx", volume) }),
|
||||
setDialogueVolume: (volume) =>
|
||||
set({ dialogueVolume: setAudioCategoryVolume("dialogue", volume) }),
|
||||
setSubtitlesEnabled: (subtitlesEnabled) => set({ subtitlesEnabled }),
|
||||
setSubtitleLanguage: (subtitleLanguage) => set({ subtitleLanguage }),
|
||||
setRepairRuntime: (repairRuntime) => set({ repairRuntime }),
|
||||
resetSettings: () => {
|
||||
applyDefaultAudioSettings();
|
||||
set(DEFAULT_SETTINGS);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,24 @@
|
||||
import { create } from "zustand";
|
||||
import type { DialogueSpeaker } from "@/types/dialogues/dialogues";
|
||||
|
||||
interface ActiveSubtitle {
|
||||
speaker: DialogueSpeaker;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface SubtitleState {
|
||||
activeSubtitle: ActiveSubtitle | null;
|
||||
}
|
||||
|
||||
interface SubtitleActions {
|
||||
setActiveSubtitle: (subtitle: ActiveSubtitle | null) => void;
|
||||
clearActiveSubtitle: () => void;
|
||||
}
|
||||
|
||||
type SubtitleStore = SubtitleState & SubtitleActions;
|
||||
|
||||
export const useSubtitleStore = create<SubtitleStore>()((set) => ({
|
||||
activeSubtitle: null,
|
||||
setActiveSubtitle: (activeSubtitle) => set({ activeSubtitle }),
|
||||
clearActiveSubtitle: () => set({ activeSubtitle: null }),
|
||||
}));
|
||||
Reference in New Issue
Block a user