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:
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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[];
|
||||
@@ -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";
|
||||
|
||||
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" />;
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -24,6 +24,7 @@ const DEBUG_FOLDER_ORDER = [
|
||||
"Interaction",
|
||||
"Hand Tracking",
|
||||
"Map",
|
||||
"Personnages",
|
||||
] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user