507 lines
22 KiB
TypeScript
507 lines
22 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { abilitiesForCharacter, abilityById } from "./abilityCatalog";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
|
|
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
|
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
|
|
import { resolveDamageValue, resolveStatValue } from "./combatAuras";
|
|
import { romAbilityNeedsRangedWeapon } from "./romCombatMechanics";
|
|
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
|
import {
|
|
overrideRomScoutRogueEffects,
|
|
ROM_SCOUT_MAGE_WIND_RETURN_TAG,
|
|
ROM_SCOUT_PRIEST_REVIVAL_ENGRAVING_TAG,
|
|
ROM_SCOUT_WARDEN_FOREST_GUIDANCE_TAG,
|
|
romScoutRogueProcActionSpec,
|
|
} from "./romScoutRogueOverrides";
|
|
|
|
describe("RoM Scout ranged weapon masteries", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
});
|
|
|
|
it.each(["bow", "crossbow"] as const)(
|
|
"applies Ranged Weapon Mastery and Speed Shooting Mastery once with an equipped %s",
|
|
(weaponType) => {
|
|
const rangedWeapon = {
|
|
instanceId: `scout-mastery-${weaponType}`,
|
|
itemId: 1,
|
|
itemKey: `scout-mastery-${weaponType}`,
|
|
name: `Test ${weaponType}`,
|
|
quality: "uncommon" as const,
|
|
inventorySlot: "ranged",
|
|
armorType: null,
|
|
weaponType,
|
|
itemLevel: 1,
|
|
sourceItemLevel: 1,
|
|
stats: EMPTY_ITEM_STATS,
|
|
quantity: 1,
|
|
sourceBossKey: "test",
|
|
sourceBossName: "test",
|
|
acquiredAt: 1,
|
|
};
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-scout",
|
|
raceId: "rom-human",
|
|
level: 50,
|
|
inventory: [rangedWeapon],
|
|
equipment: { player: { ranged: rangedWeapon.instanceId } },
|
|
});
|
|
|
|
const state = useCombatStore.getState();
|
|
expect(state.equipment.player?.ranged).toBe(rangedWeapon.instanceId);
|
|
const playerAuras = state.auras.filter((aura) => aura.targetId === PLAYER_AGGRO_ID);
|
|
const rangedDamageAuras = playerAuras.filter((aura) => aura.definition.modifiers?.some((modifier) => (
|
|
modifier.kind === "damage"
|
|
&& modifier.direction === "dealt"
|
|
&& modifier.attackKind === "ranged"
|
|
&& modifier.operation === "percent"
|
|
)));
|
|
|
|
expect(rangedDamageAuras).toHaveLength(1);
|
|
expect(resolveDamageValue(100, playerAuras, { direction: "dealt", school: "physical", attackKind: "ranged" }))
|
|
.toBeCloseTo(108);
|
|
expect(resolveStatValue(0, playerAuras, "ranged-hit")).toBe(8);
|
|
expect(resolveStatValue(0, playerAuras, "physical-hit")).toBe(0);
|
|
expect(resolveStatValue(0, playerAuras, "ranged-haste")).toBe(11);
|
|
|
|
const masteryDamageModifiers = abilityById("rom-scout-primary-2")!.effects.flatMap((effect) => (
|
|
effect.kind === "apply-aura" ? effect.aura.modifiers ?? [] : []
|
|
)).filter((modifier) => modifier.kind === "damage"
|
|
&& modifier.direction === "dealt"
|
|
&& modifier.attackKind === "ranged"
|
|
&& modifier.operation === "percent");
|
|
const speedMasteryModifiers = abilityById("rom-scout-general-8")!.effects.flatMap((effect) => (
|
|
effect.kind === "apply-aura" ? effect.aura.modifiers ?? [] : []
|
|
)).filter((modifier) => modifier.kind === "stat" && modifier.stat === "ranged-haste");
|
|
expect(masteryDamageModifiers).toHaveLength(1);
|
|
expect(speedMasteryModifiers).toHaveLength(1);
|
|
},
|
|
);
|
|
|
|
it("Cripple Joint roots on Joint Blow hits, not on other Scout attacks", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-scout",
|
|
raceId: "rom-human",
|
|
level: 50,
|
|
});
|
|
for (const id of ["shot-target", "joint-target"]) {
|
|
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000_000 });
|
|
setMobPosition(id, [3, 0, 0]);
|
|
}
|
|
|
|
// Make the proc deterministic here; its catalog regression separately checks the sourced 55% chance.
|
|
useCombatStore.setState((state) => ({
|
|
auras: state.auras.map((aura) => aura.definition.utilityTags?.includes("rom-cripple-joint")
|
|
? {
|
|
...aura,
|
|
definition: {
|
|
...aura.definition,
|
|
procs: aura.definition.procs?.map((proc) => ({ ...proc, chance: 1 })),
|
|
},
|
|
}
|
|
: aura),
|
|
}));
|
|
|
|
useCombatStore.getState().selectMob("shot-target");
|
|
expect(useCombatStore.getState().castAbility("rom-scout-general-0", undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().mobs["shot-target"]?.statuses.some((status) => status.kind === "root")).toBe(false);
|
|
|
|
useCombatStore.getState().selectMob("joint-target");
|
|
expect(useCombatStore.getState().castAbility("rom-scout-general-3", undefined, 2_600)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().mobs["joint-target"]?.statuses).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "root", endsAt: 7_600, sourceAbilityId: "rom-scout-general-3" }),
|
|
]));
|
|
});
|
|
|
|
it.each([
|
|
["rom-scout", "rom-warrior", 25, "rom-enhanced-throat-attack"],
|
|
["rom-rogue", "rom-scout", 20, "rom-slit-throat"],
|
|
] as const)("applies the local four-second Throat Attack silence for %s/%s", (
|
|
classId, secondaryClassId, level, passiveTag,
|
|
) => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId, secondaryClassId, raceId: "rom-human", level,
|
|
});
|
|
useCombatStore.getState().registerMob("throat-silence-target", { name: "Target", level: 1, maxHealth: 100_000 });
|
|
setMobPosition("throat-silence-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("throat-silence-target");
|
|
|
|
const throatAttack = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 490428);
|
|
expect(throatAttack).toBeDefined();
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes(passiveTag))).toBe(true);
|
|
expect(useCombatStore.getState().castAbility(throatAttack!.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().lastCombatResult?.landed).toBe(true);
|
|
expect(useCombatStore.getState().mobs["throat-silence-target"]?.statuses).toContainEqual(
|
|
expect.objectContaining({ kind: "silence", endsAt: 5_000, sourceAbilityId: throatAttack!.id }),
|
|
);
|
|
});
|
|
|
|
it("limits the paired Throat Attack silence proc to Throat Attack hits", () => {
|
|
expect(romScoutRogueProcActionSpec("rom-enhanced-throat-attack", 490428, false))
|
|
.toMatchObject({ kind: "status", status: "silence", durationMs: 4_000, provenance: "source" });
|
|
expect(romScoutRogueProcActionSpec("rom-slit-throat", 490428, false))
|
|
.toMatchObject({ kind: "status", status: "silence", durationMs: 4_000, provenance: "source" });
|
|
expect(romScoutRogueProcActionSpec("rom-slit-throat", 490423, false)).toBeNull();
|
|
});
|
|
|
|
it("reduces Snipe's cast by one second only with Scout/Priest Sniper", () => {
|
|
const bow = {
|
|
instanceId: "scout-sniper-bow",
|
|
itemId: 1,
|
|
itemKey: "scout-sniper-bow",
|
|
name: "Test bow",
|
|
quality: "uncommon" as const,
|
|
inventorySlot: "ranged",
|
|
armorType: null,
|
|
weaponType: "bow" as const,
|
|
itemLevel: 1,
|
|
sourceItemLevel: 1,
|
|
stats: EMPTY_ITEM_STATS,
|
|
quantity: 1,
|
|
sourceBossKey: "test",
|
|
sourceBossName: "test",
|
|
acquiredAt: 1,
|
|
};
|
|
const prepareSnipe = (secondaryClassId?: "rom-priest", targetId = "sniper-target") => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-scout",
|
|
...(secondaryClassId ? { secondaryClassId } : {}),
|
|
raceId: "rom-human",
|
|
level: 25,
|
|
inventory: [bow],
|
|
equipment: { player: { ranged: bow.instanceId } },
|
|
});
|
|
useCombatStore.getState().registerMob(targetId, { name: targetId, maxHealth: 100_000 });
|
|
setMobPosition(targetId, [3, 0, 0]);
|
|
useCombatStore.getState().selectMob(targetId);
|
|
};
|
|
|
|
prepareSnipe("rom-priest");
|
|
expect(useCombatStore.getState().castAbility("rom-scout-primary-7", undefined, 1_000)).toMatchObject({ ok: true });
|
|
const withSniper = useCombatStore.getState().activeCast?.originalDurationMs;
|
|
expect(withSniper).toBeGreaterThan(0);
|
|
|
|
prepareSnipe(undefined, "sniper-base-target");
|
|
expect(useCombatStore.getState().castAbility("rom-scout-primary-7", undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().activeCast?.originalDurationMs).toBe(withSniper! + 1_000);
|
|
});
|
|
|
|
it("marks all sourced Scout projectile abilities as requiring a ranged weapon", () => {
|
|
for (const [abilityId, sourceSpellId] of [
|
|
["rom-scout-general-0", 490423], // Shot
|
|
["rom-scout-general-2", 491292], // Vampire Arrows
|
|
["rom-scout-general-3", 490420], // Joint Blow
|
|
["rom-scout-primary-1", 491128], // Wind Arrows
|
|
["rom-scout-primary-57", 492589], // Autoshot
|
|
] as const) {
|
|
const ability = abilityById(abilityId);
|
|
expect(ability?.dbcSpellId, abilityId).toBe(sourceSpellId);
|
|
expect(romAbilityNeedsRangedWeapon(ability!), abilityId).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("leaves Blood Arrow and Accurate Targeting outside the ranged weapon requirement", () => {
|
|
const bloodArrow = abilitiesForCharacter("rom-scout", null).find((candidate) => candidate.dbcSpellId === 490434);
|
|
const accurateTargeting = abilitiesForCharacter("rom-scout", "rom-warrior").find((candidate) => candidate.dbcSpellId === 493006);
|
|
expect(bloodArrow).toBeDefined();
|
|
expect(accurateTargeting).toBeDefined();
|
|
expect(romAbilityNeedsRangedWeapon(bloodArrow!)).toBe(false);
|
|
expect(romAbilityNeedsRangedWeapon(accurateTargeting!)).toBe(false);
|
|
});
|
|
|
|
it("rejects Scout secondary projectile skills without a ranged weapon for the player", () => {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-warrior",
|
|
secondaryClassId: "rom-scout",
|
|
raceId: "rom-human",
|
|
level: 80,
|
|
inventory: [],
|
|
equipment: { player: {} },
|
|
});
|
|
useCombatStore.getState().registerMob("ranged-weapon-target", { name: "Target", maxHealth: 100_000 });
|
|
setMobPosition("ranged-weapon-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("ranged-weapon-target");
|
|
useCombatStore.setState((state) => ({
|
|
resource: 1_000,
|
|
maxResource: 1_000,
|
|
resourcePools: { ...state.resourcePools, focus: 1_000 },
|
|
maxResourcePools: { ...state.maxResourcePools, focus: 1_000 },
|
|
}));
|
|
const player = useCombatStore.getState();
|
|
expect(player.abilities.map((ability) => ability.id)).toEqual(expect.arrayContaining([
|
|
"rom-scout-general-0", "rom-scout-general-2", "rom-scout-general-3",
|
|
]));
|
|
expect(resolvedEquipmentLoadout(player.equipment, player.inventory, {
|
|
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
|
ownerKind: "player",
|
|
classId: player.classId,
|
|
secondaryClassId: player.secondaryClassId,
|
|
specializationId: player.specializationId,
|
|
role: player.gearRole,
|
|
level: player.level,
|
|
}).ranged).toBeUndefined();
|
|
|
|
for (const abilityId of [
|
|
"rom-scout-general-0", // Shot (490423)
|
|
"rom-scout-general-2", // Vampire Arrows (491292)
|
|
"rom-scout-general-3", // Joint Blow (490420)
|
|
]) {
|
|
expect(useCombatStore.getState().castAbility(abilityId, undefined, 1_000), abilityId)
|
|
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
|
}
|
|
});
|
|
|
|
it("does not let a Scout secondary companion cast Shot without a ranged weapon", () => {
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 80 });
|
|
const player = useCombatStore.getState();
|
|
usePartyStore.getState().initializeParty("scout-companion-ranged-guard", 80, player.health, "damage");
|
|
const scout = createConfiguredPartyMember({
|
|
name: "Warrior Scout companion",
|
|
classId: "rom-warrior",
|
|
secondaryClassId: "rom-scout",
|
|
raceId: "rom-human",
|
|
specialization: "Primary",
|
|
role: "damage",
|
|
}, 80, 0, "scout-companion-ranged-guard");
|
|
const shot = abilitiesForCharacter("rom-warrior", "rom-scout").find((ability) => ability.dbcSpellId === 490423);
|
|
if (!shot) throw new Error("Missing Scout Shot");
|
|
const cooldowns = Object.fromEntries(abilitiesForCharacter("rom-warrior", "rom-scout").map((ability) => [ability.id, 60_000]));
|
|
usePartyStore.setState({ members: [{
|
|
...scout,
|
|
resource: scout.maxResource,
|
|
resourcePools: { ...scout.resourcePools, focus: scout.maxResource },
|
|
cooldowns: { ...cooldowns, [shot.id]: 0 },
|
|
}] });
|
|
const combat = useCombatStore.getState();
|
|
expect(resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
|
|
ownerId: scout.id,
|
|
ownerKind: "party",
|
|
classId: scout.classId,
|
|
secondaryClassId: scout.secondaryClassId,
|
|
specializationId: scout.specializationId,
|
|
role: scout.role,
|
|
level: scout.level,
|
|
}).ranged).toBeUndefined();
|
|
registerPartyRuntimePosition(scout.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
useCombatStore.getState().registerMob("scout-companion-target", { name: "Target", maxHealth: 100_000 });
|
|
setMobPosition("scout-companion-target", [3, 0, 0]);
|
|
useCombatStore.getState().damageMob("scout-companion-target", 1, 100);
|
|
useCombatStore.getState().selectMob("scout-companion-target");
|
|
updatePartyRuntimeTargetVisibility(scout.id, "scout-companion-target", true);
|
|
|
|
advanceDungeonPartyCombat(1_000);
|
|
|
|
expect(useCombatStore.getState().combatEvents.some((event) => event.sourceActorId === scout.id
|
|
&& event.abilityId === shot.id)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("RoM Scout paired passive source markers", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
secondaryClassId: "rom-warden" as const,
|
|
sourceSkillId: 499575,
|
|
triggerSkillId: 499576,
|
|
name: "Forest Guidance",
|
|
tag: ROM_SCOUT_WARDEN_FOREST_GUIDANCE_TAG,
|
|
},
|
|
{
|
|
secondaryClassId: "rom-mage" as const,
|
|
sourceSkillId: 499548,
|
|
triggerSkillId: 499549,
|
|
name: "Wind Return",
|
|
tag: ROM_SCOUT_MAGE_WIND_RETURN_TAG,
|
|
},
|
|
{
|
|
secondaryClassId: "rom-priest" as const,
|
|
sourceSkillId: 499552,
|
|
triggerSkillId: 490443,
|
|
name: "Revival Engraving",
|
|
tag: ROM_SCOUT_PRIEST_REVIVAL_ENGRAVING_TAG,
|
|
},
|
|
])("keeps $name source components off the owner until their trigger is supported", ({
|
|
secondaryClassId, sourceSkillId, triggerSkillId, name, tag,
|
|
}) => {
|
|
const buildAbilities = abilitiesForCharacter("rom-scout", secondaryClassId);
|
|
const passive = buildAbilities.find((ability) => ability.dbcSpellId === sourceSkillId);
|
|
expect(passive?.passive).toBe(true);
|
|
expect(buildAbilities.some((ability) => ability.dbcSpellId === triggerSkillId)).toBe(false);
|
|
|
|
const marker = passive?.effects.find((effect) => effect.kind === "apply-aura");
|
|
expect(marker?.kind).toBe("apply-aura");
|
|
if (marker?.kind !== "apply-aura") return;
|
|
expect(marker.aura.utilityTags).toContain(tag);
|
|
expect(marker.aura.modifiers ?? []).toEqual([]);
|
|
expect(marker.aura.procs ?? []).toEqual([]);
|
|
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-scout",
|
|
secondaryClassId,
|
|
raceId: "rom-elf",
|
|
level: 79,
|
|
});
|
|
const ownerAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID);
|
|
const activeMarker = ownerAuras.find((aura) => aura.definition.utilityTags?.includes(tag));
|
|
expect(activeMarker).toBeDefined();
|
|
expect(activeMarker?.definition.modifiers ?? []).toEqual([]);
|
|
expect(ownerAuras.some((aura) => aura.definition.name.startsWith(name)
|
|
&& (aura.definition.modifiers?.length ?? 0) > 0)).toBe(false);
|
|
});
|
|
|
|
it("limits suppression to each exact passive row and pair", () => {
|
|
const translated = [{
|
|
kind: "apply-aura" as const,
|
|
recipient: "caster" as const,
|
|
aura: {
|
|
id: "source-forest-guidance-test",
|
|
name: "Forest Guidance source component",
|
|
disposition: "buff" as const,
|
|
durationMs: 10_000,
|
|
modifiers: [{
|
|
kind: "damage" as const,
|
|
direction: "taken" as const,
|
|
school: "physical" as const,
|
|
operation: "percent" as const,
|
|
value: 0.02,
|
|
}],
|
|
},
|
|
sourceEffectType: -1,
|
|
sourceAuraType: 143,
|
|
miscValue: -2,
|
|
triggerSpellId: 623117,
|
|
}];
|
|
const baseRow = {
|
|
name: "Forest Guidance",
|
|
description: "A source component test row.",
|
|
passive: true,
|
|
};
|
|
|
|
for (const [sourceSkillId, secondaryClassId, triggerSkillId] of [
|
|
[499575, "rom-warden", 499576],
|
|
[499548, "rom-mage", 499549],
|
|
[499552, "rom-priest", 490443],
|
|
] as const) {
|
|
expect(overrideRomScoutRogueEffects({
|
|
...baseRow, sourceSkillId, classId: "rom-scout", secondaryClassId: "rom-rogue",
|
|
}, translated)).toBeNull();
|
|
expect(overrideRomScoutRogueEffects({
|
|
...baseRow, sourceSkillId: triggerSkillId, classId: "rom-scout", secondaryClassId,
|
|
}, translated)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("RoM Warrior/Scout Calm", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
clearPartyRuntimeRegistry();
|
|
resetPartyRuntimeClock();
|
|
});
|
|
|
|
function prepareWarriorScoutTarget(targetId: string, targetLevel: number) {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-warrior",
|
|
secondaryClassId: "rom-scout",
|
|
raceId: "rom-human",
|
|
level: 80,
|
|
});
|
|
useCombatStore.setState((state) => ({
|
|
resource: 1_000,
|
|
maxResource: 1_000,
|
|
resourcePools: { ...state.resourcePools, focus: 1_000, rage: 1_000 },
|
|
maxResourcePools: { ...state.maxResourcePools, focus: 1_000, rage: 1_000 },
|
|
}));
|
|
useCombatStore.getState().registerMob(targetId, {
|
|
name: targetId,
|
|
level: targetLevel,
|
|
maxHealth: 1_000_000,
|
|
xpReward: 0,
|
|
});
|
|
setMobPosition(targetId, [3, 0, 0]);
|
|
useCombatStore.getState().selectMob(targetId);
|
|
const skullBreaker = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 491328);
|
|
if (!skullBreaker) throw new Error("Missing Warrior/Scout Skull Breaker");
|
|
return skullBreaker;
|
|
}
|
|
|
|
function calmAura() {
|
|
return useCombatStore.getState().auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes("rom-warrior-scout-calm"));
|
|
}
|
|
|
|
it("grants 20 physical hit for 15 seconds when Skull Breaker misses", () => {
|
|
const skullBreaker = prepareWarriorScoutTarget("calm-miss-target", 255);
|
|
const player = useCombatStore.getState();
|
|
const basePhysicalHit = resolveStatValue(0, player.auras.filter((entry) => entry.targetId === PLAYER_AURA_ENTITY_ID), "physical-hit");
|
|
expect(player.abilities.some((ability) => ability.dbcSpellId === 498032 && ability.passive)).toBe(true);
|
|
|
|
expect(player.castAbility(skullBreaker.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().lastCombatResult?.landed).toBe(false);
|
|
const aura = calmAura();
|
|
expect(aura?.expiresAt).toBe(16_000);
|
|
expect(resolveStatValue(0, useCombatStore.getState().auras.filter((entry) => entry.targetId === PLAYER_AURA_ENTITY_ID), "physical-hit"))
|
|
.toBe(basePhysicalHit + 20);
|
|
|
|
useCombatStore.getState().tick(0, 16_001);
|
|
expect(calmAura()).toBeUndefined();
|
|
expect(resolveStatValue(0, useCombatStore.getState().auras.filter((entry) => entry.targetId === PLAYER_AURA_ENTITY_ID), "physical-hit"))
|
|
.toBe(basePhysicalHit);
|
|
});
|
|
|
|
it("does not grant Calm when Skull Breaker lands", () => {
|
|
const skullBreaker = prepareWarriorScoutTarget("calm-landed-target", 1);
|
|
useCombatStore.setState((state) => ({
|
|
mobs: {
|
|
...state.mobs,
|
|
"calm-landed-target": {
|
|
...state.mobs["calm-landed-target"]!,
|
|
characterDefender: {
|
|
level: 1,
|
|
armor: 0,
|
|
dodgePercent: 0,
|
|
parryPercent: 0,
|
|
blockPercent: 0,
|
|
blockValue: 0,
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
expect(useCombatStore.getState().castAbility(skullBreaker.id, undefined, 1_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().lastCombatResult?.landed).toBe(true);
|
|
expect(calmAura()).toBeUndefined();
|
|
});
|
|
|
|
it("does not grant Calm for another failed Warrior attack or without the Warrior/Scout pair", () => {
|
|
const skullBreaker = prepareWarriorScoutTarget("calm-other-ability-target", 255);
|
|
const probingAttack = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 491133);
|
|
if (!probingAttack) throw new Error("Missing Probing Attack");
|
|
expect(useCombatStore.getState().castAbility(probingAttack.id, undefined, 3_000)).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().lastCombatResult?.landed).toBe(false);
|
|
expect(calmAura()).toBeUndefined();
|
|
|
|
useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 80 });
|
|
useCombatStore.getState().registerMob("calm-wrong-pair-target", { name: "Target", maxHealth: 1_000_000 });
|
|
setMobPosition("calm-wrong-pair-target", [3, 0, 0]);
|
|
useCombatStore.getState().selectMob("calm-wrong-pair-target");
|
|
expect(useCombatStore.getState().castAbility(skullBreaker.id, undefined, 1_000))
|
|
.toMatchObject({ ok: false, reason: "wrong-class" });
|
|
expect(calmAura()).toBeUndefined();
|
|
});
|
|
});
|