forked from phenom/healer-man
Release Healer Man 0.1.30
This commit is contained in:
1 parent
544c1bee91
commit
7d7fd567f0
845 files changed
+2040
-87
No files matched your search
@@ -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());
|
||||
}
|
||||
Reference in new issue
Block a user