import { describe, expect, it } from "vitest"; import type { ClassId } from "../app/characterCatalog"; import type { AbilityDefinition } from "./abilityCatalog"; import { applyAura, resolveDamageValue, resolveStatValue, type ActiveAura } from "./combatAuras"; import { coaGuardianCompanionFormationSpellId, coaGuardianFormationAbilityForCast, COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP, COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION, COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID, COA_GUARDIAN_LINE_FORMATION_AURA, COA_GUARDIAN_LINE_FORMATION_AURA_ID, COA_GUARDIAN_LINE_FORMATION_SPELL_ID, COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER, COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION, COA_GUARDIAN_TOWER_FORMATION_AURA, COA_GUARDIAN_TOWER_FORMATION_AURA_ID, COA_GUARDIAN_TOWER_FORMATION_SPELL_ID, coaGuardianFormationMovementSpeedMultiplier, getCoaGuardianTowerShieldArmorBonus, hasCoaGuardianFormationAura, projectCoaGuardianFormationAuras, resolveCoaGuardianControlDurationMs, type CoaGuardianFormationActor, } from "./coaGuardianFormationRuntime"; const actor = ( id = "guardian-player", classId: ClassId = "guardian", footmansCalling = false, health = 100, ): CoaGuardianFormationActor => ({ id, classId, health, talentRanks: footmansCalling ? { [`coa-entry-${COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID}`]: 1 } : {}, }); const activeAura = ( definition: typeof COA_GUARDIAN_TOWER_FORMATION_AURA | typeof COA_GUARDIAN_LINE_FORMATION_AURA, sourceId = "guardian-player", targetId = sourceId, now = 1_000, ): ActiveAura => applyAura([], { definition, sourceId, targetId, now, }).aura; const ability = (dbcSpellId: number, classId: ClassId = "guardian"): AbilityDefinition => ({ id: `ability-${dbcSpellId}`, classId, dbcSpellId, name: "Formation", unlockLevel: 4, icon: "icon.png", sigil: "FO", description: "Source description", target: "hostile", range: { min: 0, max: 0 }, castMode: "instant", castTimeMs: 0, cooldownMs: 0, gcdMs: 0, cost: { resource: "energy", amount: 20 }, effects: [{ kind: "apply-aura", aura: { id: "generic-import", name: "Generic import", disposition: "buff", durationMs: null }, sourceEffectType: 6, sourceAuraType: 142, miscValue: 127, triggerSpellId: 0, }], passive: false, source: "coa-progression", }); describe("CoA Guardian formation runtime", () => { it("defines persistent exclusive forms with source-specific effects", () => { expect(COA_GUARDIAN_TOWER_FORMATION_AURA).toMatchObject({ id: COA_GUARDIAN_TOWER_FORMATION_AURA_ID, durationMs: null, exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP, modifiers: [ { kind: "stat", stat: "block", operation: "flat", value: 10, perStack: false }, { kind: "damage", direction: "dealt", operation: "percent", value: -0.1 }, ], }); expect(COA_GUARDIAN_LINE_FORMATION_AURA).toMatchObject({ id: COA_GUARDIAN_LINE_FORMATION_AURA_ID, durationMs: null, exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP, modifiers: [{ kind: "stat", stat: "movement-speed", operation: "percent", value: 0.15 }], }); }); it("replaces imported effects with one self-applied runtime form aura", () => { const tower = coaGuardianFormationAbilityForCast(ability(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID)); expect(tower.cost).toEqual({ resource: "energy", amount: 20 }); expect(tower.target).toBe("self"); expect(tower.effects).toEqual([{ kind: "apply-aura", aura: COA_GUARDIAN_TOWER_FORMATION_AURA, recipient: "caster", sourceEffectType: 6, sourceAuraType: 87, miscValue: 0, triggerSpellId: 0, }]); const line = coaGuardianFormationAbilityForCast(ability(COA_GUARDIAN_LINE_FORMATION_SPELL_ID)); expect(line.effects).toEqual([expect.objectContaining({ kind: "apply-aura", aura: COA_GUARDIAN_LINE_FORMATION_AURA, recipient: "caster", sourceAuraType: 232, miscValue: 7, })]); const warriorAbility = ability(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID, "warrior"); expect(coaGuardianFormationAbilityForCast(warriorAbility)).toBe(warriorAbility); const unrelated = ability(123456); expect(coaGuardianFormationAbilityForCast(unrelated)).toBe(unrelated); }); it("recognizes only the living actor's own active self marker", () => { const self = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA); const foreign = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "another-guardian", "guardian-player"); const nonSelf = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "guardian-player", "another-guardian"); const expired = { ...self, expiresAt: 1_000 }; expect(hasCoaGuardianFormationAura([self], actor(), "tower", 1_000)).toBe(true); expect(hasCoaGuardianFormationAura([foreign, nonSelf], actor(), "tower", 1_000)).toBe(false); expect(hasCoaGuardianFormationAura([expired], actor(), "tower", 1_000)).toBe(false); expect(hasCoaGuardianFormationAura([self], actor("guardian-player", "guardian", false, 0), "tower", 999)) .toBe(false); expect(hasCoaGuardianFormationAura([self], actor("guardian-player", "warrior"), "tower", 999)).toBe(false); }); it("projects shield armor and Footman's Calling only on owned forms", () => { const tower = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA); const owner = actor("guardian-player", "guardian", true); const projected = projectCoaGuardianFormationAuras([tower], owner, 1_001, 120); expect(getCoaGuardianTowerShieldArmorBonus(owner, [tower], 1_001, 120)).toBe(60); expect(resolveStatValue(100, projected, "armor")).toBe(160); expect(resolveStatValue(5, projected, "block")).toBe(15); expect(resolveDamageValue(200, projected, { direction: "taken", school: "physical", attackKind: "melee" })) .toBe(180); expect(resolveDamageValue(200, projected, { direction: "dealt", school: "physical", attackKind: "melee" })) .toBe(180); const repeatedProjection = projectCoaGuardianFormationAuras(projected, owner, 1_001, 120); expect(resolveStatValue(100, repeatedProjection, "armor")).toBe(160); expect(resolveDamageValue(200, repeatedProjection, { direction: "taken", school: "physical", attackKind: "melee" })) .toBe(180); const withoutFootman = projectCoaGuardianFormationAuras([tower], actor(), 1_001, 120); expect(resolveStatValue(100, withoutFootman, "armor")).toBe(160); expect(resolveDamageValue(200, withoutFootman, { direction: "taken", school: "physical", attackKind: "melee" })) .toBe(200); const noLongerLearned = projectCoaGuardianFormationAuras(projected, actor(), 1_001, 120); expect(resolveDamageValue(200, noLongerLearned, { direction: "taken", school: "physical", attackKind: "melee" })) .toBe(200); expect(getCoaGuardianTowerShieldArmorBonus(actor(), [tower], 1_001, 120)).toBe(60); expect(getCoaGuardianTowerShieldArmorBonus(owner, [tower], 1_001, Number.NaN)).toBe(0); }); it("does not project conditional mitigation or shield armor from foreign or Line forms", () => { const foreignTower = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "other", "guardian-player"); const line = activeAura(COA_GUARDIAN_LINE_FORMATION_AURA); const owner = actor("guardian-player", "guardian", true); const foreignAuras = [foreignTower]; expect(projectCoaGuardianFormationAuras(foreignAuras, owner, 1_001, 120)).toBe(foreignAuras); expect(resolveStatValue(100, projectCoaGuardianFormationAuras(foreignAuras, owner, 1_001, 120), "armor")) .toBe(100); const projectedLine = projectCoaGuardianFormationAuras([line], owner, 1_001, 120); expect(resolveStatValue(100, projectedLine, "armor")).toBe(100); expect(resolveDamageValue(100, projectedLine, { direction: "taken", school: "fire", attackKind: "spell" })) .toBe(90); }); it("reduces only root duration while the actor's own Line form is active", () => { const line = activeAura(COA_GUARDIAN_LINE_FORMATION_AURA); expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [line], "root", 1_001)) .toBe(8_000 * (1 - COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION)); expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [line], "slow", 1_001)).toBe(8_000); expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [], "root", 1_001)).toBe(8_000); expect(resolveCoaGuardianControlDurationMs(8_000, actor("guardian-player", "guardian", false, 0), [line], "root", 1_001)) .toBe(8_000); expect(coaGuardianFormationMovementSpeedMultiplier([line], actor(), 1_001)) .toBe(COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER); expect(coaGuardianFormationMovementSpeedMultiplier([line], actor("guardian-player", "warrior"), 1_001)).toBe(1); expect(coaGuardianFormationMovementSpeedMultiplier([line], actor("guardian-player", "guardian", false, 0), 1_001)) .toBe(1); expect(COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION).toBe(0.1); }); it("chooses the authored companion form once and waits while its own form persists", () => { const guardian = actor("guardian-companion"); const options = { actor: guardian, role: "vanguard-tank" as const, towerAvailable: true, lineAvailable: true, auras: [] as readonly ActiveAura[], now: 1_000, }; expect(coaGuardianCompanionFormationSpellId(options)).toBe(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID); expect(coaGuardianCompanionFormationSpellId({ ...options, towerAvailable: false })) .toBe(COA_GUARDIAN_LINE_FORMATION_SPELL_ID); expect(coaGuardianCompanionFormationSpellId({ ...options, role: "other" })) .toBe(COA_GUARDIAN_LINE_FORMATION_SPELL_ID); expect(coaGuardianCompanionFormationSpellId({ ...options, towerAvailable: false, lineAvailable: false })) .toBeNull(); const activeLine = activeAura( COA_GUARDIAN_LINE_FORMATION_AURA, "guardian-companion", "guardian-companion", options.now, ); expect(coaGuardianCompanionFormationSpellId({ ...options, auras: [activeLine] })).toBeNull(); }); });