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:
Tom Boullay
2026-05-11 17:46:42 +02:00
945 changed files with 26164 additions and 1569 deletions
+13
View File
@@ -0,0 +1,13 @@
import animation from "../../../../docs/technical/animation.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsAnimationPage(): React.JSX.Element {
return (
<DocsDocument
content={animation}
frContent={animation}
meta="08"
title="Animation & 3D Model System"
/>
);
}
+14
View File
@@ -0,0 +1,14 @@
import architecture from "../../../../docs/technical/architecture.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
import { architectureFr } from "@/data/docs/docsTranslations";
export function DocsArchitecturePage(): React.JSX.Element {
return (
<DocsDocument
content={architecture}
frContent={architectureFr}
meta="02"
title="Architecture actuelle"
/>
);
}
+14
View File
@@ -0,0 +1,14 @@
import editor from "../../../../docs/user/editor.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
import { editorFr } from "@/data/docs/docsTranslations";
export function DocsEditorPage(): React.JSX.Element {
return (
<DocsDocument
content={editor}
frContent={editorFr}
meta="09"
title="Editor User Guide"
/>
);
}
+14
View File
@@ -0,0 +1,14 @@
import features from "../../../../docs/user/features.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
import { featuresFr } from "@/data/docs/docsTranslations";
export function DocsFeaturesPage(): React.JSX.Element {
return (
<DocsDocument
content={features}
frContent={featuresFr}
meta="06"
title="Features"
/>
);
}
+13
View File
@@ -0,0 +1,13 @@
import handTracking from "../../../../docs/technical/hand-tracking.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsHandTrackingPage(): React.JSX.Element {
return (
<DocsDocument
content={handTracking}
frContent={handTracking}
meta="05"
title="Hand Tracking Technical Notes"
/>
);
}
+13
View File
@@ -0,0 +1,13 @@
import mainFeature from "../../../../docs/user/main-feature.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsMainFeaturePage(): React.JSX.Element {
return (
<DocsDocument
content={mainFeature}
frContent={mainFeature}
meta="07"
title="Main Feature"
/>
);
}
+14
View File
@@ -0,0 +1,14 @@
import readme from "../../../README.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
import { readmeFr } from "@/data/docs/docsTranslations";
export function DocsReadmePage(): React.JSX.Element {
return (
<DocsDocument
content={readme}
frContent={readmeFr}
meta="01"
title="README"
/>
);
}
@@ -0,0 +1,14 @@
import targetArchitecture from "../../../../docs/technical/target-architecture.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
import { targetArchitectureFr } from "@/data/docs/docsTranslations";
export function DocsTargetArchitecturePage(): React.JSX.Element {
return (
<DocsDocument
content={targetArchitecture}
frContent={targetArchitectureFr}
meta="03"
title="Architecture cible"
/>
);
}
+13
View File
@@ -0,0 +1,13 @@
import technicalEditor from "../../../../docs/technical/editor.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsTechnicalEditorPage(): React.JSX.Element {
return (
<DocsDocument
content={technicalEditor}
frContent={technicalEditor}
meta="04"
title="Editor Technical Notes"
/>
);
}
+14
View File
@@ -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"
/>
);
}
+293
View File
@@ -0,0 +1,293 @@
import { Suspense, useCallback, useEffect, useState } from "react";
import { Canvas } from "@react-three/fiber";
import { useProgress } from "@react-three/drei";
import { EditorControls } from "@/components/editor/EditorControls";
import { EditorScene } from "@/components/editor/scene/EditorScene";
import type { EditorCinematicPreviewRequest } from "@/components/editor/scene/EditorScene";
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
import { Subtitles } from "@/components/ui/Subtitles";
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
import type { MapNode, SceneData, TransformMode } from "@/types/editor/editor";
import {
INITIAL_SCENE_LOADING_STATE,
type SceneLoadingChangeHandler,
type SceneLoadingState,
} from "@/types/world/sceneLoading";
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
interface EditorSceneLoadingTrackerProps {
onLoadingStateChange: SceneLoadingChangeHandler;
}
function serializeMapNodes(sceneData: SceneData): string {
return JSON.stringify(sceneData.mapNodes, null, 2);
}
function EditorSceneLoadingTracker({
onLoadingStateChange,
}: EditorSceneLoadingTrackerProps): null {
const { active, progress } = useProgress();
useEffect(() => {
if (active) {
onLoadingStateChange({
currentStep: "Importation des models",
progress: 0.2 + (progress / 100) * 0.7,
status: "loading",
});
return;
}
onLoadingStateChange({
currentStep: "Gameplay prêt",
progress: 1,
status: "ready",
});
}, [active, onLoadingStateChange, progress]);
return null;
}
export function EditorPage(): React.JSX.Element {
const {
hasMapJson,
isMapLoading,
sceneData,
setSceneData,
handleFolderUpload,
} = useEditorSceneData();
const [selectedNodeIndex, setSelectedNodeIndex] = useState<number | null>(
null,
);
const [hoveredNodeIndex, setHoveredNodeIndex] = useState<number | null>(null);
const [transformMode, setTransformMode] =
useState<TransformMode>("translate");
const [isPlayerMode, setIsPlayerMode] = useState(false);
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
{
...INITIAL_SCENE_LOADING_STATE,
currentStep: "Montage progressif des models",
progress: 0.2,
},
);
const handleSceneLoadingStateChange = useCallback(
(nextState: SceneLoadingState) => {
setSceneLoadingState((currentState) => {
const shouldRestartProgress = currentState.status === "ready";
return {
...nextState,
progress: shouldRestartProgress
? nextState.progress
: Math.max(currentState.progress, nextState.progress),
};
});
},
[],
);
const editorLoadingState = isMapLoading
? {
currentStep: "Récupération blocking",
progress: 0.08,
status: "loading" as const,
}
: sceneLoadingState;
const [cinematicPreviewRequest, setCinematicPreviewRequest] =
useState<EditorCinematicPreviewRequest | null>(null);
const {
undoCount,
redoCount,
handleUndo,
handleRedo,
handleTransformStart,
handleTransformEnd,
} = useEditorHistory(sceneData, setSceneData);
const handleSelectNode = useCallback((index: number | null) => {
setSelectedNodeIndex(index);
}, []);
const handleHoverNode = useCallback((index: number | null) => {
setHoveredNodeIndex(index);
}, []);
const handleTransformModeChange = useCallback((mode: TransformMode) => {
setTransformMode(mode);
}, []);
const handleSaveToServer = useCallback(async () => {
if (!sceneData) return;
const json = serializeMapNodes(sceneData);
try {
const response = await fetch("/api/save-map", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: json,
});
if (response.ok) {
alert("Map enregistrée avec succès!");
} else {
alert(SAVE_ERROR_MESSAGE);
}
} catch (err) {
console.error("Error saving map:", err);
alert(SAVE_ERROR_MESSAGE);
}
}, [sceneData]);
const handleExportJson = useCallback(() => {
if (!sceneData) return;
const json = serializeMapNodes(sceneData);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "map.json";
a.click();
window.setTimeout(() => URL.revokeObjectURL(url), 0);
}, [sceneData]);
const handlePlayerMode = useCallback(() => {
setIsPlayerMode((prev) => !prev);
}, []);
const handlePreviewCinematic = useCallback(
(cinematic: CinematicDefinition) => {
setCinematicPreviewRequest({
id: window.crypto.randomUUID(),
cinematic,
});
},
[],
);
const handleCinematicPreviewComplete = useCallback(() => {
setCinematicPreviewRequest(null);
}, []);
const handleNodeTransform = useCallback(
(nodeIndex: number, updatedNode: MapNode) => {
setSceneData((prev) => {
if (!prev) return null;
const newMapNodes = [...prev.mapNodes];
newMapNodes[nodeIndex] = updatedNode;
return { ...prev, mapNodes: newMapNodes };
});
},
[setSceneData],
);
if (isMapLoading) {
return (
<div className="editor-container">
<SceneLoadingOverlay state={editorLoadingState} />
</div>
);
}
if (!hasMapJson) {
return (
<div className="editor-container">
<div className="editor-error">
<h2>Erreur : map.json introuvable</h2>
<p>
Le fichier map.json est requis dans le dossier <code>public/</code>.
</p>
<div className="editor-upload-section">
<h3>Télécharger un dossier contenant map.json</h3>
<label className="editor-drop-zone">
<input
type="file"
className="editor-folder-input"
onChange={handleFolderUpload}
multiple
{...{ webkitdirectory: "" }}
/>
Choisir un dossier contenant map.json
</label>
<div className="editor-folder-structure">
<h4>Structure requise :</h4>
<pre>
public/ <strong>map.json</strong> (à la racine) models/
arbre/ model.glb building/ model.gltf
...
</pre>
</div>
</div>
</div>
</div>
);
}
return (
<div className="editor-container">
<Canvas
camera={{ position: [0, 50, 100], fov: 50 }}
style={{ width: "100%", height: "100%" }}
onCreated={({ gl }) => {
gl.setClearColor("#050505");
}}
>
<EditorSceneLoadingTracker
onLoadingStateChange={handleSceneLoadingStateChange}
/>
<Suspense fallback={null}>
<EditorScene
sceneData={sceneData!}
selectedNodeIndex={selectedNodeIndex}
onSelectNode={handleSelectNode}
hoveredNodeIndex={hoveredNodeIndex}
onHoverNode={handleHoverNode}
transformMode={transformMode}
onTransformModeChange={handleTransformModeChange}
onTransformStart={handleTransformStart}
onTransformEnd={handleTransformEnd}
onNodeTransform={handleNodeTransform}
onUndo={handleUndo}
onRedo={handleRedo}
isPlayerMode={isPlayerMode}
cinematicPreviewRequest={cinematicPreviewRequest}
onCinematicPreviewComplete={handleCinematicPreviewComplete}
/>
</Suspense>
</Canvas>
<SceneLoadingOverlay state={editorLoadingState} />
{sceneData && (
<EditorControls
transformMode={transformMode}
onTransformModeChange={handleTransformModeChange}
selectedNodeIndex={selectedNodeIndex}
mapNodes={sceneData.mapNodes}
nodesCount={sceneData.mapNodes.length}
selectedNodeName={
selectedNodeIndex !== null && sceneData.mapNodes[selectedNodeIndex]
? sceneData.mapNodes[selectedNodeIndex].name || null
: null
}
undoCount={undoCount}
redoCount={redoCount}
onUndo={handleUndo}
onRedo={handleRedo}
onExportJson={handleExportJson}
onSaveToServer={import.meta.env.DEV ? handleSaveToServer : undefined}
onPlayerMode={handlePlayerMode}
onPreviewCinematic={handlePreviewCinematic}
isPlayerMode={isPlayerMode}
/>
)}
<Subtitles />
</div>
);
}
+76
View File
@@ -0,0 +1,76 @@
import { Suspense, useCallback, useEffect, useState } from "react";
import { Canvas } from "@react-three/fiber";
import * as THREE from "three";
import { DebugPerf } from "@/components/debug/DebugPerf";
import { DialogMessage } from "@/components/ui/DialogMessage";
import { GameUI } from "@/components/ui/GameUI";
import { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI";
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
import { useMissionFlowStore } from "@/managers/stores/useMissionFlowStore";
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
import {
INITIAL_SCENE_LOADING_STATE,
type SceneLoadingState,
} from "@/types/world/sceneLoading";
import { World } from "@/world/World";
export function HomePage(): React.JSX.Element {
const dialogMessage = useMissionFlowStore((state) => state.dialogMessage);
const hideDialog = useMissionFlowStore((state) => state.hideDialog);
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
INITIAL_SCENE_LOADING_STATE,
);
useEffect(() => {
if (!dialogMessage) return undefined;
const timeoutId = window.setTimeout(() => {
hideDialog();
}, 3000);
return () => {
window.clearTimeout(timeoutId);
};
}, [dialogMessage, hideDialog]);
const handleSceneLoadingStateChange = useCallback(
(nextState: SceneLoadingState) => {
setSceneLoadingState((currentState) => {
const shouldRestartProgress = currentState.status === "ready";
return {
...nextState,
progress: shouldRestartProgress
? nextState.progress
: Math.max(currentState.progress, nextState.progress),
};
});
},
[],
);
return (
<HandTrackingProvider>
<Canvas
camera={{ position: [85, 60, 85], fov: 42 }}
shadows={{ type: THREE.PCFShadowMap }}
>
<Suspense fallback={null}>
<World onLoadingStateChange={handleSceneLoadingStateChange} />
<DebugPerf />
</Suspense>
</Canvas>
<GameUI />
<IntroUI />
<BienvenueDisplay />
{dialogMessage ? (
<DialogMessage
message={dialogMessage}
duration={3000}
onClose={hideDialog}
/>
) : null}
<SceneLoadingOverlay state={sceneLoadingState} />
</HandTrackingProvider>
);
}