611 lines
29 KiB
TypeScript
611 lines
29 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { ROM_ALL_ABILITIES, romTriggeredAbilityBySpellId } from "./romAbilityCatalog";
|
|
import { applyAura, absorbDamage } from "./combatAuras";
|
|
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
|
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
|
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { romEffectsForCast, type RomCastMechanics } from "./romCombatMechanics";
|
|
import {
|
|
ROM_WARDEN_BRIAR_SHIELD_TAG,
|
|
ROM_WARDEN_DAMAGE_TRANSFER_FRACTION,
|
|
ROM_WARDEN_DAMAGE_TRANSFER_TAG,
|
|
ROM_WARDEN_OAK_BLESSING_TAG,
|
|
ROM_WARDEN_TOUGH_OAK_TAG,
|
|
ROM_WARDEN_WILL_ATTACK_TAG,
|
|
romWardenBriarShieldReflectDamage,
|
|
romWardenBriarShieldReflectFraction,
|
|
romWardenBriarShieldShouldReflect,
|
|
} from "./romWardenOverrides";
|
|
|
|
const emptyContext: RomCastMechanics = {
|
|
sealStacks: 0,
|
|
bleedCount: 0,
|
|
charged: false,
|
|
mainHandDagger: false,
|
|
casterMaxHealth: 2_000,
|
|
targetHealthPercent: 1,
|
|
};
|
|
|
|
function effectsFor(spellId: number, context: Partial<RomCastMechanics> = {}) {
|
|
const ability = ROM_ALL_ABILITIES.find((entry) => entry.dbcSpellId === spellId)!;
|
|
expect(ability).toBeDefined();
|
|
return romEffectsForCast(ability, { ...emptyContext, ...context });
|
|
}
|
|
|
|
function initializeRomCharacter(
|
|
classId: "rom-warden" | "rom-warrior" | "rom-druid",
|
|
secondaryClassId?: "rom-warden" | "rom-warrior" | "rom-mage",
|
|
) {
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId,
|
|
...(secondaryClassId ? { secondaryClassId } : {}),
|
|
raceId: classId === "rom-warrior" ? "rom-human" : "rom-elf",
|
|
level: 80,
|
|
});
|
|
useCombatStore.setState((state) => ({
|
|
resource: 1_000,
|
|
maxResource: 1_000,
|
|
resourcePools: { ...state.resourcePools, mana: 1_000, rage: 1_000, "nature-power": 0 },
|
|
maxResourcePools: { ...state.maxResourcePools, mana: 1_000, rage: 1_000, "nature-power": 100 },
|
|
}));
|
|
}
|
|
|
|
describe("RoM Warden and Warden-linked source behaviors", () => {
|
|
beforeEach(() => {
|
|
clearPartyRuntimeRegistry();
|
|
clearMobRuntimeRegistry();
|
|
useCombatStore.setState({ playerPosition: [0, 0, 0] });
|
|
});
|
|
|
|
it("casts Briar Shield as its source-backed defense aura without an immediate damage hit", () => {
|
|
initializeRomCharacter("rom-warden");
|
|
const ability = ROM_ALL_ABILITIES.find((entry) => entry.dbcSpellId === 493642)!;
|
|
expect(ability.effects).toHaveLength(1);
|
|
expect(ability.effects[0]).toMatchObject({
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: {
|
|
durationMs: 900_000,
|
|
modifiers: [{ kind: "stat", stat: "armor", operation: "percent", value: 0.05 }],
|
|
utilityTags: expect.arrayContaining([ROM_WARDEN_BRIAR_SHIELD_TAG]),
|
|
},
|
|
});
|
|
expect(ability.effects.some((effect) => effect.kind === "damage")).toBe(false);
|
|
|
|
const mobId = "briar-shield-cast-target";
|
|
useCombatStore.getState().registerMob(mobId, { name: "Briar Shield target", maxHealth: 100_000 });
|
|
setMobPosition(mobId, [2, 0, 0]);
|
|
useCombatStore.getState().selectMob(mobId);
|
|
const now = 10_000;
|
|
expect(useCombatStore.getState().castAbility(ability.id, undefined, now)).toMatchObject({ ok: true });
|
|
|
|
const state = useCombatStore.getState();
|
|
expect(state.mobs[mobId]?.health).toBe(100_000);
|
|
expect(state.auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes(ROM_WARDEN_BRIAR_SHIELD_TAG))).toMatchObject({
|
|
appliedAt: now,
|
|
expiresAt: now + 900_000,
|
|
definition: {
|
|
modifiers: [{ kind: "stat", stat: "armor", operation: "percent", value: 0.05 }],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("maps Briar Shield's reflected component to source-level main-hand DPS", () => {
|
|
const weapon = { damageMin: 100, damageMax: 120, damageSchool: "physical" as const, speedMs: 2_000 };
|
|
expect(romWardenBriarShieldReflectFraction(80)).toBeCloseTo(0.074);
|
|
expect(romWardenBriarShieldReflectFraction(105)).toBeCloseTo(0.094);
|
|
expect(romWardenBriarShieldReflectDamage(80, weapon)).toBe(4);
|
|
expect(romWardenBriarShieldReflectDamage(105, weapon)).toBe(5);
|
|
expect(romWardenBriarShieldReflectDamage(80, null)).toBe(0);
|
|
|
|
const validPhysicalHit = {
|
|
auraActive: true,
|
|
landed: true,
|
|
school: "physical",
|
|
attackKind: "melee" as const,
|
|
withinRange: true,
|
|
};
|
|
expect(romWardenBriarShieldShouldReflect(validPhysicalHit)).toBe(true);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, attackKind: "ranged" })).toBe(true);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, school: "fire" })).toBe(false);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, attackKind: "spell" })).toBe(false);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, landed: false })).toBe(false);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, auraActive: false })).toBe(false);
|
|
expect(romWardenBriarShieldShouldReflect({ ...validPhysicalHit, withinRange: false })).toBe(false);
|
|
});
|
|
|
|
it("reflects landed physical projectiles, but not magic or attacks beyond 50 range", () => {
|
|
const reflectedFrom = (school: "physical" | "fire", attackerDistance: number) => {
|
|
clearMobRuntimeRegistry();
|
|
initializeRomCharacter("rom-warden");
|
|
const weapon = { damageMin: 100, damageMax: 120, damageSchool: "physical" as const, speedMs: 2_000 };
|
|
useCombatStore.setState((state) => ({
|
|
gearStats: { ...state.gearStats, meleeWeapon: weapon },
|
|
}));
|
|
const mobId = `briar-reflect-${school}-${attackerDistance}`;
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Briar Shield attacker",
|
|
maxHealth: 100_000,
|
|
level: 100,
|
|
attacks: [{
|
|
...DEFAULT_ENEMY_ATTACK,
|
|
id: `briar-reflect-${school}`,
|
|
delivery: "projectile",
|
|
school,
|
|
range: 100,
|
|
initialCooldownMs: [0, 0],
|
|
}],
|
|
});
|
|
setMobPosition(mobId, [attackerDistance, 0, 0]);
|
|
useCombatStore.getState().selectMob(mobId);
|
|
expect(useCombatStore.getState().castAbility("rom-warden-general-1", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-11", undefined, 3_000))
|
|
.toMatchObject({ ok: true });
|
|
useCombatStore.getState().damageMob(mobId, 1, 3_000);
|
|
const healthBeforeIncomingHit = useCombatStore.getState().mobs[mobId]!.health;
|
|
const incomingDamage = useCombatStore.getState().advanceMobCombat(3_450, [0, 0, 0]);
|
|
const healthAfterIncomingHit = useCombatStore.getState().mobs[mobId]!.health;
|
|
return { incomingDamage, reflectedDamage: healthBeforeIncomingHit - healthAfterIncomingHit };
|
|
};
|
|
|
|
const physicalHit = reflectedFrom("physical", 2);
|
|
expect(physicalHit.incomingDamage).toBeGreaterThan(0);
|
|
expect(physicalHit.reflectedDamage).toBe(4);
|
|
const magicHit = reflectedFrom("fire", 2);
|
|
expect(magicHit.incomingDamage).toBeGreaterThan(0);
|
|
expect(magicHit.reflectedDamage).toBe(0);
|
|
const outOfRangePhysicalHit = reflectedFrom("physical", 60);
|
|
expect(outOfRangePhysicalHit.incomingDamage).toBeGreaterThan(0);
|
|
expect(outOfRangePhysicalHit.reflectedDamage).toBe(0);
|
|
});
|
|
|
|
it("uses Briar Shield's defense aura against physical enemy attacks", () => {
|
|
const incomingDamage = (withBriarShield: boolean) => {
|
|
clearMobRuntimeRegistry();
|
|
initializeRomCharacter("rom-warden", "rom-warrior");
|
|
const mobId = "briar-shield-defense-attacker";
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Briar Shield attacker",
|
|
maxHealth: 100_000,
|
|
level: 100,
|
|
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "briar-shield-physical-swing", delivery: "projectile", initialCooldownMs: [0, 0] }],
|
|
});
|
|
setMobPosition(mobId, [2, 0, 0]);
|
|
useCombatStore.getState().selectMob(mobId);
|
|
if (withBriarShield) {
|
|
expect(useCombatStore.getState().castAbility("rom-warden-general-1", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
}
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-11", undefined, 3_000))
|
|
.toMatchObject({ ok: true });
|
|
useCombatStore.getState().damageMob(mobId, 1, 3_000);
|
|
return useCombatStore.getState().advanceMobCombat(3_450, [0, 0, 0]);
|
|
};
|
|
|
|
const withoutBriarShield = incomingDamage(false);
|
|
const withBriarShield = incomingDamage(true);
|
|
expect(withoutBriarShield).toBeGreaterThan(0);
|
|
expect(withBriarShield).toBeGreaterThan(0);
|
|
expect(withBriarShield).toBeLessThan(withoutBriarShield);
|
|
});
|
|
|
|
it("gates Will Attack's 10% damage reduction on the passive and places it on Frantic Briar's target", () => {
|
|
const passive = effectsFor(494610).find((effect) => effect.kind === "apply-aura");
|
|
expect(passive?.kind).toBe("apply-aura");
|
|
if (passive?.kind === "apply-aura") {
|
|
expect(passive.aura.utilityTags).toContain(ROM_WARDEN_WILL_ATTACK_TAG);
|
|
expect(passive.aura.modifiers ?? []).toHaveLength(0);
|
|
}
|
|
|
|
const withoutPassive = effectsFor(493391);
|
|
expect(withoutPassive.some((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 173)).toBe(false);
|
|
expect(withoutPassive.some((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 24)).toBe(true);
|
|
expect(withoutPassive.filter((effect) => effect.kind === "damage")).toHaveLength(2);
|
|
|
|
const withPassive = effectsFor(493391, { passiveTags: [ROM_WARDEN_WILL_ATTACK_TAG] });
|
|
expect(withPassive.filter((effect) => effect.kind === "damage")).toHaveLength(2);
|
|
expect(withPassive.filter((effect) => effect.kind === "apply-aura" && effect.sourceAuraType === 173)).toHaveLength(1);
|
|
expect(withPassive.find((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-warden-will-attack"))
|
|
.toMatchObject({
|
|
kind: "apply-aura",
|
|
recipient: "ability-target",
|
|
aura: {
|
|
name: "Will Attack",
|
|
disposition: "debuff",
|
|
durationMs: 10_000,
|
|
modifiers: [{ kind: "damage", direction: "dealt", attackKind: "melee", operation: "percent", value: -0.1 }],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reduces a Frantic Briar target's melee hit only while Will Attack is learned", () => {
|
|
const incomingDamage = (withWillAttack: boolean) => {
|
|
clearMobRuntimeRegistry();
|
|
initializeRomCharacter("rom-warden", "rom-warrior");
|
|
if (!withWillAttack) {
|
|
useCombatStore.setState((state) => ({
|
|
auras: state.auras.filter((aura) => !aura.definition.utilityTags?.includes(ROM_WARDEN_WILL_ATTACK_TAG)),
|
|
}));
|
|
}
|
|
const mobId = "will-attack-target";
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Will Attack target",
|
|
maxHealth: 100_000,
|
|
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "will-attack-swing", initialCooldownMs: [0, 0] }],
|
|
});
|
|
setMobPosition(mobId, [2, 0, 0]);
|
|
useCombatStore.getState().selectMob(mobId);
|
|
|
|
const hasPassive = useCombatStore.getState().auras.some((aura) => (
|
|
aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes(ROM_WARDEN_WILL_ATTACK_TAG)
|
|
));
|
|
expect(hasPassive).toBe(withWillAttack);
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-11", undefined, 1_000))
|
|
.toMatchObject({ ok: true });
|
|
const targetDebuff = useCombatStore.getState().auras.some((aura) => (
|
|
aura.targetId === mobId && aura.definition.id === "rom-warden-will-attack"
|
|
));
|
|
expect(targetDebuff).toBe(withWillAttack);
|
|
|
|
useCombatStore.getState().damageMob(mobId, 1, 1_000);
|
|
return useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]);
|
|
};
|
|
|
|
const normalDamage = incomingDamage(false);
|
|
const reducedDamage = incomingDamage(true);
|
|
expect(normalDamage).toBeGreaterThan(0);
|
|
expect(reducedDamage).toBeCloseTo(normalDamage * 0.9, 0);
|
|
});
|
|
|
|
it("reduces Warden Punch and Sepal Stab cooldowns by one second with Tough Oak", () => {
|
|
expect(effectsFor(493382).some((effect) => effect.kind === "cooldown-reset")).toBe(false);
|
|
initializeRomCharacter("rom-warden", "rom-mage");
|
|
const state = useCombatStore.getState();
|
|
const toughOak = state.abilities.find((ability) => ability.dbcSpellId === 494207);
|
|
expect(toughOak).toMatchObject({ passive: true, secondaryClassId: "rom-mage" });
|
|
expect(state.auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes(ROM_WARDEN_TOUGH_OAK_TAG))).toBe(true);
|
|
|
|
const mobId = "tough-oak-target";
|
|
useCombatStore.getState().registerMob(mobId, { name: "Tough Oak target", maxHealth: 100_000 });
|
|
setMobPosition(mobId, [3, 0, 0]);
|
|
useCombatStore.getState().selectMob(mobId);
|
|
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-2", undefined, 1_000).ok).toBe(true);
|
|
expect(useCombatStore.getState().cooldowns["rom-warden-primary-2"]).toBe(5_000);
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-14", undefined, 2_500).ok).toBe(true);
|
|
expect(useCombatStore.getState().cooldowns["rom-warden-primary-14"]).toBe(6_500);
|
|
});
|
|
|
|
it("activates Oak Blessing's attack and periodic-healing riders only with Heart of the Oak", () => {
|
|
initializeRomCharacter("rom-warden", "rom-mage");
|
|
const passive = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 494011)!;
|
|
expect(passive).toMatchObject({ passive: true, secondaryClassId: "rom-mage" });
|
|
expect(passive.effects).toHaveLength(1);
|
|
expect(passive.effects[0]).toMatchObject({
|
|
kind: "apply-aura",
|
|
aura: { utilityTags: [ROM_WARDEN_OAK_BLESSING_TAG] },
|
|
});
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.utilityTags?.includes(ROM_WARDEN_OAK_BLESSING_TAG))).toBe(true);
|
|
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
|
|
&& modifier.stat === "attack-power"))).toBe(false);
|
|
|
|
const now = 10_000;
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-10", undefined, now)).toMatchObject({ ok: true });
|
|
const stateAfterCast = useCombatStore.getState();
|
|
expect(stateAfterCast.auras.find((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID
|
|
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
|
|
&& modifier.stat === "attack-power"))).toMatchObject({
|
|
expiresAt: now + 60_000,
|
|
definition: {
|
|
modifiers: [{ kind: "stat", stat: "attack-power", operation: "percent", value: 0.05 }],
|
|
},
|
|
});
|
|
expect(stateAfterCast.effects.find((effect) => effect.kind === "hot"
|
|
&& effect.sourceAbilityId === "rom-warden-primary-10")).toMatchObject({
|
|
remainingTicks: 20,
|
|
intervalMs: 3_000,
|
|
nextTickAt: now + 3_000,
|
|
percentMaxHealth: 0.01,
|
|
});
|
|
|
|
const healthBefore = stateAfterCast.maxHealth - 200;
|
|
useCombatStore.setState({ health: healthBefore });
|
|
useCombatStore.getState().tick(3, now + 3_000);
|
|
expect(useCombatStore.getState().health).toBe(healthBefore + Math.round(stateAfterCast.maxHealth * 0.01));
|
|
});
|
|
|
|
it("keeps Nature Crystal active and makes its owner immune while blocking attack skills", () => {
|
|
initializeRomCharacter("rom-warden");
|
|
const now = Date.now();
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-6", undefined, now)).toMatchObject({ ok: true });
|
|
useCombatStore.getState().tick(4, now + 4_000);
|
|
const crystal = useCombatStore.getState().summons.find((summon) => summon.creatureId === 102325)!;
|
|
expect(crystal.expiresAt).toBeGreaterThan(now + 60_000);
|
|
const health = useCombatStore.getState().health;
|
|
expect(useCombatStore.getState().damagePlayer(100, "fire")).toBe(0);
|
|
expect(useCombatStore.getState().health).toBe(health);
|
|
|
|
useCombatStore.getState().registerMob("warden-crystal-target", { name: "Target", maxHealth: 10_000 });
|
|
useCombatStore.getState().selectMob("warden-crystal-target");
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-0", undefined, now + 6_000))
|
|
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
|
});
|
|
|
|
it("scales Crystalline Protection to 10% of each recipient's own maximum health", () => {
|
|
initializeRomCharacter("rom-warden");
|
|
usePartyStore.getState().initializeParty("warden-crystalline-protection", 80, useCombatStore.getState().health, "healer", 3, "rom");
|
|
const [first, second] = usePartyStore.getState().members;
|
|
expect(first).toBeDefined();
|
|
expect(second).toBeDefined();
|
|
const members = usePartyStore.getState().members.map((member, index) => ({
|
|
...member,
|
|
maxHealth: index === 0 ? 2_000 : 5_000,
|
|
health: index === 0 ? 1_000 : 2_500,
|
|
}));
|
|
usePartyStore.setState({ members });
|
|
members.forEach((member, index) => registerPartyRuntimePosition(member.id, [2 + index, 0, 0]));
|
|
|
|
const playerMaxHealthBeforeCast = useCombatStore.getState().maxHealth;
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-23", undefined, Date.now())).toMatchObject({ ok: true });
|
|
const auras = useCombatStore.getState().auras;
|
|
const absorbFor = (targetId: string) => auras.find((aura) => aura.targetId === targetId
|
|
&& aura.definition.name === "Crystalline Protection")?.absorbs[0]?.remaining;
|
|
expect(absorbFor(PLAYER_AURA_ENTITY_ID)).toBe(Math.round(playerMaxHealthBeforeCast * 0.1));
|
|
expect(absorbFor(members[0]!.id)).toBe(200);
|
|
expect(absorbFor(members[1]!.id)).toBe(500);
|
|
});
|
|
|
|
it("absorbs exactly two Heart of the Oak attack packets and leaves ordinary point shields unchanged", () => {
|
|
initializeRomCharacter("rom-warden");
|
|
expect(useCombatStore.getState().castAbility("rom-warden-primary-10", undefined, Date.now())).toMatchObject({ ok: true });
|
|
expect(useCombatStore.getState().damagePlayer(100, "physical")).toBe(0);
|
|
expect(useCombatStore.getState().damagePlayer(100, "fire")).toBe(0);
|
|
expect(useCombatStore.getState().damagePlayer(100, "physical")).toBeGreaterThan(0);
|
|
|
|
const flatShield = applyAura([], {
|
|
definition: {
|
|
id: "test:flat-absorb",
|
|
name: "Flat absorb",
|
|
disposition: "buff",
|
|
durationMs: 10_000,
|
|
absorbs: [{ id: "pool", amount: 5 }],
|
|
},
|
|
sourceId: "test-source",
|
|
targetId: "target",
|
|
now: 1,
|
|
}).auras;
|
|
const result = absorbDamage(flatShield, {
|
|
targetId: "target",
|
|
amount: 10,
|
|
school: "physical",
|
|
now: 2,
|
|
countsAsAttack: true,
|
|
});
|
|
expect(result).toMatchObject({ absorbed: 5, remainingDamage: 5 });
|
|
});
|
|
|
|
it("transfers 30% of incoming damage to a nearby Warden pet and disables the reduction without one", () => {
|
|
const incomingHealthLoss = (withPet: boolean, petDistance = 0) => {
|
|
initializeRomCharacter("rom-warden");
|
|
const now = Date.now();
|
|
if (withPet) {
|
|
const summonAbility = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 493333)!;
|
|
expect(useCombatStore.getState().castAbility(summonAbility.id, undefined, now)).toMatchObject({ ok: true });
|
|
const summonCast = useCombatStore.getState().activeCast;
|
|
if (summonCast) useCombatStore.getState().tick(0.1, summonCast.completesAt);
|
|
useCombatStore.setState((state) => ({
|
|
summons: state.summons.map((summon) => summon.creatureId === 102297
|
|
? { ...summon, expiresAt: now + 20_000 }
|
|
: summon),
|
|
}));
|
|
}
|
|
const petBefore = useCombatStore.getState().summons.find((summon) => summon.creatureId === 102297);
|
|
if (petBefore && petDistance > 0) {
|
|
useCombatStore.setState((state) => ({
|
|
summons: state.summons.map((summon) => summon.id === petBefore.id
|
|
? { ...summon, position: [petDistance, 0, 0] }
|
|
: summon),
|
|
}));
|
|
}
|
|
const transferAbility = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 493399)!;
|
|
expect(useCombatStore.getState().castAbility(transferAbility.id, undefined, now + 6_000)).toMatchObject({ ok: true });
|
|
const playerHealthBefore = useCombatStore.getState().health;
|
|
const petHealthBefore = useCombatStore.getState().summons.find((summon) => summon.creatureId === 102297)?.health ?? 0;
|
|
useCombatStore.getState().damagePlayer(500, "physical", 80, now + 7_000);
|
|
const state = useCombatStore.getState();
|
|
const petHealthAfter = state.summons.find((summon) => summon.creatureId === 102297)?.health ?? 0;
|
|
return {
|
|
playerDamage: playerHealthBefore - state.health,
|
|
petDamage: petHealthBefore - petHealthAfter,
|
|
};
|
|
};
|
|
|
|
const unshared = incomingHealthLoss(false);
|
|
const unsharedDamage = unshared.playerDamage;
|
|
const outOfRange = incomingHealthLoss(true, 301);
|
|
expect(unshared.petDamage).toBe(0);
|
|
expect(outOfRange.playerDamage).toBe(unsharedDamage);
|
|
expect(outOfRange.petDamage).toBe(0);
|
|
|
|
const shared = incomingHealthLoss(true);
|
|
expect(shared.petDamage).toBeGreaterThan(0);
|
|
expect(shared.playerDamage + shared.petDamage).toBeCloseTo(unsharedDamage, -1);
|
|
expect(shared.petDamage / (shared.playerDamage + shared.petDamage))
|
|
.toBeCloseTo(ROM_WARDEN_DAMAGE_TRANSFER_FRACTION, 1);
|
|
});
|
|
|
|
it("transfers party Warden damage once to a live pet within 300 range, and respects pet expiry", () => {
|
|
const resolvePartyHit = (options: {
|
|
readonly petDistance?: number;
|
|
readonly petExpiresAt?: number;
|
|
readonly transferAura?: boolean;
|
|
} = {}) => {
|
|
const seed = "warden-party-damage-transfer";
|
|
const now = 1_000;
|
|
const hitAt = 1_450;
|
|
initializeRomCharacter("rom-warden");
|
|
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health);
|
|
const warden = createConfiguredPartyMember({
|
|
name: "Warden Companion",
|
|
classId: "rom-warden",
|
|
specialization: "Primary",
|
|
specializationId: "rom-warden:primary",
|
|
talentTreeId: "rom-warden-primary",
|
|
role: "tank",
|
|
}, 80, 0, seed);
|
|
usePartyStore.setState({ members: [{ ...warden, gearStats: EMPTY_ITEM_STATS }] });
|
|
registerPartyRuntimePosition(warden.id, [0, 0, 0]);
|
|
useCombatStore.getState().synchronizeActors();
|
|
|
|
const summonAbility = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 493333)!;
|
|
const summonEffect = summonAbility.effects.find((effect) => effect.kind === "summon");
|
|
expect(summonEffect?.kind).toBe("summon");
|
|
if (!summonEffect || summonEffect.kind !== "summon") throw new Error("Warden summon effect missing");
|
|
const petId = useCombatStore.getState().summonActor(
|
|
warden.id,
|
|
summonAbility,
|
|
summonEffect,
|
|
[options.petDistance ?? 0, 0, 0],
|
|
null,
|
|
now,
|
|
);
|
|
expect(petId).toBeTruthy();
|
|
useCombatStore.setState((state) => ({
|
|
summons: state.summons.map((summon) => summon.id === petId
|
|
? { ...summon, expiresAt: options.petExpiresAt ?? now + 20_000 }
|
|
: summon),
|
|
}));
|
|
|
|
const transferEffects = effectsFor(493399).filter((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.utilityTags?.includes(ROM_WARDEN_DAMAGE_TRANSFER_TAG));
|
|
expect(transferEffects).toHaveLength(2);
|
|
expect(transferEffects.every((effect) => effect.kind === "apply-aura"
|
|
&& effect.aura.durationMs === 10_000)).toBe(true);
|
|
if (options.transferAura !== false) {
|
|
useCombatStore.setState((state) => ({ auras: transferEffects.reduce((auras, effect) => {
|
|
if (effect.kind !== "apply-aura") return auras;
|
|
return applyAura(auras, {
|
|
definition: effect.aura,
|
|
sourceId: warden.id,
|
|
targetId: warden.id,
|
|
now,
|
|
}).auras;
|
|
}, state.auras) }));
|
|
}
|
|
|
|
const mobId = "warden-party-transfer-attacker";
|
|
useCombatStore.getState().registerMob(mobId, {
|
|
name: "Warden transfer attacker",
|
|
maxHealth: 100_000,
|
|
level: 100,
|
|
attacks: [{ ...DEFAULT_ENEMY_ATTACK, id: "warden-party-transfer-swing", delivery: "projectile",
|
|
range: 100, initialCooldownMs: [0, 0] }],
|
|
});
|
|
setMobPosition(mobId, [2, 0, 0]);
|
|
useCombatStore.getState().damageMob(mobId, 1, now, { actorId: warden.id, role: "tank" });
|
|
const memberBefore = usePartyStore.getState().members.find((member) => member.id === warden.id)!.health;
|
|
const petBefore = useCombatStore.getState().summons.find((summon) => summon.id === petId)?.health ?? 0;
|
|
useCombatStore.getState().advanceMobCombat(hitAt, [0, 0, 0]);
|
|
const state = useCombatStore.getState();
|
|
const memberAfter = usePartyStore.getState().members.find((member) => member.id === warden.id)!.health;
|
|
const petAfter = state.summons.find((summon) => summon.id === petId)?.health ?? 0;
|
|
const hitEvents = state.combatEvents.filter((event) => event.occurredAt === hitAt
|
|
&& event.abilityId === "warden-party-transfer-swing");
|
|
return {
|
|
memberDamage: memberBefore - memberAfter,
|
|
petDamage: petBefore - petAfter,
|
|
petEvents: hitEvents.filter((event) => event.targetActorId === petId),
|
|
ownerEvents: hitEvents.filter((event) => event.targetActorId === warden.id),
|
|
petAlive: state.actors[petId!]?.alive ?? false,
|
|
};
|
|
};
|
|
|
|
const baseline = resolvePartyHit({ transferAura: false });
|
|
const inRange = resolvePartyHit({ petDistance: 300 });
|
|
expect(baseline.memberDamage).toBeGreaterThan(0);
|
|
expect(inRange.memberDamage).toBeGreaterThan(0);
|
|
expect(inRange.petDamage).toBeGreaterThan(0);
|
|
expect(inRange.memberDamage + inRange.petDamage).toBeCloseTo(baseline.memberDamage, 0);
|
|
expect(inRange.petDamage / (inRange.memberDamage + inRange.petDamage))
|
|
.toBeCloseTo(ROM_WARDEN_DAMAGE_TRANSFER_FRACTION, 1);
|
|
expect(inRange.ownerEvents).toHaveLength(1);
|
|
expect(inRange.petEvents).toHaveLength(1);
|
|
expect(inRange.petAlive).toBe(true);
|
|
|
|
const outOfRange = resolvePartyHit({ petDistance: 301 });
|
|
expect(outOfRange.memberDamage).toBe(baseline.memberDamage);
|
|
expect(outOfRange.petDamage).toBe(0);
|
|
expect(outOfRange.petEvents).toHaveLength(0);
|
|
|
|
const expired = resolvePartyHit({ petExpiresAt: 1_450 });
|
|
expect(expired.memberDamage).toBe(baseline.memberDamage);
|
|
expect(expired.petDamage).toBe(0);
|
|
expect(expired.petEvents).toHaveLength(0);
|
|
});
|
|
|
|
it("removes the unquantified Weak Point Strike bonus and models Awakening immunity and next critical", () => {
|
|
const weakPoint = effectsFor(494043);
|
|
expect(weakPoint.filter((effect) => effect.kind === "damage")).toHaveLength(1);
|
|
|
|
const awakening = effectsFor(499495);
|
|
const auras = awakening.filter((effect) => effect.kind === "apply-aura");
|
|
expect(auras).toHaveLength(2);
|
|
expect(auras).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({ durationMs: 10_000, controlImmunities: ["root", "slow"] }),
|
|
}),
|
|
expect.objectContaining({
|
|
recipient: "caster",
|
|
aura: expect.objectContaining({
|
|
durationMs: null,
|
|
utilityTags: expect.arrayContaining(["next-offensive-critical"]),
|
|
}),
|
|
}),
|
|
]));
|
|
expect(auras.some((effect) => effect.kind === "apply-aura" && effect.control)).toBe(false);
|
|
});
|
|
|
|
it("spends one Nature Power for Healing Ripple's additional heal", () => {
|
|
const baseline = effectsFor(494323, { naturePower: 0 });
|
|
const empowered = effectsFor(494323, { naturePower: 1 });
|
|
expect(baseline.filter((effect) => effect.kind === "heal")).toHaveLength(1);
|
|
expect(baseline.some((effect) => effect.kind === "resource-drain")).toBe(false);
|
|
expect(empowered.filter((effect) => effect.kind === "heal")).toHaveLength(2);
|
|
expect(empowered).toContainEqual({ kind: "resource-drain", amount: 1, burn: false, resource: "nature-power", recipient: "caster" });
|
|
});
|
|
|
|
it("scales Gift Pulse by spent Nature Power and targets nearby party members", () => {
|
|
expect(effectsFor(499936, { naturePower: 0 })).toEqual([]);
|
|
const empowered = effectsFor(499936, { naturePower: 3 });
|
|
const empoweredHeal = empowered.find((effect) => effect.kind === "heal");
|
|
expect(empoweredHeal?.kind).toBe("heal");
|
|
if (empoweredHeal?.kind === "heal") expect(empoweredHeal.percentMaxHealth).toBeCloseTo(0.3);
|
|
expect(empowered.find((effect) => effect.kind === "resource-drain"))
|
|
.toEqual({ kind: "resource-drain", amount: 3, burn: false, resource: "nature-power", recipient: "caster" });
|
|
expect(effectsFor(499936, { naturePower: 20 }).find((effect) => effect.kind === "heal"))
|
|
.toMatchObject({ percentMaxHealth: 1 });
|
|
expect(ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 499936))
|
|
.toMatchObject({ targeting: { shape: "circle", radius: 10 } });
|
|
});
|
|
|
|
it("puts Mysterious Grace modifier auras on the party, and leaves the Revival HolyHeal child unresolved", () => {
|
|
const grace = effectsFor(494372);
|
|
const modifierAuras = grace.filter((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.length);
|
|
expect(modifierAuras.length).toBeGreaterThan(0);
|
|
expect(modifierAuras.every((effect) => effect.kind === "apply-aura" && effect.scope === "party")).toBe(true);
|
|
expect(grace.filter((effect) => effect.kind === "apply-aura" && !effect.aura.modifiers?.length)
|
|
.every((effect) => effect.kind === "apply-aura" && effect.scope === undefined)).toBe(true);
|
|
expect(romTriggeredAbilityBySpellId(499545)).toBeNull();
|
|
});
|
|
});
|