feat: restaure l'éditeur map et ajoute les personnages
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
tom-boullay
2026-05-28 15:49:57 +02:00
parent fcdbf7270c
commit d5675fe82c
21 changed files with 454 additions and 57 deletions
+3 -4
View File
@@ -122,8 +122,7 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
- Click: select a node.
- `Shift` + right click: add or remove a node from the multi-selection.
- `Esc`: clear selection.
- Click empty space: clear selection.
- Selection lock button: prevent object clicks, empty-space clicks, and `Esc` from changing the current selection.
- Selection lock button: prevent object clicks and `Esc` from changing the current selection.
- Selection clear button: intentionally clear the current selection even when the lock is active.
- `T`: translate mode.
- `R`: rotate mode.
@@ -190,9 +189,9 @@ The state is passed to:
- `EditorControls`, to render the lock/unlock button
- `EditorScene`, to block `Esc` deselection when locked
- `EditorMap`, to block object selection and empty-space deselection when locked
- `EditorMap`, to block object selection when locked
The clear button calls `onClearSelection` directly from `EditorControls`. This is intentionally separate from scene click behavior so the user always has an explicit way to clear the selection.
The clear button calls `onClearSelection` directly from `EditorControls`. Clicking empty canvas space does not clear the current selection; use `Esc` or the explicit clear button instead.
## Dialogue SRT Editing
+2 -3
View File
@@ -72,7 +72,7 @@ Use the trash button in `Selection` to delete the selected node from the map tre
| -------------------- | -------------------------- |
| Select object | Click object |
| Toggle multi-select | `Shift` + right click |
| Deselect | `Esc` or click empty space |
| Deselect | `Esc` |
| Lock selection | `Lock` button in Selection |
| Clear selection | `X` button in Selection |
| Translate mode | `T` |
@@ -91,7 +91,7 @@ The `Selection` section shows the selected object name and its index in `public/
- Click an object to select it.
- Use `Shift + right click` on objects to add or remove them from a multi-selection.
- When several objects are selected, the gizmo appears on the selection group and applies translate, rotate, or scale to each selected node.
- Click empty space or press `Esc` to clear the selection.
- Press `Esc` to clear the selection.
- Use the `X` button to clear the selection explicitly.
- Use the `Lock` button to protect the current selection while editing.
- Use the scale fields to edit X/Y/Z scale precisely.
@@ -108,7 +108,6 @@ This is intended for map objects that should sit on the ground. Disable it when
When selection is locked:
- clicking another object does not change the selection
- clicking empty space does not clear the selection
- pressing `Esc` does not clear the selection
- the `X` button still clears the selection intentionally
+12 -1
View File
@@ -41,6 +41,7 @@ interface EditorControlsProps {
onClearSelection: () => void;
snapToTerrain: boolean;
onSnapToTerrainToggle: () => void;
onSnapAllToTerrain: () => void;
newNodeName: string;
onNewNodeNameChange: (value: string) => void;
onAddNode: () => void;
@@ -70,7 +71,7 @@ const EDITOR_SHORTCUTS = [
["Shift + Right click", "Toggle multi-selection"],
["T / R / S", "Transform mode"],
["Ctrl Z / Y", "Undo / redo"],
["Esc", "Deselect"],
["Esc / X button", "Clear selection"],
["WASD", "Move when locked"],
] as const;
@@ -117,6 +118,7 @@ export function EditorControls({
onClearSelection,
snapToTerrain,
onSnapToTerrainToggle,
onSnapAllToTerrain,
newNodeName,
onNewNodeNameChange,
onAddNode,
@@ -228,6 +230,15 @@ export function EditorControls({
/>
<span>Snap terrain on move</span>
</label>
<button
type="button"
className="editor-history-button"
onClick={onSnapAllToTerrain}
>
<ScanSearch size={15} aria-hidden="true" />
Snap all to terrain
</button>
</section>
<section
+74 -4
View File
@@ -12,6 +12,8 @@ import {
isEditorVisibleMapNode,
getTerrainMapNode,
} from "@/utils/map/mapRuntimeClassification";
import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
import { getVegetationModelScaleMultiplier } from "@/data/world/vegetationConfig";
interface EditorMapProps {
sceneData: SceneData;
@@ -28,6 +30,8 @@ interface EditorMapProps {
onTransformStart: () => void;
onTransformEnd: () => void;
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
snapAllToTerrainRequest: number;
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
}
type EditorNodeObjectRef = React.RefObject<Map<number, THREE.Object3D>>;
@@ -64,6 +68,32 @@ const TEMP_POSITION = new THREE.Vector3();
const TEMP_QUATERNION = new THREE.Quaternion();
const TEMP_SCALE = new THREE.Vector3();
function isOriginPosition(position: MapNode["position"]): boolean {
return position.every((value) => Math.abs(value) < 0.0001);
}
function isSnapAllCandidate(node: MapNode): boolean {
return (
isEditorVisibleMapNode(node) &&
node.name !== "terrain" &&
!isOriginPosition(node.position)
);
}
function shouldRenderEditorNode(
node: MapNode,
selectedNodeName: string | null,
): boolean {
if (!isEditorVisibleMapNode(node)) return false;
return selectedNodeName === null || node.name === selectedNodeName;
}
function getEditorModelVisualScaleMultiplier(name: string): number {
return (
getMapModelScaleMultiplier(name) * getVegetationModelScaleMultiplier(name)
);
}
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
object.position.set(...node.position);
object.rotation.set(...node.rotation);
@@ -177,14 +207,21 @@ export function EditorMap({
onTransformStart,
onTransformEnd,
onNodeTransform,
snapAllToTerrainRequest,
onSnapAllToTerrain,
}: EditorMapProps): React.JSX.Element {
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
const transformGroupRef = useRef<THREE.Group>(null);
const transformSnapshotRef = useRef<TransformSnapshot | null>(null);
const terrainHeight = useTerrainHeightSampler();
const lastSnapAllToTerrainRequestRef = useRef(0);
const selectedIndexSet = new Set(selectedNodeIndexes);
const isMultiSelection = selectedNodeIndexes.length > 1;
const selectedNodeName =
selectedNodeIndex !== null
? (sceneData.mapNodes[selectedNodeIndex]?.name ?? null)
: null;
const getTransformObject = useCallback(() => {
if (isMultiSelection) {
@@ -333,6 +370,37 @@ export function EditorMap({
prepareTransformGroup();
}, [prepareTransformGroup]);
useEffect(() => {
if (
snapAllToTerrainRequest === 0 ||
snapAllToTerrainRequest === lastSnapAllToTerrainRequestRef.current
) {
return;
}
lastSnapAllToTerrainRequestRef.current = snapAllToTerrainRequest;
const snappedNodes = sceneData.mapNodes.map((node) => {
if (!isSnapAllCandidate(node)) return node;
const [x, y, z] = node.position;
const terrainY = terrainHeight.getHeight(x, z);
if (terrainY === null || Math.abs(terrainY - y) < 0.0001) return node;
return {
...node,
position: [x, terrainY, z] satisfies [number, number, number],
};
});
onSnapAllToTerrain(snappedNodes);
}, [
onSnapAllToTerrain,
sceneData.mapNodes,
snapAllToTerrainRequest,
terrainHeight,
]);
// TransformControls needs the current Three object; editor refs are managed outside React rendering.
// eslint-disable-next-line react-hooks/refs
const selectedObject = getTransformObject();
@@ -370,7 +438,7 @@ export function EditorMap({
/>
) : null}
{sceneData.mapNodes.map((node, index) => {
if (!isEditorVisibleMapNode(node)) {
if (!shouldRenderEditorNode(node, selectedNodeName)) {
return null;
}
@@ -451,6 +519,7 @@ function EditorModelNode({
scale: node.scale,
});
const sceneInstance = useClonedObject(scene);
const visualScaleMultiplier = getEditorModelVisualScaleMultiplier(node.name);
const pointerHandlers = createEditorNodePointerHandlers(
index,
onSelectNode,
@@ -512,14 +581,15 @@ function EditorModelNode({
}, []);
return (
<primitive
<group
ref={groupRef}
object={sceneInstance}
position={node.position}
rotation={node.rotation}
scale={node.scale}
{...pointerHandlers}
/>
>
<primitive object={sceneInstance} scale={visualScaleMultiplier} />
</group>
);
}
@@ -6,6 +6,7 @@ import * as THREE from "three";
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
import { EditorMap } from "@/components/editor/scene/EditorMap";
import { FlyController } from "@/controls/editor/FlyController";
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
@@ -33,6 +34,8 @@ interface EditorSceneProps {
onTransformStart: () => void;
onTransformEnd: () => void;
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
snapAllToTerrainRequest: number;
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
onUndo: () => void;
onRedo: () => void;
resetCameraRequest: number;
@@ -58,6 +61,8 @@ export function EditorScene({
onTransformStart,
onTransformEnd,
onNodeTransform,
snapAllToTerrainRequest,
onSnapAllToTerrain,
onUndo,
onRedo,
resetCameraRequest,
@@ -224,8 +229,12 @@ export function EditorScene({
onTransformStart={onTransformStart}
onTransformEnd={onTransformEnd}
onNodeTransform={onNodeTransform}
snapAllToTerrainRequest={snapAllToTerrainRequest}
onSnapAllToTerrain={onSnapAllToTerrain}
/>
<PersonnageSystem />
<ambientLight intensity={0.6} />
<directionalLight position={[10, 20, 10]} intensity={1.5} castShadow />
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
+26 -28
View File
@@ -68,32 +68,6 @@ export function AnimatedModel({
}
}, [mixer, onAnimationEnd]);
const play = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fade);
});
action.reset().fadeIn(fade).play();
setCurrentAnim(name);
}
},
[actions, fadeDuration],
);
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const fadeTo = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
@@ -107,6 +81,19 @@ export function AnimatedModel({
},
[actions, fadeDuration],
);
const play = fadeTo;
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const setSpeed = useCallback(
(newSpeed: number) => {
@@ -140,10 +127,21 @@ export function AnimatedModel({
}
if (defaultAction) {
defaultAction.play();
Object.values(actions).forEach((action) => {
if (action && action !== defaultAction) action.fadeOut(fadeDuration);
});
defaultAction.reset().fadeIn(fadeDuration).play();
onLoaded?.();
}
}, [actions, defaultAnimation, modelPath, names, autoPlay, onLoaded]);
}, [
actions,
defaultAnimation,
fadeDuration,
modelPath,
names,
autoPlay,
onLoaded,
]);
const contextValue: AnimatedModelContextValue = {
play,
@@ -0,0 +1,53 @@
import type { Vector3Tuple } from "@/types/three/three";
export type PersonnageId = "electricienne" | "gerant" | "fermier";
export interface PersonnageConfig {
id: PersonnageId;
label: string;
modelPath: string;
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
animations: readonly string[];
defaultAnimation: string;
}
export const PERSONNAGE_CONFIGS = {
electricienne: {
id: "electricienne",
label: "Electricienne",
modelPath: "/models/electricienne-animated/model.gltf",
position: [-40.5, 0, 45.5],
rotation: [0, -0.35, 0],
scale: [1, 1, 1],
animations: ["Dance"],
defaultAnimation: "Dance",
},
gerant: {
id: "gerant",
label: "Gerant",
modelPath: "/models/gerant-animated/model.gltf",
position: [45.2, 0, 45.5],
rotation: [0, -1.55, 0],
scale: [1, 1, 1],
animations: ["idle", "walk"],
defaultAnimation: "idle",
},
fermier: {
id: "fermier",
label: "Fermier",
modelPath: "/models/fermier-animated/model.gltf",
position: [-6.5, 0, -69.5],
rotation: [0, -1.18, 0],
scale: [1, 1, 1],
animations: ["idle", "walk"],
defaultAnimation: "idle",
},
} satisfies Record<PersonnageId, PersonnageConfig>;
export const PERSONNAGE_IDS = [
"electricienne",
"gerant",
"fermier",
] as const satisfies readonly PersonnageId[];
+108
View File
@@ -0,0 +1,108 @@
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import {
PERSONNAGE_CONFIGS,
PERSONNAGE_IDS,
} from "@/data/world/personnages/personnageConfig";
import { usePersonnageDebugStore } from "@/managers/stores/usePersonnageDebugStore";
function createAnimationOptions(
animations: readonly string[],
): Record<string, string> {
if (animations.length === 0) {
return { None: "" };
}
return Object.fromEntries(
animations.map((animation) => [animation || "None", animation]),
);
}
export function usePersonnageDebug(): void {
useDebugFolder("Personnages", (folder) => {
const store = usePersonnageDebugStore.getState();
for (const id of PERSONNAGE_IDS) {
const config = PERSONNAGE_CONFIGS[id];
const state = store.personnages[id];
const characterFolder = folder.addFolder(config.label);
const controls = {
animation: state.animation,
positionX: state.position[0],
positionY: state.position[1],
positionZ: state.position[2],
rotationX: state.rotation[0],
rotationY: state.rotation[1],
rotationZ: state.rotation[2],
scaleX: state.scale[0],
scaleY: state.scale[1],
scaleZ: state.scale[2],
};
characterFolder
.add(controls, "animation", createAnimationOptions(config.animations))
.name("Animation")
.onChange((animation: string) => {
usePersonnageDebugStore.getState().setAnimation(id, animation);
});
characterFolder
.add(controls, "positionX", -120, 120, 0.1)
.name("Position X")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setPosition(id, 0, value);
});
characterFolder
.add(controls, "positionY", -20, 40, 0.1)
.name("Position Y")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setPosition(id, 1, value);
});
characterFolder
.add(controls, "positionZ", -120, 120, 0.1)
.name("Position Z")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setPosition(id, 2, value);
});
characterFolder
.add(controls, "rotationX", -Math.PI, Math.PI, 0.01)
.name("Rotation X")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setRotation(id, 0, value);
});
characterFolder
.add(controls, "rotationY", -Math.PI, Math.PI, 0.01)
.name("Rotation Y")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setRotation(id, 1, value);
});
characterFolder
.add(controls, "rotationZ", -Math.PI, Math.PI, 0.01)
.name("Rotation Z")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setRotation(id, 2, value);
});
characterFolder
.add(controls, "scaleX", 0.1, 5, 0.05)
.name("Scale X")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setScale(id, 0, value);
});
characterFolder
.add(controls, "scaleY", 0.1, 5, 0.05)
.name("Scale Y")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setScale(id, 1, value);
});
characterFolder
.add(controls, "scaleZ", 0.1, 5, 0.05)
.name("Scale Z")
.onChange((value: number) => {
usePersonnageDebugStore.getState().setScale(id, 2, value);
});
characterFolder.close();
}
});
}
@@ -0,0 +1,89 @@
import { create } from "zustand";
import {
PERSONNAGE_CONFIGS,
PERSONNAGE_IDS,
type PersonnageId,
} from "@/data/world/personnages/personnageConfig";
import type { Vector3Tuple } from "@/types/three/three";
interface PersonnageDebugState {
animation: string;
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
}
interface PersonnageDebugStore {
personnages: Record<PersonnageId, PersonnageDebugState>;
setAnimation: (id: PersonnageId, animation: string) => void;
setPosition: (id: PersonnageId, axis: 0 | 1 | 2, value: number) => void;
setRotation: (id: PersonnageId, axis: 0 | 1 | 2, value: number) => void;
setScale: (id: PersonnageId, axis: 0 | 1 | 2, value: number) => void;
}
function updateVector(
vector: Vector3Tuple,
axis: 0 | 1 | 2,
value: number,
): Vector3Tuple {
const next: Vector3Tuple = [...vector];
next[axis] = value;
return next;
}
const initialPersonnages = Object.fromEntries(
PERSONNAGE_IDS.map((id) => {
const config = PERSONNAGE_CONFIGS[id];
return [
id,
{
animation: config.defaultAnimation,
position: [...config.position],
rotation: [...config.rotation],
scale: [...config.scale],
},
];
}),
) as Record<PersonnageId, PersonnageDebugState>;
export const usePersonnageDebugStore = create<PersonnageDebugStore>((set) => ({
personnages: initialPersonnages,
setAnimation: (id, animation) =>
set((state) => ({
personnages: {
...state.personnages,
[id]: { ...state.personnages[id], animation },
},
})),
setPosition: (id, axis, value) =>
set((state) => ({
personnages: {
...state.personnages,
[id]: {
...state.personnages[id],
position: updateVector(state.personnages[id].position, axis, value),
},
},
})),
setRotation: (id, axis, value) =>
set((state) => ({
personnages: {
...state.personnages,
[id]: {
...state.personnages[id],
rotation: updateVector(state.personnages[id].rotation, axis, value),
},
},
})),
setScale: (id, axis, value) =>
set((state) => ({
personnages: {
...state.personnages,
[id]: {
...state.personnages[id],
scale: updateVector(state.personnages[id].scale, axis, value),
},
},
})),
}));
+1 -8
View File
@@ -2,12 +2,5 @@ import editor from "../../../../docs/user/editor.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsEditorPage(): React.JSX.Element {
return (
<DocsDocument
content={editor}
frContent={editor}
meta="14"
title="Editor User Guide"
/>
);
return <DocsDocument content={editor} meta="14" title="Editor User Guide" />;
}
+1 -8
View File
@@ -2,12 +2,5 @@ import repairGame from "../../../../docs/technical/repair-game.md?raw";
import { DocsDocument } from "@/components/docs/DocsDocument";
export function DocsRepairGamePage(): React.JSX.Element {
return (
<DocsDocument
content={repairGame}
frContent={repairGame}
meta="04"
title="Repair Game"
/>
);
return <DocsDocument content={repairGame} meta="04" title="Repair Game" />;
}
+33
View File
@@ -323,6 +323,7 @@ export function EditorPage(): React.JSX.Element {
const [newNodeName, setNewNodeName] = useState(DEFAULT_NEW_NODE_NAME);
const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
const [resetCameraRequest, setResetCameraRequest] = useState(0);
const [snapAllToTerrainRequest, setSnapAllToTerrainRequest] = useState(0);
const [focusSelectedCameraRequest, setFocusSelectedCameraRequest] =
useState(0);
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
@@ -372,9 +373,14 @@ export function EditorPage(): React.JSX.Element {
const handleSelectNode = useCallback((index: number | null) => {
setSelectedNodeIndex(index);
setSelectedNodeIndexes(index === null ? [] : [index]);
if (index !== null) {
setCameraViewMode("object");
return;
}
setCameraViewMode("home");
setResetCameraRequest((request) => request + 1);
}, []);
const handleToggleNodeSelection = useCallback((index: number) => {
@@ -387,6 +393,9 @@ export function EditorPage(): React.JSX.Element {
setSelectedNodeIndex(nextIndexes.at(-1) ?? null);
if (nextIndexes.length > 0) {
setCameraViewMode("object");
} else {
setCameraViewMode("home");
setResetCameraRequest((request) => request + 1);
}
return nextIndexes;
@@ -396,6 +405,8 @@ export function EditorPage(): React.JSX.Element {
const handleClearSelection = useCallback(() => {
setSelectedNodeIndex(null);
setSelectedNodeIndexes([]);
setCameraViewMode("home");
setResetCameraRequest((request) => request + 1);
}, []);
const handleSelectionLockToggle = useCallback(() => {
@@ -406,6 +417,25 @@ export function EditorPage(): React.JSX.Element {
setSnapToTerrain((enabled) => !enabled);
}, []);
const handleSnapAllToTerrainRequest = useCallback(() => {
setSnapAllToTerrainRequest((request) => request + 1);
}, []);
const handleSnapAllToTerrain = useCallback(
(mapNodes: MapNode[]) => {
setSceneData((prev) => {
if (!prev) return null;
const nextSceneData = { ...prev, mapNodes };
if (!prev.mapTree) return nextSceneData;
const mapTree = mergeFlatNodeTransformsIntoTree(nextSceneData);
return updateSceneDataTree(nextSceneData, mapTree);
});
},
[setSceneData],
);
const handleNewNodeNameChange = useCallback((value: string) => {
setNewNodeName(value);
}, []);
@@ -710,6 +740,8 @@ export function EditorPage(): React.JSX.Element {
onTransformStart={handleTransformStart}
onTransformEnd={handleTransformEnd}
onNodeTransform={handleNodeTransform}
snapAllToTerrainRequest={snapAllToTerrainRequest}
onSnapAllToTerrain={handleSnapAllToTerrain}
onUndo={handleUndo}
onRedo={handleRedo}
resetCameraRequest={resetCameraRequest}
@@ -748,6 +780,7 @@ export function EditorPage(): React.JSX.Element {
onClearSelection={handleClearSelection}
snapToTerrain={snapToTerrain}
onSnapToTerrainToggle={handleSnapToTerrainToggle}
onSnapAllToTerrain={handleSnapAllToTerrainRequest}
newNodeName={newNodeName}
onNewNodeNameChange={handleNewNodeNameChange}
onAddNode={handleAddNode}
+1
View File
@@ -24,6 +24,7 @@ const DEBUG_FOLDER_ORDER = [
"Interaction",
"Hand Tracking",
"Map",
"Personnages",
] as const;
function isRecord(value: unknown): value is Record<string, unknown> {
+4
View File
@@ -7,6 +7,7 @@ import {
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
import { usePersonnageDebug } from "@/hooks/debug/usePersonnageDebug";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
@@ -28,6 +29,7 @@ import { GameMusic } from "@/world/GameMusic";
import { Lighting } from "@/world/Lighting";
import { GameMap } from "@/world/GameMap";
import { GameStageContent } from "@/world/GameStageContent";
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
import { Player } from "@/world/player/Player";
import { TestMap } from "@/world/debug/TestMap";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
@@ -39,6 +41,7 @@ interface WorldProps {
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
useEnvironmentDebug();
useMapPerformanceDebug();
usePersonnageDebug();
const cameraMode = useCameraMode();
const sceneMode = useSceneMode();
@@ -87,6 +90,7 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
onLoadingStateChange={onLoadingStateChange}
onOctreeReady={handleOctreeReady}
/>
<PersonnageSystem />
{showGameStage ? (
<Physics>
<GameStageLoaded onLoaded={handleGameStageLoaded} />
+1 -1
View File
@@ -31,7 +31,7 @@ import type { OctreeReadyHandler } from "@/types/three/three";
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
const ELECTRICIENNE_ANIMATED_MODEL_PATH =
"/models/electricienne_animated/model.gltf";
"/models/electricienne-animated/model.gltf";
interface TestMapProps {
onOctreeReady: OctreeReadyHandler;
@@ -0,0 +1,37 @@
import { Suspense } from "react";
import { AnimatedModel } from "@/components/three/models/AnimatedModel";
import {
PERSONNAGE_CONFIGS,
PERSONNAGE_IDS,
type PersonnageId,
} from "@/data/world/personnages/personnageConfig";
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
import { usePersonnageDebugStore } from "@/managers/stores/usePersonnageDebugStore";
function PersonnageModel({ id }: { id: PersonnageId }): React.JSX.Element {
const config = PERSONNAGE_CONFIGS[id];
const state = usePersonnageDebugStore((store) => store.personnages[id]);
const position = useTerrainSnappedPosition(state.position);
return (
<AnimatedModel
modelPath={config.modelPath}
defaultAnimation={state.animation}
position={position}
rotation={state.rotation}
scale={state.scale}
/>
);
}
export function PersonnageSystem(): React.JSX.Element {
return (
<group name="personnage-system">
{PERSONNAGE_IDS.map((id) => (
<Suspense key={id} fallback={null}>
<PersonnageModel id={id} />
</Suspense>
))}
</group>
);
}