Update Healer Man server

This commit is contained in:
phenom committed 2026-09-24 16:42:43 -04:00
1 parent a64b2cb575
commit 71246e5b75
502 files changed
+4245 -19

No files matched your search

+105
View File
@@ -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());
}