forked from phenom/healer-man
Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useCombatStore, type EnemyActiveCast } from "./combatStore";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore, type EnemyActiveCast } from "./combatStore";
|
||||
import { applyAura } from "./combatAuras";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
@@ -67,6 +68,38 @@ function partyHealCriticalAt(sourceId: string, abilityId: string, targetId: stri
|
||||
throw new Error("Could not find a seeded companion healing crit.");
|
||||
}
|
||||
|
||||
function applyRevivalTestCharges(auras: Parameters<typeof applyAura>[0], ownerId: string, stacks: number, now: number) {
|
||||
return applyAura(auras, {
|
||||
definition: {
|
||||
id: "rom-state-revival-charge",
|
||||
name: "Revival",
|
||||
description: "Revival source-stack test fixture.",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
maxStacks: 10,
|
||||
stackBehavior: "add",
|
||||
utilityTags: ["rom-revival-charge"],
|
||||
},
|
||||
sourceId: ownerId,
|
||||
targetId: ownerId,
|
||||
stacks,
|
||||
now,
|
||||
}).auras;
|
||||
}
|
||||
|
||||
function createRomRogueWarden(seed: string, level = 80) {
|
||||
return createConfiguredPartyMember({
|
||||
name: "Rogue/Warden",
|
||||
classId: "rom-rogue",
|
||||
secondaryClassId: "rom-warden",
|
||||
raceId: "rom-elf",
|
||||
specialization: "Rogue/Warden",
|
||||
specializationId: "rom-rogue:rom-warden",
|
||||
talentTreeId: "rom-rogue-rom-warden",
|
||||
role: "damage",
|
||||
}, level, 0, seed);
|
||||
}
|
||||
|
||||
function createFrostbitePartyMage(seed: string) {
|
||||
const mage = createConfiguredPartyMember({
|
||||
name: "Frostbite Mage",
|
||||
@@ -126,6 +159,179 @@ describe("party combat runtime", () => {
|
||||
expect(activePartyTargetId(mobs, null)).toBe("pack:a");
|
||||
});
|
||||
|
||||
it("lets an injured Rogue/Warden companion cast zero-stack Revival without critting or touching another owner", () => {
|
||||
const seed = "rom-rogue-warden-revival-zero-stacks";
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const playerHealth = useCombatStore.getState().health;
|
||||
const configured = createRomRogueWarden(seed);
|
||||
const otherConfigured = createConfiguredPartyMember({
|
||||
name: "Warden/Mage",
|
||||
classId: "rom-warden",
|
||||
secondaryClassId: "rom-mage",
|
||||
raceId: "rom-elf",
|
||||
specialization: "Warden/Mage",
|
||||
specializationId: "rom-warden:rom-mage",
|
||||
talentTreeId: "rom-warden-rom-mage",
|
||||
role: "damage",
|
||||
}, 80, 0, `${seed}:other`);
|
||||
const revival = {
|
||||
...configured,
|
||||
health: Math.round(configured.maxHealth / 2),
|
||||
nextActionAt: 0,
|
||||
gearStats: { ...EMPTY_ITEM_STATS, criticalStrikeRating: 100_000 },
|
||||
};
|
||||
const other = {
|
||||
...otherConfigured,
|
||||
health: Math.round(otherConfigured.maxHealth / 2),
|
||||
nextActionAt: 0,
|
||||
};
|
||||
usePartyStore.getState().initializeParty(seed, 80, playerHealth, "damage", 2, "rom");
|
||||
usePartyStore.setState({ members: [revival, other] });
|
||||
registerPartyRuntimePosition(revival.id, [0, 0, 0]);
|
||||
registerPartyRuntimePosition(other.id, [1, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: applyRevivalTestCharges(
|
||||
applyRevivalTestCharges(state.auras, PLAYER_AURA_ENTITY_ID, 2, 1_000),
|
||||
other.id,
|
||||
4,
|
||||
1_000,
|
||||
),
|
||||
}));
|
||||
|
||||
advanceDungeonPartyCombat(1_000);
|
||||
|
||||
const after = useCombatStore.getState();
|
||||
const healed = usePartyStore.getState().members.find((member) => member.id === revival.id)!;
|
||||
const untouched = usePartyStore.getState().members.find((member) => member.id === other.id)!;
|
||||
const healEvents = after.combatEvents.filter((event) => (
|
||||
event.kind === "healing" && event.sourceActorId === revival.id
|
||||
&& event.abilityId === "rom-rogue-rom-warden-elite-60"
|
||||
));
|
||||
expect(healed.health).toBe(revival.health + Math.round(revival.maxHealth * 0.1));
|
||||
expect(healEvents).toHaveLength(1);
|
||||
expect(healEvents[0]).toMatchObject({
|
||||
targetActorId: revival.id,
|
||||
rawAmount: Math.round(revival.maxHealth * 0.1),
|
||||
effectiveAmount: Math.round(revival.maxHealth * 0.1),
|
||||
critical: false,
|
||||
});
|
||||
expect(untouched.health).toBe(other.health);
|
||||
expect(after.health).toBe(playerHealth);
|
||||
expect(after.auras.some((aura) => aura.sourceId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(true);
|
||||
expect(after.auras.some((aura) => aura.sourceId === other.id
|
||||
&& aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the companion's own capped stacks and caps Revival at its max health", () => {
|
||||
const seed = "rom-rogue-warden-revival-charges";
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const playerHealth = useCombatStore.getState().health;
|
||||
const configured = createRomRogueWarden(seed);
|
||||
const otherConfigured = createConfiguredPartyMember({
|
||||
name: "Warden/Mage",
|
||||
classId: "rom-warden",
|
||||
secondaryClassId: "rom-mage",
|
||||
raceId: "rom-elf",
|
||||
specialization: "Warden/Mage",
|
||||
specializationId: "rom-warden:rom-mage",
|
||||
talentTreeId: "rom-warden-rom-mage",
|
||||
role: "damage",
|
||||
}, 80, 0, `${seed}:other`);
|
||||
const revival = {
|
||||
...configured,
|
||||
health: configured.maxHealth - 50,
|
||||
nextActionAt: 0,
|
||||
};
|
||||
const other = {
|
||||
...otherConfigured,
|
||||
health: Math.round(otherConfigured.maxHealth / 2),
|
||||
nextActionAt: 0,
|
||||
};
|
||||
usePartyStore.getState().initializeParty(seed, 80, playerHealth, "damage", 2, "rom");
|
||||
usePartyStore.setState({ members: [revival, other] });
|
||||
registerPartyRuntimePosition(revival.id, [0, 0, 0]);
|
||||
registerPartyRuntimePosition(other.id, [1, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: applyRevivalTestCharges(
|
||||
applyRevivalTestCharges(state.auras, revival.id, 10, 1_000),
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
3,
|
||||
1_000,
|
||||
),
|
||||
}));
|
||||
|
||||
advanceDungeonPartyCombat(1_000);
|
||||
|
||||
const after = useCombatStore.getState();
|
||||
const healed = usePartyStore.getState().members.find((member) => member.id === revival.id)!;
|
||||
const healEvent = after.combatEvents.find((event) => (
|
||||
event.kind === "healing" && event.sourceActorId === revival.id
|
||||
&& event.abilityId === "rom-rogue-rom-warden-elite-60"
|
||||
));
|
||||
expect(healed.health).toBe(revival.maxHealth);
|
||||
expect(healEvent).toMatchObject({
|
||||
targetActorId: revival.id,
|
||||
rawAmount: Math.round(revival.maxHealth * 0.6),
|
||||
effectiveAmount: 50,
|
||||
overheal: Math.round(revival.maxHealth * 0.6) - 50,
|
||||
critical: false,
|
||||
});
|
||||
expect(after.health).toBe(playerHealth);
|
||||
expect(after.auras.some((aura) => aura.sourceId === revival.id
|
||||
&& aura.targetId === revival.id
|
||||
&& aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(false);
|
||||
expect(after.auras.some((aura) => aura.sourceId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(true);
|
||||
expect(after.auras.some((aura) => aura.sourceId === other.id
|
||||
&& aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not graft Rogue/Warden Revival onto below-level or Warden-primary companions", () => {
|
||||
const seed = "rom-rogue-warden-revival-gates";
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
|
||||
const playerHealth = useCombatStore.getState().health;
|
||||
const belowLevelConfigured = createRomRogueWarden(`${seed}:below-level`, 59);
|
||||
const wardenPrimaryConfigured = createConfiguredPartyMember({
|
||||
name: "Warden/Rogue",
|
||||
classId: "rom-warden",
|
||||
secondaryClassId: "rom-rogue",
|
||||
raceId: "rom-elf",
|
||||
specialization: "Warden/Rogue",
|
||||
specializationId: "rom-warden:rom-rogue",
|
||||
talentTreeId: "rom-warden-rom-rogue",
|
||||
role: "damage",
|
||||
}, 80, 0, `${seed}:warden-primary`);
|
||||
const belowLevel = {
|
||||
...belowLevelConfigured,
|
||||
health: Math.round(belowLevelConfigured.maxHealth / 2),
|
||||
nextActionAt: 0,
|
||||
};
|
||||
const wardenPrimary = {
|
||||
...wardenPrimaryConfigured,
|
||||
health: Math.round(wardenPrimaryConfigured.maxHealth / 2),
|
||||
nextActionAt: 0,
|
||||
};
|
||||
usePartyStore.getState().initializeParty(seed, 80, playerHealth, "damage", 2, "rom");
|
||||
usePartyStore.setState({ members: [belowLevel, wardenPrimary] });
|
||||
registerPartyRuntimePosition(belowLevel.id, [0, 0, 0]);
|
||||
registerPartyRuntimePosition(wardenPrimary.id, [1, 0, 0]);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
advanceDungeonPartyCombat(1_000);
|
||||
|
||||
const after = useCombatStore.getState();
|
||||
expect(usePartyStore.getState().members.find((member) => member.id === belowLevel.id)?.health)
|
||||
.toBe(belowLevel.health);
|
||||
expect(usePartyStore.getState().members.find((member) => member.id === wardenPrimary.id)?.health)
|
||||
.toBe(wardenPrimary.health);
|
||||
expect(after.combatEvents.some((event) => event.kind === "healing"
|
||||
&& event.abilityId === "rom-rogue-rom-warden-elite-60")).toBe(false);
|
||||
expect(after.health).toBe(playerHealth);
|
||||
});
|
||||
|
||||
it("extends a companion Druid's cast-time damage range with Nature's Reach", () => {
|
||||
const seed = "druid-natures-reach-party";
|
||||
const druid = createConfiguredPartyMember({
|
||||
|
||||
Reference in new issue
Block a user