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,7 +1,8 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { applyAura, expireAuras, resolveDamageValue } from "./combatAuras";
|
||||
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
||||
import { clearMobRuntimeRegistry, getMobPosition, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { PLAYER_AURA_ENTITY_ID, stableCombatUnit, useCombatStore } from "./combatStore";
|
||||
import { romEffectsForCast, type RomCastMechanics } from "./romCombatMechanics";
|
||||
import {
|
||||
ROM_CHAMPION_SOUL_FORGE_PROC_ACTION_ID,
|
||||
@@ -10,6 +11,10 @@ import {
|
||||
ROM_CHAMPION_WAY_OF_FRENZY_PROFILE,
|
||||
romChampionCastEffects,
|
||||
} from "./romChampionOverrides";
|
||||
import {
|
||||
ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID,
|
||||
ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID,
|
||||
} from "./romChampionForgeRuntime";
|
||||
|
||||
const empty: RomCastMechanics = {
|
||||
sealStacks: 0,
|
||||
@@ -139,6 +144,191 @@ describe("RoM Champion source-specific behavior", () => {
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
it("keeps rank 1 at level 21 and unlocks the rank 2 Forge source aura at level 22", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 21,
|
||||
});
|
||||
const forge = sourceAbility(498523);
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-active"))
|
||||
.toBe(true);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID))
|
||||
.toBe(false);
|
||||
|
||||
useCombatStore.setState({ level: 22 });
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 4_000)).toMatchObject({ ok: true });
|
||||
const rankTwo = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.definition.id === ROM_CHAMPION_FORGE_RANK_TWO_AURA_ID
|
||||
));
|
||||
expect(rankTwo?.expiresAt).toBe(304_000);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-active"))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it("grants Chain Drive on an eligible rank 2 auto attack and resets only learned owner cooldowns", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
||||
});
|
||||
useCombatStore.getState().registerMob("champion-chain-drive-target", {
|
||||
name: "Chain Drive Target", maxHealth: 100_000,
|
||||
});
|
||||
const forge = sourceAbility(498523);
|
||||
const runePulse = sourceAbility(498529);
|
||||
const shadowPulse = sourceAbility(498726);
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.setState((state) => ({
|
||||
resource: 0,
|
||||
resourcePools: { ...state.resourcePools, rage: 0 },
|
||||
cooldowns: { ...state.cooldowns, [runePulse.id]: 90_000, [shadowPulse.id]: 90_000 },
|
||||
}));
|
||||
|
||||
const hitSeed = Array.from({ length: 100 }, (_, index) => `champion-chain-drive-${index}`)
|
||||
.find((seed) => stableCombatUnit(`${seed}:498590:0`) < 0.3);
|
||||
expect(hitSeed).toBeDefined();
|
||||
const hit = useCombatStore.getState().damageMob(
|
||||
"champion-chain-drive-target",
|
||||
100,
|
||||
2_000,
|
||||
undefined,
|
||||
"physical",
|
||||
{
|
||||
attackKind: "melee",
|
||||
seed: hitSeed!,
|
||||
abilityId: "party-auto-attack",
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
expect(hit.amount).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
||||
&& aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.expiresAt === 10_000
|
||||
))).toBe(true);
|
||||
expect(useCombatStore.getState().cooldowns[runePulse.id]).toBe(0);
|
||||
expect(useCombatStore.getState().cooldowns[shadowPulse.id]).toBe(0);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
||||
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-stack")?.stacks)
|
||||
.toBe(1);
|
||||
});
|
||||
|
||||
it("keeps the rank 2 stack and Rage when Chain Drive's fallback roll fails", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
||||
});
|
||||
useCombatStore.getState().registerMob("champion-chain-drive-failed-target", {
|
||||
name: "Chain Drive Failed Target", maxHealth: 100_000,
|
||||
});
|
||||
const forge = sourceAbility(498523);
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 0 }, resource: 0 }));
|
||||
|
||||
const failedSeed = Array.from({ length: 100 }, (_, index) => `chain-drive-failed-${index}`)
|
||||
.find((seed) => stableCombatUnit(`${seed}:498590:0`) >= 0.8
|
||||
&& stableCombatUnit(`${seed}:498590:1`) >= 0.3)!;
|
||||
const hit = useCombatStore.getState().damageMob(
|
||||
"champion-chain-drive-failed-target", 100, 2_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: failedSeed,
|
||||
abilityId: "party-auto-attack",
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
|
||||
expect(hit.amount).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().auras.find((aura) => aura.definition.id === "rom-champion-forge-stack")?.stacks)
|
||||
.toBe(1);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
||||
))).toBe(false);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
it("does not proc rank 2 Forge from zero or fully absorbed physical packets", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
||||
});
|
||||
useCombatStore.getState().registerMob("champion-forge-zero-target", {
|
||||
name: "Zero Packet Target", maxHealth: 100_000,
|
||||
});
|
||||
useCombatStore.getState().registerMob("champion-forge-absorbed-target", {
|
||||
name: "Absorbed Packet Target", maxHealth: 100_000,
|
||||
});
|
||||
const forge = sourceAbility(498523);
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
|
||||
const zeroHit = useCombatStore.getState().damageMob(
|
||||
"champion-forge-zero-target", 0, 2_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: "champion-forge-zero-hit",
|
||||
abilityId: "party-auto-attack",
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
expect(zeroHit.amount).toBe(0);
|
||||
|
||||
const shieldedAuras = applyAura(useCombatStore.getState().auras, {
|
||||
definition: {
|
||||
id: "test:champion-forge-physical-absorb",
|
||||
name: "Physical absorb fixture",
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
absorbs: [{ id: "test:champion-forge-physical-absorb-pool", amount: 1_000_000, school: "physical" }],
|
||||
},
|
||||
sourceId: "champion-forge-absorbed-target",
|
||||
targetId: "champion-forge-absorbed-target",
|
||||
now: 3_000,
|
||||
}).auras;
|
||||
useCombatStore.setState({ auras: shieldedAuras });
|
||||
const absorbedHit = useCombatStore.getState().damageMob(
|
||||
"champion-forge-absorbed-target", 100, 3_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: "champion-forge-absorbed-hit",
|
||||
abilityId: "party-auto-attack",
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
|
||||
expect(absorbedHit.amount).toBe(0);
|
||||
const absorbedEvent = useCombatStore.getState().combatEvents.filter((event) => (
|
||||
event.kind === "damage" && event.targetActorId === "champion-forge-absorbed-target"
|
||||
)).at(-1);
|
||||
expect(absorbedEvent?.absorbed).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-stack"))
|
||||
.toBe(false);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID))
|
||||
.toBe(false);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
||||
});
|
||||
|
||||
it("does not proc rank 2 Forge from a positive melee spell packet", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 22 });
|
||||
useCombatStore.getState().registerMob("champion-forge-melee-spell-target", {
|
||||
name: "Melee Spell Target", maxHealth: 100_000,
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility(sourceAbility(498523).id, undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
useCombatStore.setState((state) => ({ resource: 0, resourcePools: { ...state.resourcePools, rage: 0 } }));
|
||||
|
||||
useCombatStore.getState().damageMob(
|
||||
"champion-forge-melee-spell-target",
|
||||
100,
|
||||
2_000,
|
||||
undefined,
|
||||
"physical",
|
||||
{
|
||||
attackKind: "melee",
|
||||
seed: "champion-direct-melee-spell",
|
||||
abilityId: sourceAbility(498529).id,
|
||||
rules: { canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-champion-forge-stack"))
|
||||
.toBe(false);
|
||||
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID))
|
||||
.toBe(false);
|
||||
expect(useCombatStore.getState().resourcePools.rage).toBe(0);
|
||||
});
|
||||
|
||||
it("restores Rune Pulse's source-defined Rage only when its damage lands", () => {
|
||||
const runePulse = sourceAbility(498529);
|
||||
expect(runePulse.description).toContain("When it hits, it will restore");
|
||||
@@ -149,6 +339,76 @@ describe("RoM Champion source-specific behavior", () => {
|
||||
if (hit?.kind === "damage") expect(hit.onDamageResource).toEqual({ resource: "rage", amount: 5 });
|
||||
expect(effects.some((effect) => effect.kind === "apply-aura" && effect.aura.id.includes("-622327-")))
|
||||
.toBe(false);
|
||||
expect(effects.some((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-aura-498604-621253-bonus"))
|
||||
.toBe(false);
|
||||
expect(castEffects(498726)
|
||||
.some((effect) => effect.kind === "apply-aura" && effect.aura.id === "rom-aura-498604-621253-bonus"))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it("grants Rune Pulse's one-second bonus at cast start and expires it before the two-second impact", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-champion", secondaryClassId: "rom-rogue", raceId: "rom-dwarf", level: 22,
|
||||
});
|
||||
useCombatStore.getState().registerMob("champion-rune-pulse-target", {
|
||||
name: "Rune Pulse Target", maxHealth: 100_000,
|
||||
});
|
||||
setMobPosition("champion-rune-pulse-target", [3, 0, 0]);
|
||||
useCombatStore.getState().selectMob("champion-rune-pulse-target");
|
||||
const forge = sourceAbility(498523);
|
||||
expect(useCombatStore.getState().castAbility(forge.id, undefined, 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.setState((state) => ({ cooldowns: { ...state.cooldowns, [forge.id]: 0 } }));
|
||||
const hitSeed = Array.from({ length: 100 }, (_, index) => `rune-pulse-window-${index}`)
|
||||
.find((seed) => stableCombatUnit(`${seed}:498590:0`) < 0.3)!;
|
||||
useCombatStore.getState().damageMob(
|
||||
"champion-rune-pulse-target", 100, 3_000, undefined, "physical", {
|
||||
attackKind: "melee",
|
||||
seed: hitSeed,
|
||||
abilityId: "party-auto-attack",
|
||||
rules: { isAutoAttack: true, canMiss: false, canDodge: false, canParry: false, canBlock: false, canCrit: false },
|
||||
},
|
||||
);
|
||||
expect(useCombatStore.getState().auras.some((aura) => (
|
||||
aura.definition.id === ROM_CHAMPION_CHAIN_DRIVE_WINDOW_AURA_ID
|
||||
&& aura.expiresAt === 11_000
|
||||
))).toBe(true);
|
||||
|
||||
const runePulse = sourceAbility(498529);
|
||||
expect(runePulse.castTimeMs).toBe(2_000);
|
||||
expect(useCombatStore.getState().castAbility(runePulse.id, undefined, 3_100)).toMatchObject({ ok: true });
|
||||
const startedAura = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.definition.id === "rom-aura-498604-621253-bonus"
|
||||
));
|
||||
const runePulseBonuses = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.definition.id === "rom-aura-498604-621253-bonus"
|
||||
));
|
||||
expect(startedAura?.appliedAt).toBe(3_100);
|
||||
expect(startedAura?.expiresAt).toBe(4_100);
|
||||
expect(runePulseBonuses).toHaveLength(1);
|
||||
const activeAurasDuringBonus = expireAuras(useCombatStore.getState().auras, 3_500).active;
|
||||
const activeAurasWithoutBonus = activeAurasDuringBonus.filter((aura) => (
|
||||
aura.definition.id !== "rom-aura-498604-621253-bonus"
|
||||
));
|
||||
const outgoingDamageWithBonus = resolveDamageValue(100, activeAurasDuringBonus, {
|
||||
direction: "dealt", school: "physical", attackKind: "melee",
|
||||
});
|
||||
const outgoingDamageWithoutBonus = resolveDamageValue(100, activeAurasWithoutBonus, {
|
||||
direction: "dealt", school: "physical", attackKind: "melee",
|
||||
});
|
||||
expect(outgoingDamageWithBonus - outgoingDamageWithoutBonus).toBeCloseTo(20, 10);
|
||||
|
||||
useCombatStore.getState().tick(2, 5_100);
|
||||
const aurasAtImpact = expireAuras(useCombatStore.getState().auras, 5_100).active;
|
||||
const runePulseBonusesAtImpact = aurasAtImpact.filter((aura) => (
|
||||
aura.definition.id === "rom-aura-498604-621253-bonus"
|
||||
));
|
||||
const outgoingDamageAtImpact = resolveDamageValue(100, aurasAtImpact, {
|
||||
direction: "dealt", school: "physical", attackKind: "melee",
|
||||
});
|
||||
expect(outgoingDamageAtImpact).toBe(resolveDamageValue(100, aurasAtImpact.filter((aura) => (
|
||||
aura.definition.id !== "rom-aura-498604-621253-bonus"
|
||||
)), { direction: "dealt", school: "physical", attackKind: "melee" }));
|
||||
expect(runePulseBonusesAtImpact).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("routes Rune Siphon's positive source component to the caster as healing", () => {
|
||||
|
||||
Reference in new issue
Block a user