47 lines
3.8 KiB
TypeScript
47 lines
3.8 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Html } from "@react-three/drei";
|
|
import { useBiteSizedLayoutsStore } from "../game/biteSizedLayoutsStore";
|
|
import { derivePartySpawnSlots } from "../game/partySpawnSlots";
|
|
import { biteSizedDefinitionForSelection } from "../game/dungeonRegistry";
|
|
import { selectBiteSizedRun, type LayoutVector } from "../../shared/bite-sized-layouts.mjs";
|
|
import { MobPopulation } from "./MobPopulation";
|
|
import { useGameStore } from "../game/store";
|
|
import { resolveDungeonAssets } from "../game/dungeonAssets";
|
|
import { cachedPartyNavigationGraph } from "../game/partyNavigationAssetCache";
|
|
import { findNearestPartyNavigationSurface } from "../game/partyNavigation";
|
|
|
|
function Marker({ id, position, yaw, color, label, selected }: {
|
|
readonly id: string; readonly position: LayoutVector; readonly yaw: number; readonly color: string; readonly label: string; readonly selected: boolean;
|
|
}) {
|
|
const choose = () => useBiteSizedLayoutsStore.setState({ editingPointId: id, panelFocused: false });
|
|
return <group position={position} rotation={[0, yaw, 0]}>
|
|
<mesh position={[0, .08, 0]} rotation={[-Math.PI / 2, 0, 0]} onClick={(e) => { e.stopPropagation(); choose(); }}>
|
|
<ringGeometry args={[selected ? .7 : .55, selected ? 1 : .8, 24]} /><meshBasicMaterial color={color} depthTest={false} transparent opacity={.85} />
|
|
</mesh>
|
|
<mesh position={[0, .65, 0]} onClick={(e) => { e.stopPropagation(); choose(); }}><coneGeometry args={[.2, 1.1, 8]} /><meshBasicMaterial color={color} transparent opacity={.6} depthTest={false} /></mesh>
|
|
<mesh position={[0, .1, 1.15]}><boxGeometry args={[.13, .13, .9]} /><meshBasicMaterial color={color} depthTest={false} /></mesh>
|
|
{selected && <Html position={[0, 1.6, 0]} center style={{ pointerEvents: "none", color, fontSize: 12, whiteSpace: "nowrap", background: "#17171ddd", padding: "4px 8px", borderRadius: 5 }}>{label}</Html>}
|
|
</group>;
|
|
}
|
|
export function BiteSizedEditorMarkers() {
|
|
const layouts = useBiteSizedLayoutsStore();
|
|
const difficultyId = useGameStore((s) => s.activeDifficultyId);
|
|
const spot = layouts.draft.spots.find((s) => s.id === layouts.editingSpotId);
|
|
const preview = useMemo(() => {
|
|
if (!spot || layouts.editorMode !== "edit") return null;
|
|
try { return biteSizedDefinitionForSelection(selectBiteSizedRun(layouts.draft, spot.dungeonId, "gm-preview", { spotId: spot.id }), "gm-preview", difficultyId); }
|
|
catch { return null; }
|
|
}, [layouts.draft, layouts.editorMode, spot, difficultyId]);
|
|
if (!spot || layouts.editorMode !== "edit") return null;
|
|
const navigationUrl = preview && resolveDungeonAssets(preview).navigation;
|
|
const graph = navigationUrl && preview ? cachedPartyNavigationGraph(navigationUrl, preview.navigationLinks, preview.traversalRamps, true)
|
|
?? cachedPartyNavigationGraph(navigationUrl, preview.navigationLinks, preview.traversalRamps) : null;
|
|
return <group name="bite-sized-editor-markers">
|
|
{preview && <MobPopulation active={false} showLabels={false} entities={preview.entities} roamingPacks={[]}
|
|
staticSpawns={[...preview.staticSpawns, ...(preview.encounterSpawnGroups ?? []).flatMap((g) => g.spawns)]} />}
|
|
{derivePartySpawnSlots(spot.partySpawn).map((p, i) => <Marker key={`party-${i}`} id="party" position={i > 0 && graph ? findNearestPartyNavigationSurface(graph, p.footPosition, 6)?.position ?? p.footPosition : p.footPosition} yaw={p.yaw} color="#55e8ff" label={`Party start · seat ${i + 1}`} selected={layouts.editingPointId === "party" && i === 0} />)}
|
|
<Marker id="boss" position={spot.bossSpawn.footPosition} yaw={spot.bossSpawn.yaw} color="#ff5570" label="Random boss spawn" selected={layouts.editingPointId === "boss"} />
|
|
{spot.mobs.map((m, i) => <Marker key={m.id} id={m.id} position={m.position} yaw={m.yaw} color="#ffd255" label={`Mob point ${i + 1}`} selected={layouts.editingPointId === m.id} />)}
|
|
</group>;
|
|
}
|