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
🔍 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:
@@ -122,8 +122,7 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
|
|||||||
- Click: select a node.
|
- Click: select a node.
|
||||||
- `Shift` + right click: add or remove a node from the multi-selection.
|
- `Shift` + right click: add or remove a node from the multi-selection.
|
||||||
- `Esc`: clear selection.
|
- `Esc`: clear selection.
|
||||||
- Click empty space: clear selection.
|
- Selection lock button: prevent object clicks and `Esc` from changing the current selection.
|
||||||
- Selection lock button: prevent object clicks, empty-space clicks, and `Esc` from changing the current selection.
|
|
||||||
- Selection clear button: intentionally clear the current selection even when the lock is active.
|
- Selection clear button: intentionally clear the current selection even when the lock is active.
|
||||||
- `T`: translate mode.
|
- `T`: translate mode.
|
||||||
- `R`: rotate mode.
|
- `R`: rotate mode.
|
||||||
@@ -190,9 +189,9 @@ The state is passed to:
|
|||||||
|
|
||||||
- `EditorControls`, to render the lock/unlock button
|
- `EditorControls`, to render the lock/unlock button
|
||||||
- `EditorScene`, to block `Esc` deselection when locked
|
- `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
|
## Dialogue SRT Editing
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -72,7 +72,7 @@ Use the trash button in `Selection` to delete the selected node from the map tre
|
|||||||
| -------------------- | -------------------------- |
|
| -------------------- | -------------------------- |
|
||||||
| Select object | Click object |
|
| Select object | Click object |
|
||||||
| Toggle multi-select | `Shift` + right click |
|
| Toggle multi-select | `Shift` + right click |
|
||||||
| Deselect | `Esc` or click empty space |
|
| Deselect | `Esc` |
|
||||||
| Lock selection | `Lock` button in Selection |
|
| Lock selection | `Lock` button in Selection |
|
||||||
| Clear selection | `X` button in Selection |
|
| Clear selection | `X` button in Selection |
|
||||||
| Translate mode | `T` |
|
| 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.
|
- Click an object to select it.
|
||||||
- Use `Shift + right click` on objects to add or remove them from a multi-selection.
|
- 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.
|
- 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 `X` button to clear the selection explicitly.
|
||||||
- Use the `Lock` button to protect the current selection while editing.
|
- Use the `Lock` button to protect the current selection while editing.
|
||||||
- Use the scale fields to edit X/Y/Z scale precisely.
|
- 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:
|
When selection is locked:
|
||||||
|
|
||||||
- clicking another object does not change the selection
|
- clicking another object does not change the selection
|
||||||
- clicking empty space does not clear the selection
|
|
||||||
- pressing `Esc` does not clear the selection
|
- pressing `Esc` does not clear the selection
|
||||||
- the `X` button still clears the selection intentionally
|
- the `X` button still clears the selection intentionally
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface EditorControlsProps {
|
|||||||
onClearSelection: () => void;
|
onClearSelection: () => void;
|
||||||
snapToTerrain: boolean;
|
snapToTerrain: boolean;
|
||||||
onSnapToTerrainToggle: () => void;
|
onSnapToTerrainToggle: () => void;
|
||||||
|
onSnapAllToTerrain: () => void;
|
||||||
newNodeName: string;
|
newNodeName: string;
|
||||||
onNewNodeNameChange: (value: string) => void;
|
onNewNodeNameChange: (value: string) => void;
|
||||||
onAddNode: () => void;
|
onAddNode: () => void;
|
||||||
@@ -70,7 +71,7 @@ const EDITOR_SHORTCUTS = [
|
|||||||
["Shift + Right click", "Toggle multi-selection"],
|
["Shift + Right click", "Toggle multi-selection"],
|
||||||
["T / R / S", "Transform mode"],
|
["T / R / S", "Transform mode"],
|
||||||
["Ctrl Z / Y", "Undo / redo"],
|
["Ctrl Z / Y", "Undo / redo"],
|
||||||
["Esc", "Deselect"],
|
["Esc / X button", "Clear selection"],
|
||||||
["WASD", "Move when locked"],
|
["WASD", "Move when locked"],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -117,6 +118,7 @@ export function EditorControls({
|
|||||||
onClearSelection,
|
onClearSelection,
|
||||||
snapToTerrain,
|
snapToTerrain,
|
||||||
onSnapToTerrainToggle,
|
onSnapToTerrainToggle,
|
||||||
|
onSnapAllToTerrain,
|
||||||
newNodeName,
|
newNodeName,
|
||||||
onNewNodeNameChange,
|
onNewNodeNameChange,
|
||||||
onAddNode,
|
onAddNode,
|
||||||
@@ -228,6 +230,15 @@ export function EditorControls({
|
|||||||
/>
|
/>
|
||||||
<span>Snap terrain on move</span>
|
<span>Snap terrain on move</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="editor-history-button"
|
||||||
|
onClick={onSnapAllToTerrain}
|
||||||
|
>
|
||||||
|
<ScanSearch size={15} aria-hidden="true" />
|
||||||
|
Snap all to terrain
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
isEditorVisibleMapNode,
|
isEditorVisibleMapNode,
|
||||||
getTerrainMapNode,
|
getTerrainMapNode,
|
||||||
} from "@/utils/map/mapRuntimeClassification";
|
} from "@/utils/map/mapRuntimeClassification";
|
||||||
|
import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
|
||||||
|
import { getVegetationModelScaleMultiplier } from "@/data/world/vegetationConfig";
|
||||||
|
|
||||||
interface EditorMapProps {
|
interface EditorMapProps {
|
||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
@@ -28,6 +30,8 @@ interface EditorMapProps {
|
|||||||
onTransformStart: () => void;
|
onTransformStart: () => void;
|
||||||
onTransformEnd: () => void;
|
onTransformEnd: () => void;
|
||||||
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
||||||
|
snapAllToTerrainRequest: number;
|
||||||
|
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorNodeObjectRef = React.RefObject<Map<number, THREE.Object3D>>;
|
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_QUATERNION = new THREE.Quaternion();
|
||||||
const TEMP_SCALE = new THREE.Vector3();
|
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 {
|
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
||||||
object.position.set(...node.position);
|
object.position.set(...node.position);
|
||||||
object.rotation.set(...node.rotation);
|
object.rotation.set(...node.rotation);
|
||||||
@@ -177,14 +207,21 @@ export function EditorMap({
|
|||||||
onTransformStart,
|
onTransformStart,
|
||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
onNodeTransform,
|
onNodeTransform,
|
||||||
|
snapAllToTerrainRequest,
|
||||||
|
onSnapAllToTerrain,
|
||||||
}: EditorMapProps): React.JSX.Element {
|
}: EditorMapProps): React.JSX.Element {
|
||||||
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
|
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
|
||||||
const transformGroupRef = useRef<THREE.Group>(null);
|
const transformGroupRef = useRef<THREE.Group>(null);
|
||||||
const transformSnapshotRef = useRef<TransformSnapshot | null>(null);
|
const transformSnapshotRef = useRef<TransformSnapshot | null>(null);
|
||||||
const terrainHeight = useTerrainHeightSampler();
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
const lastSnapAllToTerrainRequestRef = useRef(0);
|
||||||
|
|
||||||
const selectedIndexSet = new Set(selectedNodeIndexes);
|
const selectedIndexSet = new Set(selectedNodeIndexes);
|
||||||
const isMultiSelection = selectedNodeIndexes.length > 1;
|
const isMultiSelection = selectedNodeIndexes.length > 1;
|
||||||
|
const selectedNodeName =
|
||||||
|
selectedNodeIndex !== null
|
||||||
|
? (sceneData.mapNodes[selectedNodeIndex]?.name ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const getTransformObject = useCallback(() => {
|
const getTransformObject = useCallback(() => {
|
||||||
if (isMultiSelection) {
|
if (isMultiSelection) {
|
||||||
@@ -333,6 +370,37 @@ export function EditorMap({
|
|||||||
prepareTransformGroup();
|
prepareTransformGroup();
|
||||||
}, [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.
|
// TransformControls needs the current Three object; editor refs are managed outside React rendering.
|
||||||
// eslint-disable-next-line react-hooks/refs
|
// eslint-disable-next-line react-hooks/refs
|
||||||
const selectedObject = getTransformObject();
|
const selectedObject = getTransformObject();
|
||||||
@@ -370,7 +438,7 @@ export function EditorMap({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{sceneData.mapNodes.map((node, index) => {
|
{sceneData.mapNodes.map((node, index) => {
|
||||||
if (!isEditorVisibleMapNode(node)) {
|
if (!shouldRenderEditorNode(node, selectedNodeName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,6 +519,7 @@ function EditorModelNode({
|
|||||||
scale: node.scale,
|
scale: node.scale,
|
||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
|
const visualScaleMultiplier = getEditorModelVisualScaleMultiplier(node.name);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
@@ -512,14 +581,15 @@ function EditorModelNode({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<primitive
|
<group
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
object={sceneInstance}
|
|
||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
{...pointerHandlers}
|
{...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 type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
||||||
import { EditorMap } from "@/components/editor/scene/EditorMap";
|
import { EditorMap } from "@/components/editor/scene/EditorMap";
|
||||||
import { FlyController } from "@/controls/editor/FlyController";
|
import { FlyController } from "@/controls/editor/FlyController";
|
||||||
|
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
|
||||||
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
||||||
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
|
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ interface EditorSceneProps {
|
|||||||
onTransformStart: () => void;
|
onTransformStart: () => void;
|
||||||
onTransformEnd: () => void;
|
onTransformEnd: () => void;
|
||||||
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
||||||
|
snapAllToTerrainRequest: number;
|
||||||
|
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onRedo: () => void;
|
onRedo: () => void;
|
||||||
resetCameraRequest: number;
|
resetCameraRequest: number;
|
||||||
@@ -58,6 +61,8 @@ export function EditorScene({
|
|||||||
onTransformStart,
|
onTransformStart,
|
||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
onNodeTransform,
|
onNodeTransform,
|
||||||
|
snapAllToTerrainRequest,
|
||||||
|
onSnapAllToTerrain,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
resetCameraRequest,
|
resetCameraRequest,
|
||||||
@@ -224,8 +229,12 @@ export function EditorScene({
|
|||||||
onTransformStart={onTransformStart}
|
onTransformStart={onTransformStart}
|
||||||
onTransformEnd={onTransformEnd}
|
onTransformEnd={onTransformEnd}
|
||||||
onNodeTransform={onNodeTransform}
|
onNodeTransform={onNodeTransform}
|
||||||
|
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||||
|
onSnapAllToTerrain={onSnapAllToTerrain}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PersonnageSystem />
|
||||||
|
|
||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[10, 20, 10]} intensity={1.5} castShadow />
|
<directionalLight position={[10, 20, 10]} intensity={1.5} castShadow />
|
||||||
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
|
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
|
||||||
|
|||||||
@@ -68,32 +68,6 @@ export function AnimatedModel({
|
|||||||
}
|
}
|
||||||
}, [mixer, onAnimationEnd]);
|
}, [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(
|
const fadeTo = useCallback(
|
||||||
(name: string, fade = fadeDuration) => {
|
(name: string, fade = fadeDuration) => {
|
||||||
const action = actions[name];
|
const action = actions[name];
|
||||||
@@ -107,6 +81,19 @@ export function AnimatedModel({
|
|||||||
},
|
},
|
||||||
[actions, fadeDuration],
|
[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(
|
const setSpeed = useCallback(
|
||||||
(newSpeed: number) => {
|
(newSpeed: number) => {
|
||||||
@@ -140,10 +127,21 @@ export function AnimatedModel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (defaultAction) {
|
if (defaultAction) {
|
||||||
defaultAction.play();
|
Object.values(actions).forEach((action) => {
|
||||||
|
if (action && action !== defaultAction) action.fadeOut(fadeDuration);
|
||||||
|
});
|
||||||
|
defaultAction.reset().fadeIn(fadeDuration).play();
|
||||||
onLoaded?.();
|
onLoaded?.();
|
||||||
}
|
}
|
||||||
}, [actions, defaultAnimation, modelPath, names, autoPlay, onLoaded]);
|
}, [
|
||||||
|
actions,
|
||||||
|
defaultAnimation,
|
||||||
|
fadeDuration,
|
||||||
|
modelPath,
|
||||||
|
names,
|
||||||
|
autoPlay,
|
||||||
|
onLoaded,
|
||||||
|
]);
|
||||||
|
|
||||||
const contextValue: AnimatedModelContextValue = {
|
const contextValue: AnimatedModelContextValue = {
|
||||||
play,
|
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[];
|
||||||
@@ -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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
@@ -2,12 +2,5 @@ import editor from "../../../../docs/user/editor.md?raw";
|
|||||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsEditorPage(): React.JSX.Element {
|
export function DocsEditorPage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={editor} meta="14" title="Editor User Guide" />;
|
||||||
<DocsDocument
|
|
||||||
content={editor}
|
|
||||||
frContent={editor}
|
|
||||||
meta="14"
|
|
||||||
title="Editor User Guide"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,5 @@ import repairGame from "../../../../docs/technical/repair-game.md?raw";
|
|||||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsRepairGamePage(): React.JSX.Element {
|
export function DocsRepairGamePage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={repairGame} meta="04" title="Repair Game" />;
|
||||||
<DocsDocument
|
|
||||||
content={repairGame}
|
|
||||||
frContent={repairGame}
|
|
||||||
meta="04"
|
|
||||||
title="Repair Game"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const [newNodeName, setNewNodeName] = useState(DEFAULT_NEW_NODE_NAME);
|
const [newNodeName, setNewNodeName] = useState(DEFAULT_NEW_NODE_NAME);
|
||||||
const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
|
const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
|
||||||
const [resetCameraRequest, setResetCameraRequest] = useState(0);
|
const [resetCameraRequest, setResetCameraRequest] = useState(0);
|
||||||
|
const [snapAllToTerrainRequest, setSnapAllToTerrainRequest] = useState(0);
|
||||||
const [focusSelectedCameraRequest, setFocusSelectedCameraRequest] =
|
const [focusSelectedCameraRequest, setFocusSelectedCameraRequest] =
|
||||||
useState(0);
|
useState(0);
|
||||||
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
|
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
|
||||||
@@ -372,9 +373,14 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const handleSelectNode = useCallback((index: number | null) => {
|
const handleSelectNode = useCallback((index: number | null) => {
|
||||||
setSelectedNodeIndex(index);
|
setSelectedNodeIndex(index);
|
||||||
setSelectedNodeIndexes(index === null ? [] : [index]);
|
setSelectedNodeIndexes(index === null ? [] : [index]);
|
||||||
|
|
||||||
if (index !== null) {
|
if (index !== null) {
|
||||||
setCameraViewMode("object");
|
setCameraViewMode("object");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCameraViewMode("home");
|
||||||
|
setResetCameraRequest((request) => request + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToggleNodeSelection = useCallback((index: number) => {
|
const handleToggleNodeSelection = useCallback((index: number) => {
|
||||||
@@ -387,6 +393,9 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setSelectedNodeIndex(nextIndexes.at(-1) ?? null);
|
setSelectedNodeIndex(nextIndexes.at(-1) ?? null);
|
||||||
if (nextIndexes.length > 0) {
|
if (nextIndexes.length > 0) {
|
||||||
setCameraViewMode("object");
|
setCameraViewMode("object");
|
||||||
|
} else {
|
||||||
|
setCameraViewMode("home");
|
||||||
|
setResetCameraRequest((request) => request + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextIndexes;
|
return nextIndexes;
|
||||||
@@ -396,6 +405,8 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const handleClearSelection = useCallback(() => {
|
const handleClearSelection = useCallback(() => {
|
||||||
setSelectedNodeIndex(null);
|
setSelectedNodeIndex(null);
|
||||||
setSelectedNodeIndexes([]);
|
setSelectedNodeIndexes([]);
|
||||||
|
setCameraViewMode("home");
|
||||||
|
setResetCameraRequest((request) => request + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectionLockToggle = useCallback(() => {
|
const handleSelectionLockToggle = useCallback(() => {
|
||||||
@@ -406,6 +417,25 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setSnapToTerrain((enabled) => !enabled);
|
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) => {
|
const handleNewNodeNameChange = useCallback((value: string) => {
|
||||||
setNewNodeName(value);
|
setNewNodeName(value);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -710,6 +740,8 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
onTransformStart={handleTransformStart}
|
onTransformStart={handleTransformStart}
|
||||||
onTransformEnd={handleTransformEnd}
|
onTransformEnd={handleTransformEnd}
|
||||||
onNodeTransform={handleNodeTransform}
|
onNodeTransform={handleNodeTransform}
|
||||||
|
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||||
|
onSnapAllToTerrain={handleSnapAllToTerrain}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
resetCameraRequest={resetCameraRequest}
|
resetCameraRequest={resetCameraRequest}
|
||||||
@@ -748,6 +780,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
onClearSelection={handleClearSelection}
|
onClearSelection={handleClearSelection}
|
||||||
snapToTerrain={snapToTerrain}
|
snapToTerrain={snapToTerrain}
|
||||||
onSnapToTerrainToggle={handleSnapToTerrainToggle}
|
onSnapToTerrainToggle={handleSnapToTerrainToggle}
|
||||||
|
onSnapAllToTerrain={handleSnapAllToTerrainRequest}
|
||||||
newNodeName={newNodeName}
|
newNodeName={newNodeName}
|
||||||
onNewNodeNameChange={handleNewNodeNameChange}
|
onNewNodeNameChange={handleNewNodeNameChange}
|
||||||
onAddNode={handleAddNode}
|
onAddNode={handleAddNode}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const DEBUG_FOLDER_ORDER = [
|
|||||||
"Interaction",
|
"Interaction",
|
||||||
"Hand Tracking",
|
"Hand Tracking",
|
||||||
"Map",
|
"Map",
|
||||||
|
"Personnages",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
|
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
|
||||||
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
|
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
|
||||||
|
import { usePersonnageDebug } from "@/hooks/debug/usePersonnageDebug";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
||||||
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
|
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
|
||||||
@@ -28,6 +29,7 @@ import { GameMusic } from "@/world/GameMusic";
|
|||||||
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 { GameStageContent } from "@/world/GameStageContent";
|
||||||
|
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
|
||||||
import { Player } from "@/world/player/Player";
|
import { Player } from "@/world/player/Player";
|
||||||
import { TestMap } from "@/world/debug/TestMap";
|
import { TestMap } from "@/world/debug/TestMap";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
@@ -39,6 +41,7 @@ interface WorldProps {
|
|||||||
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
||||||
useEnvironmentDebug();
|
useEnvironmentDebug();
|
||||||
useMapPerformanceDebug();
|
useMapPerformanceDebug();
|
||||||
|
usePersonnageDebug();
|
||||||
|
|
||||||
const cameraMode = useCameraMode();
|
const cameraMode = useCameraMode();
|
||||||
const sceneMode = useSceneMode();
|
const sceneMode = useSceneMode();
|
||||||
@@ -87,6 +90,7 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
|||||||
onLoadingStateChange={onLoadingStateChange}
|
onLoadingStateChange={onLoadingStateChange}
|
||||||
onOctreeReady={handleOctreeReady}
|
onOctreeReady={handleOctreeReady}
|
||||||
/>
|
/>
|
||||||
|
<PersonnageSystem />
|
||||||
{showGameStage ? (
|
{showGameStage ? (
|
||||||
<Physics>
|
<Physics>
|
||||||
<GameStageLoaded onLoaded={handleGameStageLoaded} />
|
<GameStageLoaded onLoaded={handleGameStageLoaded} />
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import type { OctreeReadyHandler } from "@/types/three/three";
|
|||||||
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
|
|
||||||
const ELECTRICIENNE_ANIMATED_MODEL_PATH =
|
const ELECTRICIENNE_ANIMATED_MODEL_PATH =
|
||||||
"/models/electricienne_animated/model.gltf";
|
"/models/electricienne-animated/model.gltf";
|
||||||
|
|
||||||
interface TestMapProps {
|
interface TestMapProps {
|
||||||
onOctreeReady: OctreeReadyHandler;
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user