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(); }); });