chore: address code quality audit findings
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import type { GameState } from "@/managers/stores/useGameStore";
|
||||
|
||||
const DEBUG_GAME_STATE_COOKIE_NAME = "la-fabrik-debug-game-state";
|
||||
const DEBUG_GAME_STATE_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
|
||||
@@ -25,7 +23,7 @@ export function readDebugGameStateCookie(): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
export function writeDebugGameStateCookie(state: GameState): void {
|
||||
export function writeDebugGameStateCookie(state: unknown): void {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const value = encodeURIComponent(JSON.stringify(state));
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
DialogueManifest,
|
||||
DialogueVoice,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import type { SubtitleLanguage } from "@/managers/stores/useSettingsStore";
|
||||
import type { SubtitleLanguage } from "@/types/settings/settings";
|
||||
import { parseDialogueManifest } from "@/utils/dialogues/dialogueManifestValidation";
|
||||
import { parseSrt } from "@/utils/subtitles/parseSrt";
|
||||
import type { SubtitleCue } from "@/utils/subtitles/parseSrt";
|
||||
@@ -27,18 +27,7 @@ export async function loadDialogueManifest(): Promise<DialogueManifest | null> {
|
||||
return parseDialogueManifest(await response.json());
|
||||
}
|
||||
|
||||
export function resolveDialogueSubtitlePath(
|
||||
manifest: DialogueManifest,
|
||||
dialogue: DialogueDefinition,
|
||||
language: SubtitleLanguage,
|
||||
): string | null {
|
||||
const voice = getDialogueVoice(manifest, dialogue.voice);
|
||||
if (!voice) return null;
|
||||
|
||||
return getVoiceSubtitlePath(voice, language);
|
||||
}
|
||||
|
||||
export function getDialogueVoice(
|
||||
function getDialogueVoice(
|
||||
manifest: DialogueManifest,
|
||||
voiceId: DialogueDefinition["voice"],
|
||||
): DialogueVoice | null {
|
||||
@@ -69,7 +58,7 @@ export async function loadDialogueSubtitleCue(
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadVoiceSubtitleCues(
|
||||
async function loadVoiceSubtitleCues(
|
||||
voice: DialogueVoice,
|
||||
language: SubtitleLanguage,
|
||||
): Promise<{ path: string; cues: SubtitleCue[] } | null> {
|
||||
@@ -103,14 +92,3 @@ function getVoiceSubtitlePaths(
|
||||
.filter((path): path is string => Boolean(path))
|
||||
.filter((path, index, paths) => paths.indexOf(path) === index);
|
||||
}
|
||||
|
||||
function getVoiceSubtitlePath(
|
||||
voice: DialogueVoice,
|
||||
language: SubtitleLanguage,
|
||||
): string | null {
|
||||
return (
|
||||
voice.subtitles[language] ??
|
||||
voice.subtitles[DEFAULT_SUBTITLE_LANGUAGE] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { AudioManager } from "@/managers/AudioManager";
|
||||
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
|
||||
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
|
||||
import type { DialogueManifest } from "@/types/dialogues/dialogues";
|
||||
import {
|
||||
loadDialogueManifest,
|
||||
loadDialogueSubtitleCue,
|
||||
} from "@/utils/dialogues/loadDialogueManifest";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
import { loadDialogueSubtitleCue } from "@/utils/dialogues/loadDialogueManifest";
|
||||
|
||||
interface QueuedDialogueRequest {
|
||||
manifest: DialogueManifest;
|
||||
@@ -15,8 +13,6 @@ interface QueuedDialogueRequest {
|
||||
|
||||
const DIALOGUE_PLAY_START_TIMEOUT_MS = 800;
|
||||
const dialogueQueue: QueuedDialogueRequest[] = [];
|
||||
let gameplayDialogueManifestPromise: Promise<DialogueManifest | null> | null =
|
||||
null;
|
||||
let isDialogueQueuePlaying = false;
|
||||
|
||||
export function queueDialogueById(
|
||||
@@ -35,16 +31,6 @@ export function clearQueuedDialogues(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function playGameplayDialogueById(
|
||||
dialogueId: string,
|
||||
): Promise<HTMLAudioElement | null> {
|
||||
gameplayDialogueManifestPromise ??= loadDialogueManifest();
|
||||
const manifest = await gameplayDialogueManifestPromise;
|
||||
if (!manifest) return null;
|
||||
|
||||
return queueDialogueById(manifest, dialogueId);
|
||||
}
|
||||
|
||||
export async function playDialogueById(
|
||||
manifest: DialogueManifest,
|
||||
dialogueId: string,
|
||||
@@ -117,7 +103,11 @@ async function playNextQueuedDialogue(): Promise<void> {
|
||||
);
|
||||
request.resolve(audio);
|
||||
if (audio) await waitForDialogueToFinish(audio);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
logger.error("Dialogues", "Failed to play queued dialogue", {
|
||||
dialogueId: request.dialogueId,
|
||||
error: error instanceof Error ? error : String(error),
|
||||
});
|
||||
request.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SceneData } from "@/types/editor/editor";
|
||||
import type { SceneData } from "@/types/map/mapScene";
|
||||
import { createSceneDataFromMapPayload } from "@/utils/map/loadMapSceneData";
|
||||
|
||||
const MAP_JSON_PATH = "/map.json";
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
HierarchicalMapNode,
|
||||
MapNode,
|
||||
SceneData,
|
||||
} from "@/types/editor/editor";
|
||||
} from "@/types/map/mapScene";
|
||||
import { parseMapData } from "@/utils/map/mapNodeValidation";
|
||||
|
||||
const MAP_JSON_PATH = "/map.json";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { MapNode } from "@/types/map/mapScene";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
|
||||
export interface MapNodeInstanceTransform {
|
||||
position: Vector3Tuple;
|
||||
rotation: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
}
|
||||
|
||||
export function mapNodeToInstanceTransform(
|
||||
node: MapNode,
|
||||
): MapNodeInstanceTransform {
|
||||
return {
|
||||
position: node.position,
|
||||
rotation: node.rotation,
|
||||
scale: node.scale,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HierarchicalMapNode, MapNode } from "../../types/editor/editor";
|
||||
import type { HierarchicalMapNode, MapNode } from "@/types/map/mapScene";
|
||||
|
||||
export interface ParsedMapNodes {
|
||||
mapNodes: MapNode[];
|
||||
@@ -31,9 +31,7 @@ function isMapNode(value: unknown): value is MapNode {
|
||||
);
|
||||
}
|
||||
|
||||
export function isHierarchicalMapNode(
|
||||
value: unknown,
|
||||
): value is HierarchicalMapNode {
|
||||
function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode {
|
||||
if (!isMapNode(value)) {
|
||||
return false;
|
||||
}
|
||||
@@ -74,20 +72,6 @@ function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
|
||||
return [mapNode, ...childNodes];
|
||||
}
|
||||
|
||||
export function parseHierarchicalMapPayload(
|
||||
value: unknown,
|
||||
): HierarchicalMapNode | HierarchicalMapNode[] {
|
||||
if (Array.isArray(value) && value.every(isHierarchicalMapNode)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isHierarchicalMapNode(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error("Invalid map node data");
|
||||
}
|
||||
|
||||
export function parseMapNodes(value: unknown): MapNode[] {
|
||||
return parseMapData(value).mapNodes;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MapNode } from "@/types/editor/editor";
|
||||
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||
import type { MapNode } from "@/types/map/mapScene";
|
||||
import { isInstancedMapNodeName } from "@/data/world/mapInstancingConfig";
|
||||
|
||||
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
|
||||
const RUNTIME_VEGETATION_NODE_NAMES = new Set([
|
||||
@@ -11,7 +11,7 @@ const RUNTIME_VEGETATION_NODE_NAMES = new Set([
|
||||
"sapin",
|
||||
]);
|
||||
|
||||
export function isRuntimeStructureMapNode(name: string): boolean {
|
||||
function isRuntimeStructureMapNode(name: string): boolean {
|
||||
return MAP_STRUCTURE_NODE_NAMES.has(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,48 +5,85 @@ export interface SubtitleCue {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface SrtParseDiagnostic {
|
||||
blockIndex: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SrtParseResult {
|
||||
cues: SubtitleCue[];
|
||||
diagnostics: SrtParseDiagnostic[];
|
||||
}
|
||||
|
||||
const SRT_TIME_SEPARATOR = " --> ";
|
||||
const SRT_TIME_PATTERN = /^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/;
|
||||
|
||||
export function parseSrt(srtContent: string): SubtitleCue[] {
|
||||
return srtContent
|
||||
return parseSrtWithDiagnostics(srtContent).cues;
|
||||
}
|
||||
|
||||
export function parseSrtWithDiagnostics(srtContent: string): SrtParseResult {
|
||||
const diagnostics: SrtParseDiagnostic[] = [];
|
||||
const cues = srtContent
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/\r/g, "")
|
||||
.trim()
|
||||
.split(/\n{2,}/)
|
||||
.map(parseSrtBlock)
|
||||
.map((block, blockIndex) => {
|
||||
const result = parseSrtBlock(block);
|
||||
|
||||
if (!result.cue) {
|
||||
diagnostics.push({ blockIndex, reason: result.reason });
|
||||
}
|
||||
|
||||
return result.cue;
|
||||
})
|
||||
.filter((cue): cue is SubtitleCue => cue !== null);
|
||||
|
||||
return { cues, diagnostics };
|
||||
}
|
||||
|
||||
function parseSrtBlock(block: string): SubtitleCue | null {
|
||||
function parseSrtBlock(block: string): {
|
||||
cue: SubtitleCue | null;
|
||||
reason: string;
|
||||
} {
|
||||
const lines = block
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (lines.length < 3) return null;
|
||||
if (lines.length < 3) {
|
||||
return { cue: null, reason: "missing index, timecode, or text" };
|
||||
}
|
||||
|
||||
const index = Number(lines[0]);
|
||||
if (!Number.isInteger(index)) return null;
|
||||
if (!Number.isInteger(index)) {
|
||||
return { cue: null, reason: "invalid cue index" };
|
||||
}
|
||||
|
||||
const [start, end] = lines[1]?.split(SRT_TIME_SEPARATOR) ?? [];
|
||||
if (!start || !end) return null;
|
||||
if (!start || !end) {
|
||||
return { cue: null, reason: "invalid timecode separator" };
|
||||
}
|
||||
|
||||
const startTime = parseSrtTime(start);
|
||||
const endTime = parseSrtTime(end);
|
||||
if (startTime === null || endTime === null || endTime <= startTime) {
|
||||
return null;
|
||||
return { cue: null, reason: "invalid cue duration" };
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
startTime,
|
||||
endTime,
|
||||
text: lines.slice(2).join("\n"),
|
||||
cue: {
|
||||
index,
|
||||
startTime,
|
||||
endTime,
|
||||
text: lines.slice(2).join("\n"),
|
||||
},
|
||||
reason: "",
|
||||
};
|
||||
}
|
||||
|
||||
function parseSrtTime(value: string): number | null {
|
||||
export function parseSrtTime(value: string): number | null {
|
||||
const match = value.match(SRT_TIME_PATTERN);
|
||||
if (!match) return null;
|
||||
|
||||
|
||||
+40
-16
@@ -1,5 +1,41 @@
|
||||
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) {
|
||||
@@ -18,25 +54,13 @@ export function disposeObject3D(object: THREE.Object3D): void {
|
||||
|
||||
function disposeMaterial(material: THREE.Material): void {
|
||||
material.dispose();
|
||||
const materialWithTextures = material as MaterialWithTextureSlots;
|
||||
|
||||
for (const key of MATERIAL_TEXTURE_KEYS) {
|
||||
const value = materialWithTextures[key];
|
||||
|
||||
for (const key of Object.keys(material)) {
|
||||
const value = (material as unknown as Record<string, unknown>)[key];
|
||||
if (value instanceof THREE.Texture) {
|
||||
value.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function disposeInstancedMesh(mesh: THREE.InstancedMesh): void {
|
||||
mesh.geometry?.dispose();
|
||||
|
||||
if (Array.isArray(mesh.material)) {
|
||||
for (const material of mesh.material) {
|
||||
disposeMaterial(material);
|
||||
}
|
||||
} else if (mesh.material) {
|
||||
disposeMaterial(mesh.material);
|
||||
}
|
||||
|
||||
mesh.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user