import assert from "node:assert/strict"; import test from "node:test"; import { ArenaSimulation } from "./arena-simulation.mjs"; import { ARENA_STAGES } from "./arena-stages.mjs"; import { initializeArenaCharacter } from "./generated/arena-combat-runtime.mjs"; function match(classId = "priest", secondaryClassId) { const simulation = new ArenaSimulation({ id: `runtime-${classId}`, stage: { ...ARENA_STAGES[0], id: "test-floorless", lineOfSight: [] }, bracket: "2v2", mode: "practice", ruleset: "wow", startedAt: 1000, participants: [{ actorId: "caster", name: "My Character", team: 0, role: "healer", isBot: false, characterKit: { classId, level: 80, seed: { classId, secondaryClassId, level: 80, actionBindings: { "primary:face-left": "priest-flash-heal" } } } }, { actorId: "ally", name: "Ally", team: 0, role: "damage", isBot: false }, { actorId: "enemy", name: "Enemy", team: 1, role: "damage", isBot: false, characterKit: { classId: "mage", level: 80 } }] }); simulation.phase = "active"; simulation.activeStartedAt = 1000; simulation.phaseEndsAt = null; const caster = simulation.actor("caster"), ally = simulation.actor("ally"), enemy = simulation.actor("enemy"); caster.position = [0, 0, 0]; caster.facing = 0; ally.position = [1, 0, 0]; enemy.position = [0, 0, 3]; enemy.facing = Math.PI; const cast = (id, target = "enemy", ground) => simulation.useAbility(caster, id, target, ground); const advance = (ms) => { for (let index = 0; index < ms / 50; index++) simulation.step(); }; return { simulation, caster, ally, enemy, cast, advance }; } test("melee bots start weapon attacks and generate resources instead of waiting at zero rage", () => { const { caster, enemy, advance } = match("warrior"); caster.isBot = true; caster.role = "damage"; const health = enemy.health; advance(8000); assert.ok(enemy.health < health); assert.ok(caster.combat.combatEvents.length > 0 || caster.combat.autoAttackTargetId === enemy.id); }); test("friendly dispels remove the movement restriction as well as the visible debuff", () => { const { simulation, caster, enemy, cast } = match("priest"); assert.equal(simulation.useAbility(enemy, "mage-frost-nova", caster.id).accepted, true); assert.equal(caster.statuses.find((status) => status.kind === "root")?.endsAt, 9000); const dispel = cast("wow335-priest-dispel-magic", caster.id); assert.equal(dispel.accepted, true, JSON.stringify(dispel)); assert.equal(caster.statuses.some((status) => status.kind === "root"), false); caster.movement = [1, 0]; simulation.moveActor(caster, .05); assert.ok(caster.position[0] > 0); }); test("player crowd control diminishes once per cast, including duplicated catalog status entries", () => { const { simulation, caster, enemy, advance } = match("priest"); for (const duration of [8000, 4000, 2000, 0]) { enemy.cooldowns.clear(); enemy.combat = { ...enemy.combat, cooldowns: {}, globalCooldownEndsAt: 0 }; assert.equal(simulation.useAbility(enemy, "mage-frost-nova", caster.id).accepted, true); const now = simulation.startedAt + simulation.tickNumber * 50; assert.equal((caster.statuses.find((status) => status.kind === "root")?.endsAt ?? now) - now, duration); advance(duration + 50); } }); test("a healer can target and heal a teammate's pet", () => { const { simulation, caster, ally, cast, advance } = match("hunter"); assert.equal(cast("wow335-hunter-call-pet", caster.id).accepted, true); advance(2000); const pet = caster.combat.summons[0]; caster.combat = { ...caster.combat, summons: [{ ...pet, health: 500 }] }; ally.combat = null; ally.characterKit = { classId: "priest", level: 80 }; initializeArenaCharacter(ally, 3000); assert.equal(simulation.useAbility(ally, "priest-flash-heal", pet.id).accepted, true); advance(2000); assert.ok(caster.combat.summons[0].health > 500); }); test("channel deals successive damage, heals its caster, and stops on movement", () => { const { caster, enemy, cast, advance } = match("warlock"); caster.health = 3000; assert.equal(cast("warlock-drain-life").accepted, true); assert.equal(caster.casting.mode, "channel"); const initial = enemy.health; advance(2200); assert.ok(enemy.health < initial); assert.ok(caster.health > 3000); caster.movement = [1, 0]; advance(50); assert.equal(caster.casting, null); }); test("runes are spent, gate repeat casts, regenerate, and restore with Empower Rune Weapon", () => { const { caster, cast, advance } = match("death-knight"); assert.equal(cast("death-knight-icy-touch").accepted, true); assert.ok(caster.combat.deathKnightRunes.some((rune) => rune.readyAt > 1000)); assert.ok(caster.resource > 0); advance(1600); assert.equal(cast("wow335-death-knight-empower-rune-weapon", "caster").accepted, true); assert.ok(caster.combat.deathKnightRunes.every((rune) => rune.readyAt <= 2600)); }); test("combo points belong to the selected enemy and finishers consume them", () => { const { caster, cast, advance } = match("rogue"); assert.equal(cast("rogue-sinister-strike").accepted, true); assert.equal(caster.combat.comboPoints, 1); assert.equal(caster.combat.comboTargetId, "enemy"); advance(1600); assert.equal(cast("rogue-eviscerate").accepted, true); assert.equal(caster.combat.comboPoints, 0); }); test("pets spawn, receive orders, attack, and dismiss through the same spell engine", () => { const { simulation, caster, enemy, cast, advance } = match("hunter"); assert.equal(cast("wow335-hunter-call-pet", "caster").accepted, true); assert.ok(caster.combat.summons.some((summon) => summon.kind === "pet")); simulation.receiveIntent("caster", { type: "target", targetActorId: "enemy", sequence: 1 }); simulation.receiveIntent("caster", { type: "command", command: "attack", sequence: 2 }); const before = enemy.health; advance(3100); assert.ok(enemy.health < before); assert.equal(cast("wow335-hunter-dismiss-pet", "caster").accepted, true); advance(Math.max(50, (caster.casting?.completesAt ?? 0) - (1000 + simulation.tickNumber * 50) + 50)); assert.equal(caster.combat.summons.length, 0); }); test("an opposing player's interrupt cancels the cast and locks its school", () => { const { simulation, caster, enemy, cast, advance } = match(); assert.equal(cast("priest-smite").accepted, true); assert.equal(simulation.useAbility(enemy, "wow335-mage-counterspell", "caster").accepted, true); assert.equal(caster.casting, null); assert.ok(caster.combat.schoolLockedUntil.holy > 1000); advance(1600); assert.equal(cast("priest-smite").accepted, false); }); test("healing reaches the requested teammate, is dampened, and does not alter another match", () => { const first = match(), second = match(); first.ally.health = 500; const untouched = second.caster.resource; assert.equal(first.cast("priest-flash-heal", "ally").accepted, true); first.advance(1600); assert.ok(first.ally.health > 500); assert.ok(first.caster.resource < untouched); assert.equal(second.caster.resource, untouched); assert.equal(second.caster.casting, null); assert.ok(first.simulation.combatEvents.some((event) => event.kind === "healing" && event.targetActorId === "ally" && event.sourceActorId === "caster")); assert.ok(first.simulation.combatPresentation.some((event) => event.sourceActorId === "caster")); }); test("self shields absorb hostile-player damage and are consumed once", () => { const { simulation, caster, enemy, cast, advance } = match("mage"); assert.equal(cast("mage-mana-shield", "caster").accepted, true); const shield = caster.combat.shield; assert.ok(shield > 0 || caster.auras.some((aura) => aura.absorbs.length)); const health = caster.health; enemy.combat = { ...enemy.combat, gearStats: { ...enemy.combat.gearStats, hitRating: 100000 } }; assert.equal(simulation.useAbility(enemy, "mage-fire-blast", "caster").accepted, true); assert.ok(caster.health >= health - 1010); assert.ok(simulation.combatEvents.some((event) => event.kind === "damage" && event.targetActorId === "caster" && event.absorbed > 0), JSON.stringify(simulation.combatEvents)); advance(50); }); test("RoM primary and secondary resource pools survive arena ticks and casting", () => { const { caster, cast, advance } = match("rom-warlock", "rom-priest"); assert.ok(Object.keys(caster.combat.resourcePools).length > 1); const focus = caster.combat.resourcePools.focus; assert.equal(cast("rom-warlock-general-0").accepted, true); advance(4000); assert.ok(Number.isFinite(caster.combat.resourcePools.focus)); assert.ok(caster.combat.resourcePools.focus <= focus); assert.ok(Number.isFinite(caster.combat.resourcePools.mana)); }); test("ground spells damage targets around the confirmed location", () => { const { caster, enemy, cast } = match("mage"); const health = enemy.health; assert.equal(cast("wow335-mage-blizzard", null, enemy.position).accepted, true); assert.ok(enemy.health < health); assert.ok(caster.resource >= 0); }); test("mana burns drain an opposing character's actual resource pool", () => { const { enemy, cast, advance } = match(); const before = enemy.resource; assert.equal(cast("wow335-priest-mana-burn").accepted, true); advance(3500); assert.ok(enemy.resource < before); }); test("opponents can target and damage player pets", () => { const { simulation, caster, enemy, cast } = match("hunter"); assert.equal(cast("wow335-hunter-call-pet", "caster").accepted, true); const pet = caster.combat.summons[0], before = pet.health; enemy.combat = { ...enemy.combat, gearStats: { ...enemy.combat.gearStats, hitRating: 100000 } }; assert.equal(simulation.receiveIntent("enemy", { sequence: 1, type: "target", targetActorId: pet.id }).accepted, true); assert.equal(simulation.useAbility(enemy, "mage-fire-blast", pet.id).accepted, true); assert.ok((caster.combat.summons.find((entry) => entry.id === pet.id)?.health ?? 0) < before); });