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

This commit is contained in:
Tom Boullay
2026-05-28 08:31:42 +02:00
parent 947025cbf5
commit d654565f87
73 changed files with 890 additions and 1457 deletions
+1 -3
View File
@@ -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 -25
View File
@@ -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
);
}
+7 -17
View File
@@ -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 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+18
View File
@@ -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,
};
}
+2 -18
View File
@@ -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;
}
+3 -3
View File
@@ -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);
}
+49 -12
View File
@@ -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
View File
@@ -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();
}