258 lines
12 KiB
TypeScript
258 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { applyAura } from "./combatAuras";
|
|
import { applyEnemySourceAura } from "./enemySourceAuras";
|
|
import {
|
|
enemyTriggeredShoutContract,
|
|
enemyTriggeredShoutControlTag,
|
|
enemyTriggeredShoutMovementSpeedEffect,
|
|
enemyTriggeredShoutMovementSpeedMultiplier,
|
|
enemyTriggeredShoutMovementSpeedTag,
|
|
planEnemyTriggeredShoutTargets,
|
|
resolveEnemyTriggeredShoutControl,
|
|
} from "./enemyTriggeredShouts";
|
|
|
|
describe("pinned triggered enemy shout contracts", () => {
|
|
it("pins only parent spells with exact DBC child trigger IDs", () => {
|
|
for (const profile of ["ascension", "epoch"] as const) {
|
|
expect(enemyTriggeredShoutContract(5246, profile)).toMatchObject({
|
|
parentSpellId: 5246,
|
|
childSpellId: 20511,
|
|
triggerEffectIndex: 0,
|
|
parentSignature: {
|
|
spellMechanic: 5,
|
|
dispel: 0,
|
|
durationMs: profile === "ascension" ? 5_000 : 8_000,
|
|
attributes0: profile === "epoch" ? 327_696 : 0,
|
|
attributes1: profile === "epoch" ? 136 : 0,
|
|
attributes2: 0,
|
|
attributes3: 0,
|
|
attributes4: 0,
|
|
attributes5: 0,
|
|
attributes6: profile === "epoch" ? 8_388_608 : 33_554_432,
|
|
attributes7: 0,
|
|
damageClass: 1,
|
|
maxAffectedTargets: 5,
|
|
effects: [
|
|
{ effectType: 64, implicitTargetA: 6, triggerSpellId: 20511 },
|
|
{ effectType: 6, auraType: 7, implicitTargetA: 22, implicitTargetB: 15, radiusIndex: 14 },
|
|
{ effectType: 6, auraType: 31, implicitTargetA: 22, implicitTargetB: 15, radiusIndex: 14 },
|
|
],
|
|
},
|
|
childSignature: {
|
|
spellMechanic: 5,
|
|
dispel: 0,
|
|
stackAmount: 0,
|
|
damageClass: profile === "ascension" ? 1 : 0,
|
|
attributes0: 0,
|
|
attributes1: 0,
|
|
attributes2: 0,
|
|
attributes3: profile === "ascension" ? 589_824 : 0,
|
|
attributes4: profile === "ascension" ? 8_388_608 : 0,
|
|
attributes5: 0,
|
|
attributes6: 0,
|
|
attributes7: 0,
|
|
effects: profile === "ascension"
|
|
? [{ effectType: 6, auraType: 12, implicitTargetA: 6 }, { effectType: 6, auraType: 87, basePoints: 15, miscValue: 1 }]
|
|
: [{ effectType: 6, auraType: 12, implicitTargetA: 6 }],
|
|
},
|
|
childEffects: profile === "ascension"
|
|
? [{ kind: "control", mechanic: "stun", durationMs: 5_000 }, {
|
|
kind: "damage-taken", damageSchool: "physical", operation: "percent", value: 0.15, durationMs: 5_000,
|
|
}]
|
|
: [{ kind: "control", mechanic: "stun", durationMs: 8_000 }],
|
|
});
|
|
expect(enemyTriggeredShoutContract(19134, profile)).toMatchObject({
|
|
parentSpellId: 19134,
|
|
childSpellId: 29544,
|
|
triggerEffectIndex: 0,
|
|
parentSignature: {
|
|
spellMechanic: 5,
|
|
dispel: 0,
|
|
durationMs: profile === "ascension" ? 4_000 : 6_000,
|
|
attributes0: 327_696,
|
|
attributes1: 0,
|
|
attributes2: 0,
|
|
attributes3: 0,
|
|
attributes4: 0,
|
|
attributes5: 0,
|
|
attributes6: 0,
|
|
attributes7: 0,
|
|
damageClass: 1,
|
|
maxAffectedTargets: 0,
|
|
effects: [
|
|
{ effectType: 64, implicitTargetA: 6, triggerSpellId: 29544 },
|
|
{ effectType: 6, auraType: 7, implicitTargetA: 22, implicitTargetB: 15, radiusIndex: 14 },
|
|
{ effectType: 6, auraType: 31, implicitTargetA: 22, implicitTargetB: 15, radiusIndex: 14 },
|
|
],
|
|
},
|
|
childSignature: {
|
|
spellMechanic: 5,
|
|
dispel: 0,
|
|
stackAmount: 0,
|
|
damageClass: 0,
|
|
durationMs: profile === "ascension" ? 3_000 : 6_000,
|
|
attributes0: 0,
|
|
attributes1: 0,
|
|
attributes2: 0,
|
|
attributes3: 0,
|
|
attributes4: 0,
|
|
attributes5: 0,
|
|
attributes6: 0,
|
|
attributes7: 0,
|
|
},
|
|
childEffects: [{ kind: "control", mechanic: "fear", durationMs: profile === "ascension" ? 3_000 : 6_000 }],
|
|
});
|
|
}
|
|
expect(enemyTriggeredShoutContract(9128, "ascension")).toBeUndefined();
|
|
expect(enemyTriggeredShoutContract(16244, "epoch")).toBeUndefined();
|
|
expect(enemyTriggeredShoutContract(13730, "ascension")).toBeUndefined();
|
|
});
|
|
|
|
it("pins spell-mechanic immunity and the absence of DBC always-hit or immunity bypass flags", () => {
|
|
const bypassFlags = {
|
|
attributes0: 0x20000000,
|
|
attributes2: 0x04000000,
|
|
attributes3: 0x00040000,
|
|
attributes4: 0x00000800,
|
|
attributes7: 0x02000000,
|
|
} as const;
|
|
for (const profile of ["ascension", "epoch"] as const) {
|
|
const spells = [5246, 19134].flatMap((parentSpellId) => {
|
|
const contract = enemyTriggeredShoutContract(parentSpellId, profile)!;
|
|
return [contract.parentSignature, contract.childSignature];
|
|
});
|
|
expect(spells.every((signature) => signature.spellMechanic === 5)).toBe(true);
|
|
for (const signature of spells) {
|
|
expect(signature.dispel).toBe(0);
|
|
expect(signature.attributes0 & bypassFlags.attributes0).toBe(0);
|
|
expect(signature.attributes2 & bypassFlags.attributes2).toBe(0);
|
|
expect(signature.attributes3 & bypassFlags.attributes3).toBe(0);
|
|
expect(signature.attributes4 & bypassFlags.attributes4).toBe(0);
|
|
expect(signature.attributes7 & bypassFlags.attributes7).toBe(0);
|
|
}
|
|
expect(enemyTriggeredShoutContract(5246, profile)!.parentSignature.damageClass).toBe(1);
|
|
expect(enemyTriggeredShoutContract(19134, profile)!.parentSignature.damageClass).toBe(1);
|
|
expect(enemyTriggeredShoutContract(5246, profile)!.childSignature.damageClass)
|
|
.toBe(profile === "ascension" ? 1 : 0);
|
|
expect(enemyTriggeredShoutContract(19134, profile)!.childSignature.damageClass).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("sources the parent aura-31 speed bonus and refreshes its target-local timer", () => {
|
|
const durationFor = (parentSpellId: number, profile: "ascension" | "epoch") => (
|
|
parentSpellId === 5246 ? profile === "ascension" ? 5_000 : 8_000 : profile === "ascension" ? 4_000 : 6_000
|
|
);
|
|
const makeSpeedDefinition = (parentSpellId: number, profile: "ascension" | "epoch") => {
|
|
const sourced = enemyTriggeredShoutMovementSpeedEffect(enemyTriggeredShoutContract(parentSpellId, profile)!);
|
|
expect(sourced).toMatchObject({
|
|
effectIndex: 2,
|
|
auraType: 31,
|
|
increasePercent: 25,
|
|
durationMs: durationFor(parentSpellId, profile),
|
|
utilityTag: "enemy-triggered-shout:parent-movement-speed",
|
|
modifier: { kind: "stat", stat: "movement-speed", operation: "percent", value: 0.25 },
|
|
});
|
|
return {
|
|
id: `enemy-triggered-shout-speed:${parentSpellId}`,
|
|
name: "Enemy shout movement speed",
|
|
disposition: "buff" as const,
|
|
durationMs: sourced!.durationMs,
|
|
stackBehavior: "refresh" as const,
|
|
utilityTags: [sourced!.utilityTag],
|
|
modifiers: [sourced!.modifier],
|
|
};
|
|
};
|
|
|
|
for (const profile of ["ascension", "epoch"] as const) {
|
|
for (const parentSpellId of [5246, 19134]) {
|
|
const effect = enemyTriggeredShoutMovementSpeedEffect(enemyTriggeredShoutContract(parentSpellId, profile)!);
|
|
expect(effect?.durationMs).toBe(durationFor(parentSpellId, profile));
|
|
expect(effect?.utilityTag).toBe(enemyTriggeredShoutMovementSpeedTag());
|
|
}
|
|
}
|
|
|
|
const contract = enemyTriggeredShoutContract(5246, "ascension")!;
|
|
const definition = makeSpeedDefinition(5246, "ascension");
|
|
let auras = applyEnemySourceAura([], definition, "caster-a", "victim", 0);
|
|
expect(enemyTriggeredShoutMovementSpeedMultiplier(auras, "victim", 4_999)).toBe(1.25);
|
|
expect(enemyTriggeredShoutMovementSpeedMultiplier(auras, "other-victim", 1_000)).toBe(1);
|
|
auras = applyEnemySourceAura(auras, definition, "caster-a", "victim", 2_000);
|
|
expect(auras.find((aura) => aura.definition.id === definition.id)?.expiresAt).toBe(7_000);
|
|
expect(enemyTriggeredShoutMovementSpeedMultiplier(auras, "victim", 6_999)).toBe(1.25);
|
|
expect(enemyTriggeredShoutMovementSpeedMultiplier(auras, "victim", 7_000)).toBe(1);
|
|
const otherCaster = applyEnemySourceAura(auras, definition, "caster-b", "victim", 3_000);
|
|
expect(otherCaster).toBe(auras);
|
|
|
|
const secondShout = enemyTriggeredShoutMovementSpeedEffect(enemyTriggeredShoutContract(19134, "epoch")!);
|
|
const otherSpellAura = applyAura(auras, {
|
|
definition: {
|
|
id: "enemy-triggered-shout-speed:19134",
|
|
name: "Other shout movement speed",
|
|
disposition: "buff",
|
|
durationMs: secondShout!.durationMs,
|
|
stackBehavior: "refresh",
|
|
utilityTags: [secondShout!.utilityTag],
|
|
modifiers: [secondShout!.modifier],
|
|
},
|
|
sourceId: "caster-b",
|
|
targetId: "victim",
|
|
now: 2_000,
|
|
}).auras;
|
|
expect(enemyTriggeredShoutMovementSpeedMultiplier(otherSpellAura, "victim", 3_000)).toBe(1.25);
|
|
expect(enemyTriggeredShoutContract(contract.parentSpellId, "ascension")?.parentSignature.effects[2])
|
|
.toMatchObject({ effectType: 6, auraType: 31, basePoints: 25, implicitTargetA: 22, implicitTargetB: 15, radiusIndex: 14 });
|
|
});
|
|
|
|
it("routes only the cast victim to a child and leaves the area list independent", () => {
|
|
const contract = enemyTriggeredShoutContract(5246, "ascension")!;
|
|
const plan = planEnemyTriggeredShoutTargets(contract, {
|
|
explicitVictimId: "victim",
|
|
eligibleExplicitVictimIds: ["victim"],
|
|
areaRecipientIds: ["victim", "nearby-a", "nearby-b", "nearby-c", "nearby-d", "nearby-e"],
|
|
});
|
|
expect(plan).toEqual({
|
|
childTargetIds: ["victim"],
|
|
parentAreaTargetIds: ["victim", "nearby-a", "nearby-b", "nearby-c", "nearby-d", "nearby-e"],
|
|
childMaximumTargets: 1,
|
|
parentAreaMaximumTargets: 5,
|
|
});
|
|
|
|
const noLongerEligible = planEnemyTriggeredShoutTargets(contract, {
|
|
explicitVictimId: "victim",
|
|
eligibleExplicitVictimIds: [],
|
|
areaRecipientIds: ["nearby-a"],
|
|
});
|
|
expect(noLongerEligible.childTargetIds).toEqual([]);
|
|
expect(noLongerEligible.parentAreaTargetIds).toEqual(["nearby-a"]);
|
|
expect(enemyTriggeredShoutContract(19134, "ascension")!.targeting.parentArea.maximumTargets).toBe(0);
|
|
});
|
|
|
|
it("resolves remaining overlapping control by longest expiry, then stun on a tie", () => {
|
|
const sourceAura = (id: string, mechanic: "fear" | "stun", targetId: string, durationMs: number) => applyAura([], {
|
|
definition: {
|
|
id,
|
|
name: id,
|
|
disposition: "debuff",
|
|
durationMs,
|
|
stackBehavior: "refresh",
|
|
utilityTags: [enemyTriggeredShoutControlTag(mechanic)],
|
|
},
|
|
sourceId: "shout-caster",
|
|
targetId,
|
|
now: 0,
|
|
}).aura;
|
|
const parentFear = sourceAura("enemy-control:source-spell-19134", "fear", "victim", 4_000);
|
|
const childFear = sourceAura("enemy-control:source-spell-29544", "fear", "victim", 3_000);
|
|
const otherTargetStun = sourceAura("enemy-control:source-spell-20511", "stun", "other", 8_000);
|
|
expect(resolveEnemyTriggeredShoutControl([parentFear, childFear, otherTargetStun], "victim", 1_000))
|
|
.toEqual({ mechanic: "fear", endsAt: 4_000 });
|
|
|
|
const parentFearTie = sourceAura("enemy-control:source-spell-5246", "fear", "victim", 8_000);
|
|
const childStunTie = sourceAura("enemy-control:source-spell-20511", "stun", "victim", 8_000);
|
|
expect(resolveEnemyTriggeredShoutControl([parentFearTie, childStunTie], "victim", 1_000))
|
|
.toEqual({ mechanic: "stun", endsAt: 8_000 });
|
|
expect(resolveEnemyTriggeredShoutControl([parentFear, childFear], "victim", 4_000)).toBeNull();
|
|
expect(resolveEnemyTriggeredShoutControl([parentFear], "missing", 1_000)).toBeNull();
|
|
});
|
|
});
|