forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
@@ -0,0 +1,323 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilityById } from "./abilityCatalog";
|
||||
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import {
|
||||
ROM_KNIGHT_DIVINE_PUNISHMENT_TAG,
|
||||
ROM_KNIGHT_GODS_VENGEANCE_TAG,
|
||||
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();
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
it("limits Divine Punishment's sourced 1% bonus to 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.01);
|
||||
expect(damageValues(boosted)[index]!.basePoints).toBeCloseTo(base.basePoints * 1.01);
|
||||
expect(damageValues(boosted)[index]!.pointsPerLevel).toBeCloseTo(base.pointsPerLevel * 1.01);
|
||||
}
|
||||
expect(romEffectsForCast(abilityById("rom-knight-primary-1")!, {
|
||||
...context,
|
||||
passiveTags: passiveAura?.definition.utilityTags ?? [],
|
||||
})).toEqual(romEffectsForCast(abilityById("rom-knight-primary-1")!, context));
|
||||
});
|
||||
|
||||
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("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 }),
|
||||
]));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user