From ba50224e6e88244508ea51dce542f55c7252b5a1 Mon Sep 17 00:00:00 2001 From: tom-boullay Date: Thu, 28 May 2026 15:47:16 +0200 Subject: [PATCH] refactor: nettoie l'architecture monde et les docs --- .agent/skills/managers.md | 68 +++------- .github/workflows/quality.yml | 1 - README.md | 5 +- docs/technical/animation.md | 2 +- docs/technical/architecture.md | 3 +- docs/technical/interaction.md | 2 +- docs/technical/zustand.md | 10 +- docs/user/features.md | 9 +- docs/user/main-feature.md | 4 +- src/components/docs/DocsDocument.tsx | 4 +- src/components/editor/EditorSrtPanel.tsx | 10 +- src/components/three/world/EcoleModel.tsx | 15 +-- .../three/world/FermeVerticaleModel.tsx | 15 +-- .../three/world/GenerateurModel.tsx | 15 +-- src/components/three/world/LafabrikModel.tsx | 15 +-- .../three/world/MergedStaticMapModel.tsx | 2 +- src/components/three/world/SkyModel.tsx | 15 +-- src/data/audioConfig.ts | 4 +- src/data/world/environmentConfig.ts | 2 - src/data/world/pathConfig.ts | 12 -- src/hooks/useActivityCity.ts | 5 - src/hooks/world/useTerrainSurfaceData.ts | 64 --------- src/managers/AudioManager.ts | 7 +- src/pages/docs/animation/page.tsx | 1 - src/pages/docs/architecture/page.tsx | 1 - src/pages/docs/audio/page.tsx | 7 +- src/pages/docs/code-review/page.tsx | 7 +- src/pages/docs/features/page.tsx | 9 +- src/pages/docs/hand-tracking/page.tsx | 1 - src/pages/docs/interaction/page.tsx | 7 +- src/pages/docs/main-feature/page.tsx | 9 +- src/pages/docs/page.tsx | 9 +- src/pages/docs/scene-runtime/page.tsx | 7 +- src/pages/docs/target-architecture/page.tsx | 1 - src/pages/docs/technical-editor/page.tsx | 1 - src/pages/docs/three-debugging/page.tsx | 7 +- src/pages/docs/zustand/page.tsx | 9 +- src/types/world/terrainSurface.ts | 26 ---- src/utils/three/dispose.ts | 66 --------- src/utils/world/terrainSurfaceColor.ts | 51 ------- src/utils/world/terrainSurfaceSampler.ts | 125 ------------------ src/world/Environment.tsx | 4 - src/world/paths/PathSystem.tsx | 87 ------------ src/world/paths/usePathTileData.ts | 72 ---------- vite.config.ts | 19 ++- 45 files changed, 89 insertions(+), 726 deletions(-) delete mode 100644 src/data/world/pathConfig.ts delete mode 100644 src/hooks/useActivityCity.ts delete mode 100644 src/hooks/world/useTerrainSurfaceData.ts delete mode 100644 src/utils/three/dispose.ts delete mode 100644 src/utils/world/terrainSurfaceColor.ts delete mode 100644 src/utils/world/terrainSurfaceSampler.ts delete mode 100644 src/world/paths/PathSystem.tsx delete mode 100644 src/world/paths/usePathTileData.ts diff --git a/.agent/skills/managers.md b/.agent/skills/managers.md index 37c004c..ed8722d 100644 --- a/.agent/skills/managers.md +++ b/.agent/skills/managers.md @@ -15,12 +15,9 @@ export class SomeManager { return SomeManager._instance; } - private constructor() { - // init logic - } + private constructor() {} destroy(): void { - // cleanup logic SomeManager._instance = null; } } @@ -28,43 +25,12 @@ 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. | +| 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. | -## 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()`. diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 06fba13..2133a51 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -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 diff --git a/README.md b/README.md index 51d8622..0df0ee7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/technical/animation.md b/docs/technical/animation.md index fbea01f..70a563d 100644 --- a/docs/technical/animation.md +++ b/docs/technical/animation.md @@ -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. diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index 55b0c57..9d6e006 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -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. diff --git a/docs/technical/interaction.md b/docs/technical/interaction.md index 2c6bee5..02d99da 100644 --- a/docs/technical/interaction.md +++ b/docs/technical/interaction.md @@ -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 diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md index 63ef7f3..1bae136 100644 --- a/docs/technical/zustand.md +++ b/docs/technical/zustand.md @@ -28,11 +28,11 @@ 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 | -| `useSubtitleStore` | Currently displayed subtitle cue | +| Store | Responsibility | +| ------------------ | ------------------------------------------------------------- | +| `useGameStore` | Durable game progression, mission steps, cinematic input lock | +| `useSettingsStore` | Menu visibility, volumes, and subtitle options | +| `useSubtitleStore` | Currently displayed subtitle cue | ## Managers vs Stores diff --git a/docs/user/features.md b/docs/user/features.md index 3ddf7a0..07f4418 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -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 diff --git a/docs/user/main-feature.md b/docs/user/main-feature.md index 3b1135a..3991da3 100644 --- a/docs/user/main-feature.md +++ b/docs/user/main-feature.md @@ -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. diff --git a/src/components/docs/DocsDocument.tsx b/src/components/docs/DocsDocument.tsx index ae7cb45..b0e6258 100644 --- a/src/components/docs/DocsDocument.tsx +++ b/src/components/docs/DocsDocument.tsx @@ -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; diff --git a/src/components/editor/EditorSrtPanel.tsx b/src/components/editor/EditorSrtPanel.tsx index bc5cfe4..78950ef 100644 --- a/src/components/editor/EditorSrtPanel.tsx +++ b/src/components/editor/EditorSrtPanel.tsx @@ -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 () => { diff --git a/src/components/three/world/EcoleModel.tsx b/src/components/three/world/EcoleModel.tsx index 75398ae..aa72338 100644 --- a/src/components/three/world/EcoleModel.tsx +++ b/src/components/three/world/EcoleModel.tsx @@ -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; export function EcoleModel(props: EcoleModelProps): React.JSX.Element { return ; diff --git a/src/components/three/world/FermeVerticaleModel.tsx b/src/components/three/world/FermeVerticaleModel.tsx index 44ed4bd..53a0667 100644 --- a/src/components/three/world/FermeVerticaleModel.tsx +++ b/src/components/three/world/FermeVerticaleModel.tsx @@ -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; export function FermeVerticaleModel( props: FermeVerticaleModelProps, diff --git a/src/components/three/world/GenerateurModel.tsx b/src/components/three/world/GenerateurModel.tsx index 5b5cd39..6b2e497 100644 --- a/src/components/three/world/GenerateurModel.tsx +++ b/src/components/three/world/GenerateurModel.tsx @@ -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; export function GenerateurModel( props: GenerateurModelProps, diff --git a/src/components/three/world/LafabrikModel.tsx b/src/components/three/world/LafabrikModel.tsx index 0c080b7..e7542fd 100644 --- a/src/components/three/world/LafabrikModel.tsx +++ b/src/components/three/world/LafabrikModel.tsx @@ -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; export function LafabrikModel(props: LafabrikModelProps): React.JSX.Element { return ; diff --git a/src/components/three/world/MergedStaticMapModel.tsx b/src/components/three/world/MergedStaticMapModel.tsx index c7178dd..67b7d18 100644 --- a/src/components/three/world/MergedStaticMapModel.tsx +++ b/src/components/three/world/MergedStaticMapModel.tsx @@ -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; diff --git a/src/components/three/world/SkyModel.tsx b/src/components/three/world/SkyModel.tsx index 00be19d..bc6791d 100644 --- a/src/components/three/world/SkyModel.tsx +++ b/src/components/three/world/SkyModel.tsx @@ -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 ? ( ) : null; - const fallback = fallbackModelPath ? ( - - - - ) : ( - colorFallback - ); return ( @@ -154,4 +142,3 @@ function disposeSkyModelMaterials(model: THREE.Object3D): void { } useGLTF.preload(SKYBOX_MODEL_PATH); -useGLTF.preload(LEGACY_SKY_MODEL_PATH); diff --git a/src/data/audioConfig.ts b/src/data/audioConfig.ts index df07530..bef58ef 100644 --- a/src/data/audioConfig.ts +++ b/src/data/audioConfig.ts @@ -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 = { music: 1, sfx: 1, diff --git a/src/data/world/environmentConfig.ts b/src/data/world/environmentConfig.ts index 2fb04b5..b457f92 100644 --- a/src/data/world/environmentConfig.ts +++ b/src/data/world/environmentConfig.ts @@ -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"; diff --git a/src/data/world/pathConfig.ts b/src/data/world/pathConfig.ts deleted file mode 100644 index 8fc3c07..0000000 --- a/src/data/world/pathConfig.ts +++ /dev/null @@ -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; diff --git a/src/hooks/useActivityCity.ts b/src/hooks/useActivityCity.ts deleted file mode 100644 index d1477ae..0000000 --- a/src/hooks/useActivityCity.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useGameStore } from "@/managers/stores/useGameStore"; - -export function useActivityCity(): boolean { - return useGameStore((state) => state.missionFlow.activityCity); -} diff --git a/src/hooks/world/useTerrainSurfaceData.ts b/src/hooks/world/useTerrainSurfaceData.ts deleted file mode 100644 index 2b7c348..0000000 --- a/src/hooks/world/useTerrainSurfaceData.ts +++ /dev/null @@ -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]); -} diff --git a/src/managers/AudioManager.ts b/src/managers/AudioManager.ts index 001b035..9348b1f 100644 --- a/src/managers/AudioManager.ts +++ b/src/managers/AudioManager.ts @@ -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; interface AudioContextWindow extends Window { diff --git a/src/pages/docs/animation/page.tsx b/src/pages/docs/animation/page.tsx index af53225..ac456f6 100644 --- a/src/pages/docs/animation/page.tsx +++ b/src/pages/docs/animation/page.tsx @@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element { return ( diff --git a/src/pages/docs/architecture/page.tsx b/src/pages/docs/architecture/page.tsx index 99a3b6d..13b7e9c 100644 --- a/src/pages/docs/architecture/page.tsx +++ b/src/pages/docs/architecture/page.tsx @@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element { return ( diff --git a/src/pages/docs/audio/page.tsx b/src/pages/docs/audio/page.tsx index cb53011..4392f5f 100644 --- a/src/pages/docs/audio/page.tsx +++ b/src/pages/docs/audio/page.tsx @@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsAudioPage(): React.JSX.Element { return ( - + ); } diff --git a/src/pages/docs/code-review/page.tsx b/src/pages/docs/code-review/page.tsx index 41682bd..47dc54c 100644 --- a/src/pages/docs/code-review/page.tsx +++ b/src/pages/docs/code-review/page.tsx @@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsCodeReviewPage(): React.JSX.Element { return ( - + ); } diff --git a/src/pages/docs/features/page.tsx b/src/pages/docs/features/page.tsx index 61a026a..89fed18 100644 --- a/src/pages/docs/features/page.tsx +++ b/src/pages/docs/features/page.tsx @@ -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 ( - - ); + return ; } diff --git a/src/pages/docs/hand-tracking/page.tsx b/src/pages/docs/hand-tracking/page.tsx index bafa44f..b07dd6f 100644 --- a/src/pages/docs/hand-tracking/page.tsx +++ b/src/pages/docs/hand-tracking/page.tsx @@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element { return ( diff --git a/src/pages/docs/interaction/page.tsx b/src/pages/docs/interaction/page.tsx index f9ae53b..7d97917 100644 --- a/src/pages/docs/interaction/page.tsx +++ b/src/pages/docs/interaction/page.tsx @@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsInteractionPage(): React.JSX.Element { return ( - + ); } diff --git a/src/pages/docs/main-feature/page.tsx b/src/pages/docs/main-feature/page.tsx index f094d52..229fb06 100644 --- a/src/pages/docs/main-feature/page.tsx +++ b/src/pages/docs/main-feature/page.tsx @@ -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 ( - - ); + return ; } diff --git a/src/pages/docs/page.tsx b/src/pages/docs/page.tsx index e3d3d7f..1de5e50 100644 --- a/src/pages/docs/page.tsx +++ b/src/pages/docs/page.tsx @@ -2,12 +2,5 @@ import readme from "../../../README.md?raw"; import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsReadmePage(): React.JSX.Element { - return ( - - ); + return ; } diff --git a/src/pages/docs/scene-runtime/page.tsx b/src/pages/docs/scene-runtime/page.tsx index 3d2c267..50f6fa2 100644 --- a/src/pages/docs/scene-runtime/page.tsx +++ b/src/pages/docs/scene-runtime/page.tsx @@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsSceneRuntimePage(): React.JSX.Element { return ( - + ); } diff --git a/src/pages/docs/target-architecture/page.tsx b/src/pages/docs/target-architecture/page.tsx index 1f44783..95f6b2e 100644 --- a/src/pages/docs/target-architecture/page.tsx +++ b/src/pages/docs/target-architecture/page.tsx @@ -5,7 +5,6 @@ export function DocsTargetArchitecturePage(): React.JSX.Element { return ( diff --git a/src/pages/docs/technical-editor/page.tsx b/src/pages/docs/technical-editor/page.tsx index 3620ebd..af22a06 100644 --- a/src/pages/docs/technical-editor/page.tsx +++ b/src/pages/docs/technical-editor/page.tsx @@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element { return ( diff --git a/src/pages/docs/three-debugging/page.tsx b/src/pages/docs/three-debugging/page.tsx index 9324f9b..38848ae 100644 --- a/src/pages/docs/three-debugging/page.tsx +++ b/src/pages/docs/three-debugging/page.tsx @@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument"; export function DocsThreeDebuggingPage(): React.JSX.Element { return ( - + ); } diff --git a/src/pages/docs/zustand/page.tsx b/src/pages/docs/zustand/page.tsx index bd22028..44094d0 100644 --- a/src/pages/docs/zustand/page.tsx +++ b/src/pages/docs/zustand/page.tsx @@ -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 ( - - ); + return ; } diff --git a/src/types/world/terrainSurface.ts b/src/types/world/terrainSurface.ts index 1984c77..aa738ab 100644 --- a/src/types/world/terrainSurface.ts +++ b/src/types/world/terrainSurface.ts @@ -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; -} diff --git a/src/utils/three/dispose.ts b/src/utils/three/dispose.ts deleted file mode 100644 index fd4cfa6..0000000 --- a/src/utils/three/dispose.ts +++ /dev/null @@ -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>; - -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(); - } - } -} diff --git a/src/utils/world/terrainSurfaceColor.ts b/src/utils/world/terrainSurfaceColor.ts deleted file mode 100644 index 277c4d5..0000000 --- a/src/utils/world/terrainSurfaceColor.ts +++ /dev/null @@ -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; -} diff --git a/src/utils/world/terrainSurfaceSampler.ts b/src/utils/world/terrainSurfaceSampler.ts deleted file mode 100644 index 43eacb9..0000000 --- a/src/utils/world/terrainSurfaceSampler.ts +++ /dev/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(); -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), - ); -} diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index 394170f..9b50164 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -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 ? ( diff --git a/src/world/paths/PathSystem.tsx b/src/world/paths/PathSystem.tsx deleted file mode 100644 index e58b257..0000000 --- a/src/world/paths/PathSystem.tsx +++ /dev/null @@ -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 ; -} - -function PathTiles(): React.JSX.Element | null { - const pathTiles = usePathTileData(); - - if (pathTiles.length === 0) { - return null; - } - - if (PATH_DEBUG_PREVIEW_ENABLED) { - return ; - } - - if (!PATH_TILE_RENDER_ENABLED) { - return null; - } - - return ( - - ); -} - -function PathDebugPreview({ - instances, -}: { - instances: MapAssetInstance[]; -}): React.JSX.Element { - const instancedMeshRef = useRef(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 ( - - - - - ); -} diff --git a/src/world/paths/usePathTileData.ts b/src/world/paths/usePathTileData.ts deleted file mode 100644 index 12c0d53..0000000 --- a/src/world/paths/usePathTileData.ts +++ /dev/null @@ -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]); -} diff --git a/vite.config.ts b/vite.config.ts index b7c246c..c6a5e88 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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; 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" ); }