448 lines
20 KiB
TypeScript
448 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { abilitiesForClass, abilityById } from "./abilityCatalog";
|
|
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
|
import {
|
|
ROM_KNIGHT_DIVINE_PUNISHMENT_TAG,
|
|
ROM_KNIGHT_GODS_VENGEANCE_TAG,
|
|
ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG,
|
|
romKnightDivinePunishmentAuraForSealStacks,
|
|
ROM_KNIGHT_SHIELD_OF_VALOR_TAG,
|
|
} from "./romKnightOverrides";
|
|
import { romEffectsForCast } from "./romCombatMechanics";
|
|
|
|
function stableCombatUnit(seed: string): number {
|
|
let hash = 2166136261;
|
|
for (let index = 0; index < seed.length; index += 1) {
|
|
hash ^= seed.charCodeAt(index);
|
|
hash = Math.imul(hash, 16777619);
|
|
}
|
|
return (hash >>> 0) / 0x1_0000_0000;
|
|
}
|
|
|
|
function godsVengeanceCastTime(wantProc: boolean): number {
|
|
for (let now = 11_500; now < 100_000; now += 100) {
|
|
const roll = stableCombatUnit(
|
|
`${PLAYER_AURA_ENTITY_ID}:rom-knight-general-0:gods-vengeance-target:${now}:rom-gods-vengeance`,
|
|
);
|
|
if ((roll < 0.5) === wantProc) return now;
|
|
}
|
|
throw new Error("Could not find a deterministic God's Vengeance cast-time seed.");
|
|
}
|
|
|
|
describe("RuneWaker Knight periodic skills", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
useCombatStore.setState({ playerPosition: [0, 0, 0] });
|
|
});
|
|
|
|
it("keeps Holy Light Domain as a three-pulse Holy area effect", () => {
|
|
const domain = abilityById("rom-knight-rom-mage-elite-15")!;
|
|
expect(domain).toMatchObject({ dbcSpellId: 491606, target: "self", radius: 10 });
|
|
expect(domain.effects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "dot",
|
|
school: "holy",
|
|
ticks: 3,
|
|
intervalMs: 2_000,
|
|
tag: "rom-knight-holy-light-domain",
|
|
}),
|
|
]));
|
|
expect(domain.effects.some((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.procs?.some((proc) => proc.action.kind === "custom"
|
|
&& proc.action.id === "rom-trigger-skill"))).toBe(false);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80,
|
|
});
|
|
for (const [id, x] of [["domain-near", 5], ["domain-edge", 9], ["domain-far", 12]] as const) {
|
|
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 });
|
|
setMobPosition(id, [x, 0, 0]);
|
|
}
|
|
const initialHealth = Object.fromEntries(Object.entries(useCombatStore.getState().mobs)
|
|
.map(([id, mob]) => [id, mob.health]));
|
|
expect(useCombatStore.getState().castAbility(domain.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().effects.filter((effect) => effect.kind === "dot")
|
|
.map((effect) => effect.targetId).sort()).toEqual(["domain-edge", "domain-near"]);
|
|
expect(useCombatStore.getState().mobs["domain-near"]!.health).toBe(initialHealth["domain-near"]);
|
|
|
|
useCombatStore.getState().tick(0.1, 3_000);
|
|
const afterFirstPulse = useCombatStore.getState();
|
|
expect(afterFirstPulse.mobs["domain-near"]!.health).toBeLessThan(initialHealth["domain-near"]!);
|
|
expect(afterFirstPulse.mobs["domain-edge"]!.health).toBeLessThan(initialHealth["domain-edge"]!);
|
|
expect(afterFirstPulse.mobs["domain-far"]!.health).toBe(initialHealth["domain-far"]);
|
|
});
|
|
|
|
it("adds War Prayer Holy Seal stacks on its six three-second pulses", () => {
|
|
const prayer = abilityById("rom-knight-rom-mage-elite-50")!;
|
|
expect(prayer).toMatchObject({ dbcSpellId: 490078, target: "hostile" });
|
|
expect(prayer.effects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "damage", school: "holy" }),
|
|
expect.objectContaining({
|
|
kind: "dot",
|
|
ticks: 6,
|
|
intervalMs: 3_000,
|
|
tag: "rom-knight-war-prayer-seal",
|
|
}),
|
|
]));
|
|
expect(prayer.effects.some((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.procs?.some((proc) => proc.action.kind === "custom"
|
|
&& proc.action.id === "rom-trigger-skill"))).toBe(false);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80,
|
|
});
|
|
useCombatStore.getState().registerMob("war-prayer-target", { name: "War Prayer Target", maxHealth: 100_000 });
|
|
setMobPosition("war-prayer-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("war-prayer-target");
|
|
expect(useCombatStore.getState().castAbility(prayer.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().comboPoints).toBe(0);
|
|
expect(useCombatStore.getState().effects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "dot", targetId: "war-prayer-target", remainingTicks: 6 }),
|
|
]));
|
|
|
|
for (let tick = 1; tick <= 6; tick += 1) {
|
|
useCombatStore.getState().tick(0.1, 1_000 + tick * 3_000);
|
|
expect(useCombatStore.getState().comboTargetId).toBe("war-prayer-target");
|
|
expect(useCombatStore.getState().comboPoints).toBe(Math.min(tick, 4));
|
|
}
|
|
|
|
const punishment = abilityById("rom-knight-general-0")!;
|
|
expect(useCombatStore.getState().castAbility(punishment.id, undefined, 20_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().comboPoints).toBe(0);
|
|
expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage"
|
|
&& event.abilityId === punishment.id && event.targetActorId === "war-prayer-target"
|
|
&& event.effectiveAmount > 0)).toBe(true);
|
|
});
|
|
|
|
it("uses current CoA's 28.5% Divine Punishment damage bonus only on Punishment", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 80,
|
|
});
|
|
const state = useCombatStore.getState();
|
|
const passive = state.abilities.find((ability) => ability.dbcSpellId === 491608);
|
|
expect(passive?.passive).toBe(true);
|
|
const passiveAura = state.auras.find((aura) => aura.definition.utilityTags?.includes(ROM_KNIGHT_DIVINE_PUNISHMENT_TAG));
|
|
expect(passiveAura).toBeDefined();
|
|
expect(passive?.effects).toEqual([
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
aura: expect.objectContaining({
|
|
utilityTags: [ROM_KNIGHT_DIVINE_PUNISHMENT_TAG],
|
|
}),
|
|
}),
|
|
]);
|
|
const passiveEffect = passive?.effects[0];
|
|
expect(passiveEffect?.kind).toBe("apply-aura");
|
|
if (passiveEffect?.kind === "apply-aura") expect(passiveEffect.aura.modifiers).toBeUndefined();
|
|
|
|
const punishment = abilityById("rom-knight-general-0")!;
|
|
const context = {
|
|
sealStacks: 4,
|
|
bleedCount: 0,
|
|
charged: false,
|
|
mainHandDagger: false,
|
|
casterMaxHealth: state.maxHealth,
|
|
targetHealthPercent: 1,
|
|
} as const;
|
|
const baseline = romEffectsForCast(punishment, context);
|
|
const boosted = romEffectsForCast(punishment, {
|
|
...context,
|
|
passiveTags: passiveAura?.definition.utilityTags ?? [],
|
|
});
|
|
const damageValues = (effects: typeof baseline) => effects
|
|
.filter((effect) => effect.kind === "damage" || effect.kind === "dot")
|
|
.map((effect) => ({
|
|
coefficient: effect.coefficient,
|
|
basePoints: "basePoints" in effect ? effect.basePoints ?? 0 : 0,
|
|
pointsPerLevel: "pointsPerLevel" in effect ? effect.pointsPerLevel ?? 0 : 0,
|
|
}));
|
|
expect(damageValues(baseline)).toHaveLength(2);
|
|
expect(damageValues(boosted)).toHaveLength(2);
|
|
for (const [index, base] of damageValues(baseline).entries()) {
|
|
expect(damageValues(boosted)[index]!.coefficient).toBeCloseTo(base.coefficient * 1.285);
|
|
expect(damageValues(boosted)[index]!.basePoints).toBeCloseTo(base.basePoints * 1.285);
|
|
expect(damageValues(boosted)[index]!.pointsPerLevel).toBeCloseTo(base.pointsPerLevel * 1.285);
|
|
}
|
|
expect(romEffectsForCast(abilityById("rom-knight-primary-1")!, {
|
|
...context,
|
|
passiveTags: passiveAura?.definition.utilityTags ?? [],
|
|
})).toEqual(romEffectsForCast(abilityById("rom-knight-primary-1")!, context));
|
|
});
|
|
|
|
it("replaces the old Light-only aura with current CoA's general per-seal vulnerability data", () => {
|
|
expect(romKnightDivinePunishmentAuraForSealStacks(3)?.aura).toMatchObject({
|
|
durationMs: 10_000,
|
|
modifiers: [{ kind: "damage", direction: "taken", operation: "percent", value: 0.06 }],
|
|
});
|
|
expect(romKnightDivinePunishmentAuraForSealStacks(3)?.aura.id)
|
|
.toBe("rom-knight-divine-punishment-damage-taken");
|
|
});
|
|
|
|
it("uses the local Knight/Priest Compassion row bridged to current CoA behavior and no longer changes Holy Seal cooldown", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-priest", raceId: "rom-human", level: 80,
|
|
});
|
|
const state = useCombatStore.getState();
|
|
const holySeal = state.abilities.find((ability) => ability.dbcSpellId === 490149)!;
|
|
const compassionPassive = state.abilities.find((ability) => ability.dbcSpellId === 491635)!;
|
|
expect(compassionPassive).toMatchObject({ name: "Compassion of Holy Light", passive: true });
|
|
expect(compassionPassive.effects).toEqual([
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
aura: expect.objectContaining({ utilityTags: [ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG] }),
|
|
}),
|
|
]);
|
|
const tags = state.auras.find((aura) => aura.definition.utilityTags?.includes(ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG))
|
|
?.definition.utilityTags ?? [];
|
|
expect(romEffectsForCast(holySeal, {
|
|
sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false,
|
|
casterMaxHealth: state.maxHealth, targetHealthPercent: 1, passiveTags: tags,
|
|
})).toEqual(romEffectsForCast(holySeal, {
|
|
sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false,
|
|
casterMaxHealth: state.maxHealth, targetHealthPercent: 1,
|
|
}));
|
|
});
|
|
|
|
it.each([true, false])("lets God's Vengeance preserve Holy Seal when the 50% roll is %s", (preservesSeal) => {
|
|
const godVengeance = abilityById("rom-knight-rom-rogue-elite-50")!;
|
|
expect(godVengeance).toMatchObject({ dbcSpellId: 499934, passive: true });
|
|
expect(godVengeance.effects).toEqual([
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({ utilityTags: [ROM_KNIGHT_GODS_VENGEANCE_TAG] }),
|
|
}),
|
|
]);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", level: 80,
|
|
});
|
|
useCombatStore.getState().registerMob("gods-vengeance-target", {
|
|
name: "God's Vengeance Target", maxHealth: 1_000_000,
|
|
});
|
|
setMobPosition("gods-vengeance-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("gods-vengeance-target");
|
|
|
|
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
targetId: PLAYER_AURA_ENTITY_ID,
|
|
definition: expect.objectContaining({ utilityTags: [ROM_KNIGHT_GODS_VENGEANCE_TAG] }),
|
|
}),
|
|
]));
|
|
expect(useCombatStore.getState().castAbility("rom-knight-primary-1", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().comboPoints).toBe(4);
|
|
|
|
const castTime = godsVengeanceCastTime(preservesSeal);
|
|
expect(useCombatStore.getState().castAbility("rom-knight-general-0", undefined, castTime))
|
|
.toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().comboPoints).toBe(preservesSeal ? 4 : 0);
|
|
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "dot"
|
|
&& effect.targetId === "gods-vengeance-target" && effect.tag === "rom-holy-seal"))
|
|
.toBe(preservesSeal);
|
|
expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage"
|
|
&& event.abilityId === "rom-knight-general-0" && event.targetActorId === "gods-vengeance-target"
|
|
&& event.effectiveAmount > 0)).toBe(true);
|
|
});
|
|
|
|
it("reduces Holy Seal cooldown by one second when Shield of Valor parries", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", raceId: "rom-human", level: 80, role: "tank",
|
|
});
|
|
const holySeal = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 490149)!;
|
|
const shieldOfValor = abilityById("rom-knight-primary-10")!;
|
|
expect(shieldOfValor).toMatchObject({ dbcSpellId: 490468 });
|
|
|
|
useCombatStore.setState((state) => ({
|
|
gearStats: { ...state.gearStats, parryRating: 100_000 },
|
|
}));
|
|
expect(useCombatStore.getState().castAbility(shieldOfValor.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
targetId: PLAYER_AURA_ENTITY_ID,
|
|
definition: expect.objectContaining({ utilityTags: expect.arrayContaining([ROM_KNIGHT_SHIELD_OF_VALOR_TAG]) }),
|
|
}),
|
|
]));
|
|
useCombatStore.setState((state) => ({
|
|
cooldowns: { ...state.cooldowns, [holySeal.id]: 25_000 },
|
|
}));
|
|
|
|
useCombatStore.getState().registerMob("shield-of-valor-attacker", {
|
|
name: "Shield of Valor Attacker",
|
|
maxHealth: 100_000,
|
|
attacks: [DEFAULT_ENEMY_ATTACK],
|
|
});
|
|
setMobPosition("shield-of-valor-attacker", [2, 0, 0]);
|
|
useCombatStore.getState().damageMob("shield-of-valor-attacker", 1, 1_100);
|
|
let parryAt: number | null = null;
|
|
for (let now = 1_450; now < 16_000; now += 100) {
|
|
useCombatStore.getState().advanceMobCombat(now);
|
|
if (useCombatStore.getState().lastCombatResult?.parried) {
|
|
parryAt = now;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(parryAt).not.toBeNull();
|
|
expect(useCombatStore.getState().cooldowns[holySeal.id]).toBe(24_000);
|
|
});
|
|
|
|
it("reduces only the party Knight's Holy Seal cooldown on an active Shield of Valor parry", () => {
|
|
const runIncomingCase = (
|
|
suffix: string,
|
|
auraActive: boolean,
|
|
parryRating: number,
|
|
wantedOutcome: "parry" | "hit",
|
|
) => {
|
|
const seed = `shield-of-valor-party-${suffix}`;
|
|
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 });
|
|
const knight = createConfiguredPartyMember({
|
|
name: "Shield of Valor Knight",
|
|
classId: "rom-knight",
|
|
specialization: "Primary",
|
|
role: "tank",
|
|
}, 80, 0, seed);
|
|
const knightAbilities = abilitiesForClass("rom-knight");
|
|
const holySeal = knightAbilities.find((ability) => ability.source === "runewaker"
|
|
&& ability.dbcSpellId === 490_149)!;
|
|
const shieldOfValor = knightAbilities.find((ability) => ability.source === "runewaker"
|
|
&& ability.dbcSpellId === 490_468)!;
|
|
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "tank", 2, "rom");
|
|
usePartyStore.setState({ members: [{
|
|
...knight,
|
|
gearStats: { ...knight.gearStats, parryRating },
|
|
cooldowns: { ...knight.cooldowns, [holySeal.id]: 25_000 },
|
|
}] });
|
|
registerPartyRuntimePosition(knight.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
|
|
if (auraActive) {
|
|
const shieldAura = shieldOfValor.effects.find((effect) => effect.kind === "apply-aura");
|
|
if (!shieldAura || shieldAura.kind !== "apply-aura") throw new Error("Shield of Valor has no aura row");
|
|
useCombatStore.getState().applyActorAura(knight.id, knight.id, shieldOfValor, shieldAura, 1_000);
|
|
}
|
|
|
|
const mobId = `shield-of-valor-party-attacker-${suffix}`;
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Shield of Valor party attacker",
|
|
maxHealth: 100_000,
|
|
xpReward: 0,
|
|
attacks: [DEFAULT_ENEMY_ATTACK],
|
|
});
|
|
setMobPosition(mobId, [2, 0, 0]);
|
|
useCombatStore.getState().damageMob(mobId, 1, 1_100, {
|
|
actorId: knight.id,
|
|
role: "tank",
|
|
});
|
|
|
|
let outcome: string | undefined;
|
|
for (let now = 1_450; now < 16_000; now += 100) {
|
|
useCombatStore.getState().advanceMobCombat(now);
|
|
const event = [...useCombatStore.getState().combatEvents].reverse().find((entry) => (
|
|
entry.sourceActorId === mobId && entry.targetActorId === knight.id && entry.occurredAt === now
|
|
));
|
|
if (event?.outcome === wantedOutcome) {
|
|
outcome = event.outcome;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
outcome,
|
|
cooldown: useCombatStore.getState().actors[knight.id]?.cooldowns[holySeal.id],
|
|
};
|
|
};
|
|
|
|
const activeParry = runIncomingCase("active-parry", true, 100_000, "parry");
|
|
expect(activeParry.outcome).toBe("parry");
|
|
expect(activeParry.cooldown).toBe(24_000);
|
|
|
|
const inactiveParry = runIncomingCase("inactive-parry", false, 100_000, "parry");
|
|
expect(inactiveParry.outcome).toBe("parry");
|
|
expect(inactiveParry.cooldown).toBe(25_000);
|
|
|
|
const activeHit = runIncomingCase("active-hit", true, 0, "hit");
|
|
expect(activeHit.outcome).toBe("hit");
|
|
expect(activeHit.cooldown).toBe(25_000);
|
|
});
|
|
|
|
it("activates the source-backed Evil Shackles passive state", () => {
|
|
const passive = abilityById("rom-knight-rom-scout-elite-60")!;
|
|
expect(passive).toMatchObject({ dbcSpellId: 499926, passive: true });
|
|
expect(passive.effects).toEqual([
|
|
expect.objectContaining({
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({
|
|
durationMs: null,
|
|
utilityTags: ["rom-evil-shackles"],
|
|
}),
|
|
}),
|
|
]);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80,
|
|
});
|
|
expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
definition: expect.objectContaining({ utilityTags: ["rom-evil-shackles"] }),
|
|
}),
|
|
]));
|
|
});
|
|
|
|
it("lets Evil Shackles root non-Undead/Demon targets for half duration", () => {
|
|
const holyChains = abilityById("rom-knight-rom-scout-elite-25")!;
|
|
const rootDuration = (targetTags: readonly string[]) => {
|
|
const root = romEffectsForCast(holyChains, {
|
|
sealStacks: 0,
|
|
bleedCount: 0,
|
|
charged: false,
|
|
mainHandDagger: false,
|
|
casterMaxHealth: 2_000,
|
|
targetHealthPercent: 1,
|
|
passiveTags: ["rom-evil-shackles"],
|
|
targetTags,
|
|
}).find((effect) => effect.kind === "apply-aura" && effect.control?.kind === "root");
|
|
return root?.kind === "apply-aura" ? root.aura.durationMs : undefined;
|
|
};
|
|
expect(rootDuration(["rom-target-undead-demon"])).toBe(10_000);
|
|
expect(rootDuration(["rom-target-living"])).toBe(5_000);
|
|
expect(rootDuration([])).toBe(5_000);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 50,
|
|
});
|
|
useCombatStore.getState().registerMob("shackles-target", {
|
|
name: "Living target", maxHealth: 100_000, creatureFamily: "living",
|
|
});
|
|
setMobPosition("shackles-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("shackles-target");
|
|
expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 1_000)).toMatchObject({
|
|
ok: false, reason: "condition-not-met",
|
|
});
|
|
useCombatStore.getState().registerMob("shackles-target", {
|
|
name: "Unknown target", maxHealth: 100_000, creatureFamily: "unknown",
|
|
});
|
|
expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 2_000)).toMatchObject({
|
|
ok: false, reason: "condition-not-met",
|
|
});
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80,
|
|
});
|
|
useCombatStore.getState().registerMob("shackles-target", {
|
|
name: "Living target", maxHealth: 100_000, creatureFamily: "living",
|
|
});
|
|
setMobPosition("shackles-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("shackles-target");
|
|
expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 3_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().mobs["shackles-target"]!.statuses).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "root", endsAt: 8_000 }),
|
|
]));
|
|
});
|
|
});
|