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
🔍 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:
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user