Release Healer Man 0.1.14

This commit is contained in:
phenom committed 2026-09-05 11:40:50 -04:00
1 parent 67d7a8ebe7
commit 7b41b102fa
147 files changed
+169892 -1342

No files matched your search

+48 -15
View File
@@ -1,5 +1,5 @@
import { memo, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import { useFrame } from "@react-three/fiber";
import { useFrame, useThree } from "@react-three/fiber";
import {
AdditiveBlending,
BufferAttribute,
@@ -15,6 +15,7 @@ import {
Texture,
TextureLoader,
Vector2,
Vector3,
} from "three";
import {
combatOriginalPhaseAssets,
@@ -23,6 +24,7 @@ import {
type CombatPresentationEvent,
type CombatVfxQuality,
} from "../game/combatPresentation";
import { fetchCombatMediaAsset } from "../game/combatMediaPack";
import { abilityById } from "../game/abilityCatalog";
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "../game/combatStore";
import { getMobPosition } from "../game/mobRuntimeRegistry";
@@ -44,6 +46,7 @@ import {
parseOriginalCombatMeshGraph,
type OriginalCombatMeshGraph,
} from "./originalCombatMeshRuntime";
import { updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry";
type OriginalSourceEffectGraph = OriginalCombatEffectGraph | OriginalCombatMeshGraph;
@@ -201,7 +204,7 @@ async function loadGraph(url: string): Promise<OriginalSourceEffectGraph> {
// Graph filenames are keyed to the immutable source M2, while the decoded
// browser schema can improve between releases. Revalidate the small JSON so
// an older cached decoder payload cannot fail the current strict parser.
const request = fetch(url, { cache: "no-cache" })
const request = fetchCombatMediaAsset(url)
.then((response) => response.ok
? response.json()
: Promise.reject(new Error(`Original effect graph request failed (${response.status}): ${url}`)))
@@ -218,11 +221,18 @@ async function loadGraph(url: string): Promise<OriginalSourceEffectGraph> {
async function loadTexture(url: string): Promise<Texture> {
const existing = textureRequestCache.get(url);
if (existing) return existing;
const request = textureLoader.loadAsync(url).then((texture) => {
texture.colorSpace = SRGBColorSpace;
texture.name = `original-combat-effect:${url}`;
texture.needsUpdate = true;
return texture;
const request = fetchCombatMediaAsset(url).then(async (response) => {
if (!response.ok) throw new Error(`Original effect texture request failed (${response.status}): ${url}`);
const objectUrl = URL.createObjectURL(await response.blob());
try {
const texture = await textureLoader.loadAsync(objectUrl);
texture.colorSpace = SRGBColorSpace;
texture.name = `original-combat-effect:${url}`;
texture.needsUpdate = true;
return texture;
} finally {
URL.revokeObjectURL(objectUrl);
}
});
textureRequestCache.set(url, request);
request.catch(() => textureRequestCache.delete(url));
@@ -446,7 +456,8 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
readonly active: boolean;
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
}) {
const billboardRef = useRef<Group>(null);
const camera = useThree((state) => state.camera);
const sourcePlaneRef = useRef<Group>(null);
const geometry = useMemo(() => {
const next = new BufferGeometry();
const sourcePositions = graph.geometry.positions;
@@ -457,7 +468,7 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
const positions = new Float32Array(sourcePositions.length);
const normals = new Float32Array(sourceNormals.length);
for (let index = 0; index < sourcePositions.length; index += 3) {
// Turn WoW's Z-up source plane into an upright, camera-facing local XY plane.
// Turn WoW's Z-up source plane into an upright local XY plane.
positions[index] = -sourcePositions[index + 1];
positions[index + 1] = sourcePositions[index + 2];
positions[index + 2] = sourcePositions[index] - xCenter;
@@ -501,9 +512,14 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
for (const material of materials) material.dispose();
}, [geometry, materials]);
useFrame(({ camera }) => {
useEffect(() => {
// WoW authored this mesh as a billboard. Resolve that orientation once at
// spawn so it stays readable without rotating whenever the camera moves.
sourcePlaneRef.current?.lookAt(camera.position);
}, [camera]);
useFrame(() => {
if (!active) return;
if (billboardRef.current) billboardRef.current.lookAt(camera.position);
const playback = originalCombatMeshPlaybackFrame(
graph,
playbackRef.current.elapsedMs,
@@ -517,8 +533,22 @@ const OriginalStaticMesh = memo(function OriginalStaticMesh({
});
return (
<group ref={billboardRef}>
<mesh geometry={geometry} material={materials} renderOrder={20} frustumCulled={false} />
<group ref={sourcePlaneRef}>
<mesh
name="original-combat-static-source-plane"
geometry={geometry}
material={materials}
renderOrder={20}
frustumCulled={false}
/>
<mesh
name="original-combat-static-cross-plane"
geometry={geometry}
material={materials}
rotation={[0, Math.PI / 2, 0]}
renderOrder={20}
frustumCulled={false}
/>
</group>
);
});
@@ -533,6 +563,7 @@ function LoadedOriginalCombatEffect({
const elapsedRef = useRef(0);
const doneRef = useRef(false);
const groupRef = useRef<Group>(null);
const liveAnchorPosition = useMemo(() => new Vector3(), []);
const auraDecayStartedAtRef = useRef<number | null>(null);
const staticMeshPlaybackRef = useRef({ elapsedMs: 0, decaying: false });
const particleBudgets = useMemo(() => {
@@ -617,8 +648,10 @@ function LoadedOriginalCombatEffect({
);
} else {
const actorId = originalCombatAnchorActorId(event);
const live = liveActorPosition(actorId);
if (live) groupRef.current.position.set(live[0], live[1], live[2]);
if (!updateCombatEffectAnchorPosition(actorId, groupRef.current, liveAnchorPosition)) {
const live = liveActorPosition(actorId);
if (live) groupRef.current.position.set(live[0], live[1], live[2]);
}
}
}
const terminalDuration = projectile