big clean up

This commit is contained in:
Tom Boullay
2026-05-08 03:02:26 +01:00
parent 0b519a20dc
commit ee44b44432
20 changed files with 208 additions and 94 deletions
+2 -1
View File
@@ -101,7 +101,8 @@ la-fabrik/
│ ├── editor/ # Editor-only parsing utilities │ ├── editor/ # Editor-only parsing utilities
│ ├── map/ # Map loading and validation │ ├── map/ # Map loading and validation
│ └── three/ # Three.js helpers │ └── three/ # Three.js helpers
├── App.tsx # Canvas bootstrap ├── types/ # Shared TypeScript domain types
├── App.tsx # App bootstrap and route switch
└── main.tsx └── main.tsx
``` ```
+1
View File
@@ -77,6 +77,7 @@ Keep the player and map octree outside the Rapier provider until there is a deli
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing. - `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
- `src/utils/map/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs. - `src/utils/map/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
- `src/types/editor/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types. - `src/types/editor/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used across store, config, debug UI, and gameplay components.
## Map Data ## Map Data
+1 -1
View File
@@ -137,7 +137,7 @@ For repair missions, it mounts the reusable `RepairGame` component with a missio
<RepairGame mission="bike" position={[8, 0, -6]} /> <RepairGame mission="bike" position={[8, 0, -6]} />
``` ```
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. This keeps the scene component small and avoids mission-specific branching inside the repair flow. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions. `RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. Shared repair ids, mission steps, and runtime guards live in `src/types/gameplay/repairMission.ts` so static mission config does not depend on the Zustand store. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions.
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`. Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
+1
View File
@@ -63,6 +63,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
- `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants. - `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants.
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`. - `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`.
- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers. - `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers.
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used by the store, data config, debug UI, and gameplay components.
## Runtime Requirements ## Runtime Requirements
+1 -1
View File
@@ -14,7 +14,7 @@ import { REPAIR_FRAGMENTATION_SEQUENCE_SECONDS } from "@/data/gameplay/repairGam
import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions"; import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions";
import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput"; import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput";
import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep"; import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep";
import type { RepairMissionId } from "@/managers/stores/useGameStore"; import type { RepairMissionId } from "@/types/gameplay/repairMission";
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three"; import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
import { toVector3Scale } from "@/utils/three/scale"; import { toVector3Scale } from "@/utils/three/scale";
@@ -3,6 +3,7 @@ import { Component } from "react";
import { SimpleModel } from "@/components/three/models/SimpleModel"; import { SimpleModel } from "@/components/three/models/SimpleModel";
import type { ModelTransformProps } from "@/types/three/three"; import type { ModelTransformProps } from "@/types/three/three";
import { logModelLoadError } from "@/utils/three/modelLoadLogger"; import { logModelLoadError } from "@/utils/three/modelLoadLogger";
import { toVector3Scale } from "@/utils/three/scale";
interface RepairObjectModelProps extends ModelTransformProps { interface RepairObjectModelProps extends ModelTransformProps {
label: string; label: string;
@@ -17,6 +18,13 @@ interface RepairObjectModelBoundaryState {
hasError: boolean; hasError: boolean;
} }
interface RepairObjectFallbackProps {
label: string;
position?: ModelTransformProps["position"] | undefined;
rotation?: ModelTransformProps["rotation"] | undefined;
scale?: ModelTransformProps["scale"] | undefined;
}
class RepairObjectModelBoundary extends Component< class RepairObjectModelBoundary extends Component<
RepairObjectModelBoundaryProps, RepairObjectModelBoundaryProps,
RepairObjectModelBoundaryState RepairObjectModelBoundaryState
@@ -45,7 +53,14 @@ class RepairObjectModelBoundary extends Component<
render(): ReactNode { render(): ReactNode {
if (this.state.hasError) { if (this.state.hasError) {
return <RepairObjectFallback label={this.props.label} />; return (
<RepairObjectFallback
label={this.props.label}
position={this.props.position}
rotation={this.props.rotation}
scale={this.props.scale}
/>
);
} }
return this.props.children; return this.props.children;
@@ -77,9 +92,21 @@ export function RepairObjectModel({
); );
} }
function RepairObjectFallback({ label }: { label: string }): React.JSX.Element { function RepairObjectFallback({
label,
position = [0, 0, 0],
rotation = [0, 0, 0],
scale = 1,
}: Pick<
RepairObjectFallbackProps,
"label" | "position" | "rotation" | "scale"
>): React.JSX.Element {
return ( return (
<group> <group
position={position}
rotation={rotation}
scale={toVector3Scale(scale)}
>
<mesh castShadow receiveShadow> <mesh castShadow receiveShadow>
<boxGeometry args={[1.4, 1.4, 1.4]} /> <boxGeometry args={[1.4, 1.4, 1.4]} />
<meshStandardMaterial color="#facc15" roughness={0.6} wireframe /> <meshStandardMaterial color="#facc15" roughness={0.6} wireframe />
@@ -19,7 +19,7 @@ import type { Vector3Tuple } from "@/types/three/three";
const INSTALL_TARGET_POSITION: Vector3Tuple = [0, 0.8, 0]; const INSTALL_TARGET_POSITION: Vector3Tuple = [0, 0.8, 0];
const _placeholderPosition = new THREE.Vector3(); const _placeholderPosition = new THREE.Vector3();
const REPLACEMENT_START_OFFSETS: Vector3Tuple[] = [ const FALLBACK_PLACEHOLDER_OFFSETS: Vector3Tuple[] = [
[-1.15, 1, 0.25], [-1.15, 1, 0.25],
[0, 1.05, 0.45], [0, 1.05, 0.45],
[1.15, 1, 0.25], [1.15, 1, 0.25],
@@ -38,6 +38,18 @@ interface RepairRepairingStepProps {
onRepair: () => void; onRepair: () => void;
} }
interface RepairInstallTargetProps {
fillColor: string;
isReadyToInstall: boolean;
label: string;
ringColor: string;
onRepair: () => void;
}
interface RepairPlaceholderMarkersProps {
positions: readonly Vector3Tuple[];
}
export function RepairRepairingStep({ export function RepairRepairingStep({
brokenParts, brokenParts,
config, config,
@@ -82,6 +94,13 @@ export function RepairRepairingStep({
: hasWrongPartPlaced : hasWrongPartPlaced
? "#fecaca" ? "#fecaca"
: "#fed7aa"; : "#fed7aa";
const installLabel = isReadyToInstall
? `Installer ${requiredReplacementLabel}`
: hasWrongPartPlaced
? `Mauvaise piece`
: hasCorrectPartPlaced
? `Ranger piece cassee`
: `Approcher ${requiredReplacementLabel}`;
function handleReplacementPosition( function handleReplacementPosition(
partId: string, partId: string,
@@ -126,48 +145,15 @@ export function RepairRepairingStep({
return ( return (
<group> <group>
<TriggerObject <RepairInstallTarget
position={INSTALL_TARGET_POSITION} fillColor={installFillColor}
colliders="ball" isReadyToInstall={isReadyToInstall}
label={ label={installLabel}
isReadyToInstall ringColor={installColor}
? `Installer ${requiredReplacementLabel}` onRepair={onRepair}
: hasWrongPartPlaced
? `Mauvaise piece`
: hasCorrectPartPlaced
? `Ranger piece cassee`
: `Approcher ${requiredReplacementLabel}`
}
onTrigger={() => {
if (!isReadyToInstall) return;
onRepair();
}}
>
<mesh>
<torusGeometry args={[0.95, 0.045, 12, 96]} />
<meshBasicMaterial color={installColor} transparent opacity={0.85} />
</mesh>
<mesh position={[0, 0.02, 0]} rotation={[Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.15, 0.9, 96]} />
<meshBasicMaterial
color={installFillColor}
transparent
opacity={0.35}
/> />
</mesh>
</TriggerObject>
{placeholderPositions.map((position, index) => ( <RepairPlaceholderMarkers positions={placeholderPositions} />
<mesh
key={`${position.join(":")}-${index}`}
position={position}
rotation={[Math.PI / 2, 0, 0]}
>
<torusGeometry args={[0.26, 0.018, 8, 48]} />
<meshBasicMaterial color="#38bdf8" transparent opacity={0.55} />
</mesh>
))}
{replacementParts.map((part, index) => { {replacementParts.map((part, index) => {
const placeholderPosition = const placeholderPosition =
@@ -251,6 +237,55 @@ export function RepairRepairingStep({
); );
} }
function RepairInstallTarget({
fillColor,
isReadyToInstall,
label,
ringColor,
onRepair,
}: RepairInstallTargetProps): React.JSX.Element {
return (
<TriggerObject
position={INSTALL_TARGET_POSITION}
colliders="ball"
label={label}
onTrigger={() => {
if (!isReadyToInstall) return;
onRepair();
}}
>
<mesh>
<torusGeometry args={[0.95, 0.045, 12, 96]} />
<meshBasicMaterial color={ringColor} transparent opacity={0.85} />
</mesh>
<mesh position={[0, 0.02, 0]} rotation={[Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.15, 0.9, 96]} />
<meshBasicMaterial color={fillColor} transparent opacity={0.35} />
</mesh>
</TriggerObject>
);
}
function RepairPlaceholderMarkers({
positions,
}: RepairPlaceholderMarkersProps): React.JSX.Element {
return (
<>
{positions.map((position, index) => (
<mesh
key={`${position.join(":")}-${index}`}
position={position}
rotation={[Math.PI / 2, 0, 0]}
>
<torusGeometry args={[0.26, 0.018, 8, 48]} />
<meshBasicMaterial color="#38bdf8" transparent opacity={0.55} />
</mesh>
))}
</>
);
}
function getPlaceholderTargets( function getPlaceholderTargets(
placeholders: readonly RepairCasePlaceholder[], placeholders: readonly RepairCasePlaceholder[],
): readonly RepairCasePlaceholder[] { ): readonly RepairCasePlaceholder[] {
@@ -258,7 +293,7 @@ function getPlaceholderTargets(
return placeholders; return placeholders;
} }
return REPLACEMENT_START_OFFSETS.map( return FALLBACK_PLACEHOLDER_OFFSETS.map(
(offset, index): RepairCasePlaceholder => ({ (offset, index): RepairCasePlaceholder => ({
name: `placeholder_${index + 1}`, name: `placeholder_${index + 1}`,
position: [ position: [
@@ -13,6 +13,8 @@ interface ModelErrorBoundaryProps {
children: ReactNode; children: ReactNode;
modelPath: string; modelPath: string;
position?: Vector3Tuple | undefined; position?: Vector3Tuple | undefined;
rotation?: Vector3Tuple | undefined;
scale?: ModelTransformProps["scale"] | undefined;
} }
interface ModelErrorBoundaryState { interface ModelErrorBoundaryState {
@@ -38,6 +40,8 @@ class ModelErrorBoundary extends Component<
modelPath: this.props.modelPath, modelPath: this.props.modelPath,
scope: "ExplodableModel", scope: "ExplodableModel",
position: this.props.position, position: this.props.position,
rotation: this.props.rotation,
scale: this.props.scale,
}, },
error, error,
); );
@@ -45,7 +49,13 @@ class ModelErrorBoundary extends Component<
render(): ReactNode { render(): ReactNode {
if (this.state.hasError) { if (this.state.hasError) {
return <MissingModelFallback position={this.props.position} />; return (
<MissingModelFallback
position={this.props.position}
rotation={this.props.rotation}
scale={this.props.scale}
/>
);
} }
return this.props.children; return this.props.children;
@@ -67,6 +77,8 @@ export function ExplodableModel(
key={props.modelPath} key={props.modelPath}
modelPath={props.modelPath} modelPath={props.modelPath}
position={props.position} position={props.position}
rotation={props.rotation}
scale={props.scale}
> >
<ExplodableModelInner {...props} /> <ExplodableModelInner {...props} />
</ModelErrorBoundary> </ModelErrorBoundary>
@@ -116,11 +128,15 @@ function ExplodableModelInner({
function MissingModelFallback({ function MissingModelFallback({
position = [0, 0, 0], position = [0, 0, 0],
rotation = [0, 0, 0],
scale = 1,
}: { }: {
position?: Vector3Tuple | undefined; position?: Vector3Tuple | undefined;
rotation?: Vector3Tuple | undefined;
scale?: ModelTransformProps["scale"] | undefined;
}): React.JSX.Element { }): React.JSX.Element {
return ( return (
<mesh position={position}> <mesh position={position} rotation={rotation} scale={toVector3Scale(scale)}>
<boxGeometry args={[0.7, 0.7, 0.7]} /> <boxGeometry args={[0.7, 0.7, 0.7]} />
<meshStandardMaterial color="#7f1d1d" wireframe /> <meshStandardMaterial color="#7f1d1d" wireframe />
</mesh> </mesh>
+11 -17
View File
@@ -1,9 +1,9 @@
import { RotateCcw, StepBack, StepForward } from "lucide-react"; import { RotateCcw, StepBack, StepForward } from "lucide-react";
import { import {
type MainGameState, type MainGameState,
type MissionStep,
useGameStore, useGameStore,
} from "@/managers/stores/useGameStore"; } from "@/managers/stores/useGameStore";
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
const MAIN_STATES: MainGameState[] = [ const MAIN_STATES: MainGameState[] = [
"intro", "intro",
@@ -13,17 +13,6 @@ const MAIN_STATES: MainGameState[] = [
"outro", "outro",
]; ];
const MISSION_STEPS: MissionStep[] = [
"locked",
"waiting",
"inspected",
"fragmented",
"scanning",
"repairing",
"reassembling",
"done",
];
function toPascalCase(value: string): string { function toPascalCase(value: string): string {
return value return value
.split(/[-_\s]+/) .split(/[-_\s]+/)
@@ -71,22 +60,27 @@ export function GameStateDebugPanel(): React.JSX.Element {
return; return;
} }
if (mainState === "outro") {
setOutroState({ hasStarted: nextSubState === "started" });
return;
}
if (!isMissionStep(nextSubState)) return;
if (mainState === "bike") { if (mainState === "bike") {
setBikeState({ currentStep: nextSubState as MissionStep }); setBikeState({ currentStep: nextSubState });
return; return;
} }
if (mainState === "pylone") { if (mainState === "pylone") {
setPyloneState({ currentStep: nextSubState as MissionStep }); setPyloneState({ currentStep: nextSubState });
return; return;
} }
if (mainState === "ferme") { if (mainState === "ferme") {
setFermeState({ currentStep: nextSubState as MissionStep }); setFermeState({ currentStep: nextSubState });
return; return;
} }
setOutroState({ hasStarted: nextSubState === "started" });
} }
return ( return (
+1 -1
View File
@@ -361,7 +361,7 @@ Pour les missions de réparation, il monte le composant réutilisable \`RepairGa
<RepairGame mission="bike" position={[8, 0, -6]} /> <RepairGame mission="bike" position={[8, 0, -6]} />
\`\`\` \`\`\`
\`RepairGame\` lit l'étape de mission active depuis le store et écrit les transitions via des actions génériques comme \`setMissionStep\` et \`completeMission\`. Cela garde le composant de scène petit et évite les branches spécifiques à chaque mission dans le flow de réparation. Le flow de réparation de production supporte actuellement les transitions \`waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission\`. \`RepairGame\` lit l'étape de mission active depuis le store et écrit les transitions via des actions génériques comme \`setMissionStep\` et \`completeMission\`. Les ids de mission, étapes de mission et guards partagés vivent dans \`src/types/gameplay/repairMission.ts\`, ce qui évite à la configuration statique des missions de dépendre du store Zustand. Le flow de réparation de production supporte actuellement les transitions \`waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission\`.
La scène peut donc évoluer progressivement vers ce pattern : La scène peut donc évoluer progressivement vers ce pattern :
+1 -1
View File
@@ -1,4 +1,4 @@
import type { RepairMissionId } from "@/managers/stores/useGameStore"; import type { RepairMissionId } from "@/types/gameplay/repairMission";
import type { Vector3Scale, Vector3Tuple } from "@/types/three/three"; import type { Vector3Scale, Vector3Tuple } from "@/types/three/three";
export interface RepairMissionCaseConfig { export interface RepairMissionCaseConfig {
+2 -2
View File
@@ -1,8 +1,8 @@
import { useGameStore } from "@/managers/stores/useGameStore";
import type { import type {
MissionStep, MissionStep,
RepairMissionId, RepairMissionId,
} from "@/managers/stores/useGameStore"; } from "@/types/gameplay/repairMission";
import { useGameStore } from "@/managers/stores/useGameStore";
export function useRepairMissionStep(mission: RepairMissionId): MissionStep { export function useRepairMissionStep(mission: RepairMissionId): MissionStep {
return useGameStore((state) => state[mission].currentStep); return useGameStore((state) => state[mission].currentStep);
+1 -1
View File
@@ -20,7 +20,7 @@ export function getCameraStreamWithTimeout(
didTimeout = true; didTimeout = true;
reject( reject(
new Error( new Error(
"Camera request timed out. Restart Arc or check camera permissions for localhost:5173.", "Camera request timed out. Restart the browser or check camera permissions for localhost:5173.",
), ),
); );
}, HAND_TRACKING_CAMERA_TIMEOUT_MS); }, HAND_TRACKING_CAMERA_TIMEOUT_MS);
+16 -1
View File
@@ -125,7 +125,22 @@ export class AudioManager {
this._musicUnlockHandler = () => { this._musicUnlockHandler = () => {
this._removeMusicUnlockHandler(); this._removeMusicUnlockHandler();
void this._music?.play(); const music = this._music;
if (!music) return;
void music.play().catch((error: unknown) => {
if (
error instanceof DOMException &&
AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
) {
return;
}
logger.error("AudioManager", "Failed to unlock music playback", {
path: this._musicPath,
error: AudioManager._toLogValue(error),
});
});
}; };
window.addEventListener("pointerdown", this._musicUnlockHandler, { window.addEventListener("pointerdown", this._musicUnlockHandler, {
+6 -16
View File
@@ -1,16 +1,12 @@
import { create } from "zustand"; import { create } from "zustand";
import {
isRepairMissionId,
type MissionStep,
type RepairMissionId,
} from "@/types/gameplay/repairMission";
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
export type RepairMissionId = "bike" | "pylone" | "ferme"; export type { MissionStep, RepairMissionId };
export type MissionStep =
| "locked"
| "waiting"
| "inspected"
| "fragmented"
| "scanning"
| "repairing"
| "reassembling"
| "done";
interface IntroState { interface IntroState {
dialogueAudio: string | null; dialogueAudio: string | null;
@@ -63,12 +59,6 @@ interface GameActions {
type GameStore = GameState & GameActions; type GameStore = GameState & GameActions;
type GameStateUpdate = Partial<GameState>; type GameStateUpdate = Partial<GameState>;
export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const;
function isRepairMissionId(value: MainGameState): value is RepairMissionId {
return REPAIR_MISSION_IDS.includes(value as RepairMissionId);
}
function getNextMissionStep(step: MissionStep): MissionStep { function getNextMissionStep(step: MissionStep): MissionStep {
switch (step) { switch (step) {
case "locked": case "locked":
@@ -9,7 +9,7 @@ import {
import { useBrowserHandTracking } from "@/hooks/handTracking/useBrowserHandTracking"; import { useBrowserHandTracking } from "@/hooks/handTracking/useBrowserHandTracking";
import { useRemoteHandTracking } from "@/hooks/handTracking/useRemoteHandTracking"; import { useRemoteHandTracking } from "@/hooks/handTracking/useRemoteHandTracking";
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import type { MissionStep } from "@/managers/stores/useGameStore"; import type { MissionStep } from "@/types/gameplay/repairMission";
const REPAIR_HAND_TRACKING_STEPS = new Set<MissionStep>([ const REPAIR_HAND_TRACKING_STEPS = new Set<MissionStep>([
"inspected", "inspected",
+32
View File
@@ -0,0 +1,32 @@
export type RepairMissionId = "bike" | "pylone" | "ferme";
export type MissionStep =
| "locked"
| "waiting"
| "inspected"
| "fragmented"
| "scanning"
| "repairing"
| "reassembling"
| "done";
export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const;
export const MISSION_STEPS = [
"locked",
"waiting",
"inspected",
"fragmented",
"scanning",
"repairing",
"reassembling",
"done",
] as const satisfies readonly MissionStep[];
export function isRepairMissionId(value: string): value is RepairMissionId {
return (REPAIR_MISSION_IDS as readonly string[]).includes(value);
}
export function isMissionStep(value: string): value is MissionStep {
return (MISSION_STEPS as readonly string[]).includes(value);
}
+2 -1
View File
@@ -17,7 +17,8 @@ export async function createSceneDataFromFiles(
throw new Error("Fichier map.json manquant à la racine du dossier"); throw new Error("Fichier map.json manquant à la racine du dossier");
} }
const mapNodes = parseMapNodes(JSON.parse(await mapFile.text())); const mapPayload: unknown = JSON.parse(await mapFile.text());
const mapNodes = parseMapNodes(mapPayload);
const models = new Map<string, string>(); const models = new Map<string, string>();
for (const [path, file] of fileMap.entries()) { for (const [path, file] of fileMap.entries()) {
+2 -1
View File
@@ -13,7 +13,8 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
return null; return null;
} }
const mapNodes = parseMapNodes(await response.json()); const mapPayload: unknown = await response.json();
const mapNodes = parseMapNodes(mapPayload);
return createSceneData(mapNodes); return createSceneData(mapNodes);
} }
+1 -1
View File
@@ -48,7 +48,7 @@ const saveMapPlugin = (): Plugin => ({
} }
try { try {
const data = JSON.parse(Buffer.concat(chunks).toString()); const data: unknown = JSON.parse(Buffer.concat(chunks).toString());
try { try {
parseMapNodes(data); parseMapNodes(data);
} catch { } catch {