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