Release Healer Man 0.1.13

This commit is contained in:
phenom committed 2026-09-01 16:31:26 -04:00
1 parent 65e24f644e
commit 67d7a8ebe7
324 files changed
+917 -48

No files matched your search

+91 -16
View File
@@ -1,4 +1,4 @@
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { memo, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import { useFrame } from "@react-three/fiber";
import {
AdditiveBlending,
@@ -30,6 +30,7 @@ import { getPartyRuntimePosition } from "../game/partyRuntimeRegistry";
import {
interpolateOriginalLifetime,
interpolateOriginalScalar,
originalCombatAnchorActorId,
originalAuraMatchesPresentationEvent,
originalEmitterParticleBudget,
parseOriginalCombatEffectGraph,
@@ -38,6 +39,8 @@ import {
type OriginalCombatEffectParticle,
} from "./originalCombatEffectRuntime";
import {
originalCombatMeshLayerAppearance,
originalCombatMeshPlaybackFrame,
parseOriginalCombatMeshGraph,
type OriginalCombatMeshGraph,
} from "./originalCombatMeshRuntime";
@@ -180,6 +183,18 @@ function effectScale(asset: CombatEffectPhaseAsset): number {
: 1;
}
function effectPlacementHeight(
asset: CombatEffectPhaseAsset,
graph: OriginalSourceEffectGraph,
event: CombatPresentationEvent,
): number {
const fallback = placementHeight(asset.model, event);
if (graph.kind !== "wow-m2-static-mesh") return fallback;
// The converted mesh remains in WoW's source scale. Lift its lowest Z-up
// vertex to the actor's feet after applying the packaged presentation scale.
return Math.max(fallback, -graph.bounds.minimum[2] * effectScale(asset));
}
async function loadGraph(url: string): Promise<OriginalSourceEffectGraph> {
const existing = graphRequestCache.get(url);
if (existing) return existing;
@@ -424,10 +439,12 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
graph,
textures,
active,
playbackRef,
}: {
readonly graph: OriginalCombatMeshGraph;
readonly textures: ReadonlyMap<number, Texture>;
readonly active: boolean;
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
}) {
const billboardRef = useRef<Group>(null);
const geometry = useMemo(() => {
@@ -459,16 +476,25 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
next.computeBoundingSphere();
return next;
}, [graph]);
const materials = useMemo(() => graph.layers.map((layer) => new MeshBasicMaterial({
name: `wow-m2-static-layer-${layer.submeshIndex}`,
map: textures.get(layer.textureIndex) ?? null,
blending: meshBlendMode(layer.blendMode),
transparent: layer.blendMode !== 0,
alphaTest: layer.blendMode === 1 ? 0.5 : 0,
depthWrite: layer.blendMode === 0,
side: DoubleSide,
toneMapped: false,
})), [graph.layers, textures]);
const materials = useMemo(() => {
const initialFrame = originalCombatMeshPlaybackFrame(graph, 0);
return graph.layers.map((layer) => {
const appearance = originalCombatMeshLayerAppearance(graph, layer, initialFrame);
const material = new MeshBasicMaterial({
name: `wow-m2-static-layer-${layer.submeshIndex}`,
map: textures.get(layer.textureIndex) ?? null,
blending: meshBlendMode(layer.blendMode),
transparent: layer.blendMode !== 0,
opacity: appearance.opacity,
alphaTest: layer.blendMode === 1 ? 0.5 : 0,
depthWrite: layer.blendMode === 0,
side: DoubleSide,
toneMapped: false,
});
material.color.setRGB(...appearance.color);
return material;
});
}, [graph, textures]);
useEffect(() => () => {
geometry.dispose();
@@ -476,7 +502,18 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
}, [geometry, materials]);
useFrame(({ camera }) => {
if (active && billboardRef.current) billboardRef.current.lookAt(camera.position);
if (!active) return;
if (billboardRef.current) billboardRef.current.lookAt(camera.position);
const playback = originalCombatMeshPlaybackFrame(
graph,
playbackRef.current.elapsedMs,
playbackRef.current.decaying,
);
for (const [index, layer] of graph.layers.entries()) {
const appearance = originalCombatMeshLayerAppearance(graph, layer, playback);
materials[index].color.setRGB(...appearance.color);
materials[index].opacity = appearance.opacity;
}
});
return (
@@ -496,6 +533,8 @@ function LoadedOriginalCombatEffect({
const elapsedRef = useRef(0);
const doneRef = useRef(false);
const groupRef = useRef<Group>(null);
const auraDecayStartedAtRef = useRef<number | null>(null);
const staticMeshPlaybackRef = useRef({ elapsedMs: 0, decaying: false });
const particleBudgets = useMemo(() => {
const emitters = loaded.flatMap((item, loadedIndex) => {
const graph = item.graph;
@@ -532,6 +571,14 @@ function LoadedOriginalCombatEffect({
if (event.phase === "cast-start") return Math.max(120, event.durationMs ?? longestGraph);
return Math.max(120, Math.min(6_000, longestGraph));
}, [event.durationMs, event.phase, loaded]);
const staticMeshDecayDurationMs = useMemo(() => loaded.reduce((maximum, item) => (
item.graph.kind === "wow-m2-static-mesh"
? Math.max(
maximum,
item.graph.animations.find((animation) => animation.id === 159)?.durationMs ?? 0,
)
: maximum
), 0), [loaded]);
const projectile = event.phase === "release"
&& event.delivery === "projectile"
&& event.targetPosition !== undefined;
@@ -550,6 +597,16 @@ function LoadedOriginalCombatEffect({
useFrame((_state, delta) => {
if (!active || doneRef.current) return;
elapsedRef.current += Math.min(100, Math.max(0, delta * 1_000));
if (auraDecayStartedAtRef.current === null) {
staticMeshPlaybackRef.current.elapsedMs = elapsedRef.current;
staticMeshPlaybackRef.current.decaying = false;
} else {
staticMeshPlaybackRef.current.elapsedMs = Math.max(
0,
elapsedRef.current - auraDecayStartedAtRef.current,
);
staticMeshPlaybackRef.current.decaying = true;
}
if (groupRef.current) {
if (projectile && event.targetPosition) {
const progress = Math.min(1, elapsedRef.current / projectileDurationMs);
@@ -559,7 +616,7 @@ function LoadedOriginalCombatEffect({
event.origin[2] + (event.targetPosition[2] - event.origin[2]) * progress,
);
} else {
const actorId = event.phase === "cast-start" ? event.sourceActorId : event.targetActorId;
const actorId = originalCombatAnchorActorId(event);
const live = liveActorPosition(actorId);
if (live) groupRef.current.position.set(live[0], live[1], live[2]);
}
@@ -569,9 +626,21 @@ function LoadedOriginalCombatEffect({
: event.phase === "aura-start"
? auraDurationMs(event)
: durationMs;
if (auraDecayStartedAtRef.current !== null) {
if (staticMeshPlaybackRef.current.elapsedMs < staticMeshDecayDurationMs) return;
doneRef.current = true;
onDone();
return;
}
if (event.phase === "aura-start"
&& elapsedRef.current > 350
&& !auraStillActive(event)) {
if (staticMeshDecayDurationMs > 0) {
auraDecayStartedAtRef.current = elapsedRef.current;
staticMeshPlaybackRef.current.elapsedMs = 0;
staticMeshPlaybackRef.current.decaying = true;
return;
}
doneRef.current = true;
onDone();
return;
@@ -585,14 +654,20 @@ function LoadedOriginalCombatEffect({
<group ref={groupRef} position={anchor}>
{loaded.map((item, loadedIndex) => {
const graph = item.graph;
const scale = effectScale(item.asset);
return (
<group
key={`${item.asset.effectGraphUrl}:${item.asset.model ?? "model"}`}
position={[0, placementHeight(item.asset.model, event), 0]}
scale={effectScale(item.asset)}
position={[0, effectPlacementHeight(item.asset, graph, event), 0]}
scale={scale}
>
{graph.kind === "wow-m2-static-mesh" ? (
<OriginalStaticMesh graph={graph} textures={item.textures} active={active} />
<OriginalStaticMesh
graph={graph}
textures={item.textures}
active={active}
playbackRef={staticMeshPlaybackRef}
/>
) : graph.runtime.readyParticleIndices.map((particleIndex) => {
const particle = graph.particles[particleIndex];
const texture = item.textures.get(particle?.textureIndex ?? -1);