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,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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user