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;
|
return SomeManager._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {}
|
||||||
// init logic
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
// cleanup logic
|
|
||||||
SomeManager._instance = null;
|
SomeManager._instance = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,42 +26,11 @@ export class SomeManager {
|
|||||||
## Managers in this project
|
## Managers in this project
|
||||||
|
|
||||||
| Manager | File | Role |
|
| Manager | File | Role |
|
||||||
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------ | -------------------------------------------------------------- |
|
||||||
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
|
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
|
||||||
| `InteractionManager` | `src/managers/InteractionManager.ts` | Focus, nearby, trigger, grab, and hand-grab interaction state. |
|
| `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
|
## Subscribe Pattern
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
private listeners = new Set<() => void>()
|
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
|
```ts
|
||||||
// hooks/useGameState.ts
|
// hooks/interaction/useInteraction.ts
|
||||||
export function useGameState() {
|
const manager = InteractionManager.getInstance();
|
||||||
const game = GameManager.getInstance();
|
|
||||||
const [state, setState] = useState(game.getState());
|
|
||||||
|
|
||||||
useEffect(() => {
|
export function useInteraction(): InteractionSnapshot {
|
||||||
return game.subscribe(() => setState({ ...game.getState() }));
|
return useSyncExternalStore(
|
||||||
}, [game]);
|
manager.subscribe.bind(manager),
|
||||||
|
manager.getState.bind(manager),
|
||||||
return state;
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Do not add a `GameManager` unless the feature requires a real shared gameplay state owner.
|
- 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.
|
- Keep singleton managers limited to side-effect services or shared interaction state.
|
||||||
- Always call `destroy()` on cleanup when a manager owns external resources.
|
- Always call `destroy()` on cleanup when a manager owns external resources.
|
||||||
- Never create manager instances with `new` — always use `.getInstance()`.
|
- Never create manager instances with `new` — always use `.getInstance()`.
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ jobs:
|
|||||||
SIZE=$(du -k dist/assets | cut -f1)
|
SIZE=$(du -k dist/assets | cut -f1)
|
||||||
echo "Bundle size: ${SIZE}KB"
|
echo "Bundle size: ${SIZE}KB"
|
||||||
|
|
||||||
# Threshold: 5000KB (configurable)
|
|
||||||
THRESHOLD=5000
|
THRESHOLD=5000
|
||||||
|
|
||||||
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
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
|
- Category-based audio manager for music, SFX, and dialogue
|
||||||
- Dialogue manifest, SRT subtitles, subtitle overlay, and dialogue queueing
|
- Dialogue manifest, SRT subtitles, subtitle overlay, and dialogue queueing
|
||||||
- Cinematic manifest with GSAP camera keyframes and optional dialogue cues
|
- 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
|
- 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
|
- `/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
|
- `/docs` route that renders the repository documentation inside the app
|
||||||
@@ -154,8 +154,7 @@ WS ws://localhost:8000/ws
|
|||||||
## Current Caveats
|
## Current Caveats
|
||||||
|
|
||||||
- This is still a prototype, not a complete game runtime.
|
- 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()` locks player movement during focused repair steps and drives the repair movement indicator.
|
||||||
- `useRepairMovementLocked()` currently returns `false`, so the movement-lock rule and indicator are present but disabled on `develop`.
|
|
||||||
- Production editor persistence does not exist. Save endpoints in `vite.config.ts` are local Vite dev-server helpers.
|
- 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.
|
- 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:
|
The debug physics scene currently uses it to preview:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
public/models/electricienne_animated/model.gltf
|
public/models/electricienne-animated/model.gltf
|
||||||
```
|
```
|
||||||
|
|
||||||
with the `Dance` animation.
|
with the `Dance` animation.
|
||||||
|
|||||||
@@ -297,8 +297,7 @@ public/models/{name}/model.gltf
|
|||||||
- The repository is still a prototype.
|
- The repository is still a prototype.
|
||||||
- There is no central production `GameManager`.
|
- There is no central production `GameManager`.
|
||||||
- The repair game is implemented, but broader mission orchestration is still light.
|
- 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.
|
- `useRepairMovementLocked()` locks player movement during focused repair steps.
|
||||||
- The repair-runtime setting is stored in settings but not consumed by the repair-game implementation.
|
|
||||||
- Player collision and Rapier gameplay physics are separate systems.
|
- Player collision and Rapier gameplay physics are separate systems.
|
||||||
- Editor persistence is local development tooling only.
|
- Editor persistence is local development tooling only.
|
||||||
- Debug systems are still part of active scene composition and should remain easy to identify.
|
- 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
|
- the settings menu is open
|
||||||
- a cinematic is playing
|
- 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
|
## UI Prompt
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ They are under `src/managers/stores/` because they are shared runtime state, not
|
|||||||
## Store Responsibilities
|
## Store Responsibilities
|
||||||
|
|
||||||
| Store | Responsibility |
|
| Store | Responsibility |
|
||||||
| ------------------ | ----------------------------------------------------------------- |
|
| ------------------ | ------------------------------------------------------------- |
|
||||||
| `useGameStore` | Durable game progression, mission steps, cinematic input lock |
|
| `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 |
|
| `useSubtitleStore` | Currently displayed subtitle cue |
|
||||||
|
|
||||||
## Managers vs Stores
|
## 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 the settings menu is open
|
||||||
- Input lock while a cinematic is playing
|
- Input lock while a cinematic is playing
|
||||||
- Octree collision against dedicated map collision nodes, currently scoped to the `terrain` node
|
- 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
|
## 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
|
- Music, SFX, and dialogue volume sliders
|
||||||
- Subtitle visibility toggle
|
- Subtitle visibility toggle
|
||||||
- Subtitle language choice between French and English
|
- 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 `/`
|
- Quit action that clears browser-accessible cookies and returns to `/`
|
||||||
- Crosshair overlay
|
- Crosshair overlay
|
||||||
- Interaction prompt
|
- Interaction prompt
|
||||||
- Subtitle overlay
|
- Subtitle overlay
|
||||||
- Repair movement-lock indicator component, currently inactive because the lock hook returns `false`
|
- Repair movement-lock indicator
|
||||||
- Debug overlay layout
|
- Debug overlay layout
|
||||||
- Scene loading overlay
|
- Scene loading overlay
|
||||||
|
|
||||||
@@ -192,7 +191,7 @@ This document lists the user-visible and developer-facing features implemented i
|
|||||||
- Debug game-state panel
|
- Debug game-state panel
|
||||||
- Debug hand-tracking panel
|
- Debug hand-tracking panel
|
||||||
- Physics test scene with floor, grabbable object, trigger object, repair zones, and animated model preview
|
- 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
|
## 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
|
- 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
|
- User docs for implemented features, main feature, editor usage, and code-review preparation
|
||||||
|
|
||||||
## Not Implemented Or Incomplete
|
## Known Gaps
|
||||||
|
|
||||||
- Complete production mission manager/orchestrator
|
- Complete production mission manager/orchestrator
|
||||||
- Full mission HUD or minimap
|
- 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/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/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/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/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/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/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/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.
|
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ interface DocsDocumentProps {
|
|||||||
title: string;
|
title: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
content: string;
|
content: string;
|
||||||
frContent: string;
|
frContent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocsDocument({
|
export function DocsDocument({
|
||||||
title,
|
title,
|
||||||
meta,
|
meta,
|
||||||
content,
|
content,
|
||||||
frContent,
|
frContent = content,
|
||||||
}: DocsDocumentProps): React.JSX.Element {
|
}: DocsDocumentProps): React.JSX.Element {
|
||||||
const { language, toggleLanguage } = useDocsLanguage();
|
const { language, toggleLanguage } = useDocsLanguage();
|
||||||
const hasAlternateContent = frContent !== content;
|
const hasAlternateContent = frContent !== content;
|
||||||
|
|||||||
@@ -496,10 +496,16 @@ export function EditorSrtPanel(): React.JSX.Element {
|
|||||||
setContent(await response.text());
|
setContent(await response.text());
|
||||||
setStatus(`Charge depuis ${srtPath}`);
|
setStatus(`Charge depuis ${srtPath}`);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error: unknown) => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setContent(srtTemplate);
|
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 () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
import {
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
MergedStaticMapModel,
|
||||||
|
type MergedStaticMapModelProps,
|
||||||
|
} from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
|
||||||
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
|
||||||
|
|
||||||
interface EcoleModelProps {
|
type EcoleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
castShadow?: boolean;
|
|
||||||
receiveShadow?: boolean;
|
|
||||||
onLoaded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
|
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
|
||||||
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
|
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
import {
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
MergedStaticMapModel,
|
||||||
|
type MergedStaticMapModelProps,
|
||||||
|
} from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
|
||||||
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
|
const FERME_VERTICALE_MODEL_PATH = "/models/fermeverticale/model.gltf";
|
||||||
|
|
||||||
interface FermeVerticaleModelProps {
|
type FermeVerticaleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
castShadow?: boolean;
|
|
||||||
receiveShadow?: boolean;
|
|
||||||
onLoaded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FermeVerticaleModel(
|
export function FermeVerticaleModel(
|
||||||
props: FermeVerticaleModelProps,
|
props: FermeVerticaleModelProps,
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
import {
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
MergedStaticMapModel,
|
||||||
|
type MergedStaticMapModelProps,
|
||||||
|
} from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
|
||||||
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
|
const GENERATEUR_MODEL_PATH = "/models/generateur/model.gltf";
|
||||||
|
|
||||||
interface GenerateurModelProps {
|
type GenerateurModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
castShadow?: boolean;
|
|
||||||
receiveShadow?: boolean;
|
|
||||||
onLoaded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GenerateurModel(
|
export function GenerateurModel(
|
||||||
props: GenerateurModelProps,
|
props: GenerateurModelProps,
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { MergedStaticMapModel } from "@/components/three/world/MergedStaticMapModel";
|
import {
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
MergedStaticMapModel,
|
||||||
|
type MergedStaticMapModelProps,
|
||||||
|
} from "@/components/three/world/MergedStaticMapModel";
|
||||||
|
|
||||||
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
|
const LAFABRIK_MODEL_PATH = "/models/lafabrik/model.gltf";
|
||||||
|
|
||||||
interface LafabrikModelProps {
|
type LafabrikModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
castShadow?: boolean;
|
|
||||||
receiveShadow?: boolean;
|
|
||||||
onLoaded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
|
export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element {
|
||||||
return <MergedStaticMapModel modelPath={LAFABRIK_MODEL_PATH} {...props} />;
|
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 type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
|
||||||
|
|
||||||
interface MergedStaticMapModelProps {
|
export interface MergedStaticMapModelProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
rotation: Vector3Tuple;
|
rotation: Vector3Tuple;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
|||||||
interface SkyModelProps {
|
interface SkyModelProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
fallbackColor?: string | undefined;
|
fallbackColor?: string | undefined;
|
||||||
fallbackModelPath?: string | undefined;
|
|
||||||
fallbackScale?: number | undefined;
|
|
||||||
scale?: number | undefined;
|
scale?: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +27,6 @@ interface SkyModelErrorBoundaryState {
|
|||||||
const SKY_MODEL_SCALE = 1;
|
const SKY_MODEL_SCALE = 1;
|
||||||
const SKY_MODEL_RENDER_ORDER = -1000;
|
const SKY_MODEL_RENDER_ORDER = -1000;
|
||||||
const SKYBOX_MODEL_PATH = "/models/skybox/model.gltf";
|
const SKYBOX_MODEL_PATH = "/models/skybox/model.gltf";
|
||||||
const LEGACY_SKY_MODEL_PATH = "/models/sky/model.glb";
|
|
||||||
|
|
||||||
class SkyModelErrorBoundary extends Component<
|
class SkyModelErrorBoundary extends Component<
|
||||||
SkyModelErrorBoundaryProps,
|
SkyModelErrorBoundaryProps,
|
||||||
@@ -55,21 +52,12 @@ class SkyModelErrorBoundary extends Component<
|
|||||||
|
|
||||||
export function SkyModel({
|
export function SkyModel({
|
||||||
fallbackColor,
|
fallbackColor,
|
||||||
fallbackModelPath,
|
|
||||||
fallbackScale = SKY_MODEL_SCALE,
|
|
||||||
modelPath,
|
modelPath,
|
||||||
scale = SKY_MODEL_SCALE,
|
scale = SKY_MODEL_SCALE,
|
||||||
}: SkyModelProps): React.JSX.Element {
|
}: SkyModelProps): React.JSX.Element {
|
||||||
const colorFallback = fallbackColor ? (
|
const fallback = fallbackColor ? (
|
||||||
<color attach="background" args={[fallbackColor]} />
|
<color attach="background" args={[fallbackColor]} />
|
||||||
) : null;
|
) : null;
|
||||||
const fallback = fallbackModelPath ? (
|
|
||||||
<SkyModelErrorBoundary key={fallbackModelPath} fallback={colorFallback}>
|
|
||||||
<SkyModelContent modelPath={fallbackModelPath} scale={fallbackScale} />
|
|
||||||
</SkyModelErrorBoundary>
|
|
||||||
) : (
|
|
||||||
colorFallback
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
|
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
|
||||||
@@ -154,4 +142,3 @@ function disposeSkyModelMaterials(model: THREE.Object3D): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useGLTF.preload(SKYBOX_MODEL_PATH);
|
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 = {
|
export const AUDIO_PATHS = {
|
||||||
intro: "/sounds/effect/fa.mp3",
|
intro: "/sounds/effect/fa.mp3",
|
||||||
bienvenue: "/sounds/effect/fa.mp3",
|
bienvenue: "/sounds/effect/fa.mp3",
|
||||||
@@ -8,6 +6,8 @@ export const AUDIO_PATHS = {
|
|||||||
helped: "/sounds/effect/fa.mp3",
|
helped: "/sounds/effect/fa.mp3",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type AudioCategory = "music" | "sfx" | "dialogue";
|
||||||
|
|
||||||
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
|
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
|
||||||
music: 1,
|
music: 1,
|
||||||
sfx: 1,
|
sfx: 1,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
|
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_SKY_MODEL_SCALE = 100;
|
||||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
|
||||||
export const GAME_SCENE_FALLBACK_BACKGROUND_COLOR = "#0b1018";
|
export const GAME_SCENE_FALLBACK_BACKGROUND_COLOR = "#0b1018";
|
||||||
export const PHYSICS_SCENE_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";
|
import { logger } from "@/utils/core/Logger";
|
||||||
|
|
||||||
export type AudioCategory = "music" | "sfx" | "dialogue";
|
export type { AudioCategory } from "@/data/audioConfig";
|
||||||
export type OneShotAudioCategory = Exclude<AudioCategory, "music">;
|
export type OneShotAudioCategory = Exclude<AudioCategory, "music">;
|
||||||
|
|
||||||
interface AudioContextWindow extends Window {
|
interface AudioContextWindow extends Window {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={animation}
|
content={animation}
|
||||||
frContent={animation}
|
|
||||||
meta="15"
|
meta="15"
|
||||||
title="Animation & 3D Model System"
|
title="Animation & 3D Model System"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={architecture}
|
content={architecture}
|
||||||
frContent={architecture}
|
|
||||||
meta="02"
|
meta="02"
|
||||||
title="Current Architecture"
|
title="Current Architecture"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
|||||||
|
|
||||||
export function DocsAudioPage(): React.JSX.Element {
|
export function DocsAudioPage(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument content={audio} meta="08" title="Audio Technical Notes" />
|
||||||
content={audio}
|
|
||||||
frContent={audio}
|
|
||||||
meta="08"
|
|
||||||
title="Audio Technical Notes"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
|||||||
|
|
||||||
export function DocsCodeReviewPage(): React.JSX.Element {
|
export function DocsCodeReviewPage(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument content={codeReview} meta="16" title="Code Review Prep" />
|
||||||
content={codeReview}
|
|
||||||
frContent={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";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsFeaturesPage(): React.JSX.Element {
|
export function DocsFeaturesPage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={features} meta="12" title="Features" />;
|
||||||
<DocsDocument
|
|
||||||
content={features}
|
|
||||||
frContent={features}
|
|
||||||
meta="12"
|
|
||||||
title="Features"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={handTracking}
|
content={handTracking}
|
||||||
frContent={handTracking}
|
|
||||||
meta="09"
|
meta="09"
|
||||||
title="Hand Tracking Technical Notes"
|
title="Hand Tracking Technical Notes"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
|||||||
|
|
||||||
export function DocsInteractionPage(): React.JSX.Element {
|
export function DocsInteractionPage(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument content={interaction} meta="05" title="Interaction System" />
|
||||||
content={interaction}
|
|
||||||
frContent={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";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsMainFeaturePage(): React.JSX.Element {
|
export function DocsMainFeaturePage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={mainFeature} meta="13" title="Main Feature" />;
|
||||||
<DocsDocument
|
|
||||||
content={mainFeature}
|
|
||||||
frContent={mainFeature}
|
|
||||||
meta="13"
|
|
||||||
title="Main Feature"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,5 @@ import readme from "../../../README.md?raw";
|
|||||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsReadmePage(): React.JSX.Element {
|
export function DocsReadmePage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={readme} meta="01" title="README" />;
|
||||||
<DocsDocument
|
|
||||||
content={readme}
|
|
||||||
frContent={readme}
|
|
||||||
meta="01"
|
|
||||||
title="README"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
|||||||
|
|
||||||
export function DocsSceneRuntimePage(): React.JSX.Element {
|
export function DocsSceneRuntimePage(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument content={sceneRuntime} meta="03" title="Scene Runtime" />
|
||||||
content={sceneRuntime}
|
|
||||||
frContent={sceneRuntime}
|
|
||||||
meta="03"
|
|
||||||
title="Scene Runtime"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export function DocsTargetArchitecturePage(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={targetArchitecture}
|
content={targetArchitecture}
|
||||||
frContent={targetArchitecture}
|
|
||||||
meta="06"
|
meta="06"
|
||||||
title="Target Architecture"
|
title="Target Architecture"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument
|
||||||
content={technicalEditor}
|
content={technicalEditor}
|
||||||
frContent={technicalEditor}
|
|
||||||
meta="07"
|
meta="07"
|
||||||
title="Editor Technical Notes"
|
title="Editor Technical Notes"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
|||||||
|
|
||||||
export function DocsThreeDebuggingPage(): React.JSX.Element {
|
export function DocsThreeDebuggingPage(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocsDocument
|
<DocsDocument content={threeDebugging} meta="11" title="Three Debugging" />
|
||||||
content={threeDebugging}
|
|
||||||
frContent={threeDebugging}
|
|
||||||
meta="11"
|
|
||||||
title="Three Debugging"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,5 @@ import zustand from "../../../../docs/technical/zustand.md?raw";
|
|||||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||||
|
|
||||||
export function DocsZustandPage(): React.JSX.Element {
|
export function DocsZustandPage(): React.JSX.Element {
|
||||||
return (
|
return <DocsDocument content={zustand} meta="10" title="Zustand Stores" />;
|
||||||
<DocsDocument
|
|
||||||
content={zustand}
|
|
||||||
frContent={zustand}
|
|
||||||
meta="10"
|
|
||||||
title="Zustand Stores"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type * as THREE from "three";
|
|
||||||
|
|
||||||
export type TerrainSurfaceKind =
|
export type TerrainSurfaceKind =
|
||||||
| "grass"
|
| "grass"
|
||||||
| "path"
|
| "path"
|
||||||
@@ -10,18 +8,6 @@ export type TerrainSurfaceKind =
|
|||||||
|
|
||||||
export type TerrainSurfaceRgb = readonly [number, number, number];
|
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 {
|
export interface TerrainSurfaceBounds {
|
||||||
minX: number;
|
minX: number;
|
||||||
maxX: number;
|
maxX: number;
|
||||||
@@ -37,15 +23,3 @@ export interface TerrainSurfaceColorConfig {
|
|||||||
modelPath?: string;
|
modelPath?: string;
|
||||||
tileSize?: number;
|
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 {
|
import {
|
||||||
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
|
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_PATH,
|
||||||
GAME_SCENE_SKY_MODEL_SCALE,
|
GAME_SCENE_SKY_MODEL_SCALE,
|
||||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||||
@@ -37,8 +35,6 @@ export function Environment(): React.JSX.Element {
|
|||||||
{showSky ? (
|
{showSky ? (
|
||||||
<SkyModel
|
<SkyModel
|
||||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
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}
|
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
||||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
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 {
|
try {
|
||||||
const data = JSON.parse(Buffer.concat(chunks).toString()) as unknown;
|
const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
|
||||||
if (!isSrtPayload(data)) {
|
if (!isSrtPayload(data)) {
|
||||||
sendJson(res, 400, { error: "Invalid SRT payload" });
|
sendJson(res, 400, { error: "Invalid SRT payload" });
|
||||||
return;
|
return;
|
||||||
@@ -189,7 +189,7 @@ const saveDialogueManifestPlugin = (): Plugin => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(Buffer.concat(chunks).toString()) as unknown;
|
const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
|
||||||
parseDialogueManifestData(data);
|
parseDialogueManifestData(data);
|
||||||
|
|
||||||
const manifestPath = path.resolve(
|
const manifestPath = path.resolve(
|
||||||
@@ -235,7 +235,7 @@ const saveCinematicManifestPlugin = (): Plugin => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 manifest = parseCinematicManifestData(data);
|
||||||
const dialogueManifest = await loadDialogueManifestData();
|
const dialogueManifest = await loadDialogueManifestData();
|
||||||
validateCinematicDialogueCues(manifest, dialogueManifest);
|
validateCinematicDialogueCues(manifest, dialogueManifest);
|
||||||
@@ -304,15 +304,14 @@ interface CinematicKeyframeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSrtPayload(data: unknown): data is SrtPayload {
|
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 (
|
return (
|
||||||
typeof payload.voice === "string" &&
|
typeof data.voice === "string" &&
|
||||||
SRT_VOICES.has(payload.voice) &&
|
SRT_VOICES.has(data.voice) &&
|
||||||
typeof payload.language === "string" &&
|
typeof data.language === "string" &&
|
||||||
SRT_LANGUAGES.has(payload.language) &&
|
SRT_LANGUAGES.has(data.language) &&
|
||||||
typeof payload.content === "string"
|
typeof data.content === "string"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user