forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
1fedb9368d
commit
cc3b2fa4d5
27 files changed
+1988
-83
No files matched your search
@@ -25,6 +25,7 @@ import { subscribeCombatPresentation, type CombatPresentationEvent } from "./com
|
||||
import { subscribeCombatEvents, type StructuredCombatEvent } from "./combatEvents";
|
||||
import { abilitiesForClass } from "./abilityCatalog";
|
||||
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
|
||||
import { baseClassFor } from "../app/characterCatalog";
|
||||
|
||||
function registerEngagedMob(id = "pack:a", maxHealth = 200): void {
|
||||
useCombatStore.getState().registerMob(id, { name: id, maxHealth, xpReward: 0 });
|
||||
@@ -145,6 +146,65 @@ describe("party combat runtime", () => {
|
||||
expect(useCombatStore.getState().mobs["feral-spirit-boss"].health).toBeLessThan(beforeSummonAttack);
|
||||
});
|
||||
|
||||
it("lets an Assassination companion stack poisons and scale Envenom from them", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const rogue = createConfiguredPartyMember({
|
||||
name: "Assassination",
|
||||
classId: "rogue",
|
||||
specialization: "Assassination",
|
||||
specializationId: "rogue:assassination",
|
||||
talentTreeId: "rogue-assassination",
|
||||
talentRanks: runtimePartyTalentRanksForLevel("rogue", "Assassination", 80),
|
||||
role: "damage",
|
||||
}, 80, 0, "assassination-poison-runtime");
|
||||
usePartyStore.getState().initializeParty(
|
||||
"assassination-poison-runtime",
|
||||
80,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
);
|
||||
usePartyStore.setState({ members: [rogue] });
|
||||
registerPartyRuntimePosition(rogue.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.getState().applyPrecombatPreparation(500);
|
||||
expect(useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.targetId === rogue.id && aura.definition.utilityTags?.includes("weapon-enchant")
|
||||
))).toHaveLength(2);
|
||||
|
||||
registerEngagedMob("poison-runtime-boss", 10_000_000);
|
||||
const events: StructuredCombatEvent[] = [];
|
||||
const stop = subscribeCombatEvents((event) => events.push(event));
|
||||
for (let now = 1_000; now <= 60_000; now += 100) {
|
||||
useCombatStore.getState().tick(0.1, now);
|
||||
advanceDungeonPartyCombat(now);
|
||||
}
|
||||
stop();
|
||||
|
||||
expect(events.some((event) => (
|
||||
event.sourceActorId === rogue.id && event.abilityId === "rogue-instant-poison"
|
||||
))).toBe(true);
|
||||
expect(events.some((event) => (
|
||||
event.sourceActorId === rogue.id && event.abilityId === "rogue-deadly-poison"
|
||||
))).toBe(true);
|
||||
expect(events.some((event) => (
|
||||
event.sourceActorId === rogue.id
|
||||
&& event.abilityId === "wow335-rogue-envenom"
|
||||
&& event.school === "nature"
|
||||
&& event.effectiveAmount > 0
|
||||
))).toBe(true);
|
||||
expect(events.some((event) => (
|
||||
event.sourceActorId === rogue.id && event.abilityId === "wow335-rogue-shiv"
|
||||
))).toBe(true);
|
||||
expect(events.some((event) => (
|
||||
event.sourceActorId === rogue.id && event.abilityId === "wow335-rogue-rupture"
|
||||
))).toBe(false);
|
||||
expect(useCombatStore.getState().auras.find((aura) => (
|
||||
aura.sourceId === rogue.id
|
||||
&& aura.targetId === "poison-runtime-boss"
|
||||
&& aura.definition.utilityTags?.includes("rogue-deadly-poison-dose")
|
||||
))?.stacks).toBe(5);
|
||||
});
|
||||
|
||||
it("gives companion Hunter and Warlock pets a hostile target when summoned from self", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const hunter = createConfiguredPartyMember({
|
||||
@@ -237,6 +297,63 @@ describe("party combat runtime", () => {
|
||||
expect(applied?.absorbs.reduce((total, absorb) => total + absorb.remaining, 0)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("casts Inner Focus before an emergency direct heal and makes that heal free", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const tank = createConfiguredPartyMember({
|
||||
name: "Tank",
|
||||
classId: "warrior",
|
||||
specialization: "Protection",
|
||||
specializationId: "warrior:protection",
|
||||
talentTreeId: "warrior-protection",
|
||||
role: "tank",
|
||||
}, 80, 0, "inner-focus-runtime");
|
||||
const healer = createConfiguredPartyMember({
|
||||
name: "Discipline",
|
||||
classId: "priest",
|
||||
specialization: "Discipline",
|
||||
specializationId: "priest:discipline",
|
||||
talentTreeId: "priest-discipline",
|
||||
talentRanks: runtimePartyTalentRanksForLevel("priest", "Discipline", 80),
|
||||
role: "healer",
|
||||
}, 80, 1, "inner-focus-runtime");
|
||||
const priestAbilities = abilitiesForClass("priest");
|
||||
const innerFocus = priestAbilities.find((ability) => ability.name === "Inner Focus")!;
|
||||
const greaterHeal = priestAbilities.find((ability) => ability.name === "Greater Heal")!;
|
||||
const startingHealth = Math.round(tank.maxHealth * 0.3);
|
||||
usePartyStore.getState().initializeParty("inner-focus-runtime", 80, useCombatStore.getState().health, "damage");
|
||||
usePartyStore.setState({
|
||||
members: [
|
||||
{ ...tank, health: startingHealth },
|
||||
{
|
||||
...healer,
|
||||
resource: 0,
|
||||
maxResource: 0,
|
||||
resourcePools: { mana: 0 },
|
||||
cooldowns: Object.fromEntries(priestAbilities
|
||||
.filter((ability) => ![innerFocus.id, greaterHeal.id].includes(ability.id))
|
||||
.map((ability) => [ability.id, 60_000])),
|
||||
},
|
||||
],
|
||||
});
|
||||
for (const member of usePartyStore.getState().members) registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
registerEngagedMob("inner-focus-boss", 100_000);
|
||||
const events: CombatPresentationEvent[] = [];
|
||||
const stop = subscribeCombatPresentation((message) => {
|
||||
if (message.kind === "event" && message.event.sourceActorId === healer.id) events.push(message.event);
|
||||
});
|
||||
|
||||
for (const now of [1_000, 2_500, 6_000]) advanceDungeonPartyCombat(now);
|
||||
stop();
|
||||
|
||||
const releases = events.filter((event) => event.phase === "release").map((event) => event.name);
|
||||
expect(releases).toEqual(expect.arrayContaining(["Inner Focus", "Greater Heal"]));
|
||||
expect(releases.indexOf("Inner Focus")).toBeLessThan(releases.indexOf("Greater Heal"));
|
||||
expect(usePartyStore.getState().members.find((member) => member.id === healer.id)?.resource).toBe(0);
|
||||
expect(usePartyStore.getState().members.find((member) => member.id === tank.id)?.health)
|
||||
.toBeGreaterThan(startingHealth);
|
||||
});
|
||||
|
||||
it("lets a resurrected tank rejoin independently instead of taunting or casting from its checkpoint", () => {
|
||||
registerEngagedMob();
|
||||
const tank = usePartyStore.getState().members.find((member) => member.role === "tank")!;
|
||||
@@ -393,9 +510,14 @@ describe("party combat runtime", () => {
|
||||
it("gives damage dealers a higher baseline hit than the tank", () => {
|
||||
const [tank, damage] = usePartyStore.getState().members;
|
||||
expect(partyAttackDamage(damage)).toBeGreaterThan(partyAttackDamage(tank));
|
||||
const attackStat = baseClassFor(damage.classId) === "mage"
|
||||
? { spellPower: 126 }
|
||||
: baseClassFor(damage.classId) === "hunter"
|
||||
? { rangedAttackPower: 126 }
|
||||
: { attackPower: 126 };
|
||||
expect(partyAttackDamage({
|
||||
...damage,
|
||||
gearStats: { ...EMPTY_ITEM_STATS, attackPower: 126 },
|
||||
gearStats: { ...EMPTY_ITEM_STATS, ...attackStat },
|
||||
})).toBeGreaterThan(partyAttackDamage(damage));
|
||||
});
|
||||
|
||||
@@ -541,7 +663,10 @@ describe("party combat runtime", () => {
|
||||
advanceDungeonPartyCombat(1_000);
|
||||
|
||||
expect(useCombatStore.getState().mobs["pack:a"].health).toBe(before);
|
||||
expect(usePartyStore.getState().members.every((member) => member.status === "advancing")).toBe(true);
|
||||
expect(usePartyStore.getState().members.every((member) => (
|
||||
member.status === "advancing"
|
||||
|| ((member.status === "attacking" || member.status === "guarding") && member.nextActionAt > 1_000)
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not deal remote damage when a target has no world position", () => {
|
||||
|
||||
Reference in new issue
Block a user