Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -1,13 +1,15 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilitiesForCharacter, isAbilityUnlocked, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { characterCombatDefender, useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { commitPartyAbilityUse } from "./partyAbilityAi";
|
||||
import { applyAura as applyCombatAura } from "./combatAuras";
|
||||
import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES } from "./romAbilityCatalog";
|
||||
import { romPriestKnightAdditionalAbilities, romPriestKnightCastEffects } from "./romPriestKnightOverrides";
|
||||
import { ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, ROM_KNIGHT_MASTER_OF_PARRY_TAG } from "./romKnightOverrides";
|
||||
|
||||
const PAIRED_SKILLS = [
|
||||
["rom-priest", "rom-knight", 15, 492355, "Enhanced Grace of Life", "passive"],
|
||||
@@ -500,4 +502,132 @@ describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => {
|
||||
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat"
|
||||
&& modifier.stat === "parry"))).toBe(true);
|
||||
});
|
||||
|
||||
it("shares 40% of one incoming party hit, then leaves later hits with the ally", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 60, role: "tank",
|
||||
});
|
||||
usePartyStore.getState().initializeParty("knight-redirect-hit", 60, useCombatStore.getState().health, "tank", 2, "rom");
|
||||
const member = usePartyStore.getState().members[0]!;
|
||||
registerPartyRuntimePosition(member.id, [3, 0, 0]);
|
||||
usePartyStore.getState().selectMember(member.id);
|
||||
|
||||
expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-50", undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
const redirectAura = useCombatStore.getState().auras.find((aura) => aura.targetId === member.id
|
||||
&& aura.definition.utilityTags?.includes("rom-disregard-danger-one-hit"));
|
||||
expect(redirectAura?.definition.redirectDamagePercent).toBe(0.4);
|
||||
expect(redirectAura?.expiresAt).toBe(11_000);
|
||||
|
||||
useCombatStore.getState().registerMob("disregard-danger-attacker", {
|
||||
name: "Disregard Danger attacker", maxHealth: 200, xpReward: 0,
|
||||
});
|
||||
setMobPosition("disregard-danger-attacker", [5, 0, 0]);
|
||||
useCombatStore.getState().damageMob("disregard-danger-attacker", 5, 1_000, {
|
||||
actorId: member.id,
|
||||
role: "tank",
|
||||
});
|
||||
|
||||
const playerBeforeFirstHit = useCombatStore.getState().health;
|
||||
const memberBeforeFirstHit = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
|
||||
expect(useCombatStore.getState().advanceMobCombat(1_450)).toBeGreaterThan(0);
|
||||
const playerLossOnFirstHit = playerBeforeFirstHit - useCombatStore.getState().health;
|
||||
const memberLossOnFirstHit = memberBeforeFirstHit
|
||||
- usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
|
||||
const firstHitDamage = playerLossOnFirstHit + memberLossOnFirstHit;
|
||||
expect(playerLossOnFirstHit).toBeGreaterThan(0);
|
||||
expect(memberLossOnFirstHit).toBeGreaterThan(0);
|
||||
expect(playerLossOnFirstHit).toBe(Math.round(firstHitDamage * 0.4));
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === redirectAura?.instanceId)).toBe(false);
|
||||
|
||||
const playerBeforeSecondHit = useCombatStore.getState().health;
|
||||
const memberBeforeSecondHit = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health;
|
||||
expect(useCombatStore.getState().advanceMobCombat(4_000)).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().health).toBe(playerBeforeSecondHit);
|
||||
expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health)
|
||||
.toBeLessThan(memberBeforeSecondHit);
|
||||
});
|
||||
|
||||
it("keeps Master of Parry through one block and removes it after the second", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-knight", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 60, role: "tank",
|
||||
});
|
||||
useCombatStore.setState((state) => ({
|
||||
resource: 100,
|
||||
resourcePools: { ...state.resourcePools, rage: 100 },
|
||||
}));
|
||||
const castResult = useCombatStore.getState().castAbility("rom-knight-rom-warrior-elite-40", undefined, 1_000);
|
||||
expect(castResult.ok, JSON.stringify(castResult)).toBe(true);
|
||||
|
||||
const masterOfParry = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AGGRO_ID
|
||||
&& aura.definition.utilityTags?.includes(ROM_KNIGHT_MASTER_OF_PARRY_TAG)
|
||||
&& aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "parry")
|
||||
));
|
||||
expect(masterOfParry?.definition.durationMs).toBe(15_000);
|
||||
expect(masterOfParry?.expiresAt).toBe(16_000);
|
||||
expect(masterOfParry?.definition.modifiers).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "stat", stat: "parry", operation: "flat", value: 5_000 }),
|
||||
]));
|
||||
expect(masterOfParry?.procStates).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ procId: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, remainingCharges: 2 }),
|
||||
]));
|
||||
const timedTriggerAura = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AGGRO_ID
|
||||
&& aura.definition.procs?.some((proc) => (
|
||||
proc.action.kind === "custom"
|
||||
&& proc.action.id === "rom-trigger-skill"
|
||||
&& proc.action.data?.spellId === 493094
|
||||
))
|
||||
));
|
||||
expect(timedTriggerAura?.definition.durationMs).toBe(15_000);
|
||||
expect(timedTriggerAura?.expiresAt).toBe(16_000);
|
||||
|
||||
// Make each incoming melee attack a deterministic successful block so the
|
||||
// test exercises the same block event used by normal combat.
|
||||
const forcedBlockAuras = applyCombatAura(useCombatStore.getState().auras, {
|
||||
definition: {
|
||||
id: "test-forced-block",
|
||||
name: "Test forced block",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [
|
||||
{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 1_000 },
|
||||
{ kind: "stat", stat: "incoming-critical-strike", operation: "flat", value: -1_000 },
|
||||
{ kind: "stat", stat: "dodge", operation: "flat", value: -1_000 },
|
||||
{ kind: "stat", stat: "parry", operation: "flat", value: -10_000 },
|
||||
{ kind: "stat", stat: "block-chance", operation: "flat", value: 1_000 },
|
||||
],
|
||||
},
|
||||
sourceId: PLAYER_AGGRO_ID,
|
||||
targetId: PLAYER_AGGRO_ID,
|
||||
now: 1_000,
|
||||
}).auras;
|
||||
useCombatStore.setState({ auras: [...forcedBlockAuras] });
|
||||
const forcedDefender = characterCombatDefender(useCombatStore.getState());
|
||||
expect(forcedDefender.dodgePercent).toBe(0);
|
||||
expect(forcedDefender.parryPercent).toBe(0);
|
||||
expect(forcedDefender.blockPercent).toBeGreaterThan(100);
|
||||
useCombatStore.getState().registerMob("master-of-parry-attacker", {
|
||||
name: "Master of Parry attacker", maxHealth: 200, xpReward: 0,
|
||||
});
|
||||
setMobPosition("master-of-parry-attacker", [3, 0, 0]);
|
||||
useCombatStore.getState().selectMob("master-of-parry-attacker");
|
||||
useCombatStore.getState().damageMob("master-of-parry-attacker", 5, 1_000);
|
||||
expect(useCombatStore.getState().mobs["master-of-parry-attacker"]).toMatchObject({ engaged: true });
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]);
|
||||
const firstMobState = useCombatStore.getState().mobs["master-of-parry-attacker"];
|
||||
expect(firstMobState?.attackRevision, JSON.stringify(firstMobState)).toBe(1);
|
||||
expect(useCombatStore.getState().lastCombatResult?.blocked).toBe(true);
|
||||
expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === masterOfParry?.instanceId)
|
||||
?.procStates).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ procId: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, remainingCharges: 1 }),
|
||||
]));
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === masterOfParry?.instanceId)).toBe(true);
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]);
|
||||
expect(useCombatStore.getState().mobs["master-of-parry-attacker"]?.attackRevision).toBe(2);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === masterOfParry?.instanceId)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user