186 lines
9.7 KiB
JavaScript
186 lines
9.7 KiB
JavaScript
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);
|
|
});
|