Update Healer Man server

This commit is contained in:
phenom committed 2026-09-27 14:31:15 -04:00
1 parent ab1434a3b8
commit e284f4fc76
60 files changed
+2704 -118

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import { useFrame } from "@react-three/fiber";
import { DoubleSide, Group, SRGBColorSpace, Texture, TextureLoader } from "three";
import { subscribeContentManager } from "../content/contentManager";
import { fetchCombatMediaAsset, combatMediaPackReady } from "../game/combatMediaPack";
import { preloadCombatEffectManifest, shadowfiendOriginalModelAsset } from "../game/combatPresentation";
import { type CombatSummon, useCombatStore } from "../game/combatStore";
import { getMobPosition } from "../game/mobRuntimeRegistry";
import { OriginalWowModel } from "./OriginalWowModel";
import { parseOriginalWowModelGraph, type OriginalWowModelGraph } from "./originalWowModelRuntime";
const SHADOWFIEND_CREATURE_ID = 19668;
interface ShadowfiendMedia {
readonly graph: OriginalWowModelGraph;
readonly textures: ReadonlyMap<number, Texture>;
}
function ShadowfiendFallback() {
return <group name="shadowfiend-fallback">
<mesh position={[0, 0.66, 0]} scale={[0.56, 0.77, 0.46]}>
<icosahedronGeometry args={[1, 1]} />
<meshStandardMaterial color="#241335" emissive="#492269" emissiveIntensity={0.7} roughness={0.85} />
</mesh>
{[-1, 1].map((side) => <group key={side} position={[side * 0.55, 0.9, 0]} rotation={[0, 0, side * -0.42]}>
<mesh position={[0, -0.37, 0]}><coneGeometry args={[0.18, 0.88, 5]} />
<meshStandardMaterial color="#2c1741" emissive="#371456" emissiveIntensity={0.5} />
</mesh>
<mesh position={[0, -0.83, 0.1]}><coneGeometry args={[0.12, 0.4, 5]} />
<meshStandardMaterial color="#a352dd" emissive="#8d35cc" emissiveIntensity={1.4} />
</mesh>
</group>)}
{[-1, 1].map((side) => <mesh key={side} position={[0.49, 0.87, side * 0.18]}>
<sphereGeometry args={[0.075, 8, 8]} />
<meshBasicMaterial color="#d68cff" />
</mesh>)}
<pointLight color="#7739b8" intensity={0.7} distance={2.5} />
</group>;
}
function ShadowfiendMember({ summon, active }: { readonly summon: CombatSummon; readonly active: boolean }) {
const [media, setMedia] = useState<ShadowfiendMedia | null>(null);
const mediaReady = useSyncExternalStore(subscribeContentManager, combatMediaPackReady, () => false);
const group = useRef<Group>(null);
const playback = useRef({ elapsedMs: 0, decaying: false });
useEffect(() => {
if (!mediaReady) {
setMedia(null);
return;
}
let cancelled = false;
const ownedTextures: Texture[] = [];
void (async () => {
await preloadCombatEffectManifest();
const url = shadowfiendOriginalModelAsset()?.effectGraphUrl;
if (!url) return;
const response = await fetchCombatMediaAsset(url);
if (!response.ok) throw new Error(`Shadowfiend model request failed: ${response.status}`);
const graph = parseOriginalWowModelGraph(await response.json());
const textures = new Map<number, Texture>();
const loader = new TextureLoader();
for (const [index, source] of graph.textures.entries()) {
if (!source.url) continue;
const image = await fetchCombatMediaAsset(source.url);
if (!image.ok) throw new Error(`Shadowfiend texture request failed: ${image.status}`);
const objectUrl = URL.createObjectURL(await image.blob());
try {
const texture = await loader.loadAsync(objectUrl);
texture.colorSpace = SRGBColorSpace;
ownedTextures.push(texture);
textures.set(index, texture);
} finally {
URL.revokeObjectURL(objectUrl);
}
}
if (cancelled) ownedTextures.forEach((texture) => texture.dispose());
else setMedia({ graph, textures });
})().catch(() => { if (cancelled) ownedTextures.forEach((texture) => texture.dispose()); });
return () => {
cancelled = true;
ownedTextures.forEach((texture) => texture.dispose());
};
}, [mediaReady]);
useFrame((_, delta) => {
if (!group.current) return;
const live = useCombatStore.getState().summons.find((candidate) => candidate.id === summon.id);
if (!live) return;
const goal = live.position;
const blend = Math.min(1, delta * 9);
group.current.position.x += (goal[0] - group.current.position.x) * blend;
group.current.position.y += (goal[1] + 0.13 - group.current.position.y) * blend;
group.current.position.z += (goal[2] - group.current.position.z) * blend;
const target = live.targetId ? getMobPosition(live.targetId) : null;
if (target) group.current.rotation.y = Math.atan2(goal[2] - target[2], target[0] - goal[0]);
if (active) playback.current.elapsedMs += Math.min(delta, 0.1) * 1_000;
});
return <group ref={group} name={`combat-summon:${summon.id}`}
position={[summon.position[0], summon.position[1] + 0.13, summon.position[2]]}>
<mesh name="shadowfiend-ground-aura" rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.1, 0]}>
<ringGeometry args={[0.38, 0.76, 28]} />
<meshBasicMaterial color="#8a46be" transparent opacity={0.38} depthWrite={false} side={DoubleSide} />
</mesh>
{media
? <group position={[0, 1.3, 0]} scale={0.65}><OriginalWowModel graph={media.graph} textures={media.textures}
active={active} playbackRef={playback} loop /></group>
: <ShadowfiendFallback />}
</group>;
}
/** Player and companion combat summons live outside the static dungeon population. */
export function CombatSummonPopulation({ active }: { readonly active: boolean }) {
const summons = useCombatStore((state) => state.summons);
return <group name="combat-summon-population">
{summons.filter((summon) => summon.creatureId === SHADOWFIEND_CREATURE_ID && summon.health > 0)
.map((summon) => <ShadowfiendMember key={summon.id} summon={summon} active={active} />)}
</group>;
}