import { beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { applyAura } from "./combatAuras"; import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog"; import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore"; import { requireDungeonDefinition } from "./dungeonRegistry"; import { DEFAULT_ENEMY_ATTACK, type EnemyAttackDefinition } from "./enemyAttacks"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; const SOURCE_START_AT = 1_000; const SOURCE_ID = "stockade-infected-wound-caster"; const TARGET_AURA_ID = "enemy-source-spell-17230"; const stockadeWound = requireDungeonDefinition("stormwind-stockade").entities["entry-1707"]?.combat; const importedWound = stockadeWound?.attacks.find((attack) => attack.spellId === 17_230); if (!stockadeWound || !importedWound) { throw new Error("Stockade entry 1707 is missing imported Infected Wound 17230."); } type DamageCase = "normal" | "direct-physical" | "periodic-physical" | "direct-nature" | "direct-magic"; interface ScenarioResult { readonly applicationAt?: number; readonly event: ReturnType[number] | undefined; readonly combatResult: ReturnType["lastCombatResult"]; } function initCombat(): void { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.setState({ mobs: {}, auras: [], actors: {}, enemyPeriodicEffects: [], combatEvents: [], effects: [], lastCombatResult: null, }); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 30, }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); usePartyStore.getState().initializeParty( "stockade-infected-wound", 30, useCombatStore.getState().health, "damage", 1, "wow", ); useCombatStore.getState().synchronizeActors(); } function registerMob(id: string, attacks: readonly EnemyAttackDefinition[], serverEntry?: number): void { useCombatStore.getState().registerMob(id, { name: id, ...(serverEntry === undefined ? {} : { serverEntry }), level: 30, maxHealth: 50_000, xpReward: 0, attacks, }); setMobPosition(id, [2, 0, 0]); } function engageMob(id: string, now: number): void { useCombatStore.getState().selectMob(id); useCombatStore.getState().damageMob(id, 1, now); } function sourceWoundAttack(): EnemyAttackDefinition { return { ...importedWound!, castTimeMs: 0, initialCooldownMs: [0, 0], repeatCooldownMs: [300_000, 300_000], maximumCasts: 1, }; } function applyImportedWoundAndRemoveCaster(cleanup: "remove" | "kill" = "remove"): number { expect(importedWound).toMatchObject({ spellId: 17_230, effect: { kind: "target-aura", wowAuraSource: { spellId: 17_230, sourceProfile: "ascension" }, }, }); registerMob(SOURCE_ID, [sourceWoundAttack()], stockadeWound!.serverEntry); engageMob(SOURCE_ID, SOURCE_START_AT); useCombatStore.getState().advanceMobCombat(SOURCE_START_AT); const applicationAt = SOURCE_START_AT + 1; useCombatStore.getState().advanceMobCombat(applicationAt); const wound = useCombatStore.getState().auras.find((aura) => ( aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID )); expect(wound).toBeDefined(); expect(wound).toMatchObject({ sourceId: SOURCE_ID, targetId: PLAYER_AURA_ENTITY_ID, appliedAt: expect.any(Number), expiresAt: expect.any(Number), definition: { name: "Infected Wound", disposition: "debuff", dispelCategory: "disease", durationMs: 300_000, }, }); expect(wound!.expiresAt! - wound!.appliedAt).toBe(300_000); expect(useCombatStore.getState().combatEvents.filter((event) => ( event.kind === "damage" && event.sourceActorId === SOURCE_ID ))).toEqual([]); // A sourced timer belongs to the target. Removing the caster must leave the // target's five-minute disease active until a cleanse or its own expiry. if (cleanup === "kill") { const killed = useCombatStore.getState().damageMob(SOURCE_ID, 100_000, applicationAt + 1); expect(killed.killed).toBe(true); } else { useCombatStore.getState().removeMob(SOURCE_ID); } expect(useCombatStore.getState().auras.some((aura) => ( aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID ))).toBe(true); return wound!.appliedAt; } function attackForCase(scenario: DamageCase): EnemyAttackDefinition { const common: EnemyAttackDefinition = { ...DEFAULT_ENEMY_ATTACK, id: `infected-wound-${scenario}`, name: `Fixture ${scenario}`, range: 5, // Deterministic fixture cadence; source spell 17230 itself is still the // unchanged imported attack exercised in applyImportedWoundAndRemoveCaster. castTimeMs: 0, cooldownMs: 10_000, initialCooldownMs: [0, 0], repeatCooldownMs: [10_000, 10_000], maximumCasts: 1, }; if (scenario === "normal") return { ...common, attackKind: "melee", animation: "attack" }; if (scenario === "periodic-physical") { return { ...common, attackKind: "spell", school: "physical", delivery: "projectile", animation: "cast", effect: { kind: "dot", multiplier: 1, ticks: 1, intervalMs: 1_000 }, }; } return { ...common, attackKind: "spell", school: scenario === "direct-nature" ? "nature" : scenario === "direct-magic" ? "fire" : "physical", delivery: "projectile", animation: "cast", effect: { kind: "damage", multiplier: 1 }, }; } function damageEvents(sourceId: string) { return useCombatStore.getState().combatEvents.filter((event) => event.kind === "damage" && event.sourceActorId === sourceId && event.targetActorId === PLAYER_AURA_ENTITY_ID); } function runDamageScenario(scenario: DamageCase, applyWound: boolean): ScenarioResult { initCombat(); const applicationAt = applyWound ? applyImportedWoundAndRemoveCaster() : undefined; const attackerId = "infected-wound-damage-fixture"; registerMob(attackerId, [attackForCase(scenario)]); const attackAt = 5_000; engageMob(attackerId, attackAt); useCombatStore.getState().advanceMobCombat(attackAt); useCombatStore.getState().advanceMobCombat(attackAt + 1); if (scenario === "periodic-physical") { useCombatStore.getState().advanceMobCombat(attackAt + 1_001); } return { applicationAt, event: damageEvents(attackerId).at(-1), combatResult: useCombatStore.getState().lastCombatResult, }; } function cureDiseaseAbility() { const ability = abilitiesForClass("priest").find((candidate) => candidate.name === "Cure Disease"); if (!ability) throw new Error("Level-30 Priest catalog is missing Cure Disease."); return ability; } beforeEach(initCombat); describe("imported Stockade Infected Wound 17230", () => { it.each(["remove", "kill"] as const)( "applies its sourced disease without a hit and survives caster %s", (cleanup) => { const playerHealth = useCombatStore.getState().health; const appliedAt = applyImportedWoundAndRemoveCaster(cleanup); const wound = useCombatStore.getState().auras.find((aura) => ( aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID )); expect(wound?.appliedAt).toBeGreaterThanOrEqual(SOURCE_START_AT); expect(wound?.appliedAt).toBeLessThanOrEqual(appliedAt); expect(useCombatStore.getState().health).toBe(playerHealth); if (cleanup === "remove") expect(useCombatStore.getState().mobs[SOURCE_ID]).toBeUndefined(); else expect(useCombatStore.getState().mobs[SOURCE_ID]?.dead).toBe(true); expect(damageEvents(SOURCE_ID)).toEqual([]); }, ); it.each(["normal", "direct-physical", "periodic-physical"] as const)( "adds exactly six physical damage before mitigation to %s attacks", (scenario) => { const baseline = runDamageScenario(scenario, false); const infected = runDamageScenario(scenario, true); expect(baseline.event).toBeDefined(); expect(infected.event).toBeDefined(); expect(infected.event!.rawAmount - baseline.event!.rawAmount).toBe(6); expect(infected.event!.school).toBe("physical"); expect(infected.combatResult?.rawDamage).toBe(infected.event!.rawAmount); expect(infected.combatResult?.damageBeforeMitigation).toBeGreaterThanOrEqual(infected.event!.rawAmount); }, ); it.each(["direct-nature", "direct-magic"] as const)( "does not change %s damage", (scenario) => { const baseline = runDamageScenario(scenario, false); const infected = runDamageScenario(scenario, true); expect(baseline.event).toBeDefined(); expect(infected.event).toBeDefined(); expect(infected.event!.school).toBe(scenario === "direct-nature" ? "nature" : "fire"); expect(infected.event!.rawAmount).toBe(baseline.event!.rawAmount); }, ); it("places the physical bonus ahead of armor and block, with the remaining packet then absorbed", () => { initCombat(); applyImportedWoundAndRemoveCaster(); useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, armor: 50_000 } })); const guard = applyAura(useCombatStore.getState().auras, { definition: { id: "fixture:infected-wound-block-and-absorb", name: "Test-only block and absorb", disposition: "buff", durationMs: 30_000, modifiers: [ { kind: "stat", stat: "block", operation: "flat", value: 100, perStack: false }, { kind: "stat", stat: "block-value", operation: "flat", value: 8, perStack: false }, ], absorbs: [{ id: "test-only-absorb", amount: 1_000, school: "physical" }], }, sourceId: "fixture:infected-wound-block-and-absorb", targetId: PLAYER_AURA_ENTITY_ID, now: 4_000, }).auras; useCombatStore.setState({ auras: [...guard] }); const attack = { ...attackForCase("normal"), id: "infected-wound-blocked-normal", repeatCooldownMs: [1, 1] as const, maximumCasts: 100, }; const attackerId = "infected-wound-blocked-attacker"; registerMob(attackerId, [attack]); engageMob(attackerId, 5_000); for (let now = 5_000; now < 5_200; now += 1) { useCombatStore.getState().advanceMobCombat(now); if (useCombatStore.getState().lastCombatResult?.blocked) break; } const result = useCombatStore.getState().lastCombatResult; const event = damageEvents(attackerId).find((candidate) => candidate.outcome === "block"); expect(result?.blocked).toBe(true); expect(result?.landed).toBe(true); expect(result?.effectiveArmor).toBeGreaterThan(0); expect(result?.armorMitigatedAmount).toBeGreaterThan(0); expect(result?.blockedAmount).toBeGreaterThan(0); expect(event).toBeDefined(); expect(event!.rawAmount).toBe(result!.rawDamage); expect(event!.rawAmount).toBe(useCombatStore.getState().mobs[attackerId]!.attackDamage + 6); expect(result!.damageBeforeMitigation).toBe(event!.rawAmount); expect(event!.absorbed).toBeGreaterThan(0); expect(event!.effectiveAmount).toBe(0); }); it("cleanses the imported disease with the actual level-30 Priest Cure Disease ability", () => { const appliedAt = applyImportedWoundAndRemoveCaster(); const cure = abilityAtLevel(cureDiseaseAbility(), 30); expect(cure).toMatchObject({ dbcSpellId: 528, effects: [expect.objectContaining({ kind: "dispel", mode: "cleanse", categories: ["disease"], })], }); // Cure Disease targets the player's self slot when no party actor is selected. expect(useCombatStore.getState().selectFriendlyActor(null)).toBe(true); const result = useCombatStore.getState().castAbility(cure.id, { now: appliedAt + 1, }); expect(result.ok, JSON.stringify(result)).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID)).toBe(false); }); it("expires at the sourced five-minute duration after the caster has been removed", () => { const appliedAt = applyImportedWoundAndRemoveCaster(); const expirationAt = useCombatStore.getState().auras.find((aura) => aura.definition.id === TARGET_AURA_ID)!.expiresAt!; expect(expirationAt - appliedAt).toBe(300_000); useCombatStore.getState().advanceMobCombat(expirationAt); expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === TARGET_AURA_ID && aura.targetId === PLAYER_AURA_ENTITY_ID)).toBe(false); }); });