refactor: nettoie l'architecture monde et les docs
This commit is contained in:
+13
-49
@@ -15,12 +15,9 @@ export class SomeManager {
|
||||
return SomeManager._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
// init logic
|
||||
}
|
||||
private constructor() {}
|
||||
|
||||
destroy(): void {
|
||||
// cleanup logic
|
||||
SomeManager._instance = null;
|
||||
}
|
||||
}
|
||||
@@ -29,42 +26,11 @@ export class SomeManager {
|
||||
## Managers in this project
|
||||
|
||||
| Manager | File | Role |
|
||||
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| -------------------- | ------------------------------------ | -------------------------------------------------------------- |
|
||||
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
|
||||
| `InteractionManager` | `src/managers/InteractionManager.ts` | Focus, nearby, trigger, grab, and hand-grab interaction state. |
|
||||
| `GameManager` | target-state only | Future single source of truth for phase, zone, mission, input lock, dialogue. |
|
||||
| `CinematicManager` | target-state only | Future GSAP timeline orchestrator. |
|
||||
| `ZoneManager` | target-state only | Future zone entry/exit detection and LOD triggers. |
|
||||
|
||||
## Target-State GameManager
|
||||
|
||||
`GameManager` does not exist in the current implementation. The following pattern is target-state guidance only and should not be applied until the manager exists in code.
|
||||
|
||||
```ts
|
||||
export class GameManager {
|
||||
cinematic!: CinematicManager;
|
||||
audio!: AudioManager;
|
||||
zone!: ZoneManager;
|
||||
|
||||
private constructor() {
|
||||
this.cinematic = CinematicManager.getInstance();
|
||||
this.audio = AudioManager.getInstance();
|
||||
this.zone = ZoneManager.getInstance();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a `GameManager` exists, components and hooks should access other managers through it:
|
||||
|
||||
```ts
|
||||
// Correct
|
||||
GameManager.getInstance().cinematic.play("intro");
|
||||
|
||||
// Wrong — never import sub-managers directly in components
|
||||
CinematicManager.getInstance().play("intro");
|
||||
```
|
||||
|
||||
## Target-State Subscribe Pattern
|
||||
## Subscribe Pattern
|
||||
|
||||
```ts
|
||||
private listeners = new Set<() => void>()
|
||||
@@ -79,28 +45,26 @@ private emit(): void {
|
||||
}
|
||||
```
|
||||
|
||||
In that target-state manager, every `set*()` method calls `this.emit()` to notify subscribers.
|
||||
Managers that expose state to React call `this.emit()` from every `set*()` method that changes subscribed state.
|
||||
|
||||
## Target-State React Bridge Hook
|
||||
## React Bridge Hook
|
||||
|
||||
```ts
|
||||
// hooks/useGameState.ts
|
||||
export function useGameState() {
|
||||
const game = GameManager.getInstance();
|
||||
const [state, setState] = useState(game.getState());
|
||||
// hooks/interaction/useInteraction.ts
|
||||
const manager = InteractionManager.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
return game.subscribe(() => setState({ ...game.getState() }));
|
||||
}, [game]);
|
||||
|
||||
return state;
|
||||
export function useInteraction(): InteractionSnapshot {
|
||||
return useSyncExternalStore(
|
||||
manager.subscribe.bind(manager),
|
||||
manager.getState.bind(manager),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Do not add a `GameManager` unless the feature requires a real shared gameplay state owner.
|
||||
- Current managers may be imported directly until the target-state orchestrator exists.
|
||||
- Current managers may be imported directly.
|
||||
- Keep singleton managers limited to side-effect services or shared interaction state.
|
||||
- Always call `destroy()` on cleanup when a manager owns external resources.
|
||||
- Never create manager instances with `new` — always use `.getInstance()`.
|
||||
|
||||
@@ -84,7 +84,6 @@ jobs:
|
||||
SIZE=$(du -k dist/assets | cut -f1)
|
||||
echo "Bundle size: ${SIZE}KB"
|
||||
|
||||
# Threshold: 5000KB (configurable)
|
||||
THRESHOLD=5000
|
||||
|
||||
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
||||
|
||||
@@ -18,7 +18,7 @@ The current prototype puts the player in a repair-oriented world where they prog
|
||||
- Category-based audio manager for music, SFX, and dialogue
|
||||
- Dialogue manifest, SRT subtitles, subtitle overlay, and dialogue queueing
|
||||
- Cinematic manifest with GSAP camera keyframes and optional dialogue cues
|
||||
- In-game settings menu for volumes, subtitles, subtitle language, and the currently staged repair-runtime toggle
|
||||
- In-game settings menu for volumes, subtitles, and subtitle language
|
||||
- Debug mode with `?debug`, lil-gui controls, game-state panel, hand-tracking panel, debug camera, physics playground, and R3F perf
|
||||
- `/editor` route for map transforms, SRT editing, dialogue manifest editing, cinematic manifest editing, preview, validation, export, and dev-server saves
|
||||
- `/docs` route that renders the repository documentation inside the app
|
||||
@@ -154,8 +154,7 @@ WS ws://localhost:8000/ws
|
||||
## Current Caveats
|
||||
|
||||
- This is still a prototype, not a complete game runtime.
|
||||
- The repair-runtime toggle is stored in settings and displayed in the UI, but the repair game currently still runs locally in React/Three.
|
||||
- `useRepairMovementLocked()` currently returns `false`, so the movement-lock rule and indicator are present but disabled on `develop`.
|
||||
- `useRepairMovementLocked()` locks player movement during focused repair steps and drives the repair movement indicator.
|
||||
- Production editor persistence does not exist. Save endpoints in `vite.config.ts` are local Vite dev-server helpers.
|
||||
- The player uses octree collision while gameplay objects use Rapier. Keep that boundary deliberate unless the whole player controller is migrated.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ It supports:
|
||||
The debug physics scene currently uses it to preview:
|
||||
|
||||
```txt
|
||||
public/models/electricienne_animated/model.gltf
|
||||
public/models/electricienne-animated/model.gltf
|
||||
```
|
||||
|
||||
with the `Dance` animation.
|
||||
|
||||
@@ -297,8 +297,7 @@ public/models/{name}/model.gltf
|
||||
- The repository is still a prototype.
|
||||
- There is no central production `GameManager`.
|
||||
- The repair game is implemented, but broader mission orchestration is still light.
|
||||
- `useRepairMovementLocked()` currently returns `false`, so repair movement lock is disabled even though the rule and UI component exist.
|
||||
- The repair-runtime setting is stored in settings but not consumed by the repair-game implementation.
|
||||
- `useRepairMovementLocked()` locks player movement during focused repair steps.
|
||||
- Player collision and Rapier gameplay physics are separate systems.
|
||||
- Editor persistence is local development tooling only.
|
||||
- Debug systems are still part of active scene composition and should remain easy to identify.
|
||||
|
||||
@@ -184,7 +184,7 @@ Input is ignored while:
|
||||
- the settings menu is open
|
||||
- a cinematic is playing
|
||||
|
||||
Movement lock is read separately from `useRepairMovementLocked`, but that hook currently returns `false` on this branch.
|
||||
Movement lock is read separately from `useRepairMovementLocked`, which locks the player during focused repair steps.
|
||||
|
||||
## UI Prompt
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ They are under `src/managers/stores/` because they are shared runtime state, not
|
||||
## Store Responsibilities
|
||||
|
||||
| Store | Responsibility |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| ------------------ | ------------------------------------------------------------- |
|
||||
| `useGameStore` | Durable game progression, mission steps, cinematic input lock |
|
||||
| `useSettingsStore` | Menu visibility, volumes, subtitle options, repair-runtime toggle |
|
||||
| `useSettingsStore` | Menu visibility, volumes, and subtitle options |
|
||||
| `useSubtitleStore` | Currently displayed subtitle cue |
|
||||
|
||||
## Managers vs Stores
|
||||
|
||||
@@ -37,7 +37,7 @@ This document lists the user-visible and developer-facing features implemented i
|
||||
- Input lock while the settings menu is open
|
||||
- Input lock while a cinematic is playing
|
||||
- Octree collision against dedicated map collision nodes, currently scoped to the `terrain` node
|
||||
- Repair movement-lock hook and indicator exist, but the hook currently returns `false`, so movement is not locked during repair on the current branch
|
||||
- Repair movement lock during focused repair steps, with a matching UI indicator
|
||||
|
||||
## Physics And Collision
|
||||
|
||||
@@ -108,12 +108,11 @@ This document lists the user-visible and developer-facing features implemented i
|
||||
- Music, SFX, and dialogue volume sliders
|
||||
- Subtitle visibility toggle
|
||||
- Subtitle language choice between French and English
|
||||
- Repair-runtime choice between JavaScript and Python modes stored in settings
|
||||
- Quit action that clears browser-accessible cookies and returns to `/`
|
||||
- Crosshair overlay
|
||||
- Interaction prompt
|
||||
- Subtitle overlay
|
||||
- Repair movement-lock indicator component, currently inactive because the lock hook returns `false`
|
||||
- Repair movement-lock indicator
|
||||
- Debug overlay layout
|
||||
- Scene loading overlay
|
||||
|
||||
@@ -192,7 +191,7 @@ This document lists the user-visible and developer-facing features implemented i
|
||||
- Debug game-state panel
|
||||
- Debug hand-tracking panel
|
||||
- Physics test scene with floor, grabbable object, trigger object, repair zones, and animated model preview
|
||||
- Animated `electricienne_animated` model preview restored in the debug physics scene
|
||||
- Animated `electricienne-animated` model preview restored in the debug physics scene
|
||||
|
||||
## Map And Content Editor
|
||||
|
||||
@@ -230,7 +229,7 @@ This document lists the user-visible and developer-facing features implemented i
|
||||
- Technical docs for architecture, scene runtime, repair game, interaction, editor, audio, hand tracking, Zustand, Three debugging, animation, and target architecture
|
||||
- User docs for implemented features, main feature, editor usage, and code-review preparation
|
||||
|
||||
## Not Implemented Or Incomplete
|
||||
## Known Gaps
|
||||
|
||||
- Complete production mission manager/orchestrator
|
||||
- Full mission HUD or minimap
|
||||
|
||||
@@ -56,10 +56,10 @@ The mission config now carries the mission-specific variations. `ebike` repairs
|
||||
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
|
||||
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part.
|
||||
- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
|
||||
- `src/components/ui/RepairMovementLockIndicator.tsx` renders the HTML indicator intended for repair movement lock.
|
||||
- `src/components/ui/RepairMovementLockIndicator.tsx` renders the HTML repair movement-lock indicator.
|
||||
- `src/hooks/gameplay/useRepairFragmentationInput.ts` handles the `inspected -> fragmented` two-fists input and can optionally bind keyboard input for non-trigger flows.
|
||||
- `src/hooks/gameplay/useRepairMissionStep.ts` reads the active mission step from the game store.
|
||||
- `src/hooks/gameplay/useRepairMovementLocked.ts` exposes the shared repair movement-lock rule used by the player controller and UI indicator, but currently returns `false`.
|
||||
- `src/hooks/gameplay/useRepairMovementLocked.ts` exposes the shared repair movement-lock rule used by the player controller and UI indicator.
|
||||
- `src/hooks/handTracking/useBothFistsHold.ts` detects the reusable two-fists hold gesture.
|
||||
- `src/components/three/gameplay/RepairCaseModel.tsx` renders and animates the case model, and exposes `placeholder_*` transforms when the GLTF provides them.
|
||||
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
|
||||
|
||||
@@ -6,14 +6,14 @@ interface DocsDocumentProps {
|
||||
title: string;
|
||||
meta: string;
|
||||
content: string;
|
||||
frContent: string;
|
||||
frContent?: string;
|
||||
}
|
||||
|
||||
export function DocsDocument({
|
||||
title,
|
||||
meta,
|
||||
content,
|
||||
frContent,
|
||||
frContent = content,
|
||||
}: DocsDocumentProps): React.JSX.Element {
|
||||
const { language, toggleLanguage } = useDocsLanguage();
|
||||
const hasAlternateContent = frContent !== content;
|
||||
|
||||
@@ -496,10 +496,16 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
setContent(await response.text());
|
||||
setStatus(`Charge depuis ${srtPath}`);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error: unknown) => {
|
||||
if (!mounted) return;
|
||||
setContent(srtTemplate);
|
||||
setStatus("Erreur de chargement, template local cree");
|
||||
setStatus(
|
||||
`Erreur de chargement, template local cree: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
);
|
||||
logger.warn("EditorSrt", "Falling back to local SRT template", {
|
||||
srtPath,
|
||||
error: error instanceof Error ? error : String(error),
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import {
|
||||
MergedStaticMapModel,
|
||||
type MergedStaticMapModelProps,
|
||||
} from "@/components/three/world/MergedStaticMapModel";
|
||||
|
||||
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
||||
|
||||
interface EcoleModelProps {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
castShadow?: boolean;
|
||||
receiveShadow?: boolean;
|
||||
onLoaded?: () => void;
|
||||
}
|
||||
type EcoleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||
|
||||
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
|
||||
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import {
|
||||
MergedStaticMapModel,
|
||||
type MergedStaticMapModelProps,
|
||||
} from "@/components/three/world/MergedStaticMapModel";
|
||||
|
||||
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
|
||||
|
||||
interface FermeVerticaleModelProps {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
castShadow?: boolean;
|
||||
receiveShadow?: boolean;
|
||||
onLoaded?: () => void;
|
||||
}
|
||||
type FermeVerticaleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||
|
||||
export function FermeVerticaleModel(
|
||||
props: FermeVerticaleModelProps,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import {
|
||||
MergedStaticMapModel,
|
||||
type MergedStaticMapModelProps,
|
||||
} from "@/components/three/world/MergedStaticMapModel";
|
||||
|
||||
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
|
||||
|
||||
interface GenerateurModelProps {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
castShadow?: boolean;
|
||||
receiveShadow?: boolean;
|
||||
onLoaded?: () => void;
|
||||
}
|
||||
type GenerateurModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||
|
||||
export function GenerateurModel(
|
||||
props: GenerateurModelProps,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import {
|
||||
MergedStaticMapModel,
|
||||
type MergedStaticMapModelProps,
|
||||
} from "@/components/three/world/MergedStaticMapModel";
|
||||
|
||||
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
|
||||
|
||||
interface LafabrikModelProps {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
castShadow?: boolean;
|
||||
receiveShadow?: boolean;
|
||||
onLoaded?: () => void;
|
||||
}
|
||||
type LafabrikModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||
|
||||
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
|
||||
return <MergedStaticMapModel modelPath={LAFABRIK_MODEL_PATH} {...props} />;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||
|
||||
interface MergedStaticMapModelProps {
|
||||
export interface MergedStaticMapModelProps {
|
||||
modelPath: string;
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
|
||||
@@ -7,8 +7,6 @@ import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
interface SkyModelProps {
|
||||
modelPath: string;
|
||||
fallbackColor?: string | undefined;
|
||||
fallbackModelPath?: string | undefined;
|
||||
fallbackScale?: number | undefined;
|
||||
scale?: number | undefined;
|
||||
}
|
||||
|
||||
@@ -29,7 +27,6 @@ interface SkyModelErrorBoundaryState {
|
||||
const SKY_MODEL_SCALE = 1;
|
||||
const SKY_MODEL_RENDER_ORDER = -1000;
|
||||
const SKYBOX_MODEL_PATH = "/models/skybox/model.gltf";
|
||||
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
|
||||
class SkyModelErrorBoundary extends Component<
|
||||
SkyModelErrorBoundaryProps,
|
||||
@@ -55,21 +52,12 @@ class SkyModelErrorBoundary extends Component<
|
||||
|
||||
export function SkyModel({
|
||||
fallbackColor,
|
||||
fallbackModelPath,
|
||||
fallbackScale = SKY_MODEL_SCALE,
|
||||
modelPath,
|
||||
scale = SKY_MODEL_SCALE,
|
||||
}: SkyModelProps): React.JSX.Element {
|
||||
const colorFallback = fallbackColor ? (
|
||||
const fallback = fallbackColor ? (
|
||||
<color attach="background" args={[fallbackColor]} />
|
||||
) : null;
|
||||
const fallback = fallbackModelPath ? (
|
||||
<SkyModelErrorBoundary key={fallbackModelPath} fallback={colorFallback}>
|
||||
<SkyModelContent modelPath={fallbackModelPath} scale={fallbackScale} />
|
||||
</SkyModelErrorBoundary>
|
||||
) : (
|
||||
colorFallback
|
||||
);
|
||||
|
||||
return (
|
||||
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
|
||||
@@ -154,4 +142,3 @@ function disposeSkyModelMaterials(model: THREE.Object3D): void {
|
||||
}
|
||||
|
||||
useGLTF.preload(SKYBOX_MODEL_PATH);
|
||||
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { AudioCategory } from "@/managers/AudioManager";
|
||||
|
||||
export const AUDIO_PATHS = {
|
||||
intro: "/sounds/effect/fa.mp3",
|
||||
bienvenue: "/sounds/effect/fa.mp3",
|
||||
@@ -8,6 +6,8 @@ export const AUDIO_PATHS = {
|
||||
helped: "/sounds/effect/fa.mp3",
|
||||
} as const;
|
||||
|
||||
export type AudioCategory = "music" | "sfx" | "dialogue";
|
||||
|
||||
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
|
||||
music: 1,
|
||||
sfx: 1,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
|
||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
|
||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
||||
export const GAME_SCENE_FALLBACK_BACKGROUND_COLOR = "#0b1018";
|
||||
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { TERRAIN_COLORS, TERRAIN_TILE_SIZE } from "@/data/world/terrainConfig";
|
||||
|
||||
export const PATH_SURFACE_KEY = "chemin";
|
||||
export const PATH_DEBUG_PREVIEW_ENABLED = false;
|
||||
export const PATH_TILE_RENDER_ENABLED = false;
|
||||
export const PATH_TILE_MODEL_PATH = TERRAIN_COLORS.chemin.modelPath;
|
||||
export const PATH_TILE_SIZE =
|
||||
TERRAIN_COLORS.chemin.tileSize ?? TERRAIN_TILE_SIZE;
|
||||
export const PATH_TILE_SAMPLE_STEP = 2;
|
||||
export const PATH_TILE_MAX_COUNT = 1500;
|
||||
export const PATH_TILE_ROTATION = [0, 0, 0] as const;
|
||||
export const PATH_TILE_SCALE = [1, 1, 1] as const;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
|
||||
export function useActivityCity(): boolean {
|
||||
return useGameStore((state) => state.missionFlow.activityCity);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||
import type {
|
||||
TerrainSurfaceBounds,
|
||||
TerrainSurfaceData,
|
||||
} from "@/types/world/terrainSurface";
|
||||
import { createTerrainSurfaceImageData } from "@/utils/world/terrainSurfaceSampler";
|
||||
|
||||
function findTerrainBaseColorTexture(
|
||||
scene: THREE.Object3D,
|
||||
): THREE.Texture | null {
|
||||
let texture: THREE.Texture | null = null;
|
||||
|
||||
scene.traverse((child) => {
|
||||
if (texture || !(child instanceof THREE.Mesh)) return;
|
||||
|
||||
const materials = Array.isArray(child.material)
|
||||
? child.material
|
||||
: [child.material];
|
||||
|
||||
for (const material of materials) {
|
||||
if (material instanceof THREE.MeshStandardMaterial && material.map) {
|
||||
texture = material.map;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createTerrainSurfaceBounds(
|
||||
scene: THREE.Object3D,
|
||||
): TerrainSurfaceBounds {
|
||||
scene.updateWorldMatrix(true, true);
|
||||
|
||||
const box = new THREE.Box3().setFromObject(scene);
|
||||
return {
|
||||
minX: box.min.x,
|
||||
maxX: box.max.x,
|
||||
minZ: box.min.z,
|
||||
maxZ: box.max.z,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTerrainSurfaceData(): TerrainSurfaceData | null {
|
||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||
|
||||
return useMemo(() => {
|
||||
const texture = findTerrainBaseColorTexture(scene);
|
||||
if (!texture) return null;
|
||||
|
||||
const imageData = createTerrainSurfaceImageData(texture);
|
||||
if (!imageData) return null;
|
||||
|
||||
return {
|
||||
bounds: createTerrainSurfaceBounds(scene),
|
||||
imageData,
|
||||
raycastTarget: scene,
|
||||
};
|
||||
}, [scene]);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { DEFAULT_CATEGORY_VOLUMES } from "@/data/audioConfig";
|
||||
import {
|
||||
DEFAULT_CATEGORY_VOLUMES,
|
||||
type AudioCategory,
|
||||
} from "@/data/audioConfig";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
|
||||
export type AudioCategory = "music" | "sfx" | "dialogue";
|
||||
export type { AudioCategory } from "@/data/audioConfig";
|
||||
export type OneShotAudioCategory = Exclude<AudioCategory, "music">;
|
||||
|
||||
interface AudioContextWindow extends Window {
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={animation}
|
||||
frContent={animation}
|
||||
meta="15"
|
||||
title="Animation & 3D Model System"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={architecture}
|
||||
frContent={architecture}
|
||||
meta="02"
|
||||
title="Current Architecture"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsAudioPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={audio}
|
||||
frContent={audio}
|
||||
meta="08"
|
||||
title="Audio Technical Notes"
|
||||
/>
|
||||
<DocsDocument content={audio} meta="08" title="Audio Technical Notes" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsCodeReviewPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={codeReview}
|
||||
frContent={codeReview}
|
||||
meta="16"
|
||||
title="Code Review Prep"
|
||||
/>
|
||||
<DocsDocument content={codeReview} meta="16" title="Code Review Prep" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import features from "../../../../docs/user/features.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsFeaturesPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={features}
|
||||
frContent={features}
|
||||
meta="12"
|
||||
title="Features"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={features} meta="12" title="Features" />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={handTracking}
|
||||
frContent={handTracking}
|
||||
meta="09"
|
||||
title="Hand Tracking Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsInteractionPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={interaction}
|
||||
frContent={interaction}
|
||||
meta="05"
|
||||
title="Interaction System"
|
||||
/>
|
||||
<DocsDocument content={interaction} meta="05" title="Interaction System" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import mainFeature from "../../../../docs/user/main-feature.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsMainFeaturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={mainFeature}
|
||||
frContent={mainFeature}
|
||||
meta="13"
|
||||
title="Main Feature"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={mainFeature} meta="13" title="Main Feature" />;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import readme from "../../../README.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsReadmePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={readme}
|
||||
frContent={readme}
|
||||
meta="01"
|
||||
title="README"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={readme} meta="01" title="README" />;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsSceneRuntimePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={sceneRuntime}
|
||||
frContent={sceneRuntime}
|
||||
meta="03"
|
||||
title="Scene Runtime"
|
||||
/>
|
||||
<DocsDocument content={sceneRuntime} meta="03" title="Scene Runtime" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsTargetArchitecturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={targetArchitecture}
|
||||
frContent={targetArchitecture}
|
||||
meta="06"
|
||||
title="Target Architecture"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={technicalEditor}
|
||||
frContent={technicalEditor}
|
||||
meta="07"
|
||||
title="Editor Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsThreeDebuggingPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={threeDebugging}
|
||||
frContent={threeDebugging}
|
||||
meta="11"
|
||||
title="Three Debugging"
|
||||
/>
|
||||
<DocsDocument content={threeDebugging} meta="11" title="Three Debugging" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import zustand from "../../../../docs/technical/zustand.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsZustandPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={zustand}
|
||||
frContent={zustand}
|
||||
meta="10"
|
||||
title="Zustand Stores"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={zustand} meta="10" title="Zustand Stores" />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type * as THREE from "three";
|
||||
|
||||
export type TerrainSurfaceKind =
|
||||
| "grass"
|
||||
| "path"
|
||||
@@ -10,18 +8,6 @@ export type TerrainSurfaceKind =
|
||||
|
||||
export type TerrainSurfaceRgb = readonly [number, number, number];
|
||||
|
||||
export interface TerrainSurfaceUv {
|
||||
u: number;
|
||||
v: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceProjectionConfig {
|
||||
flipX?: boolean;
|
||||
flipZ?: boolean;
|
||||
offsetX?: number;
|
||||
offsetZ?: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceBounds {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
@@ -37,15 +23,3 @@ export interface TerrainSurfaceColorConfig {
|
||||
modelPath?: string;
|
||||
tileSize?: number;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceSample {
|
||||
rgb: TerrainSurfaceRgb;
|
||||
key: string | null;
|
||||
config: TerrainSurfaceColorConfig | null;
|
||||
}
|
||||
|
||||
export interface TerrainSurfaceData {
|
||||
bounds: TerrainSurfaceBounds;
|
||||
imageData: ImageData;
|
||||
raycastTarget: THREE.Object3D;
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
type TextureMaterialKey = Extract<
|
||||
| keyof THREE.MeshBasicMaterial
|
||||
| keyof THREE.MeshStandardMaterial
|
||||
| keyof THREE.MeshPhysicalMaterial
|
||||
| keyof THREE.MeshToonMaterial,
|
||||
string
|
||||
>;
|
||||
|
||||
type MaterialWithTextureSlots = THREE.Material &
|
||||
Partial<Record<TextureMaterialKey, THREE.Texture | null>>;
|
||||
|
||||
const MATERIAL_TEXTURE_KEYS = [
|
||||
"alphaMap",
|
||||
"aoMap",
|
||||
"bumpMap",
|
||||
"clearcoatMap",
|
||||
"clearcoatNormalMap",
|
||||
"clearcoatRoughnessMap",
|
||||
"displacementMap",
|
||||
"emissiveMap",
|
||||
"envMap",
|
||||
"gradientMap",
|
||||
"lightMap",
|
||||
"map",
|
||||
"metalnessMap",
|
||||
"normalMap",
|
||||
"roughnessMap",
|
||||
"sheenColorMap",
|
||||
"sheenRoughnessMap",
|
||||
"specularColorMap",
|
||||
"specularIntensityMap",
|
||||
"specularMap",
|
||||
"thicknessMap",
|
||||
"transmissionMap",
|
||||
] as const satisfies readonly TextureMaterialKey[];
|
||||
|
||||
export function disposeObject3D(object: THREE.Object3D): void {
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry?.dispose();
|
||||
|
||||
if (Array.isArray(child.material)) {
|
||||
for (const material of child.material) {
|
||||
disposeMaterial(material);
|
||||
}
|
||||
} else if (child.material) {
|
||||
disposeMaterial(child.material);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disposeMaterial(material: THREE.Material): void {
|
||||
material.dispose();
|
||||
const materialWithTextures = material as MaterialWithTextureSlots;
|
||||
|
||||
for (const key of MATERIAL_TEXTURE_KEYS) {
|
||||
const value = materialWithTextures[key];
|
||||
|
||||
if (value instanceof THREE.Texture) {
|
||||
value.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
TERRAIN_COLORS,
|
||||
TERRAIN_SURFACE_COLOR_TOLERANCE,
|
||||
type TerrainColorKey,
|
||||
} from "@/data/world/terrainConfig";
|
||||
import type { TerrainSurfaceRgb } from "@/types/world/terrainSurface";
|
||||
|
||||
export function colorMatchesTerrainSurface(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
targetRgb: TerrainSurfaceRgb,
|
||||
tolerance: number = TERRAIN_SURFACE_COLOR_TOLERANCE,
|
||||
): boolean {
|
||||
return (
|
||||
Math.abs(r - targetRgb[0]) <= tolerance &&
|
||||
Math.abs(g - targetRgb[1]) <= tolerance &&
|
||||
Math.abs(b - targetRgb[2]) <= tolerance
|
||||
);
|
||||
}
|
||||
|
||||
export function getTerrainColorKeyFromRgb(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): TerrainColorKey | null {
|
||||
for (const [key, config] of Object.entries(TERRAIN_COLORS)) {
|
||||
if (colorMatchesTerrainSurface(r, g, b, config.rgb)) {
|
||||
return key as TerrainColorKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isGrassTerrainColor(r: number, g: number, b: number): boolean {
|
||||
const key = getTerrainColorKeyFromRgb(r, g, b);
|
||||
return key !== null && TERRAIN_COLORS[key].kind === "grass";
|
||||
}
|
||||
|
||||
export function getGrassTipColorFromRgb(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
): string | null {
|
||||
const key = getTerrainColorKeyFromRgb(r, g, b);
|
||||
if (key === null) return null;
|
||||
|
||||
const terrainColor = TERRAIN_COLORS[key];
|
||||
return "grassTipColor" in terrainColor ? terrainColor.grassTipColor : null;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import * as THREE from "three";
|
||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||
import type {
|
||||
TerrainSurfaceBounds,
|
||||
TerrainSurfaceProjectionConfig,
|
||||
TerrainSurfaceRgb,
|
||||
TerrainSurfaceSample,
|
||||
TerrainSurfaceUv,
|
||||
} from "@/types/world/terrainSurface";
|
||||
import { getTerrainColorKeyFromRgb } from "@/utils/world/terrainSurfaceColor";
|
||||
|
||||
type TerrainSurfaceImageSource =
|
||||
| HTMLImageElement
|
||||
| HTMLCanvasElement
|
||||
| ImageBitmap;
|
||||
|
||||
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
||||
function clamp01(value: number): number {
|
||||
return Math.min(Math.max(value, 0), 1);
|
||||
}
|
||||
|
||||
function wrap01(value: number): number {
|
||||
return ((value % 1) + 1) % 1;
|
||||
}
|
||||
|
||||
function isTerrainSurfaceImageSource(
|
||||
value: unknown,
|
||||
): value is TerrainSurfaceImageSource {
|
||||
return (
|
||||
value instanceof HTMLImageElement ||
|
||||
value instanceof HTMLCanvasElement ||
|
||||
(typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap)
|
||||
);
|
||||
}
|
||||
|
||||
export function createTerrainSurfaceImageData(
|
||||
texture: THREE.Texture,
|
||||
): ImageData | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
const image = texture.image as unknown;
|
||||
if (!isTerrainSurfaceImageSource(image)) return null;
|
||||
|
||||
const cachedImageData = imageDataCache.get(image);
|
||||
if (cachedImageData) return cachedImageData;
|
||||
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return null;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
|
||||
const imageData = context.getImageData(0, 0, width, height);
|
||||
imageDataCache.set(image, imageData);
|
||||
return imageData;
|
||||
}
|
||||
|
||||
export function sampleTerrainSurfaceAtUv(
|
||||
imageData: ImageData,
|
||||
uv: TerrainSurfaceUv,
|
||||
): TerrainSurfaceSample {
|
||||
const x = Math.round(clamp01(uv.u) * (imageData.width - 1));
|
||||
const y = Math.round((1 - clamp01(uv.v)) * (imageData.height - 1));
|
||||
const index = (y * imageData.width + x) * 4;
|
||||
|
||||
const rgb: TerrainSurfaceRgb = [
|
||||
imageData.data[index] ?? 0,
|
||||
imageData.data[index + 1] ?? 0,
|
||||
imageData.data[index + 2] ?? 0,
|
||||
];
|
||||
const key = getTerrainColorKeyFromRgb(rgb[0], rgb[1], rgb[2]);
|
||||
|
||||
return {
|
||||
rgb,
|
||||
key,
|
||||
config: key === null ? null : TERRAIN_COLORS[key],
|
||||
};
|
||||
}
|
||||
|
||||
export function terrainSurfaceUvFromXZ(
|
||||
x: number,
|
||||
z: number,
|
||||
bounds: TerrainSurfaceBounds,
|
||||
projection?: TerrainSurfaceProjectionConfig,
|
||||
): TerrainSurfaceUv {
|
||||
const width = bounds.maxX - bounds.minX;
|
||||
const depth = bounds.maxZ - bounds.minZ;
|
||||
let u = width === 0 ? 0 : x / width + 0.5;
|
||||
let v = depth === 0 ? 0 : z / depth + 0.5;
|
||||
|
||||
if (projection?.flipX) {
|
||||
u = 1 - u;
|
||||
}
|
||||
|
||||
if (projection?.flipZ) {
|
||||
v = 1 - v;
|
||||
}
|
||||
|
||||
u = wrap01(u + (projection?.offsetX ?? 0));
|
||||
v = wrap01(v + (projection?.offsetZ ?? 0));
|
||||
|
||||
return {
|
||||
u,
|
||||
v,
|
||||
};
|
||||
}
|
||||
|
||||
export function sampleTerrainSurfaceAtXZ(
|
||||
imageData: ImageData,
|
||||
x: number,
|
||||
z: number,
|
||||
bounds: TerrainSurfaceBounds,
|
||||
projection?: TerrainSurfaceProjectionConfig,
|
||||
): TerrainSurfaceSample {
|
||||
return sampleTerrainSurfaceAtUv(
|
||||
imageData,
|
||||
terrainSurfaceUvFromXZ(x, z, bounds, projection),
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_SCALE,
|
||||
GAME_SCENE_SKY_MODEL_PATH,
|
||||
GAME_SCENE_SKY_MODEL_SCALE,
|
||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||
@@ -37,8 +35,6 @@ export function Environment(): React.JSX.Element {
|
||||
{showSky ? (
|
||||
<SkyModel
|
||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
||||
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
|
||||
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
|
||||
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
||||
/>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||
import {
|
||||
PATH_DEBUG_PREVIEW_ENABLED,
|
||||
PATH_TILE_RENDER_ENABLED,
|
||||
PATH_TILE_MODEL_PATH,
|
||||
} from "@/data/world/pathConfig";
|
||||
import { usePathTileData } from "@/world/paths/usePathTileData";
|
||||
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
|
||||
|
||||
export function PathSystem(): React.JSX.Element | null {
|
||||
if (!PATH_DEBUG_PREVIEW_ENABLED && !PATH_TILE_RENDER_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PathTiles />;
|
||||
}
|
||||
|
||||
function PathTiles(): React.JSX.Element | null {
|
||||
const pathTiles = usePathTileData();
|
||||
|
||||
if (pathTiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PATH_DEBUG_PREVIEW_ENABLED) {
|
||||
return <PathDebugPreview instances={pathTiles} />;
|
||||
}
|
||||
|
||||
if (!PATH_TILE_RENDER_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InstancedMapAsset
|
||||
castShadow={false}
|
||||
instances={pathTiles}
|
||||
modelPath={PATH_TILE_MODEL_PATH}
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PathDebugPreview({
|
||||
instances,
|
||||
}: {
|
||||
instances: MapAssetInstance[];
|
||||
}): React.JSX.Element {
|
||||
const instancedMeshRef = useRef<THREE.InstancedMesh>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instancedMesh = instancedMeshRef.current;
|
||||
if (!instancedMesh) return;
|
||||
|
||||
const matrix = new THREE.Matrix4();
|
||||
const position = new THREE.Vector3();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
const scale = new THREE.Vector3(1, 1, 1);
|
||||
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
if (!instance) continue;
|
||||
|
||||
position.set(
|
||||
instance.position[0],
|
||||
instance.position[1] + 0.08,
|
||||
instance.position[2],
|
||||
);
|
||||
matrix.compose(position, quaternion, scale);
|
||||
instancedMesh.setMatrixAt(i, matrix);
|
||||
}
|
||||
|
||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||
instancedMesh.computeBoundingSphere();
|
||||
}, [instances]);
|
||||
|
||||
return (
|
||||
<instancedMesh
|
||||
ref={instancedMeshRef}
|
||||
args={[undefined, undefined, instances.length]}
|
||||
>
|
||||
<boxGeometry args={[0.35, 0.08, 0.35]} />
|
||||
<meshBasicMaterial color="#ff00ff" />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig";
|
||||
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
||||
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
|
||||
import {
|
||||
PATH_TILE_MAX_COUNT,
|
||||
PATH_SURFACE_KEY,
|
||||
PATH_TILE_ROTATION,
|
||||
PATH_TILE_SAMPLE_STEP,
|
||||
PATH_TILE_SCALE,
|
||||
} from "@/data/world/pathConfig";
|
||||
|
||||
function createSampleCenters(min: number, max: number, step: number): number[] {
|
||||
const start = Math.ceil(min / step) * step + step * 0.5;
|
||||
const centers: number[] = [];
|
||||
|
||||
for (let value = start; value <= max; value += step) {
|
||||
centers.push(value);
|
||||
}
|
||||
|
||||
return centers;
|
||||
}
|
||||
|
||||
export function usePathTileData(): MapAssetInstance[] {
|
||||
const terrainSurfaceData = useTerrainSurfaceData();
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!terrainSurfaceData) return [];
|
||||
|
||||
const instances: MapAssetInstance[] = [];
|
||||
const xCenters = createSampleCenters(
|
||||
terrainSurfaceData.bounds.minX,
|
||||
terrainSurfaceData.bounds.maxX,
|
||||
PATH_TILE_SAMPLE_STEP,
|
||||
);
|
||||
const zCenters = createSampleCenters(
|
||||
terrainSurfaceData.bounds.minZ,
|
||||
terrainSurfaceData.bounds.maxZ,
|
||||
PATH_TILE_SAMPLE_STEP,
|
||||
);
|
||||
|
||||
for (const x of xCenters) {
|
||||
for (const z of zCenters) {
|
||||
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
|
||||
|
||||
const sample = sampleTerrainSurfaceAtXZ(
|
||||
terrainSurfaceData.imageData,
|
||||
x,
|
||||
z,
|
||||
terrainSurfaceData.bounds,
|
||||
TERRAIN_SURFACE_PROJECTION,
|
||||
);
|
||||
|
||||
if (sample.key !== PATH_SURFACE_KEY) continue;
|
||||
|
||||
const height = terrainHeight.getHeight(x, z) ?? 0;
|
||||
|
||||
instances.push({
|
||||
position: [x, height, z],
|
||||
rotation: [...PATH_TILE_ROTATION] as Vector3Tuple,
|
||||
scale: [...PATH_TILE_SCALE] as Vector3Tuple,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}, [terrainHeight, terrainSurfaceData]);
|
||||
}
|
||||
+9
-10
@@ -107,7 +107,7 @@ const saveSrtPlugin = (): Plugin => ({
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(Buffer.concat(chunks).toString()) as unknown;
|
||||
const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
|
||||
if (!isSrtPayload(data)) {
|
||||
sendJson(res, 400, { error: "Invalid SRT payload" });
|
||||
return;
|
||||
@@ -189,7 +189,7 @@ const saveDialogueManifestPlugin = (): Plugin => ({
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(Buffer.concat(chunks).toString()) as unknown;
|
||||
const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
|
||||
parseDialogueManifestData(data);
|
||||
|
||||
const manifestPath = path.resolve(
|
||||
@@ -235,7 +235,7 @@ const saveCinematicManifestPlugin = (): Plugin => ({
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(Buffer.concat(chunks).toString()) as unknown;
|
||||
const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
|
||||
const manifest = parseCinematicManifestData(data);
|
||||
const dialogueManifest = await loadDialogueManifestData();
|
||||
validateCinematicDialogueCues(manifest, dialogueManifest);
|
||||
@@ -304,15 +304,14 @@ interface CinematicKeyframeData {
|
||||
}
|
||||
|
||||
function isSrtPayload(data: unknown): data is SrtPayload {
|
||||
if (!data || typeof data !== "object") return false;
|
||||
if (!isRecord(data)) return false;
|
||||
|
||||
const payload = data as Partial<SrtPayload>;
|
||||
return (
|
||||
typeof payload.voice === "string" &&
|
||||
SRT_VOICES.has(payload.voice) &&
|
||||
typeof payload.language === "string" &&
|
||||
SRT_LANGUAGES.has(payload.language) &&
|
||||
typeof payload.content === "string"
|
||||
typeof data.voice === "string" &&
|
||||
SRT_VOICES.has(data.voice) &&
|
||||
typeof data.language === "string" &&
|
||||
SRT_LANGUAGES.has(data.language) &&
|
||||
typeof data.content === "string"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user