forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -2,10 +2,11 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { applyAura, resolveStatValue } from "./combatAuras";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
||||
|
||||
const TARGET_ID = "druid-proc-target";
|
||||
|
||||
@@ -48,6 +49,50 @@ function castAndFinish(name: string, startedAt: number) {
|
||||
)).at(-1);
|
||||
}
|
||||
|
||||
function guaranteeDruidMeleeCrits(): void {
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: [...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-melee-crit",
|
||||
name: "Test Melee Critical Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [
|
||||
{ kind: "stat", stat: "hit", operation: "flat", value: 100 },
|
||||
{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 100 },
|
||||
{ kind: "stat", stat: "spell-hit", operation: "flat", value: 100 },
|
||||
{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 },
|
||||
],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: PLAYER_AURA_ENTITY_ID,
|
||||
now: 1_000,
|
||||
}).auras],
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[TARGET_ID]: {
|
||||
...state.mobs[TARGET_ID]!,
|
||||
characterDefender: {
|
||||
level: 80,
|
||||
armor: 0,
|
||||
dodgePercent: 0,
|
||||
parryPercent: 0,
|
||||
blockPercent: 0,
|
||||
blockValue: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function findDruidPrimalFurySeed(procTag: string, chance: number, shouldProc: boolean): string {
|
||||
for (let index = 0; index < 100_000; index += 1) {
|
||||
const seed = `druid-primal-fury:${procTag}:${index}`;
|
||||
if ((unit(`${seed}:wow335-druid-primal-fury-${procTag}`) < chance) === shouldProc) return seed;
|
||||
}
|
||||
throw new Error(`No deterministic Primal Fury ${procTag} seed found.`);
|
||||
}
|
||||
|
||||
describe("WoW Druid proc runtime", () => {
|
||||
beforeEach(async () => {
|
||||
clearMobRuntimeRegistry();
|
||||
@@ -246,4 +291,311 @@ describe("WoW Druid proc runtime", () => {
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-clearcasting")
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("stores 30% of an eligible critical heal and blooms on the target's next direct hit", () => {
|
||||
const healingTouch = abilityId("Healing Touch");
|
||||
const startedAt = 1_000;
|
||||
useCombatStore.setState((state) => ({
|
||||
health: 1,
|
||||
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
|
||||
auras: [...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-spell-crit",
|
||||
name: "Test Critical Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: PLAYER_AURA_ENTITY_ID,
|
||||
now: startedAt,
|
||||
}).auras],
|
||||
}));
|
||||
|
||||
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
|
||||
const cast = useCombatStore.getState().activeCast;
|
||||
expect(cast?.abilityId).toBe(healingTouch);
|
||||
useCombatStore.getState().tick(0.1, cast!.completesAt);
|
||||
|
||||
const heal = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "healing" && event.abilityId === healingTouch && event.occurredAt === cast!.completesAt
|
||||
));
|
||||
expect(heal).toMatchObject({ critical: true, effectiveAmount: expect.any(Number) });
|
||||
const storedSeed = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
));
|
||||
expect(storedSeed?.expiresAt).toBe(cast!.completesAt + 15_000);
|
||||
expect(storedSeed?.druidLivingSeedAmount).toBe(Math.round(heal!.effectiveAmount * 0.3));
|
||||
|
||||
const healthBeforeHit = useCombatStore.getState().health;
|
||||
const damage = useCombatStore.getState().damagePlayer(100, "physical", 80, cast!.completesAt + 1);
|
||||
expect(damage).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().health).toBeGreaterThan(healthBeforeHit - damage);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
))).toBe(false);
|
||||
expect(useCombatStore.getState().combatEvents.some((event) => (
|
||||
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed" && event.effectiveAmount > 0
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("expires an untriggered Living Seed after fifteen seconds", () => {
|
||||
const healingTouch = abilityId("Healing Touch");
|
||||
const startedAt = 1_000;
|
||||
useCombatStore.setState((state) => ({
|
||||
health: 1,
|
||||
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
|
||||
auras: [...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-spell-crit",
|
||||
name: "Test Critical Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: PLAYER_AURA_ENTITY_ID,
|
||||
now: startedAt,
|
||||
}).auras],
|
||||
}));
|
||||
expect(useCombatStore.getState().castAbility(healingTouch, { targetActorId: null, now: startedAt }).ok).toBe(true);
|
||||
const cast = useCombatStore.getState().activeCast;
|
||||
useCombatStore.getState().tick(0.1, cast!.completesAt);
|
||||
const expiresAt = cast!.completesAt + 15_000;
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
))).toBe(true);
|
||||
|
||||
const healthBeforeHit = useCombatStore.getState().health;
|
||||
const damage = useCombatStore.getState().damagePlayer(100, "physical", 80, expiresAt);
|
||||
expect(damage).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().health).toBe(healthBeforeHit - damage);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
))).toBe(false);
|
||||
expect(useCombatStore.getState().combatEvents.some((event) => (
|
||||
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed"
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
it("blooms on a landed direct enemy hit to a party ally", () => {
|
||||
const member = usePartyStore.getState().members[0]!;
|
||||
const memberMaxHealth = 1_000;
|
||||
usePartyStore.setState((state) => ({
|
||||
members: state.members.map((candidate) => candidate.id === member.id
|
||||
? { ...candidate, health: 100, maxHealth: memberMaxHealth }
|
||||
: candidate),
|
||||
}));
|
||||
useCombatStore.setState((state) => ({
|
||||
actors: {
|
||||
...state.actors,
|
||||
[member.id]: { ...state.actors[member.id]!, health: 100, maxHealth: memberMaxHealth, alive: true },
|
||||
},
|
||||
talentRanks: { ...state.talentRanks, "talent-1922": 3 },
|
||||
auras: [...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-spell-crit",
|
||||
name: "Test Critical Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [{ kind: "stat", stat: "spell-critical-strike", operation: "flat", value: 100 }],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: PLAYER_AURA_ENTITY_ID,
|
||||
now: 1_000,
|
||||
}).auras,
|
||||
...applyAura(state.auras, {
|
||||
definition: {
|
||||
id: "test-druid-guaranteed-ally-hit",
|
||||
name: "Test Incoming Hit Chance",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
modifiers: [{ kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 100 }],
|
||||
},
|
||||
sourceId: PLAYER_AURA_ENTITY_ID,
|
||||
targetId: member.id,
|
||||
now: 1_000,
|
||||
}).auras],
|
||||
}));
|
||||
registerPartyRuntimePosition(member.id, [2, 0, 0]);
|
||||
|
||||
const healingTouch = abilityId("Healing Touch");
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.getState().selectFriendlyActor(member.id);
|
||||
const cast = useCombatStore.getState().castAbility(healingTouch, { targetActorId: member.id, now: 1_000 });
|
||||
expect(cast.ok, JSON.stringify(cast)).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
useCombatStore.getState().tick(0.1, activeCast!.completesAt);
|
||||
const storedSeed = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === member.id
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
));
|
||||
expect(storedSeed?.druidLivingSeedAmount).toBeGreaterThan(0);
|
||||
|
||||
const mobId = "druid-living-seed-party-hit";
|
||||
const attackId = "druid-living-seed-party-swing";
|
||||
useCombatStore.getState().registerMob(mobId, {
|
||||
name: "Party attacker",
|
||||
level: 1,
|
||||
maxHealth: 1_000,
|
||||
xpReward: 0,
|
||||
attacks: [{
|
||||
...DEFAULT_ENEMY_ATTACK,
|
||||
id: attackId,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [1_000, 1_000],
|
||||
}],
|
||||
});
|
||||
setMobPosition(mobId, [4, 0, 0]);
|
||||
useCombatStore.setState((state) => ({
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[mobId]: {
|
||||
...state.mobs[mobId]!,
|
||||
engaged: true,
|
||||
combatPhase: "attacking",
|
||||
targetActorId: member.id,
|
||||
threatByActor: { [member.id]: 100 },
|
||||
},
|
||||
},
|
||||
}));
|
||||
useCombatStore.getState().advanceMobCombat(activeCast!.completesAt + 500);
|
||||
|
||||
const enemyDamage = useCombatStore.getState().combatEvents.find((event) => (
|
||||
event.kind === "damage" && event.sourceActorId === mobId && event.targetActorId === member.id
|
||||
));
|
||||
expect(enemyDamage).toMatchObject({ outcome: "hit" });
|
||||
const remainingSeed = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === member.id
|
||||
&& aura.definition.utilityTags?.includes("wow335-druid-living-seed")
|
||||
));
|
||||
expect(remainingSeed).toBeUndefined();
|
||||
expect(useCombatStore.getState().combatEvents.some((event) => (
|
||||
event.kind === "healing" && event.abilityId === "wow335:druid:living-seed"
|
||||
&& event.targetActorId === member.id && event.effectiveAmount > 0
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("grants five Rage on Bear and Dire Bear Form critical auto-attacks at Primal Fury's 50% rank", () => {
|
||||
useCombatStore.setState((state) => ({
|
||||
talentRanks: { ...state.talentRanks, "talent-801": 1 },
|
||||
resourcePools: { ...state.resourcePools, rage: 0 },
|
||||
maxResourcePools: { ...state.maxResourcePools, rage: 100 },
|
||||
}));
|
||||
guaranteeDruidMeleeCrits();
|
||||
const form = useCombatStore.getState().castAbility(abilityId("Bear Form"), {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
});
|
||||
expect(form.ok, JSON.stringify(form)).toBe(true);
|
||||
|
||||
const procSeed = findDruidPrimalFurySeed("rage", 0.5, true);
|
||||
const proc = useCombatStore.getState().damageMob(TARGET_ID, 100, 2_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: procSeed,
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
|
||||
});
|
||||
expect(proc.combatResult?.critical).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(5);
|
||||
expect(useCombatStore.getState().combatEvents.at(-1)).toMatchObject({
|
||||
kind: "resource",
|
||||
sourceActorId: PLAYER_AGGRO_ID,
|
||||
targetActorId: PLAYER_AURA_ENTITY_ID,
|
||||
abilityId: "wow335-druid-primal-fury",
|
||||
rawAmount: 5,
|
||||
effectiveAmount: 5,
|
||||
});
|
||||
|
||||
const noProcSeed = findDruidPrimalFurySeed("rage", 0.5, false);
|
||||
const noProc = useCombatStore.getState().damageMob(TARGET_ID, 100, 3_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: noProcSeed,
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
|
||||
});
|
||||
expect(noProc.combatResult?.critical).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(5);
|
||||
|
||||
const direBearForm = useCombatStore.getState().castAbility(abilityId("Dire Bear Form"), {
|
||||
targetActorId: null,
|
||||
now: 4_000,
|
||||
});
|
||||
expect(direBearForm.ok, JSON.stringify(direBearForm)).toBe(true);
|
||||
const direBearProc = useCombatStore.getState().damageMob(TARGET_ID, 100, 5_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: findDruidPrimalFurySeed("rage", 0.5, true),
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
|
||||
});
|
||||
expect(direBearProc.combatResult?.critical).toBe(true);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(10);
|
||||
});
|
||||
|
||||
it("adds one capped combo point on a Cat Form combo-builder critical and ignores other hits", () => {
|
||||
useCombatStore.setState((state) => ({
|
||||
talentRanks: { ...state.talentRanks, "talent-801": 2 },
|
||||
resourcePools: { ...state.resourcePools, rage: 0 },
|
||||
maxResourcePools: { ...state.maxResourcePools, rage: 100 },
|
||||
}));
|
||||
guaranteeDruidMeleeCrits();
|
||||
const form = useCombatStore.getState().castAbility(abilityId("Cat Form"), {
|
||||
targetActorId: null,
|
||||
now: 1_000,
|
||||
});
|
||||
expect(form.ok, JSON.stringify(form)).toBe(true);
|
||||
|
||||
const claw = castAndFinish("Claw", 3_000);
|
||||
expect(claw).toMatchObject({ critical: true, sourceActorId: PLAYER_AGGRO_ID, targetActorId: TARGET_ID });
|
||||
expect(useCombatStore.getState().comboPoints).toBe(2);
|
||||
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
||||
|
||||
useCombatStore.setState((state) => ({
|
||||
comboPoints: 4,
|
||||
comboTargetId: TARGET_ID,
|
||||
resourcePools: {
|
||||
...state.resourcePools,
|
||||
energy: state.maxResourcePools.energy,
|
||||
},
|
||||
}));
|
||||
const cappedClaw = castAndFinish("Claw", 5_000);
|
||||
expect(cappedClaw?.critical).toBe(true);
|
||||
expect(useCombatStore.getState().comboPoints).toBe(5);
|
||||
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
|
||||
|
||||
useCombatStore.setState((state) => ({ comboPoints: 0, comboTargetId: null }));
|
||||
const clawId = abilityId("Claw");
|
||||
useCombatStore.getState().damageMob(TARGET_ID, 100, 7_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: "druid-primal-fury-cat-auto-attack",
|
||||
abilityId: clawId,
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false },
|
||||
});
|
||||
expect(useCombatStore.getState().comboPoints).toBe(0);
|
||||
expect(useCombatStore.getState().comboTargetId).toBeNull();
|
||||
|
||||
useCombatStore.setState((state) => ({
|
||||
comboPoints: 0,
|
||||
comboTargetId: null,
|
||||
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy },
|
||||
}));
|
||||
const rake = castAndFinish("Rake", 8_000);
|
||||
expect(rake).toMatchObject({ critical: true, sourceActorId: PLAYER_AGGRO_ID, targetActorId: TARGET_ID });
|
||||
expect(useCombatStore.getState().comboPoints).toBe(2);
|
||||
useCombatStore.getState().tick(9, 17_000);
|
||||
expect(useCombatStore.getState().comboPoints).toBe(2);
|
||||
expect(useCombatStore.getState().comboTargetId).toBe(TARGET_ID);
|
||||
|
||||
useCombatStore.setState((state) => ({ comboPoints: 0, comboTargetId: null }));
|
||||
useCombatStore.getState().damageMob(TARGET_ID, 100, 18_000, undefined, "arcane", {
|
||||
attackKind: "spell",
|
||||
seed: "druid-primal-fury-cat-non-combo",
|
||||
abilityId: abilityId("Moonfire"),
|
||||
rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false },
|
||||
});
|
||||
expect(useCombatStore.getState().comboPoints).toBe(0);
|
||||
expect(useCombatStore.getState().comboTargetId).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user