forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
a64b2cb575
commit
71246e5b75
502 files changed
+4245
-19
No files matched your search
@@ -0,0 +1,105 @@
|
||||
// Development-only visual QA surface. Loaded from scripts/combat-effects/rom-preview.html.
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { Grid, OrbitControls } from "@react-three/drei";
|
||||
import { SRGBColorSpace, TextureLoader, type Texture } from "three";
|
||||
import { RunewakerParticleEffect } from "../scene/RunewakerParticleEffect";
|
||||
import { parseRunewakerParticleGraph, runeLayers, type RunewakerParticleGraph } from "../scene/runewakerParticleRuntime";
|
||||
import type { CombatPresentationPhase } from "../game/combatPresentation";
|
||||
|
||||
interface Choice { readonly model: string; readonly graphUrl: string }
|
||||
interface Loaded { readonly graph: RunewakerParticleGraph; readonly textures: ReadonlyMap<string, Texture> }
|
||||
|
||||
function Effect({ loaded, phase, playing, replay, freezeAt }: {
|
||||
readonly loaded: Loaded; readonly phase: CombatPresentationPhase; readonly playing: boolean;
|
||||
readonly replay: number; readonly freezeAt: boolean;
|
||||
}) {
|
||||
const playback = useRef({ elapsedMs: 0, decaying: false });
|
||||
useEffect(() => { playback.current.elapsedMs = 0; }, [replay, loaded]);
|
||||
useFrame((_, delta) => {
|
||||
if (!playing) return;
|
||||
playback.current.elapsedMs += Math.min(0.1, delta) * 1_000;
|
||||
if (freezeAt && playback.current.elapsedMs >= 250) playback.current.elapsedMs = 250;
|
||||
else if (playback.current.elapsedMs > 2_000) playback.current.elapsedMs = 0;
|
||||
});
|
||||
return <group position={[0, 1.1, 0]}>
|
||||
<RunewakerParticleEffect graph={loaded.graph} textures={loaded.textures} phase={phase}
|
||||
quality="high" active eventId={1} playbackRef={playback} />
|
||||
</group>;
|
||||
}
|
||||
|
||||
function Preview() {
|
||||
const [choices, setChoices] = useState<readonly Choice[]>([]);
|
||||
const [selected, setSelected] = useState("");
|
||||
const [loaded, setLoaded] = useState<Loaded | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [phase, setPhase] = useState<CombatPresentationPhase>("impact");
|
||||
const [playing, setPlaying] = useState(true);
|
||||
const [freezeAt, setFreezeAt] = useState(false);
|
||||
const [replay, setReplay] = useState(0);
|
||||
const loader = useMemo(() => new TextureLoader(), []);
|
||||
useEffect(() => {
|
||||
void fetch("/assets/game/combat-effects/original/runewaker/manifest.json")
|
||||
.then((response) => response.json())
|
||||
.then((index: { results: { model: string; graphUrl: string; status: string; rendererReady?: boolean }[] }) => {
|
||||
const available = index.results.filter((entry) => entry.status === "decoded" || entry.rendererReady)
|
||||
.map(({ model, graphUrl }) => ({ model, graphUrl }));
|
||||
setChoices(available);
|
||||
setSelected(available.find((entry) => entry.model.includes("electric_ball_t_freeze_hit"))?.graphUrl
|
||||
?? available[0]?.graphUrl ?? "");
|
||||
}).catch((reason) => setError(String(reason)));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!selected) return;
|
||||
let cancelled = false;
|
||||
const ownedTextures: Texture[] = [];
|
||||
setLoaded(null); setError("");
|
||||
void fetch(selected).then((response) => response.json()).then(async (value) => {
|
||||
const graph = parseRunewakerParticleGraph(value);
|
||||
const textures = new Map<string, Texture>();
|
||||
await Promise.all(graph.textures.map(async (asset) => {
|
||||
const texture = await loader.loadAsync(asset.url);
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
ownedTextures.push(texture);
|
||||
textures.set(asset.url, texture);
|
||||
}));
|
||||
if (!cancelled) setLoaded({ graph, textures });
|
||||
}).catch((reason) => { if (!cancelled) setError(String(reason)); });
|
||||
return () => { cancelled = true; ownedTextures.forEach((texture) => texture.dispose()); };
|
||||
}, [selected, loader]);
|
||||
return <>
|
||||
<div style={{ padding: "14px 20px", display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<strong>RuneWaker particle preview · {choices.length} sprite models</strong>
|
||||
<label>Model <select aria-label="Model" value={selected} onChange={(event) => setSelected(event.target.value)}
|
||||
style={{ maxWidth: "55vw", padding: 8 }}>
|
||||
{choices.map((choice) => <option key={choice.graphUrl} value={choice.graphUrl}>{choice.model}</option>)}
|
||||
</select></label>
|
||||
<label>Phase <select aria-label="Phase" value={phase} onChange={(event) => setPhase(event.target.value as CombatPresentationPhase)}>
|
||||
<option value="cast-start">Cast start</option><option value="release">Release</option><option value="impact">Impact</option>
|
||||
</select></label>
|
||||
<button onClick={() => setPlaying(!playing)}>{playing ? "Pause" : "Play"}</button>
|
||||
<button onClick={() => setReplay(replay + 1)}>Replay</button>
|
||||
<label><input type="checkbox" checked={freezeAt} onChange={(event) => { setFreezeAt(event.target.checked); setReplay(replay + 1); }} />Freeze at 250 ms</label>
|
||||
</div>
|
||||
<div role="status" style={{ padding: "0 20px 8px" }}>{error || (loaded
|
||||
? `Ready · ${runeLayers(loaded.graph, phase).length} sprite layers · ${loaded.graph.textures.length} textures`
|
||||
: "Loading RuneWaker media…")}</div>
|
||||
<div style={{ height: "calc(100vh - 100px)" }}>
|
||||
<Canvas camera={{ position: [3, 2.5, 5], fov: 45 }}>
|
||||
<color attach="background" args={["#0b121b"]} />
|
||||
<Grid args={[20, 20]} cellColor="#304051" sectionColor="#4a637d" fadeDistance={25} />
|
||||
<mesh position={[-1.8, 0.9, 0]}><capsuleGeometry args={[0.25, 1.25, 6, 12]} /><meshBasicMaterial color="#35465c" /></mesh>
|
||||
{loaded && <Effect key={`${selected}:${replay}`} loaded={loaded} phase={phase} playing={playing}
|
||||
replay={replay} freezeAt={freezeAt} />}
|
||||
<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