Release Healer Man 0.1.30

This commit is contained in:
phenom committed 2026-09-11 18:49:33 -04:00
1 parent 544c1bee91
commit 7d7fd567f0
845 files changed
+2040 -87

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
// Development-only inspection surface. Loaded explicitly from scripts/combat-effects/preview.html.
import { useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls, Grid } from "@react-three/drei";
import { TextureLoader, SRGBColorSpace, type Texture } from "three";
import { OriginalWowModel } from "../scene/OriginalWowModel";
import { OriginalParticleEmitter } from "../scene/OriginalCombatEffect";
import { originalEmitterParticleBudget } from "../scene/originalCombatEffectRuntime";
import { parseOriginalWowModelGraph, wowModelTextureUrls, type OriginalWowModelGraph } from "../scene/originalWowModelRuntime";
interface ModelChoice { model: string; url: string }
function Effect({ graph, textures, sourceTextures, active, freeze, onFrozen }: { graph: OriginalWowModelGraph; textures: ReadonlyMap<number, Texture>; sourceTextures: ReadonlyMap<string, Texture>; active: boolean; freeze: boolean; onFrozen: (value: boolean) => void }) {
const playback = useRef({ elapsedMs: 0, decaying: false });
const [frozen, setFrozen] = useState(false);
useFrame((_, delta) => {
if (active && !frozen) playback.current.elapsedMs += Math.min(0.1, delta) * 1000;
if (freeze && !frozen && playback.current.elapsedMs >= 250) { setFrozen(true); onFrozen(true); }
});
const particleGraph = graph.particleGraph;
return <group position={[0, 0.15, 0]}>
<OriginalWowModel graph={graph} textures={textures} active={active && !frozen} playbackRef={playback} loop />
{particleGraph?.runtime.readyParticleIndices.map((index) => {
const particle = particleGraph.particles[index];
return <OriginalParticleEmitter key={index} graph={particleGraph} modelGraph={graph} particle={particle}
sourceTextures={sourceTextures}
texture={textures.get(particle.textureIndex)!} budget={Math.min(128, originalEmitterParticleBudget(particle, "high"))}
active={active && !frozen} eventId={1} emitterIndex={index} loopTimeline />;
})}
</group>;
}
function Preview() {
const [choices, setChoices] = useState<ModelChoice[]>([]);
const [selected, setSelected] = useState("");
const [ready, setReady] = useState<{ graph: OriginalWowModelGraph; textures: ReadonlyMap<number, Texture>; sourceTextures: ReadonlyMap<string, Texture> } | null>(null);
const [error, setError] = useState("");
const [active, setActive] = useState(true);
const [replay, setReplay] = useState(0);
const [freeze, setFreeze] = useState(false);
const [frozen, setFrozen] = useState(false);
const loader = useMemo(() => new TextureLoader(), []);
useEffect(() => {
void fetch("/assets/game/combat-effects/manifest.json").then((r) => r.json()).then((manifest) => {
const entries = manifest.entries as { source: string; phases: Record<string, { model: string; effectGraphUrl: string }[]> }[];
const models = new Map<string, string>();
entries.filter((e) => e.source === "wow335").forEach((e) => Object.values(e.phases ?? {}).flat().forEach((a) => {
if (a.effectGraphUrl) models.set(a.model.toLowerCase().replace(/\.mdx$/, ".m2"), a.effectGraphUrl);
}));
const list = [...models].sort(([a], [b]) => a.localeCompare(b)).map(([model, url]) => ({ model, url }));
setChoices(list); setSelected(list.find((c) => c.model.includes("fireball_missile"))?.url ?? list[0].url);
}).catch((e) => setError(String(e)));
}, []);
useEffect(() => {
if (!selected) return;
let cancelled = false;
const loaded: Texture[] = [];
setReady(null); setError(""); setFrozen(false);
void fetch(selected).then((r) => r.json()).then(async (value) => {
const graph = parseOriginalWowModelGraph(value);
const textures = new Map<number, Texture>();
const sourceTextures = new Map<string, Texture>();
await Promise.all(wowModelTextureUrls(graph).map(async (url) => {
const texture = await loader.loadAsync(url); loaded.push(texture);
texture.colorSpace = SRGBColorSpace; sourceTextures.set(url, texture);
}));
graph.textures.forEach((source, index) => { if (source.url) textures.set(index, sourceTextures.get(source.url)!); });
if (!cancelled) setReady({ graph, textures, sourceTextures });
else loaded.forEach((texture) => texture.dispose());
}).catch((e) => { if (!cancelled) setError(String(e)); });
return () => { cancelled = true; loaded.forEach((texture) => texture.dispose()); };
}, [selected, loader]);
return <>
<div style={{ padding: "14px 20px", display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
<strong>Original WoW effects · {choices.length} models</strong>
<label>Model <select aria-label="Model" value={selected} onChange={(e) => setSelected(e.target.value)}
style={{ maxWidth: "55vw", padding: 8 }}>{choices.map((c) => <option key={c.url} value={c.url}>{c.model}</option>)}</select></label>
<button onClick={() => setActive(!active)}>{active ? "Pause" : "Play"}</button>
<button onClick={() => { setReplay(replay + 1); setFrozen(false); }}>Replay</button>
<label><input type="checkbox" checked={freeze} onChange={(e) => { setFreeze(e.target.checked); setReplay(replay + 1); setFrozen(false); }} />Freeze at 250 ms</label>
</div>
<div role="status" style={{ padding: "0 20px 8px" }}>{error || (ready
? `Ready · ${ready.graph.mesh?.layers.length ?? 0} mesh layers · ${ready.graph.particleGraph?.runtime.readyParticleIndices.length ?? 0} particle emitters · ${ready.graph.ribbons.length} ribbons${frozen ? " · Frozen at 250 ms" : ""}`
: "Loading original media…")}</div>
<div style={{ height: "calc(100vh - 100px)" }}>
<Canvas camera={{ position: [6, 4, 8], fov: 45 }}>
<color attach="background" args={["#0b121b"]} />
<Grid args={[30, 30]} cellColor="#304051" sectionColor="#4a637d" fadeDistance={35} />
<mesh position={[-2, 0.9, 0]}><capsuleGeometry args={[0.25, 1.25, 6, 12]} /><meshBasicMaterial color="#35465c" /></mesh>
{ready && <Effect key={`${selected}:${replay}`} {...ready} active={active} freeze={freeze} onFrozen={setFrozen} />}
<OrbitControls target={[0, 1, 0]} />
</Canvas>
</div>
</>;
}
if (import.meta.env.DEV) {
const root = createRoot(document.getElementById("root")!);
root.render(<Preview />);
if (import.meta.hot) import.meta.hot.dispose(() => root.unmount());
}