clean branch-scoped code quality issues
This commit is contained in:
@@ -44,12 +44,12 @@ This document describes the code that exists today in the repository.
|
||||
## Editor System
|
||||
|
||||
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
||||
- `src/features/editor/components/EditorControls.tsx` renders the HTML editor control panel.
|
||||
- `src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||
- `src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
- `src/features/editor/controls/FlyController.tsx` provides player-style editor navigation.
|
||||
- `src/features/editor/hooks/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||
- `src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo state.
|
||||
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
|
||||
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
|
||||
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
|
||||
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
||||
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
||||
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
||||
@@ -63,9 +63,9 @@ This document describes the code that exists today in the repository.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- The repository is still a prototype, not the full intended game runtime.
|
||||
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
||||
- There is no central gameplay orchestrator such as `GameManager` yet.
|
||||
- The repository is a prototype, not the full intended game runtime.
|
||||
- `src/world/debug/TestScene.tsx` is part of the active scene composition.
|
||||
- There is no central gameplay orchestrator such as `GameManager`.
|
||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
||||
|
||||
+20
-19
@@ -20,18 +20,19 @@ src/
|
||||
├── pages/
|
||||
│ └── editor/
|
||||
│ └── EditorPage.tsx
|
||||
├── features/
|
||||
├── components/
|
||||
│ └── editor/
|
||||
│ ├── components/
|
||||
│ │ └── EditorControls.tsx
|
||||
│ ├── controls/
|
||||
│ │ └── FlyController.tsx
|
||||
│ ├── hooks/
|
||||
│ │ ├── useEditorHistory.ts
|
||||
│ │ └── useEditorSceneData.ts
|
||||
│ ├── scene/
|
||||
│ │ ├── EditorMap.tsx
|
||||
│ │ └── EditorScene.tsx
|
||||
│ ├── EditorControls.tsx
|
||||
│ └── scene/
|
||||
│ ├── EditorMap.tsx
|
||||
│ └── EditorScene.tsx
|
||||
├── controls/
|
||||
│ └── editor/
|
||||
│ └── FlyController.tsx
|
||||
├── hooks/
|
||||
│ └── editor/
|
||||
│ ├── useEditorHistory.ts
|
||||
│ └── useEditorSceneData.ts
|
||||
├── types/
|
||||
│ └── editor.ts
|
||||
└── utils/
|
||||
@@ -44,17 +45,17 @@ src/
|
||||
|
||||
`src/pages/editor/EditorPage.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
||||
|
||||
`src/features/editor/hooks/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||
|
||||
`src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo history.
|
||||
`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history.
|
||||
|
||||
`src/features/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||
|
||||
`src/features/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
|
||||
`src/features/editor/components/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||
|
||||
`src/features/editor/controls/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||
|
||||
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
||||
|
||||
@@ -138,6 +139,6 @@ Editor styles are in `src/index.css` under the `/* Editor page */` section. Clas
|
||||
## Known Limitations
|
||||
|
||||
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
||||
- Large `map.json` files may need virtualization, culling, or LOD support later.
|
||||
- There is no snap-to-grid, duplication, material editing, or object creation workflow yet.
|
||||
- Large `map.json` files are not virtualized, culled, or LOD-managed.
|
||||
- There is no snap-to-grid, duplication, material editing, or object creation workflow.
|
||||
- Save to Server is a Vite dev-server helper, not a production backend API.
|
||||
|
||||
@@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project.
|
||||
## Relationship To The Current Code
|
||||
|
||||
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
||||
- This document is intentionally aspirational.
|
||||
- This document describes intended direction, not implemented behavior.
|
||||
- If this document conflicts with the current implementation, the current implementation wins.
|
||||
|
||||
## Goals
|
||||
@@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project.
|
||||
- performance overlay
|
||||
- scene helpers
|
||||
- free camera and calibration controls
|
||||
- temporary test scenes used during development
|
||||
- debug test scenes used during development
|
||||
|
||||
### UI Layer
|
||||
|
||||
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||
- Expected future examples:
|
||||
- Candidate examples:
|
||||
- crosshair
|
||||
- loading flow
|
||||
- mission HUD
|
||||
@@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project.
|
||||
### Gameplay Layer
|
||||
|
||||
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||
- Likely future concerns:
|
||||
- Likely concerns:
|
||||
- missions
|
||||
- zones
|
||||
- cinematics
|
||||
@@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project.
|
||||
- Prefer direct, working code over speculative scaffolding.
|
||||
- Shared types should stay close to their domain until they have multiple real consumers.
|
||||
- Avoid creating new managers or service layers without an active runtime need.
|
||||
- Debug-only runtime paths should be clearly marked and easy to remove later.
|
||||
- Debug-only runtime paths should be clearly marked and easy to remove when obsolete.
|
||||
|
||||
+2
-2
@@ -72,12 +72,12 @@ This is useful for checking numeric transform values before saving or exporting.
|
||||
|
||||
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
||||
|
||||
The button is hidden in production builds because production persistence is not implemented yet.
|
||||
The button is hidden in production builds because production persistence is not implemented.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- The editor only modifies existing nodes.
|
||||
- It does not create or delete objects yet.
|
||||
- It does not create or delete objects.
|
||||
- It does not edit model files or textures.
|
||||
- It does not provide production persistence.
|
||||
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
|
||||
import type { Vector3Tuple } from "@/types/3d";
|
||||
import type { InteractableHandle, InteractableKind } from "@/types/interaction";
|
||||
import type { InteractableHandle } from "@/types/interaction";
|
||||
|
||||
interface InteractableObjectBaseProps {
|
||||
label: string;
|
||||
@@ -37,46 +37,67 @@ type InteractableObjectProps =
|
||||
| TriggerInteractableObjectProps
|
||||
| GrabInteractableObjectProps;
|
||||
|
||||
type MutableInteractableHandle = {
|
||||
kind: InteractableKind;
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
onRelease?: () => void;
|
||||
};
|
||||
|
||||
const _cameraPos = new THREE.Vector3();
|
||||
const _cameraDir = new THREE.Vector3();
|
||||
const _objectPos = new THREE.Vector3();
|
||||
const _raycaster = new THREE.Raycaster();
|
||||
|
||||
function createInteractableHandle(
|
||||
props: InteractableObjectProps,
|
||||
): InteractableHandle {
|
||||
if (props.kind === "grab") {
|
||||
return {
|
||||
kind: props.kind,
|
||||
label: props.label,
|
||||
onPress: props.onPress,
|
||||
onRelease: props.onRelease,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: props.kind,
|
||||
label: props.label,
|
||||
onPress: props.onPress,
|
||||
};
|
||||
}
|
||||
|
||||
export function InteractableObject(
|
||||
props: InteractableObjectProps,
|
||||
): React.JSX.Element {
|
||||
const { kind, label, position, bodyRef, onPress, children } = props;
|
||||
const onRelease = props.kind === "grab" ? props.onRelease : undefined;
|
||||
const onRelease = props.kind === "grab" ? props.onRelease : null;
|
||||
const camera = useThree((state) => state.camera);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const debugSphereRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
const handle = useRef<InteractableHandle>(
|
||||
props.kind === "grab"
|
||||
? { kind: props.kind, label, onPress, onRelease: props.onRelease }
|
||||
: { kind: props.kind, label, onPress },
|
||||
);
|
||||
const handle = useRef<InteractableHandle>(createInteractableHandle(props));
|
||||
|
||||
useEffect(() => {
|
||||
const current = handle.current as MutableInteractableHandle;
|
||||
current.kind = kind;
|
||||
current.label = label;
|
||||
current.onPress = onPress;
|
||||
const currentHandle = handle.current;
|
||||
|
||||
if (currentHandle.kind === kind) {
|
||||
currentHandle.label = label;
|
||||
currentHandle.onPress = onPress;
|
||||
|
||||
if (currentHandle.kind === "grab") {
|
||||
if (!onRelease) return;
|
||||
currentHandle.onRelease = onRelease;
|
||||
}
|
||||
|
||||
if (kind === "grab" && onRelease) {
|
||||
current.onRelease = onRelease;
|
||||
return;
|
||||
}
|
||||
|
||||
delete current.onRelease;
|
||||
return undefined;
|
||||
if (kind === "grab") {
|
||||
if (!onRelease) return;
|
||||
handle.current = { kind, label, onPress, onRelease };
|
||||
} else {
|
||||
handle.current = { kind, label, onPress };
|
||||
}
|
||||
|
||||
const manager = InteractionManager.getInstance();
|
||||
if (manager.getState().focused === currentHandle) {
|
||||
manager.setFocused(handle.current);
|
||||
}
|
||||
}, [kind, label, onPress, onRelease]);
|
||||
|
||||
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
||||
|
||||
@@ -31,6 +31,20 @@ interface EditorControlsProps {
|
||||
isPlayerMode?: boolean;
|
||||
}
|
||||
|
||||
const TRANSFORM_OPTIONS = [
|
||||
{ mode: "translate", label: "Translate", shortcut: "T", Icon: Move3D },
|
||||
{ mode: "rotate", label: "Rotate", shortcut: "R", Icon: RotateCw },
|
||||
{ mode: "scale", label: "Scale", shortcut: "S", Icon: Expand },
|
||||
] as const;
|
||||
|
||||
const EDITOR_SHORTCUTS = [
|
||||
["Click", "Select object"],
|
||||
["T / R / S", "Transform mode"],
|
||||
["Ctrl Z / Y", "Undo / redo"],
|
||||
["Esc", "Deselect"],
|
||||
["WASD", "Move when locked"],
|
||||
] as const;
|
||||
|
||||
export function EditorControls({
|
||||
transformMode,
|
||||
onTransformModeChange,
|
||||
@@ -69,33 +83,18 @@ export function EditorControls({
|
||||
</div>
|
||||
|
||||
<div className="editor-transform-buttons">
|
||||
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
|
||||
<button
|
||||
className={`editor-transform-button ${transformMode === "translate" ? "active" : ""}`}
|
||||
onClick={() => onTransformModeChange("translate")}
|
||||
aria-pressed={transformMode === "translate"}
|
||||
key={mode}
|
||||
className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
|
||||
onClick={() => onTransformModeChange(mode)}
|
||||
aria-pressed={transformMode === mode}
|
||||
>
|
||||
<Move3D size={16} aria-hidden="true" />
|
||||
<span>Translate</span>
|
||||
<kbd>T</kbd>
|
||||
</button>
|
||||
<button
|
||||
className={`editor-transform-button ${transformMode === "rotate" ? "active" : ""}`}
|
||||
onClick={() => onTransformModeChange("rotate")}
|
||||
aria-pressed={transformMode === "rotate"}
|
||||
>
|
||||
<RotateCw size={16} aria-hidden="true" />
|
||||
<span>Rotate</span>
|
||||
<kbd>R</kbd>
|
||||
</button>
|
||||
<button
|
||||
className={`editor-transform-button ${transformMode === "scale" ? "active" : ""}`}
|
||||
onClick={() => onTransformModeChange("scale")}
|
||||
aria-pressed={transformMode === "scale"}
|
||||
>
|
||||
<Expand size={16} aria-hidden="true" />
|
||||
<span>Scale</span>
|
||||
<kbd>S</kbd>
|
||||
<Icon size={16} aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
<kbd>{shortcut}</kbd>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="editor-history-buttons">
|
||||
@@ -203,26 +202,12 @@ export function EditorControls({
|
||||
</div>
|
||||
|
||||
<dl className="editor-shortcuts-list">
|
||||
<div>
|
||||
<dt>Click</dt>
|
||||
<dd>Select object</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>T / R / S</dt>
|
||||
<dd>Transform mode</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Ctrl Z / Y</dt>
|
||||
<dd>Undo / redo</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Esc</dt>
|
||||
<dd>Deselect</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>WASD</dt>
|
||||
<dd>Move when locked</dd>
|
||||
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
||||
<div key={keys}>
|
||||
<dt>{keys}</dt>
|
||||
<dd>{description}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ interface EditorNodeCommonProps {
|
||||
onHoverNode: (index: number | null) => void;
|
||||
}
|
||||
|
||||
interface EditorNodePointerHandlers {
|
||||
onClick: (event: ThreeEvent<MouseEvent>) => void;
|
||||
onPointerEnter: (event: ThreeEvent<PointerEvent>) => void;
|
||||
onPointerLeave: (event: ThreeEvent<PointerEvent>) => void;
|
||||
}
|
||||
|
||||
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
||||
object.position.set(...node.position);
|
||||
object.rotation.set(...node.rotation);
|
||||
@@ -88,6 +94,36 @@ function cloneHighlightedMaterial(
|
||||
return clone;
|
||||
}
|
||||
|
||||
function getNodeHighlightColor(
|
||||
isSelected: boolean,
|
||||
isHovered: boolean,
|
||||
): string | null {
|
||||
if (isSelected) return "#ffffff";
|
||||
if (isHovered) return "#b8b8b8";
|
||||
return null;
|
||||
}
|
||||
|
||||
function createEditorNodePointerHandlers(
|
||||
index: number,
|
||||
onSelectNode: (index: number | null) => void,
|
||||
onHoverNode: (index: number | null) => void,
|
||||
): EditorNodePointerHandlers {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
onSelectNode(index);
|
||||
},
|
||||
onPointerEnter: (event) => {
|
||||
event.stopPropagation();
|
||||
onHoverNode(index);
|
||||
},
|
||||
onPointerLeave: (event) => {
|
||||
event.stopPropagation();
|
||||
onHoverNode(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function EditorMap({
|
||||
sceneData,
|
||||
selectedNodeIndex,
|
||||
@@ -224,15 +260,16 @@ function EditorModelNode({
|
||||
const { scene } = useGLTF(modelUrl);
|
||||
|
||||
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||
const pointerHandlers = createEditorNodePointerHandlers(
|
||||
index,
|
||||
onSelectNode,
|
||||
onHoverNode,
|
||||
);
|
||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupRef.current) return;
|
||||
const highlightColor = isSelected
|
||||
? "#ffffff"
|
||||
: isHovered
|
||||
? "#b8b8b8"
|
||||
: null;
|
||||
const highlightColor = getNodeHighlightColor(isSelected, isHovered);
|
||||
|
||||
groupRef.current.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh)) {
|
||||
@@ -288,18 +325,7 @@ function EditorModelNode({
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
scale={node.scale}
|
||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
onSelectNode(index);
|
||||
}}
|
||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(index);
|
||||
}}
|
||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(null);
|
||||
}}
|
||||
{...pointerHandlers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -314,9 +340,14 @@ function EditorFallbackNode({
|
||||
onHoverNode,
|
||||
}: EditorNodeCommonProps) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
const pointerHandlers = createEditorNodePointerHandlers(
|
||||
index,
|
||||
onSelectNode,
|
||||
onHoverNode,
|
||||
);
|
||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||
|
||||
const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f";
|
||||
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
||||
|
||||
return (
|
||||
<mesh
|
||||
@@ -324,18 +355,7 @@ function EditorFallbackNode({
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
scale={node.scale}
|
||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
onSelectNode(index);
|
||||
}}
|
||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(index);
|
||||
}}
|
||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(null);
|
||||
}}
|
||||
{...pointerHandlers}
|
||||
>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createContext } from "react";
|
||||
|
||||
export type DocsLanguage = "en" | "fr";
|
||||
|
||||
export interface DocsLanguageContextValue {
|
||||
interface DocsLanguageContextValue {
|
||||
language: DocsLanguage;
|
||||
toggleLanguage: () => void;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
|
||||
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
||||
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
||||
|
||||
export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88;
|
||||
|
||||
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
||||
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
||||
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export interface DocSection {
|
||||
interface DocSection {
|
||||
path: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export interface DocGroup {
|
||||
interface DocGroup {
|
||||
label: string;
|
||||
sections: DocSection[];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { EditorControls } from "@/components/editor/EditorControls";
|
||||
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||
import type { MapNode, TransformMode } from "@/types/editor";
|
||||
import type { MapNode, SceneData, TransformMode } from "@/types/editor";
|
||||
|
||||
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
|
||||
|
||||
function serializeMapNodes(sceneData: SceneData): string {
|
||||
return JSON.stringify(sceneData.mapNodes, null, 2);
|
||||
}
|
||||
|
||||
export function EditorPage(): React.JSX.Element {
|
||||
const {
|
||||
@@ -46,7 +52,7 @@ export function EditorPage(): React.JSX.Element {
|
||||
|
||||
const handleSaveToServer = useCallback(async () => {
|
||||
if (!sceneData) return;
|
||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
||||
const json = serializeMapNodes(sceneData);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/save-map", {
|
||||
@@ -58,17 +64,17 @@ export function EditorPage(): React.JSX.Element {
|
||||
if (response.ok) {
|
||||
alert("Map enregistrée avec succès!");
|
||||
} else {
|
||||
alert("Erreur lors de l'enregistrement");
|
||||
alert(SAVE_ERROR_MESSAGE);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error saving map:", err);
|
||||
alert("Erreur lors de l'enregistrement");
|
||||
alert(SAVE_ERROR_MESSAGE);
|
||||
}
|
||||
}, [sceneData]);
|
||||
|
||||
const handleExportJson = useCallback(() => {
|
||||
if (!sceneData) return;
|
||||
const json = JSON.stringify(sceneData.mapNodes, null, 2);
|
||||
const json = serializeMapNodes(sceneData);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
PLAYER_EYE_HEIGHT,
|
||||
PLAYER_SPAWN_POSITION_GAME,
|
||||
} from "@/data/player/playerConfig";
|
||||
import type { Vector3Tuple } from "@/types/3d";
|
||||
|
||||
const DEBUG_CAMERA_TARGET = [
|
||||
const DEBUG_CAMERA_TARGET: Vector3Tuple = [
|
||||
PLAYER_SPAWN_POSITION_GAME[0],
|
||||
PLAYER_EYE_HEIGHT,
|
||||
PLAYER_SPAWN_POSITION_GAME[2],
|
||||
] as const;
|
||||
];
|
||||
|
||||
export function DebugCameraControls(): React.JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { MapNode, SceneData } from "@/types/editor";
|
||||
|
||||
const MAP_JSON_PATH = "/map.json";
|
||||
const MODEL_FILE_NAME = "model.gltf";
|
||||
type ModelEntry = [modelName: string, modelUrl: string];
|
||||
|
||||
export async function loadMapSceneData(): Promise<SceneData | null> {
|
||||
const response = await fetch(MAP_JSON_PATH);
|
||||
@@ -29,7 +30,8 @@ async function loadMapModelUrls(
|
||||
|
||||
try {
|
||||
const response = await fetch(modelUrl, { method: "HEAD" });
|
||||
return response.ok ? ([modelName, modelUrl] as const) : null;
|
||||
const modelEntry: ModelEntry = [modelName, modelUrl];
|
||||
return response.ok ? modelEntry : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debug/debugConfig";
|
||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||
import type { OctreeReadyHandler } from "@/types/3d";
|
||||
import { Debug } from "@/utils/debug/Debug";
|
||||
|
||||
const MAP_PATH = "/models/map/model.gltf";
|
||||
|
||||
interface MapProps {
|
||||
onOctreeReady: OctreeReadyHandler;
|
||||
}
|
||||
|
||||
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
||||
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
||||
const { scene } = useThree();
|
||||
|
||||
useOctreeGraphNode(groupRef, onOctreeReady);
|
||||
|
||||
useEffect(() => {
|
||||
const debug = Debug.getInstance();
|
||||
if (!debug.active || !groupRef.current) return;
|
||||
|
||||
const helpers: THREE.BoxHelper[] = [];
|
||||
|
||||
groupRef.current.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR);
|
||||
scene.add(helper);
|
||||
helpers.push(helper);
|
||||
});
|
||||
|
||||
boxHelpersRef.current = helpers;
|
||||
|
||||
return () => {
|
||||
helpers.forEach((h) => {
|
||||
scene.remove(h);
|
||||
h.dispose();
|
||||
});
|
||||
boxHelpersRef.current = [];
|
||||
};
|
||||
}, [scene]);
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<primitive object={gltfScene} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload(MAP_PATH);
|
||||
@@ -54,6 +54,25 @@ const _up = new THREE.Vector3(0, 1, 0);
|
||||
const _translateVec = new THREE.Vector3();
|
||||
const _collisionCorrection = new THREE.Vector3();
|
||||
|
||||
function setMovementKey(keys: Keys, key: string, pressed: boolean): boolean {
|
||||
switch (key.toLowerCase()) {
|
||||
case MOVE_FORWARD_KEY:
|
||||
keys.forward = pressed;
|
||||
return true;
|
||||
case MOVE_BACKWARD_KEY:
|
||||
keys.backward = pressed;
|
||||
return true;
|
||||
case MOVE_LEFT_KEY:
|
||||
keys.left = pressed;
|
||||
return true;
|
||||
case MOVE_RIGHT_KEY:
|
||||
keys.right = pressed;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function PlayerController({
|
||||
octree,
|
||||
spawnPosition,
|
||||
@@ -89,51 +108,29 @@ export function PlayerController({
|
||||
const interaction = InteractionManager.getInstance();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case MOVE_FORWARD_KEY:
|
||||
keys.current.forward = true;
|
||||
break;
|
||||
case MOVE_BACKWARD_KEY:
|
||||
keys.current.backward = true;
|
||||
break;
|
||||
case MOVE_LEFT_KEY:
|
||||
keys.current.left = true;
|
||||
break;
|
||||
case MOVE_RIGHT_KEY:
|
||||
keys.current.right = true;
|
||||
break;
|
||||
case JUMP_KEY:
|
||||
if (setMovementKey(keys.current, event.key, true)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === JUMP_KEY) {
|
||||
wantsJump.current = true;
|
||||
break;
|
||||
case INTERACT_KEY:
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === INTERACT_KEY) {
|
||||
if (interaction.getState().focused?.kind === "trigger") {
|
||||
interaction.pressInteract();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case MOVE_FORWARD_KEY:
|
||||
keys.current.forward = false;
|
||||
break;
|
||||
case MOVE_BACKWARD_KEY:
|
||||
keys.current.backward = false;
|
||||
break;
|
||||
case MOVE_LEFT_KEY:
|
||||
keys.current.left = false;
|
||||
break;
|
||||
case MOVE_RIGHT_KEY:
|
||||
keys.current.right = false;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (setMovementKey(keys.current, event.key, false)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (event: MouseEvent): void => {
|
||||
|
||||
Reference in New Issue
Block a user