From 0a3966a339e0b20205e94b4d87855f17c963855e Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:53:34 +0200 Subject: [PATCH] animate and fix electricienne --- public/sounds/dialogue/dialogues.json | 18 ++ .../gameplay/pylon/PylonDownedPylon.tsx | 16 +- .../gameplay/pylon/PylonFarmerNPC.tsx | 180 +++++++++++++++--- .../gameplay/pylon/PylonLightingEffect.tsx | 54 ++++++ .../gameplay/pylon/PylonNarrativeFlow.tsx | 7 +- .../gameplay/pylon/PylonNarratorOutro.tsx | 71 ++++++- src/components/gameplay/pylon/pylonSignals.ts | 6 +- src/data/gameplay/pylonConfig.ts | 22 ++- src/data/gameplay/zones.ts | 18 +- src/world/GameStageContent.tsx | 2 + 10 files changed, 339 insertions(+), 55 deletions(-) create mode 100644 src/components/gameplay/pylon/PylonLightingEffect.tsx diff --git a/public/sounds/dialogue/dialogues.json b/public/sounds/dialogue/dialogues.json index bd738f0..f53a902 100644 --- a/public/sounds/dialogue/dialogues.json +++ b/public/sounds/dialogue/dialogues.json @@ -188,6 +188,24 @@ "voice": "fermier", "audio": "/sounds/dialogue/fermier_findemission.mp3", "subtitleCueIndex": 3 + }, + { + "id": "electricienne_welcome", + "voice": "electricienne", + "audio": "/sounds/dialogue/electricienne_welcome.mp3", + "subtitleCueIndex": 1 + }, + { + "id": "electricienne_apresMontage", + "voice": "electricienne", + "audio": "/sounds/dialogue/electricienne_aprèsmontage.mp3", + "subtitleCueIndex": 2 + }, + { + "id": "electricienne_aurevoir", + "voice": "electricienne", + "audio": "/sounds/dialogue/electricienne_aurevoir.mp3", + "subtitleCueIndex": 3 } ] } diff --git a/src/components/gameplay/pylon/PylonDownedPylon.tsx b/src/components/gameplay/pylon/PylonDownedPylon.tsx index ff7bd74..d863314 100644 --- a/src/components/gameplay/pylon/PylonDownedPylon.tsx +++ b/src/components/gameplay/pylon/PylonDownedPylon.tsx @@ -21,15 +21,20 @@ const PYLON_MODEL_PATH = "/models/pylone/model.glb"; export function PylonDownedPylon(): React.JSX.Element | null { const mainState = useGameStore((state) => state.mainState); const step = useGameStore((state) => state.pylon.currentStep); - const setMissionStep = useGameStore((state) => state.setMissionStep); const setCanMove = useGameStore((state) => state.setCanMove); const [isStraightening, setIsStraightening] = useState(false); + // Keeps the pylon upright after the animation completes while + // PylonFarmerNPC plays the post-raise audio sequence. + const [isRaised, setIsRaised] = useState(false); const groupRef = useRef(null); const straightenStartRef = useRef(null); const hasPlayedFirstAudioRef = useRef(false); useEffect(() => { - if (step === "arrived") hasPlayedFirstAudioRef.current = false; + if (step === "arrived") { + hasPlayedFirstAudioRef.current = false; + setIsRaised(false); + } }, [step]); const { scene } = useGLTF(PYLON_MODEL_PATH); @@ -56,6 +61,7 @@ export function PylonDownedPylon(): React.JSX.Element | null { }); const showUpright = + isRaised || mainState !== "pylon" || step === "waiting" || step === "inspected" || @@ -71,6 +77,7 @@ export function PylonDownedPylon(): React.JSX.Element | null { const beginStraighten = (): void => { setIsStraightening(true); pylonStraighteningSignal.started = true; + pylonStraighteningSignal.completed = false; straightenStartRef.current = performance.now(); setCanMove(false); if (groupRef.current) { @@ -79,8 +86,11 @@ export function PylonDownedPylon(): React.JSX.Element | null { window.setTimeout(() => { setIsStraightening(false); pylonStraighteningSignal.started = false; + // Keep pylon upright while PylonFarmerNPC plays the audio sequence. + // PylonFarmerNPC will call setMissionStep("pylon", "inspected") once done. + setIsRaised(true); setCanMove(true); - setMissionStep("pylon", "inspected"); + pylonStraighteningSignal.completed = true; }, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS); }; diff --git a/src/components/gameplay/pylon/PylonFarmerNPC.tsx b/src/components/gameplay/pylon/PylonFarmerNPC.tsx index 5811469..638248b 100644 --- a/src/components/gameplay/pylon/PylonFarmerNPC.tsx +++ b/src/components/gameplay/pylon/PylonFarmerNPC.tsx @@ -1,80 +1,206 @@ -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import * as THREE from "three"; -import { useFrame } from "@react-three/fiber"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useAnimations } from "@react-three/drei"; +import { useGLTF } from "@react-three/drei"; +import { SkeletonUtils } from "three-stdlib"; import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { useGameStore } from "@/managers/stores/useGameStore"; import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; import { playDialogueById } from "@/utils/dialogues/playDialogue"; +import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { PYLON_FARMER_NPC_AFTER_POSITION, PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight, - PYLON_FARMER_NPC_AFTER_ROTATION, PYLON_FARMER_NPC_AFTER_SCALE, PYLON_FARMER_NPC_POSITION, + PYLON_FARMER_NPC_WALK_LOOK_AT, PYLON_FARMER_NPC_WALK_SPEED, PYLON_NARRATIVE_DIALOGUES, PYLON_NARRATIVE_INTERACT_RADIUS, + PYLON_WORLD_POSITION, } from "@/data/gameplay/pylonConfig"; import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals"; +const ELECTRICIENNE_MODEL_PATH = "/models/electricienne-animated/model.gltf"; +const ANIM_FADE = 0.3; +const ARRIVE_THRESHOLD = 0.12; + +type NPCAnimation = "idle" | "walk" | "push"; + const _target = new THREE.Vector3(); +/** + * Compute the Y rotation (radians) for a model whose default forward + * direction is +Z, so that it faces from `from` toward `to`. + */ +function faceToward(from: THREE.Vector3, to: readonly [number, number, number]): number { + const dx = to[0] - from.x; + const dz = to[2] - from.z; + return Math.atan2(dx, dz); +} + export function PylonFarmerNPC(): React.JSX.Element | null { const mainState = useGameStore((state) => state.mainState); const step = useGameStore((state) => state.pylon.currentStep); const setMissionStep = useGameStore((state) => state.setMissionStep); + const camera = useThree((state) => state.camera); + const groupRef = useRef(null); - const currentPosRef = useRef( - new THREE.Vector3(...PYLON_FARMER_NPC_POSITION), + const currentPosRef = useRef(new THREE.Vector3(...PYLON_FARMER_NPC_POSITION)); + + // Animation state guard — null forces playAnim to always trigger + const currentAnimRef = useRef(null); + + // Signal edge tracking + const wasStraighteningRef = useRef(false); + const wasCompletedRef = useRef(false); + + // Saved Y rotation used whenever the NPC is stationary + const savedRotationYRef = useRef(0); + + const { scene, animations } = useLoggedGLTF(ELECTRICIENNE_MODEL_PATH, { + scope: "PylonFarmerNPC", + }); + const model = useMemo(() => SkeletonUtils.clone(scene), [scene]); + + // actions is in deps of playAnim: when useAnimations populates it (async useState + // inside drei), playAnim recreates → useEffect([step, playAnim]) re-fires → animation plays. + const { actions } = useAnimations(animations, model); + + // ─── playAnim ───────────────────────────────────────────────────────────── + // NOTE: actions is intentionally in the dep array so this callback is + // recreated when drei's internal state populates the actions map. + const playAnim = useCallback( + (name: NPCAnimation, fade = ANIM_FADE): void => { + if (currentAnimRef.current === name) return; + currentAnimRef.current = name; + + Object.values(actions).forEach((a) => a?.fadeOut(fade)); + + const action = actions[name]; + if (!action) return; + + if (name === "push") { + action.setLoop(THREE.LoopOnce, 1); + action.clampWhenFinished = true; + } + action.reset().fadeIn(fade).play(); + }, + [actions], ); - // Reset position when entering arrived, set target when entering npc-return + // ─── Async audio after pylon is raised ──────────────────────────────────── + const playPostRaiseAudioAndAdvance = useCallback(async () => { + const manifest = await loadDialogueManifest(); + if (manifest) { + // "N'hésite pas, si tu as besoin d'autre chose !" + const audio = await playDialogueById( + manifest, + PYLON_NARRATIVE_DIALOGUES.electricienneApresMontage, + ); + if (audio) { + await new Promise((resolve) => { + audio.addEventListener("ended", () => resolve(), { once: true }); + audio.addEventListener("error", () => resolve(), { once: true }); + }); + } + } + pylonStraighteningSignal.completed = false; + setMissionStep("pylon", "inspected"); + }, [setMissionStep]); + + // ─── Step-driven animation ──────────────────────────────────────────────── + // Fires when step changes OR when playAnim changes (i.e. when actions load). useEffect(() => { + currentAnimRef.current = null; if (step === "arrived") { currentPosRef.current.set(...PYLON_FARMER_NPC_POSITION); + wasStraighteningRef.current = false; + wasCompletedRef.current = false; + savedRotationYRef.current = 0; + playAnim("idle"); + } else if (step === "npc-return") { + playAnim("walk"); + } else if (step === "inspected") { + playAnim("idle"); } - }, [step]); + }, [step, playAnim]); + // ─── Per-frame: movement + rotation + signal detection ─────────────────── useFrame((_, delta) => { const group = groupRef.current; if (!group) return; - if (step === "npc-return") { - const targetPos = pylonStraighteningSignal.started + const isStraightening = pylonStraighteningSignal.started; + const isCompleted = pylonStraighteningSignal.completed; + + // Rising edge: pylon straightening starts → push + if (isStraightening && !wasStraighteningRef.current) { + wasStraighteningRef.current = true; + currentAnimRef.current = null; + playAnim("push"); + } + + // Rising edge: straightening completed → idle + face player + audio + if (isCompleted && !wasCompletedRef.current) { + wasCompletedRef.current = true; + currentAnimRef.current = null; + playAnim("idle"); + savedRotationYRef.current = faceToward(currentPosRef.current, [ + camera.position.x, + camera.position.y, + camera.position.z, + ]); + void playPostRaiseAudioAndAdvance(); + } + + // ── Position ────────────────────────────────────────────────────────── + if (step === "npc-return" && !isCompleted) { + const targetPos = isStraightening ? PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight : PYLON_FARMER_NPC_AFTER_POSITION; _target.set(...targetPos); - currentPosRef.current.lerp(_target, Math.min(PYLON_FARMER_NPC_WALK_SPEED * delta, 1)); + + const dist = currentPosRef.current.distanceTo(_target); + if (dist > ARRIVE_THRESHOLD) { + const t = Math.min((PYLON_FARMER_NPC_WALK_SPEED * delta) / dist, 1); + currentPosRef.current.lerp(_target, t); + } else if (!isStraightening && currentAnimRef.current === "walk") { + playAnim("idle"); + savedRotationYRef.current = faceToward(currentPosRef.current, PYLON_WORLD_POSITION); + } group.position.copy(currentPosRef.current); - group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION); - group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE); } else if (step === "inspected") { group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight); - group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION); - group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE); + } else if (isCompleted) { + group.position.copy(currentPosRef.current); } else { group.position.set(...PYLON_FARMER_NPC_POSITION); } + + // ── Rotation ────────────────────────────────────────────────────────── + if (step === "npc-return" && !isCompleted && currentAnimRef.current === "walk") { + const walkRotY = faceToward(currentPosRef.current, PYLON_FARMER_NPC_WALK_LOOK_AT); + group.rotation.set(0, walkRotY, 0); + } else { + group.rotation.set(0, savedRotationYRef.current, 0); + } + + group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE); }); if (mainState !== "pylon") return null; - if (step !== "arrived" && step !== "npc-return" && step !== "inspected") return null; + if (step !== "arrived" && step !== "npc-return" && step !== "inspected") + return null; return ( - - - - - - - - - + {step === "arrived" ? ( { @@ -86,7 +212,7 @@ export function PylonFarmerNPC(): React.JSX.Element | null { } const audio = await playDialogueById( manifest, - PYLON_NARRATIVE_DIALOGUES.farmerHelp, + PYLON_NARRATIVE_DIALOGUES.electricienneWelcome, ); if (!audio) { setMissionStep("pylon", "npc-return"); @@ -109,3 +235,5 @@ export function PylonFarmerNPC(): React.JSX.Element | null { ); } + +useGLTF.preload(ELECTRICIENNE_MODEL_PATH); diff --git a/src/components/gameplay/pylon/PylonLightingEffect.tsx b/src/components/gameplay/pylon/PylonLightingEffect.tsx new file mode 100644 index 0000000..c71648f --- /dev/null +++ b/src/components/gameplay/pylon/PylonLightingEffect.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef } from "react"; +import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import { LIGHTING_STATE } from "@/world/lightingState"; +import { LIGHTING_DEFAULTS } from "@/data/world/lightingConfig"; + +// ─── Pylon atmosphere colours ───────────────────────────────────────────────── +// Applied from "approaching" until the pylon mission ends. +const PYLON_AMBIENT_COLOR = "#7b87c8"; // blue-violet +const PYLON_SUN_COLOR = "#a882d4"; // lavender-purple + +// Lerp speed (1 = full transition in ~1 s at 60 fps) +const TRANSITION_SPEED = 0.8; + +// ───────────────────────────────────────────────────────────────────────────── + +export function PylonLightingEffect(): null { + const mainState = useGameStore((state) => state.mainState); + const step = useGameStore((state) => state.pylon.currentStep); + + // True from "approaching" until narrator-outro (lighting resets before the outro audio) + const isActive = mainState === "pylon" && step !== "locked" && step !== "narrator-outro"; + + // Working THREE.Color instances — lerped every frame + const ambientRef = useRef(new THREE.Color(LIGHTING_STATE.ambientColor)); + const sunRef = useRef(new THREE.Color(LIGHTING_STATE.sunColor)); + + // Target colours — updated reactively when isActive changes + const targetAmbientRef = useRef(new THREE.Color(LIGHTING_DEFAULTS.ambientColor)); + const targetSunRef = useRef(new THREE.Color(LIGHTING_DEFAULTS.sunColor)); + + useEffect(() => { + if (isActive) { + targetAmbientRef.current.set(PYLON_AMBIENT_COLOR); + targetSunRef.current.set(PYLON_SUN_COLOR); + } else { + targetAmbientRef.current.set(LIGHTING_DEFAULTS.ambientColor); + targetSunRef.current.set(LIGHTING_DEFAULTS.sunColor); + } + }, [isActive]); + + useFrame((_, delta) => { + const t = Math.min(TRANSITION_SPEED * delta, 1); + + ambientRef.current.lerp(targetAmbientRef.current, t); + sunRef.current.lerp(targetSunRef.current, t); + + LIGHTING_STATE.ambientColor = `#${ambientRef.current.getHexString()}`; + LIGHTING_STATE.sunColor = `#${sunRef.current.getHexString()}`; + }); + + return null; +} diff --git a/src/components/gameplay/pylon/PylonNarrativeFlow.tsx b/src/components/gameplay/pylon/PylonNarrativeFlow.tsx index 218a47a..11a9016 100644 --- a/src/components/gameplay/pylon/PylonNarrativeFlow.tsx +++ b/src/components/gameplay/pylon/PylonNarrativeFlow.tsx @@ -10,7 +10,6 @@ export function PylonNarrativeFlow(): React.JSX.Element | null { const mainState = useGameStore((state) => state.mainState); const step = useGameStore((state) => state.pylon.currentStep); const setMissionStep = useGameStore((state) => state.setMissionStep); - const completeMission = useGameStore((state) => state.completeMission); useDialoguePlayback({ enabled: mainState === "pylon" && step === "approaching", @@ -22,11 +21,7 @@ export function PylonNarrativeFlow(): React.JSX.Element | null { dialogueId: PYLON_NARRATIVE_DIALOGUES.searchCentral, }); - useDialoguePlayback({ - enabled: mainState === "pylon" && step === "narrator-outro", - dialogueId: PYLON_NARRATIVE_DIALOGUES.powerRestored, - onComplete: () => completeMission("pylon"), - }); + // narrator-outro audio sequence + completeMission are handled in PylonNarratorOutro if (mainState !== "pylon") return null; diff --git a/src/components/gameplay/pylon/PylonNarratorOutro.tsx b/src/components/gameplay/pylon/PylonNarratorOutro.tsx index aaaea02..95fbf3e 100644 --- a/src/components/gameplay/pylon/PylonNarratorOutro.tsx +++ b/src/components/gameplay/pylon/PylonNarratorOutro.tsx @@ -1,11 +1,72 @@ +import { useEffect } from "react"; import { useGameStore } from "@/managers/stores/useGameStore"; +import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; +import { playDialogueById } from "@/utils/dialogues/playDialogue"; +import { PYLON_NARRATIVE_DIALOGUES } from "@/data/gameplay/pylonConfig"; -export function PylonNarratorOutro(): React.JSX.Element | null { - const mainState = useGameStore((state) => state.mainState); - const step = useGameStore((state) => state.pylon.currentStep); +/** + * Plays the narrator-outro audio sequence: + * 1. electricienne_aurevoir ("À la prochaine !") + * 2. narrateur_courantrepare ("powerRestored") + * then completes the pylon mission. + */ +export function PylonNarratorOutro(): null { + const completeMission = useGameStore((state) => state.completeMission); + const setCanMove = useGameStore((state) => state.setCanMove); - if (mainState !== "pylon") return null; - if (step !== "narrator-outro") return null; + useEffect(() => { + let cancelled = false; + setCanMove(false); + + void (async () => { + const manifest = await loadDialogueManifest(); + if (cancelled || !manifest) { + setCanMove(true); + return; + } + + // 1. Électricienne : "À la prochaine !" + const audio1 = await playDialogueById( + manifest, + PYLON_NARRATIVE_DIALOGUES.electricienneAurevoir, + ); + if (audio1 && !cancelled) { + await new Promise((resolve) => { + audio1.addEventListener("ended", () => resolve(), { once: true }); + audio1.addEventListener("error", () => resolve(), { once: true }); + }); + } + + if (cancelled) { + setCanMove(true); + return; + } + + // 2. Narrateur : "Le courant est réparé" + const audio2 = await playDialogueById( + manifest, + PYLON_NARRATIVE_DIALOGUES.powerRestored, + ); + if (audio2 && !cancelled) { + audio2.addEventListener( + "ended", + () => { + setCanMove(true); + completeMission("pylon"); + }, + { once: true }, + ); + } else { + setCanMove(true); + completeMission("pylon"); + } + })(); + + return () => { + cancelled = true; + setCanMove(true); + }; + }, [completeMission, setCanMove]); return null; } diff --git a/src/components/gameplay/pylon/pylonSignals.ts b/src/components/gameplay/pylon/pylonSignals.ts index eafa24d..e5c9de2 100644 --- a/src/components/gameplay/pylon/pylonSignals.ts +++ b/src/components/gameplay/pylon/pylonSignals.ts @@ -1,5 +1,9 @@ /** * Shared runtime signal set by PylonDownedPylon when the straighten * animation starts, so PylonFarmerNPC can switch its lerp target. + * + * `completed` is set after the straighten animation finishes so + * PylonFarmerNPC can play the post-raise audio sequence before + * transitioning to the repair game. */ -export const pylonStraighteningSignal = { started: false }; +export const pylonStraighteningSignal = { started: false, completed: false }; diff --git a/src/data/gameplay/pylonConfig.ts b/src/data/gameplay/pylonConfig.ts index c249947..acc7edc 100644 --- a/src/data/gameplay/pylonConfig.ts +++ b/src/data/gameplay/pylonConfig.ts @@ -1,20 +1,27 @@ import type { Vector3Tuple } from "@/types/three/three"; -export const PYLON_WORLD_POSITION: Vector3Tuple = [43, 5, 45]; +export const PYLON_WORLD_POSITION: Vector3Tuple = [-31.5, 3.5, 36.04]; export const PYLON_DOWNED_ROTATION: Vector3Tuple = [0, 0, -0.9]; export const PYLON_UPRIGHT_ROTATION: Vector3Tuple = [0, 0, 0]; export const PYLON_FARMER_NPC_POSITION: Vector3Tuple = [ - PYLON_WORLD_POSITION[0] - 6, - PYLON_WORLD_POSITION[1], - PYLON_WORLD_POSITION[2] + 4, + -16.13, + 3.2, + 52.46 ]; export const PYLON_FARMER_NPC_AFTER_POSITION: Vector3Tuple = [ PYLON_WORLD_POSITION[0] + 3, - PYLON_WORLD_POSITION[1], + PYLON_WORLD_POSITION[1] + 0.2, + PYLON_WORLD_POSITION[2], +]; + +/** Point vers lequel l'électricienne regarde pendant sa marche vers le pylône (ajustable) */ +export const PYLON_FARMER_NPC_WALK_LOOK_AT: Vector3Tuple = [ + PYLON_WORLD_POSITION[0] + 3, + PYLON_WORLD_POSITION[1] + 0.2, PYLON_WORLD_POSITION[2], ]; @@ -29,7 +36,7 @@ export const PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight: Vector3Tuple = [ export const PYLON_FARMER_NPC_AFTER_ROTATION: Vector3Tuple = [0, 0, 0]; /** Scale uniforme du PNJ une fois arrivé sous le pylône */ -export const PYLON_FARMER_NPC_AFTER_SCALE = 1; +export const PYLON_FARMER_NPC_AFTER_SCALE = 1.55; /** Vitesse du lerp de déplacement du PNJ (unités/s) */ export const PYLON_FARMER_NPC_WALK_SPEED = 2; @@ -44,5 +51,8 @@ export const PYLON_NARRATIVE_DIALOGUES = { brokenPylon: "narrateur_poteaueleccasse", demandeAide: "narrateur_demande_aide", farmerHelp: "fermier_coupdemain", + electricienneWelcome: "electricienne_welcome", + electricienneApresMontage: "electricienne_apresMontage", + electricienneAurevoir: "electricienne_aurevoir", powerRestored: "narrateur_courantrepare", } as const; diff --git a/src/data/gameplay/zones.ts b/src/data/gameplay/zones.ts index 14675b5..f8362bf 100644 --- a/src/data/gameplay/zones.ts +++ b/src/data/gameplay/zones.ts @@ -1,26 +1,28 @@ import type { ZoneConfig } from "@/types/gameplay/zone"; import { PYLON_WORLD_POSITION } from "@/data/gameplay/pylonConfig"; +// Zones qui active la coupure de courant export const PYLON_APPROACH_ZONE: ZoneConfig = { id: "pylon-approach", position: [ - PYLON_WORLD_POSITION[0], - PYLON_WORLD_POSITION[1]- 5, - PYLON_WORLD_POSITION[2], + 5, + 4, + -21.5 ], - radius: 5, + radius: 10, height: 18, oneShot: true, }; +// Zone qui active la cinématique d'arrivée du pylône export const PYLON_ARRIVED_ZONE: ZoneConfig = { id: "pylon-arrived", position: [ - PYLON_WORLD_POSITION[0] + 5, - PYLON_WORLD_POSITION[1] - 5, - PYLON_WORLD_POSITION[2] + 5, + PYLON_WORLD_POSITION[0], + PYLON_WORLD_POSITION[1], + PYLON_WORLD_POSITION[2], ], - radius: 5, + radius: 30, height: 15, oneShot: true, }; diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx index 830575d..e602ae6 100644 --- a/src/world/GameStageContent.tsx +++ b/src/world/GameStageContent.tsx @@ -2,6 +2,7 @@ import { Ebike } from "@/components/ebike/Ebike"; import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { RepairGame } from "@/components/three/gameplay/RepairGame"; 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"; @@ -99,6 +100,7 @@ export function GameStageContent(): React.JSX.Element { <> {mainState === "intro" ? : null} + {isDebugEnabled() ? ( <>