fix(types): satisfy strict tsc for production build (deploy unblock)
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled

This commit is contained in:
Tom Boullay
2026-06-03 02:40:54 +02:00
parent e9808f8473
commit d8b916d31f
12 changed files with 56 additions and 51 deletions
+4 -1
View File
@@ -181,9 +181,12 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
// Sync texture into uniform when it changes (canvas resize)
useEffect(() => {
const mapUniform = shaderMat.uniforms.map;
if (!mapUniform) return;
// External Three.js material uniform sync — intentional side effect.
// eslint-disable-next-line react-hooks/immutability
shaderMat.uniforms.map.value = texture;
mapUniform.value = texture;
}, [shaderMat, texture]);
// Cleanup on unmount
@@ -146,16 +146,6 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
return null;
}
if (mainState == "pylon") {
if (pylonStep === "approaching") {
return <MissionNotification mission="pylon" visible />;
}
if (pylonStep === "narrator-outro") {
return <MissionNotification mission="farm" visible />;
}
return null;
}
if (
introStep !== "reveal" &&
introStep !== "await-ebike-mount" &&
@@ -3,7 +3,8 @@ import { useGameStore } from "@/managers/stores/useGameStore";
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
import { AudioManager } from "@/managers/AudioManager";
const HISTOIRE_AUDIO_PATH = "/sounds/dialogue/narrateur_histoireelectricienne.mp3";
const HISTOIRE_AUDIO_PATH =
"/sounds/dialogue/narrateur_histoireelectricienne.mp3";
const OUTRO_DELAY_MS = 5_000; // delay after audio ends before transitioning to outro
/**
@@ -78,9 +79,12 @@ function useHistoireSubtitlePlayback(
({ start, end }) => t >= start && t < end,
);
if (idx >= 0) {
const text = HISTOIRE_BLOCKS[idx];
if (text === undefined) return;
setActiveSubtitle({
speaker: "Narrateur",
text: HISTOIRE_BLOCKS[idx],
text,
});
}
}
@@ -136,7 +140,9 @@ export function FarmNarrativeFlow(): null {
// After the audio finishes, wait 5 s then transition to outro.
// The timeout ID is kept in a ref so we can cancel on unmount.
const outroTimeoutRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
const outroTimeoutRef = useRef<ReturnType<typeof window.setTimeout> | null>(
null,
);
useEffect(() => {
return () => {
@@ -33,7 +33,9 @@ export function PylonDownedPylon(): React.JSX.Element | null {
);
// Snap to terrain so the downed/upright model sits flush on the ground,
// matching the Y adjustment that InstancedMapAsset applies to the same node.
const position = useTerrainSnappedPosition(pylonAnchor ?? PYLON_WORLD_POSITION);
const position = useTerrainSnappedPosition(
pylonAnchor ?? PYLON_WORLD_POSITION,
);
const [isStraightening, setIsStraightening] = useState(false);
// Keeps the pylon upright after the animation completes while
// PylonFarmerNPC plays the post-raise audio sequence.
@@ -63,7 +65,9 @@ export function PylonDownedPylon(): React.JSX.Element | null {
if (!group) return;
if (!isStraightening || straightenStartRef.current === null) {
group.rotation.set(...(isRaised ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
group.rotation.set(
...(isRaised ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION),
);
return;
}
@@ -104,11 +108,7 @@ export function PylonDownedPylon(): React.JSX.Element | null {
if (!shouldRender) return null;
return (
<group
ref={groupRef}
position={position}
rotation={PYLON_DOWNED_ROTATION}
>
<group ref={groupRef} position={position} rotation={PYLON_DOWNED_ROTATION}>
<primitive object={scene.clone(true)} />
{isPylonInteractive ? (
<InteractableObject
@@ -159,7 +159,9 @@ function PylonFarmerNPCContent(): React.JSX.Element {
} else if (step === "done") {
// NPC reappears at repair completion — position at the post-raise spot,
// facing the pylon, playing idle.
currentPosRef.current.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight);
currentPosRef.current.set(
...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight,
);
savedRotationYRef.current = faceToward(
currentPosRef.current,
PYLON_WORLD_POSITION,
@@ -28,11 +28,9 @@ export function PylonNarrativeFlow(): React.JSX.Element | null {
void (async () => {
// 1. Play the generator powerdown sound effect
const sfx = AudioManager.getInstance().playSound(
PYLON_POWERDOWN_SFX,
1,
{ category: "sfx" },
);
const sfx = AudioManager.getInstance().playSound(PYLON_POWERDOWN_SFX, 1, {
category: "sfx",
});
// 2. Wait for it to finish (or skip if it can't load)
if (sfx) {
-1
View File
@@ -15,7 +15,6 @@ import {
} from "@/utils/dialogues/playDialogue";
const TYPEWRITER_CHAR_DELAY_MS = 150;
const TYPEWRITER_START_DELAY_MS = 12000;
// Fallback in case nothing else triggers the typewriter (audio failed to
// load, no subtitles, "ended" never fires). Long enough not to fire
// before the narration on a slow load.
+4 -1
View File
@@ -16,7 +16,10 @@ export function OutroVideoOverlay(): React.JSX.Element | null {
setVisible(true);
}
window.addEventListener("outro-cinematic-complete", handleCinematicComplete);
window.addEventListener(
"outro-cinematic-complete",
handleCinematicComplete,
);
return () => {
window.removeEventListener(
"outro-cinematic-complete",
+3 -5
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
@@ -50,11 +50,10 @@ export function ZoneDetection({
zone,
onEnter,
height,
}: ZoneDetectionProps): React.JSX.Element {
}: ZoneDetectionProps): React.JSX.Element | null {
const camera = useThree((state) => state.camera);
const hasTriggeredRef = useRef(false);
const onEnterRef = useRef(onEnter);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
onEnterRef.current = onEnter;
@@ -75,9 +74,8 @@ export function ZoneDetection({
if (_cameraPos.y > zone.position[1] + zoneHeight / 2) return;
hasTriggeredRef.current = true;
setIsActive(true);
onEnterRef.current();
});
return <ZoneDebugVisual zone={zone} active={isActive} />;
return null;
}
+15 -2
View File
@@ -10,6 +10,7 @@ const REPAIR_MISSION_ID_VALUES: ReadonlySet<string> = new Set(
export const MISSION_STEPS = [
"locked",
"electricienne_history",
"approaching",
"arrived",
"npc-return",
@@ -30,12 +31,20 @@ const PYLON_ONLY_MISSION_STEPS = new Set<MissionStep>([
"npc-return",
"narrator-outro",
]);
const FARM_ONLY_MISSION_STEPS = new Set<MissionStep>(["electricienne_history"]);
export function getMissionStepsFor(
mission: RepairMissionId,
): readonly MissionStep[] {
if (mission === "pylon") return MISSION_STEPS;
return MISSION_STEPS.filter((step) => !PYLON_ONLY_MISSION_STEPS.has(step));
return MISSION_STEPS.filter((step) => {
if (mission !== "pylon" && PYLON_ONLY_MISSION_STEPS.has(step)) {
return false;
}
if (mission !== "farm" && FARM_ONLY_MISSION_STEPS.has(step)) {
return false;
}
return true;
});
}
export function isRepairMissionId(value: string): value is RepairMissionId {
@@ -53,6 +62,8 @@ export function getNextMissionStep(
switch (step) {
case "locked":
return mission === "pylon" ? "approaching" : "waiting";
case "electricienne_history":
return "done";
case "approaching":
return "arrived";
case "arrived":
@@ -85,6 +96,8 @@ export function getPreviousMissionStep(
switch (step) {
case "locked":
return "locked";
case "electricienne_history":
return "locked";
case "approaching":
return "locked";
case "arrived":
+7 -2
View File
@@ -3,10 +3,15 @@ import type { MapNode, MapNodeInstanceTransform } from "@/types/map/mapScene";
export function mapNodeToInstanceTransform(
node: MapNode,
): MapNodeInstanceTransform {
return {
id: node.id,
const transform: MapNodeInstanceTransform = {
position: node.position,
rotation: node.rotation,
scale: node.scale,
};
if (node.id !== undefined) {
transform.id = node.id;
}
return transform;
}
+1 -13
View File
@@ -6,9 +6,6 @@ import { FarmNarrativeFlow } from "@/components/gameplay/farm/FarmNarrativeFlow"
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
import { PylonLightingEffect } from "@/components/gameplay/pylon/PylonLightingEffect";
import { PylonNarrativeFlow } from "@/components/gameplay/pylon/PylonNarrativeFlow";
import { ZoneDebugVisual } from "@/components/zone/ZoneDetection";
import { PYLON_APPROACH_ZONE, PYLON_ARRIVED_ZONE } from "@/data/gameplay/zones";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
import {
REPAIR_MISSION_POSITION_ENTRIES,
REPAIR_MISSION_TRIGGERS,
@@ -18,7 +15,6 @@ import {
OUTRO_STAGE_ANCHOR,
} from "@/data/gameplay/gameStageAnchors";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useRepairFocusStore } from "@/managers/stores/useRepairFocusStore";
import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore";
import {
isFarmNarrativeStep,
@@ -92,14 +88,12 @@ export function GameStageContent(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState);
const pylonStep = useGameStore((state) => state.pylon.currentStep);
const anchors = useRepairMissionAnchorStore((state) => state.anchors);
const repairFocusActive = useRepairFocusStore((state) => state.active);
const farmStep = useGameStore((state) => state.farm.currentStep);
const pylonInNarrative =
mainState === "pylon" && isPylonNarrativeStep(pylonStep);
const farmInNarrative =
mainState === "farm" && isFarmNarrativeStep(farmStep);
const farmInNarrative = mainState === "farm" && isFarmNarrativeStep(farmStep);
return (
<>
@@ -107,12 +101,6 @@ export function GameStageContent(): React.JSX.Element {
<Ebike position={EBIKE_WORLD_POSITION} />
<PylonLightingEffect />
<PylonDownedPylon />
{isDebugEnabled() && !repairFocusActive ? (
<>
<ZoneDebugVisual zone={PYLON_APPROACH_ZONE} active={false} />
<ZoneDebugVisual zone={PYLON_ARRIVED_ZONE} active={false} />
</>
) : null}
{mainState === "pylon" ? <PylonNarrativeFlow /> : null}
{mainState === "farm" ? <FarmNarrativeFlow /> : null}
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {