Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+207 -1
View File
@@ -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({