Release Healer Man 0.1.12
This commit is contained in:
1 parent
13970d03c8
commit
65e24f644e
201 files changed
+5061
-123
No files matched your search
@@ -0,0 +1,287 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ORIGINAL_PARTICLE_LIMIT_PER_EMITTER,
|
||||
interpolateOriginalLifetime,
|
||||
interpolateOriginalScalar,
|
||||
isOriginalCombatEffectGraph,
|
||||
originalAuraMatchesPresentationEvent,
|
||||
originalEmitterParticleBudget,
|
||||
parseOriginalCombatEffectGraph,
|
||||
selectCombatEffectRender,
|
||||
} from "./originalCombatEffectRuntime";
|
||||
|
||||
const shieldPresentationEvent = {
|
||||
id: 1,
|
||||
sessionId: 1,
|
||||
occurredAt: 0,
|
||||
phase: "aura-start" as const,
|
||||
actorGroup: "player" as const,
|
||||
sourceActorId: "player",
|
||||
targetActorId: "party-tank",
|
||||
abilityId: "wow335-priest-power-word-shield",
|
||||
sourceSpellId: 17,
|
||||
name: "Power Word: Shield",
|
||||
source: "wow335" as const,
|
||||
school: "holy" as const,
|
||||
delivery: "aura" as const,
|
||||
origin: [0, 0, 0] as const,
|
||||
};
|
||||
|
||||
const zeroTimeSamples = [{ timeMs: 0, value: 0 }];
|
||||
const particleSamples = {
|
||||
emissionSpeed: zeroTimeSamples,
|
||||
speedVariation: zeroTimeSamples,
|
||||
verticalRange: zeroTimeSamples,
|
||||
horizontalRange: zeroTimeSamples,
|
||||
gravity: zeroTimeSamples,
|
||||
lifespan: [{ timeMs: 0, value: 1 }],
|
||||
emissionRate: [{ timeMs: 0, value: 0 }, { timeMs: 500, value: 150 }],
|
||||
emissionAreaLength: zeroTimeSamples,
|
||||
emissionAreaWidth: zeroTimeSamples,
|
||||
zSource: zeroTimeSamples,
|
||||
color: [
|
||||
{ timestamp: 0, normalizedTime: 0, value: [1, 0, 0] },
|
||||
{ timestamp: 32_767, normalizedTime: 1, value: [0, 0, 1] },
|
||||
],
|
||||
alpha: [
|
||||
{ timestamp: 0, normalizedTime: 0, value: 32_767 },
|
||||
{ timestamp: 32_767, normalizedTime: 1, value: 0 },
|
||||
],
|
||||
scale: [
|
||||
{ timestamp: 0, normalizedTime: 0, value: [1, 1] },
|
||||
{ timestamp: 32_767, normalizedTime: 1, value: [2, 2] },
|
||||
],
|
||||
headCell: [{ timestamp: 0, normalizedTime: 0, value: 3 }],
|
||||
tailCell: [],
|
||||
enabled: [{ timeMs: 0, value: 1 }],
|
||||
};
|
||||
|
||||
const trackMetadata = {
|
||||
emissionSpeed: 0,
|
||||
speedVariation: 0,
|
||||
verticalRange: 0,
|
||||
horizontalRange: 0,
|
||||
gravity: 0,
|
||||
lifespan: 0,
|
||||
emissionRate: 1,
|
||||
emissionAreaLength: 0,
|
||||
emissionAreaWidth: 0,
|
||||
zSource: 0,
|
||||
enabled: 0,
|
||||
};
|
||||
|
||||
function graphFixture(): Record<string, unknown> {
|
||||
const particle = {
|
||||
id: 7,
|
||||
flags: 0,
|
||||
position: [0, 1, 2],
|
||||
parentBone: 0,
|
||||
textureIndex: 0,
|
||||
geometryModel: "",
|
||||
recursionModel: "",
|
||||
blendMode: 4,
|
||||
emitterType: 1,
|
||||
particleColorIndex: 0,
|
||||
particleType: 0,
|
||||
headOrTail: 0,
|
||||
textureTileRotation: 2,
|
||||
textureRows: 4,
|
||||
textureColumns: 8,
|
||||
lifespanVariation: 0,
|
||||
emissionRateVariation: 0,
|
||||
scaleVariation: [0, 0],
|
||||
tailLength: 0,
|
||||
interpolation: trackMetadata,
|
||||
globalSequence: Object.fromEntries(Object.keys(trackMetadata).map((key) => [key, -1])),
|
||||
samples: particleSamples,
|
||||
};
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
kind: "wow-m2-particle-effect",
|
||||
source: "wow335",
|
||||
sourceModel: "Spells/HolySmiteImpact.mdx",
|
||||
normalizedSourceModel: "Spells/HolySmiteImpact.m2",
|
||||
sourceSha256: "a".repeat(64),
|
||||
durationMs: 1_000,
|
||||
animations: [{ index: 0, id: 0, variationIndex: 0, durationMs: 1_000, moveSpeed: 0, flags: 0 }],
|
||||
textures: [
|
||||
{
|
||||
type: 0,
|
||||
flags: 0,
|
||||
filename: "Spells\\HolySmite.blp",
|
||||
sourcePath: "Spells/HolySmite.BLP",
|
||||
sourceSha256: "b".repeat(64),
|
||||
url: "/assets/game/combat-effects/original/wow335/priest/textures/holy-smite.png",
|
||||
},
|
||||
{
|
||||
type: 0,
|
||||
flags: 0,
|
||||
filename: "Spells\\GLOWSTAR.blp",
|
||||
sourcePath: "Spells/GLOWSTAR.BLP",
|
||||
},
|
||||
],
|
||||
particles: [particle, { ...particle, id: 8, textureIndex: 1 }],
|
||||
runtime: {
|
||||
renderer: "wow-m2-particles-v1",
|
||||
readyParticleIndices: [0],
|
||||
unsupportedRibbonCount: 1,
|
||||
unsupportedGeometryVertexCount: 12,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cloneFixture(): Record<string, unknown> {
|
||||
return JSON.parse(JSON.stringify(graphFixture())) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("original combat-effect graph contract", () => {
|
||||
it("keeps an original aura visual attached to its active ranked WoW aura", () => {
|
||||
const activeShield = {
|
||||
targetId: "party-tank",
|
||||
definition: {
|
||||
id: "wow335:17:effect:0:aura:69",
|
||||
name: "Power Word: Shield (DBC 17, Aura 69)",
|
||||
},
|
||||
};
|
||||
expect(originalAuraMatchesPresentationEvent(activeShield, shieldPresentationEvent)).toBe(true);
|
||||
expect(originalAuraMatchesPresentationEvent(
|
||||
{ ...activeShield, targetId: "other-party-member" },
|
||||
shieldPresentationEvent,
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
it("parses the shipped Holy Smite graph without contract drift", () => {
|
||||
const graphUrl = new URL(
|
||||
"../../public/assets/game/combat-effects/original/wow335/priest/graphs/holysmite-low-chest-5e638a90a1be.json",
|
||||
import.meta.url,
|
||||
);
|
||||
const graph = parseOriginalCombatEffectGraph(JSON.parse(readFileSync(graphUrl, "utf8")));
|
||||
|
||||
expect(graph.normalizedSourceModel).toMatch(/\.m2$/i);
|
||||
expect(graph.runtime.renderer).toBe("wow-m2-particles-v1");
|
||||
expect(graph.runtime.readyParticleIndices.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("parses the shipped Lesser Heal cast and impact graphs", () => {
|
||||
const graphNames = [
|
||||
"holy-precast-med-hand-3001a24bb669.json",
|
||||
"lesserheal-base-a2863103c7d8.json",
|
||||
];
|
||||
const graphs = graphNames.map((name) => parseOriginalCombatEffectGraph(JSON.parse(readFileSync(
|
||||
new URL(`../../public/assets/game/combat-effects/original/wow335/priest/graphs/${name}`, import.meta.url),
|
||||
"utf8",
|
||||
))));
|
||||
|
||||
expect(graphs.map((graph) => graph.runtime.readyParticleIndices.length)).toEqual([1, 5]);
|
||||
expect(graphs.flatMap((graph) => graph.textures).filter((texture) => texture.url).length)
|
||||
.toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("strictly validates the packaged decoder output and derives renderer conveniences", () => {
|
||||
const graph = parseOriginalCombatEffectGraph(graphFixture());
|
||||
const particle = graph.particles[0];
|
||||
|
||||
expect(graph).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
kind: "wow-m2-particle-effect",
|
||||
source: "wow335",
|
||||
normalizedSourceModel: "Spells/HolySmiteImpact.m2",
|
||||
runtime: {
|
||||
renderer: "wow-m2-particles-v1",
|
||||
readyParticleIndices: [0],
|
||||
},
|
||||
});
|
||||
expect(particle).toMatchObject({
|
||||
textureIndex: 0,
|
||||
blend: "add",
|
||||
emitter: "plane",
|
||||
atlas: { rows: 4, columns: 8, rotation: 2 },
|
||||
});
|
||||
expect(particle.samples.emissionRate).toEqual([
|
||||
{ timeMs: 0, value: 0 },
|
||||
{ timeMs: 500, value: 150 },
|
||||
]);
|
||||
expect(graph.textures[1]).toEqual({
|
||||
type: 0,
|
||||
flags: 0,
|
||||
filename: "Spells\\GLOWSTAR.blp",
|
||||
sourcePath: "Spells/GLOWSTAR.BLP",
|
||||
});
|
||||
expect(isOriginalCombatEffectGraph(graphFixture())).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects wrong schemas, unknown fields, invalid references, and malformed samples", () => {
|
||||
expect(() => parseOriginalCombatEffectGraph({ ...graphFixture(), source: "runewaker" }))
|
||||
.toThrow(/source/);
|
||||
expect(() => parseOriginalCombatEffectGraph({ ...graphFixture(), fallback: "generated" }))
|
||||
.toThrow(/not part of schema/);
|
||||
|
||||
const badReference = cloneFixture();
|
||||
(badReference.particles as Array<Record<string, unknown>>)[0].textureIndex = 2;
|
||||
expect(() => parseOriginalCombatEffectGraph(badReference)).toThrow(/matching texture/);
|
||||
|
||||
const badSample = cloneFixture();
|
||||
const samples = (badSample.particles as Array<Record<string, unknown>>)[0].samples as Record<string, unknown>;
|
||||
(samples.emissionRate as Array<Record<string, unknown>>)[1].value = "150";
|
||||
expect(() => parseOriginalCombatEffectGraph(badSample)).toThrow(/emissionRate\[1\]\.value/);
|
||||
expect(isOriginalCombatEffectGraph(badSample)).toBe(false);
|
||||
|
||||
const badRuntime = cloneFixture();
|
||||
(badRuntime.runtime as Record<string, unknown>).readyParticleIndices = [1];
|
||||
expect(() => parseOriginalCombatEffectGraph(badRuntime)).toThrow(/unpackaged texture/);
|
||||
});
|
||||
|
||||
it("linearly samples animated scalars and scalar or vector lifetime keys", () => {
|
||||
expect(interpolateOriginalScalar([
|
||||
{ timeMs: 100, value: 4 },
|
||||
{ timeMs: 300, value: 12 },
|
||||
], 200)).toBe(8);
|
||||
expect(interpolateOriginalScalar([{ timeMs: 100, value: 4 }], 1_000)).toBe(4);
|
||||
expect(interpolateOriginalScalar([], 100)).toBeNull();
|
||||
expect(interpolateOriginalScalar([
|
||||
{ timeMs: 0, value: 0 },
|
||||
{ timeMs: 500, value: 10 },
|
||||
], 250, 0)).toBe(0);
|
||||
|
||||
expect(interpolateOriginalLifetime([
|
||||
{ normalizedTime: 0, value: 1 },
|
||||
{ normalizedTime: 1, value: 3 },
|
||||
], 0.25)).toBe(1.5);
|
||||
expect(interpolateOriginalLifetime([
|
||||
{ normalizedTime: 0, value: [1, 2, 3] as const },
|
||||
{ normalizedTime: 1, value: [3, 4, 5] as const },
|
||||
], 0.5)).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
it("uses the peak animated emission rate for deterministic per-emitter budgets", () => {
|
||||
const particle = parseOriginalCombatEffectGraph(graphFixture()).particles[0];
|
||||
|
||||
expect(particle.samples.emissionRate[0].value).toBe(0);
|
||||
expect(originalEmitterParticleBudget(particle, "low"))
|
||||
.toBe(ORIGINAL_PARTICLE_LIMIT_PER_EMITTER.low);
|
||||
expect(originalEmitterParticleBudget(particle, "high"))
|
||||
.toBe(ORIGINAL_PARTICLE_LIMIT_PER_EMITTER.high);
|
||||
expect(originalEmitterParticleBudget(particle, "off")).toBe(0);
|
||||
expect(originalEmitterParticleBudget(particle, "high"))
|
||||
.toBe(originalEmitterParticleBudget(particle, "high"));
|
||||
|
||||
const inactive = {
|
||||
samples: { ...particle.samples, emissionRate: [{ timeMs: 0, value: 0 }] },
|
||||
};
|
||||
expect(originalEmitterParticleBudget(inactive, "high")).toBe(0);
|
||||
});
|
||||
|
||||
it("never substitutes generated rendering when Original has no graph asset", () => {
|
||||
expect(selectCombatEffectRender("generated", undefined)).toEqual({ kind: "generated" });
|
||||
expect(selectCombatEffectRender("original", [
|
||||
{},
|
||||
{ effectGraphUrl: "/assets/game/combat-effects/original/wow335/priest/graphs/smite.json" },
|
||||
])).toEqual({
|
||||
kind: "original",
|
||||
effectGraphUrl: "/assets/game/combat-effects/original/wow335/priest/graphs/smite.json",
|
||||
});
|
||||
expect(selectCombatEffectRender("original", undefined)).toBeNull();
|
||||
expect(selectCombatEffectRender("original", [{ effectGraphUrl: " " }])).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user