forked from phenom/healer-man
Release Healer Man 0.1.13
This commit is contained in:
1 parent
65e24f644e
commit
67d7a8ebe7
324 files changed
+917
-48
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user