clean branch-scoped code quality issues

This commit is contained in:
2026-04-28 14:23:37 +02:00
parent 356bb5ef88
commit 324aa9dc0f
15 changed files with 218 additions and 242 deletions
+9 -9
View File
@@ -44,12 +44,12 @@ This document describes the code that exists today in the repository.
## Editor System ## Editor System
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`. - `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/components/editor/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/components/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/components/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/controls/editor/FlyController.tsx` provides player-style editor navigation.
- `src/features/editor/hooks/useEditorSceneData.ts` loads scene data and handles folder upload fallback. - `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
- `src/features/editor/hooks/useEditorHistory.ts` owns editor undo and redo state. - `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing. - `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/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. - `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 ## Current Limitations
- The repository is still a prototype, not the full intended game runtime. - The repository is a prototype, not the full intended game runtime.
- `src/world/debug/TestScene.tsx` is still part of the active scene composition. - `src/world/debug/TestScene.tsx` is part of the active scene composition.
- There is no central gameplay orchestrator such as `GameManager` yet. - There is no central gameplay orchestrator such as `GameManager`.
- Missions, zones, cinematics, and dialogue systems are not implemented. - Missions, zones, cinematics, and dialogue systems are not implemented.
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack. - 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. - Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
+20 -19
View File
@@ -20,18 +20,19 @@ src/
├── pages/ ├── pages/
│ └── editor/ │ └── editor/
│ └── EditorPage.tsx │ └── EditorPage.tsx
├── features/ ├── components/
│ └── editor/ │ └── editor/
│ ├── components/ │ ├── EditorControls.tsx
│ └── EditorControls.tsx └── scene/
├── controls/ ├── EditorMap.tsx
└── FlyController.tsx └── EditorScene.tsx
│ ├── hooks/ ├── controls/
│ ├── useEditorHistory.ts └── editor/
│ └── useEditorSceneData.ts └── FlyController.tsx
│ ├── scene/ ├── hooks/
│ ├── EditorMap.tsx ── editor/
│ └── EditorScene.tsx ├── useEditorHistory.ts
│ └── useEditorSceneData.ts
├── types/ ├── types/
│ └── editor.ts │ └── editor.ts
└── utils/ └── 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/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. `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 ## Known Limitations
- Uploaded model object URLs are not currently revoked after replacement or unmount. - Uploaded model object URLs are not currently revoked after replacement or unmount.
- Large `map.json` files may need virtualization, culling, or LOD support later. - Large `map.json` files are not virtualized, culled, or LOD-managed.
- There is no snap-to-grid, duplication, material editing, or object creation workflow yet. - 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. - Save to Server is a Vite dev-server helper, not a production backend API.
+5 -5
View File
@@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project.
## Relationship To The Current Code ## Relationship To The Current Code
- `docs/technical/architecture.md` is the source of truth for what exists now. - `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. - If this document conflicts with the current implementation, the current implementation wins.
## Goals ## Goals
@@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project.
- performance overlay - performance overlay
- scene helpers - scene helpers
- free camera and calibration controls - free camera and calibration controls
- temporary test scenes used during development - debug test scenes used during development
### UI Layer ### UI Layer
- `src/components/ui/` should contain player-facing HTML overlays. - `src/components/ui/` should contain player-facing HTML overlays.
- Expected future examples: - Candidate examples:
- crosshair - crosshair
- loading flow - loading flow
- mission HUD - mission HUD
@@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project.
### Gameplay Layer ### Gameplay Layer
- As the project grows, gameplay state can move toward a clearer orchestration layer. - As the project grows, gameplay state can move toward a clearer orchestration layer.
- Likely future concerns: - Likely concerns:
- missions - missions
- zones - zones
- cinematics - cinematics
@@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project.
- Prefer direct, working code over speculative scaffolding. - Prefer direct, working code over speculative scaffolding.
- Shared types should stay close to their domain until they have multiple real consumers. - 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. - 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
View File
@@ -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. `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 ## Current Limitations
- The editor only modifies existing nodes. - 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 edit model files or textures.
- It does not provide production persistence. - It does not provide production persistence.
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets. - Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
+43 -22
View File
@@ -14,7 +14,7 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { InteractionManager } from "@/stateManager/InteractionManager"; import { InteractionManager } from "@/stateManager/InteractionManager";
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig"; import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
import type { Vector3Tuple } from "@/types/3d"; import type { Vector3Tuple } from "@/types/3d";
import type { InteractableHandle, InteractableKind } from "@/types/interaction"; import type { InteractableHandle } from "@/types/interaction";
interface InteractableObjectBaseProps { interface InteractableObjectBaseProps {
label: string; label: string;
@@ -37,46 +37,67 @@ type InteractableObjectProps =
| TriggerInteractableObjectProps | TriggerInteractableObjectProps
| GrabInteractableObjectProps; | GrabInteractableObjectProps;
type MutableInteractableHandle = {
kind: InteractableKind;
label: string;
onPress: () => void;
onRelease?: () => void;
};
const _cameraPos = new THREE.Vector3(); const _cameraPos = new THREE.Vector3();
const _cameraDir = new THREE.Vector3(); const _cameraDir = new THREE.Vector3();
const _objectPos = new THREE.Vector3(); const _objectPos = new THREE.Vector3();
const _raycaster = new THREE.Raycaster(); 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( export function InteractableObject(
props: InteractableObjectProps, props: InteractableObjectProps,
): React.JSX.Element { ): React.JSX.Element {
const { kind, label, position, bodyRef, onPress, children } = props; 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 camera = useThree((state) => state.camera);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const debugSphereRef = useRef<THREE.Mesh>(null); const debugSphereRef = useRef<THREE.Mesh>(null);
const handle = useRef<InteractableHandle>( const handle = useRef<InteractableHandle>(createInteractableHandle(props));
props.kind === "grab"
? { kind: props.kind, label, onPress, onRelease: props.onRelease }
: { kind: props.kind, label, onPress },
);
useEffect(() => { useEffect(() => {
const current = handle.current as MutableInteractableHandle; const currentHandle = handle.current;
current.kind = kind;
current.label = label; if (currentHandle.kind === kind) {
current.onPress = onPress; currentHandle.label = label;
currentHandle.onPress = onPress;
if (currentHandle.kind === "grab") {
if (!onRelease) return;
currentHandle.onRelease = onRelease;
}
if (kind === "grab" && onRelease) {
current.onRelease = onRelease;
return; return;
} }
delete current.onRelease; if (kind === "grab") {
return undefined; 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]); }, [kind, label, onPress, onRelease]);
const setupInteractionDebugFolder = useCallback((folder: GUI) => { const setupInteractionDebugFolder = useCallback((folder: GUI) => {
+28 -43
View File
@@ -31,6 +31,20 @@ interface EditorControlsProps {
isPlayerMode?: boolean; 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({ export function EditorControls({
transformMode, transformMode,
onTransformModeChange, onTransformModeChange,
@@ -69,33 +83,18 @@ export function EditorControls({
</div> </div>
<div className="editor-transform-buttons"> <div className="editor-transform-buttons">
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
<button <button
className={`editor-transform-button ${transformMode === "translate" ? "active" : ""}`} key={mode}
onClick={() => onTransformModeChange("translate")} className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
aria-pressed={transformMode === "translate"} onClick={() => onTransformModeChange(mode)}
aria-pressed={transformMode === mode}
> >
<Move3D size={16} aria-hidden="true" /> <Icon size={16} aria-hidden="true" />
<span>Translate</span> <span>{label}</span>
<kbd>T</kbd> <kbd>{shortcut}</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>
</button> </button>
))}
</div> </div>
<div className="editor-history-buttons"> <div className="editor-history-buttons">
@@ -203,26 +202,12 @@ export function EditorControls({
</div> </div>
<dl className="editor-shortcuts-list"> <dl className="editor-shortcuts-list">
<div> {EDITOR_SHORTCUTS.map(([keys, description]) => (
<dt>Click</dt> <div key={keys}>
<dd>Select object</dd> <dt>{keys}</dt>
</div> <dd>{description}</dd>
<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>
</div> </div>
))}
</dl> </dl>
</section> </section>
+50 -30
View File
@@ -29,6 +29,12 @@ interface EditorNodeCommonProps {
onHoverNode: (index: number | null) => void; 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 { 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);
@@ -88,6 +94,36 @@ function cloneHighlightedMaterial(
return clone; 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({ export function EditorMap({
sceneData, sceneData,
selectedNodeIndex, selectedNodeIndex,
@@ -224,15 +260,16 @@ function EditorModelNode({
const { scene } = useGLTF(modelUrl); const { scene } = useGLTF(modelUrl);
const sceneInstance = useMemo(() => scene.clone(true), [scene]); const sceneInstance = useMemo(() => scene.clone(true), [scene]);
const pointerHandlers = createEditorNodePointerHandlers(
index,
onSelectNode,
onHoverNode,
);
useRegisteredEditorNode(groupRef, index, node, objectsMapRef); useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
useEffect(() => { useEffect(() => {
if (!groupRef.current) return; if (!groupRef.current) return;
const highlightColor = isSelected const highlightColor = getNodeHighlightColor(isSelected, isHovered);
? "#ffffff"
: isHovered
? "#b8b8b8"
: null;
groupRef.current.traverse((child) => { groupRef.current.traverse((child) => {
if (!(child instanceof THREE.Mesh)) { if (!(child instanceof THREE.Mesh)) {
@@ -288,18 +325,7 @@ function EditorModelNode({
position={node.position} position={node.position}
rotation={node.rotation} rotation={node.rotation}
scale={node.scale} scale={node.scale}
onClick={(e: ThreeEvent<MouseEvent>) => { {...pointerHandlers}
e.stopPropagation();
onSelectNode(index);
}}
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
onHoverNode(index);
}}
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
onHoverNode(null);
}}
/> />
); );
} }
@@ -314,9 +340,14 @@ function EditorFallbackNode({
onHoverNode, onHoverNode,
}: EditorNodeCommonProps) { }: EditorNodeCommonProps) {
const meshRef = useRef<THREE.Mesh>(null); const meshRef = useRef<THREE.Mesh>(null);
const pointerHandlers = createEditorNodePointerHandlers(
index,
onSelectNode,
onHoverNode,
);
useRegisteredEditorNode(meshRef, index, node, objectsMapRef); useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f"; const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
return ( return (
<mesh <mesh
@@ -324,18 +355,7 @@ function EditorFallbackNode({
position={node.position} position={node.position}
rotation={node.rotation} rotation={node.rotation}
scale={node.scale} scale={node.scale}
onClick={(e: ThreeEvent<MouseEvent>) => { {...pointerHandlers}
e.stopPropagation();
onSelectNode(index);
}}
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
onHoverNode(index);
}}
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
onHoverNode(null);
}}
> >
<boxGeometry args={[1, 1, 1]} /> <boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={color} /> <meshStandardMaterial color={color} />
+1 -1
View File
@@ -2,7 +2,7 @@ import { createContext } from "react";
export type DocsLanguage = "en" | "fr"; export type DocsLanguage = "en" | "fr";
export interface DocsLanguageContextValue { interface DocsLanguageContextValue {
language: DocsLanguage; language: DocsLanguage;
toggleLanguage: () => void; toggleLanguage: () => void;
} }
-2
View File
@@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15"; export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; 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_DAMPING_FACTOR = 0.05;
export const DEBUG_CAMERA_MIN_DISTANCE = 100; export const DEBUG_CAMERA_MIN_DISTANCE = 100;
export const DEBUG_CAMERA_MAX_DISTANCE = 1000; export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
+2 -2
View File
@@ -1,11 +1,11 @@
export interface DocSection { interface DocSection {
path: string; path: string;
title: string; title: string;
subtitle: string; subtitle: string;
meta: string; meta: string;
} }
export interface DocGroup { interface DocGroup {
label: string; label: string;
sections: DocSection[]; sections: DocSection[];
} }
+11 -5
View File
@@ -4,7 +4,13 @@ import { EditorControls } from "@/components/editor/EditorControls";
import { EditorScene } from "@/components/editor/scene/EditorScene"; import { EditorScene } from "@/components/editor/scene/EditorScene";
import { useEditorHistory } from "@/hooks/editor/useEditorHistory"; import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData"; 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 { export function EditorPage(): React.JSX.Element {
const { const {
@@ -46,7 +52,7 @@ export function EditorPage(): React.JSX.Element {
const handleSaveToServer = useCallback(async () => { const handleSaveToServer = useCallback(async () => {
if (!sceneData) return; if (!sceneData) return;
const json = JSON.stringify(sceneData.mapNodes, null, 2); const json = serializeMapNodes(sceneData);
try { try {
const response = await fetch("/api/save-map", { const response = await fetch("/api/save-map", {
@@ -58,17 +64,17 @@ export function EditorPage(): React.JSX.Element {
if (response.ok) { if (response.ok) {
alert("Map enregistrée avec succès!"); alert("Map enregistrée avec succès!");
} else { } else {
alert("Erreur lors de l'enregistrement"); alert(SAVE_ERROR_MESSAGE);
} }
} catch (err) { } catch (err) {
console.error("Error saving map:", err); console.error("Error saving map:", err);
alert("Erreur lors de l'enregistrement"); alert(SAVE_ERROR_MESSAGE);
} }
}, [sceneData]); }, [sceneData]);
const handleExportJson = useCallback(() => { const handleExportJson = useCallback(() => {
if (!sceneData) return; if (!sceneData) return;
const json = JSON.stringify(sceneData.mapNodes, null, 2); const json = serializeMapNodes(sceneData);
const blob = new Blob([json], { type: "application/json" }); const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
@@ -8,12 +8,13 @@ import {
PLAYER_EYE_HEIGHT, PLAYER_EYE_HEIGHT,
PLAYER_SPAWN_POSITION_GAME, PLAYER_SPAWN_POSITION_GAME,
} from "@/data/player/playerConfig"; } 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_SPAWN_POSITION_GAME[0],
PLAYER_EYE_HEIGHT, PLAYER_EYE_HEIGHT,
PLAYER_SPAWN_POSITION_GAME[2], PLAYER_SPAWN_POSITION_GAME[2],
] as const; ];
export function DebugCameraControls(): React.JSX.Element { export function DebugCameraControls(): React.JSX.Element {
return ( return (
+3 -1
View File
@@ -2,6 +2,7 @@ import type { MapNode, SceneData } from "@/types/editor";
const MAP_JSON_PATH = "/map.json"; const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAME = "model.gltf"; const MODEL_FILE_NAME = "model.gltf";
type ModelEntry = [modelName: string, modelUrl: string];
export async function loadMapSceneData(): Promise<SceneData | null> { export async function loadMapSceneData(): Promise<SceneData | null> {
const response = await fetch(MAP_JSON_PATH); const response = await fetch(MAP_JSON_PATH);
@@ -29,7 +30,8 @@ async function loadMapModelUrls(
try { try {
const response = await fetch(modelUrl, { method: "HEAD" }); 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 { } catch {
return null; return null;
} }
-55
View File
@@ -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);
+33 -36
View File
@@ -54,6 +54,25 @@ const _up = new THREE.Vector3(0, 1, 0);
const _translateVec = new THREE.Vector3(); const _translateVec = new THREE.Vector3();
const _collisionCorrection = 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({ export function PlayerController({
octree, octree,
spawnPosition, spawnPosition,
@@ -89,51 +108,29 @@ export function PlayerController({
const interaction = InteractionManager.getInstance(); const interaction = InteractionManager.getInstance();
const handleKeyDown = (event: KeyboardEvent): void => { const handleKeyDown = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) { if (setMovementKey(keys.current, event.key, true)) {
case MOVE_FORWARD_KEY: event.preventDefault();
keys.current.forward = true; return;
break; }
case MOVE_BACKWARD_KEY:
keys.current.backward = true; if (event.key === JUMP_KEY) {
break;
case MOVE_LEFT_KEY:
keys.current.left = true;
break;
case MOVE_RIGHT_KEY:
keys.current.right = true;
break;
case JUMP_KEY:
wantsJump.current = true; wantsJump.current = true;
break; event.preventDefault();
case INTERACT_KEY: return;
}
if (event.key.toLowerCase() === INTERACT_KEY) {
if (interaction.getState().focused?.kind === "trigger") { if (interaction.getState().focused?.kind === "trigger") {
interaction.pressInteract(); interaction.pressInteract();
} }
break;
default:
return;
}
event.preventDefault(); event.preventDefault();
}
}; };
const handleKeyUp = (event: KeyboardEvent): void => { const handleKeyUp = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) { if (setMovementKey(keys.current, event.key, false)) {
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;
}
event.preventDefault(); event.preventDefault();
}
}; };
const handleMouseDown = (event: MouseEvent): void => { const handleMouseDown = (event: MouseEvent): void => {