Files
healer-man/src/scene/WailingNaralexEvent.tsx
T
2026-09-26 17:51:56 -04:00

99 lines
4.0 KiB
TypeScript

import { useFrame } from "@react-three/fiber";
import { useRef, useSyncExternalStore } from "react";
import type { Group } from "three";
import { BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID } from "../game/dungeonContentSource";
import {
dispatchDungeonEncounterSignal,
dungeonEncounterSnapshot,
subscribeDungeonEncounterSnapshots,
} from "../game/dungeonEncounterRuntime";
import { useGameStore } from "../game/store";
import {
useWailingEncounterStore,
wailingDisciplePosition,
type WailingEncounterPhase,
} from "../game/wailingCavernsEncounter";
import { WorldNpcPrompt } from "./WorldNpcPrompt";
import { dungeonSimulationNow } from "../game/dungeonSimulationStore";
export function WailingNaralexEvent() {
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
const gameMode = useGameStore((state) => state.gameMode);
const sourcePhase = useWailingEncounterStore((state) => state.phase);
const sourcePhaseStartedAt = useWailingEncounterStore((state) => state.phaseStartedAt);
const biteEncounterId = `${BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID}:naralex-escort`;
useSyncExternalStore(
subscribeDungeonEncounterSnapshots,
() => dungeonEncounterSnapshot(biteEncounterId)?.revision ?? 0,
() => 0,
);
const biteSnapshot = dungeonEncounterSnapshot(biteEncounterId);
const phase = activeDungeonId === BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID
? biteSnapshot?.phase ?? "ready"
: sourcePhase;
const phaseStartedAt = activeDungeonId === BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID
? biteSnapshot?.phaseStartedAt ?? 0
: sourcePhaseStartedAt;
const isBiteSized = activeDungeonId === BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID;
const groupRef = useRef<Group>(null);
useFrame(() => {
if (
(activeDungeonId !== "wailing-caverns" && !isBiteSized)
|| gameMode !== "dungeon"
) return;
const liveSnapshot = isBiteSized ? dungeonEncounterSnapshot(biteEncounterId) : null;
const now = dungeonSimulationNow();
const position = wailingDisciplePosition(
(liveSnapshot?.phase ?? useWailingEncounterStore.getState().phase) as Parameters<typeof wailingDisciplePosition>[0],
liveSnapshot?.phaseStartedAt ?? useWailingEncounterStore.getState().phaseStartedAt,
now,
);
groupRef.current?.position.set(...position);
});
if (
(activeDungeonId !== "wailing-caverns" && !isBiteSized)
|| gameMode !== "dungeon"
) return null;
const initialPosition = wailingDisciplePosition(phase as WailingEncounterPhase, phaseStartedAt);
return (
<group ref={groupRef} name="disciple-of-naralex-event" position={initialPosition}>
<mesh position={[0, 0.9, 0]} castShadow>
<capsuleGeometry args={[0.34, 1.15, 6, 10]} />
<meshStandardMaterial color="#526d3d" roughness={0.76} />
</mesh>
<mesh position={[0, 1.72, 0]} castShadow>
<sphereGeometry args={[0.3, 12, 8]} />
<meshStandardMaterial color="#b9d67a" roughness={0.7} />
</mesh>
<WorldNpcPrompt position={[0, 2.45, 0]}>
<div className="naralex-event-prompt">
<strong>Disciple of Naralex</strong>
{phase === "locked" ? <small>Defeat the four Fanglords</small> : null}
{phase === "ready" ? (
<button
type="button"
onClick={(event) => {
event.stopPropagation();
if (isBiteSized) {
dispatchDungeonEncounterSignal(
`${BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID}:naralex-start`,
dungeonSimulationNow(),
);
} else {
useWailingEncounterStore.getState().start();
}
}}
>
Begin the awakening
</button>
) : null}
{phase === "cleansing" ? <small>Channeling Serpentine Cleansing…</small> : null}
{phase === "complete" ? <small>{isBiteSized ? "Naralex has been rescued" : "Naralex has awakened"}</small> : null}
</div>
</WorldNpcPrompt>
</group>
);
}