Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
@@ -0,0 +1,189 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { createDeathKnightRunes } from "./combatActors";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
advanceDungeonPartyCombat,
|
||||
resetPartyRuntimeClock,
|
||||
} from "./partyRuntime";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeTargetVisibility,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog";
|
||||
|
||||
const TARGET_ID = "merciless-combat-target";
|
||||
const TARGET_MAX_HEALTH = 1_000_000;
|
||||
const CAST_AT = 1_000;
|
||||
|
||||
function setTargetHealthFraction(fraction: number): void {
|
||||
useCombatStore.setState((state) => ({ mobs: {
|
||||
...state.mobs,
|
||||
[TARGET_ID]: {
|
||||
...state.mobs[TARGET_ID],
|
||||
health: Math.round(TARGET_MAX_HEALTH * fraction),
|
||||
dead: false,
|
||||
},
|
||||
} }));
|
||||
}
|
||||
|
||||
function targetDamageEvent(sourceActorId: string, abilityId: string, occurredAt: number) {
|
||||
return useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === sourceActorId
|
||||
&& event.targetActorId === TARGET_ID
|
||||
&& event.abilityId === abilityId
|
||||
&& event.occurredAt === occurredAt
|
||||
));
|
||||
}
|
||||
|
||||
function resolvePlayerStrike(
|
||||
abilityName: string,
|
||||
rank: number,
|
||||
targetHealthFraction: number,
|
||||
includeFirstDiseaseTick = false,
|
||||
) {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80, xp: 0 });
|
||||
useCombatStore.setState((state) => ({
|
||||
talentRanks: { ...state.talentRanks, "talent-1993": rank },
|
||||
gearStats: { ...EMPTY_ITEM_STATS, hitRating: 10_000, expertiseRating: 10_000 },
|
||||
}));
|
||||
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||
name: "Merciless Combat target",
|
||||
maxHealth: TARGET_MAX_HEALTH,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(TARGET_ID);
|
||||
setTargetHealthFraction(targetHealthFraction);
|
||||
|
||||
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.name === abilityName);
|
||||
if (!ability) throw new Error(`Missing Death Knight ability: ${abilityName}`);
|
||||
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], CAST_AT).ok).toBe(true);
|
||||
const direct = targetDamageEvent(PLAYER_AGGRO_ID, ability.id, CAST_AT);
|
||||
if (!direct) throw new Error(`${abilityName} did not produce a player damage event.`);
|
||||
|
||||
let firstDiseaseTick: ReturnType<typeof targetDamageEvent>;
|
||||
if (includeFirstDiseaseTick) {
|
||||
useCombatStore.getState().tick(0.1, CAST_AT + 3_000);
|
||||
firstDiseaseTick = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage"
|
||||
&& event.sourceActorId === PLAYER_AGGRO_ID
|
||||
&& event.targetActorId === TARGET_ID
|
||||
&& event.abilityId === ability.id
|
||||
&& event.occurredAt === CAST_AT + 3_000
|
||||
));
|
||||
if (!firstDiseaseTick) throw new Error("Icy Touch did not produce its first disease tick.");
|
||||
}
|
||||
return { direct, firstDiseaseTick };
|
||||
}
|
||||
|
||||
function resolveCompanionIcyTouch(rank: number, targetHealthFraction: number) {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80, xp: 0 });
|
||||
const seed = `merciless-combat-companion-${rank}-${targetHealthFraction}`;
|
||||
const configured = createConfiguredPartyMember({
|
||||
name: "Frost Death Knight",
|
||||
classId: "death-knight",
|
||||
specialization: "Frost",
|
||||
specializationId: "death-knight:frost",
|
||||
talentTreeId: "death-knight-frost",
|
||||
talentRanks: { "talent-1993": rank },
|
||||
role: "damage",
|
||||
}, 80, 0, seed);
|
||||
const icyTouch = abilitiesForCharacter("death-knight", null).find((ability) => ability.name === "Icy Touch");
|
||||
if (!icyTouch) throw new Error("Death Knight Icy Touch is missing from the ability catalog.");
|
||||
const learnedIcyTouch = learnedAbilityForBuild(icyTouch, 80, configured.talentRanks);
|
||||
if (!learnedIcyTouch) throw new Error("Companion Death Knight did not learn Icy Touch.");
|
||||
|
||||
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2);
|
||||
usePartyStore.setState({
|
||||
members: [{
|
||||
...configured,
|
||||
gearStats: { ...EMPTY_ITEM_STATS, hitRating: 10_000, expertiseRating: 10_000 },
|
||||
resource: 100,
|
||||
maxResource: 100,
|
||||
resourcePools: { ...configured.resourcePools, "runic-power": 100 },
|
||||
runes: createDeathKnightRunes(CAST_AT),
|
||||
globalCooldownEndsAt: 0,
|
||||
nextActionAt: 0,
|
||||
activeCast: {
|
||||
abilityId: learnedIcyTouch.id,
|
||||
school: "frost",
|
||||
mode: "cast",
|
||||
startedAt: CAST_AT,
|
||||
completesAt: CAST_AT,
|
||||
originalDurationMs: 0,
|
||||
pushbackHits: 0,
|
||||
origin: [0, 0, 0],
|
||||
targetId: TARGET_ID,
|
||||
},
|
||||
activeCastTargetId: TARGET_ID,
|
||||
status: "attacking",
|
||||
}],
|
||||
command: "attack",
|
||||
activeMobId: TARGET_ID,
|
||||
});
|
||||
registerPartyRuntimePosition(configured.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||
name: "Merciless Combat companion target",
|
||||
maxHealth: TARGET_MAX_HEALTH,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(TARGET_ID, 1, 100);
|
||||
updatePartyRuntimeTargetVisibility(configured.id, TARGET_ID, true);
|
||||
setTargetHealthFraction(targetHealthFraction);
|
||||
|
||||
advanceDungeonPartyCombat(CAST_AT);
|
||||
const hit = targetDamageEvent(configured.id, learnedIcyTouch.id, CAST_AT);
|
||||
if (!hit) throw new Error("Companion Icy Touch did not produce a damage event.");
|
||||
return hit;
|
||||
}
|
||||
|
||||
describe("WoW Death Knight Merciless Combat integration", () => {
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
});
|
||||
|
||||
it("applies both player ranks below 35%, with the strict threshold and spell-list controls", () => {
|
||||
const baseline = resolvePlayerStrike("Icy Touch", 0, 0.34).direct;
|
||||
const rankOne = resolvePlayerStrike("Icy Touch", 1, 0.34).direct;
|
||||
const rankTwo = resolvePlayerStrike("Icy Touch", 2, 0.34).direct;
|
||||
const atThreshold = resolvePlayerStrike("Icy Touch", 2, 0.35).direct;
|
||||
const otherStrikeBaseline = resolvePlayerStrike("Plague Strike", 0, 0.34).direct;
|
||||
const otherStrikeWithTalent = resolvePlayerStrike("Plague Strike", 2, 0.34).direct;
|
||||
|
||||
expect(rankOne.rawAmount).toBe(Math.round(baseline.rawAmount * 1.06));
|
||||
expect(rankTwo.rawAmount).toBe(Math.round(baseline.rawAmount * 1.12));
|
||||
expect(atThreshold.rawAmount).toBe(baseline.rawAmount);
|
||||
expect(otherStrikeWithTalent.rawAmount).toBe(otherStrikeBaseline.rawAmount);
|
||||
});
|
||||
|
||||
it("does not apply the execute bonus to Icy Touch's periodic Fever ticks", () => {
|
||||
const baseline = resolvePlayerStrike("Icy Touch", 0, 0.34, true).firstDiseaseTick;
|
||||
const talented = resolvePlayerStrike("Icy Touch", 2, 0.34, true).firstDiseaseTick;
|
||||
expect(talented?.rawAmount).toBe(baseline?.rawAmount);
|
||||
});
|
||||
|
||||
it("applies both ranks to generated companion direct hits only below 35%", () => {
|
||||
const baseline = resolveCompanionIcyTouch(0, 0.34);
|
||||
const rankOne = resolveCompanionIcyTouch(1, 0.34);
|
||||
const rankTwo = resolveCompanionIcyTouch(2, 0.34);
|
||||
const atThreshold = resolveCompanionIcyTouch(2, 0.35);
|
||||
|
||||
expect(rankOne.rawAmount).toBe(Math.round(baseline.rawAmount * 1.06));
|
||||
expect(rankTwo.rawAmount).toBe(Math.round(baseline.rawAmount * 1.12));
|
||||
expect(atThreshold.rawAmount).toBe(baseline.rawAmount);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user