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
@@ -0,0 +1,328 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { abilitiesForClass, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
clearPartyRuntimeRegistry,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeTargetVisibility,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import {
|
||||
WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG,
|
||||
WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG,
|
||||
} from "./wow335ShamanNaturesGuardianRuntime";
|
||||
|
||||
const TARGET_ID = "wow335-shaman-natures-guardian-target";
|
||||
const ENEMY_HIT_AT = 1_550;
|
||||
const PLAYER_NEXT_ACTION_AT = 2_000;
|
||||
|
||||
function natureGuardianAuras(actorId: string) {
|
||||
return useCombatStore.getState().auras.filter((aura) => aura.sourceId === actorId && aura.targetId === actorId);
|
||||
}
|
||||
|
||||
function armShamanForHit(targetActorId: string): void {
|
||||
const actorId = targetActorId === PLAYER_AURA_ENTITY_ID ? PLAYER_AGGRO_ID : targetActorId;
|
||||
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||
name: "Nature's Guardian target",
|
||||
level: 80,
|
||||
maxHealth: 1_000_000,
|
||||
xpReward: 0,
|
||||
attacks: [{
|
||||
...DEFAULT_ENEMY_ATTACK,
|
||||
id: "nature-guardian-incoming-hit",
|
||||
name: "Nature Guardian hit",
|
||||
delivery: "projectile",
|
||||
school: "physical",
|
||||
range: 30,
|
||||
damageMultiplier: 6,
|
||||
}],
|
||||
});
|
||||
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(
|
||||
TARGET_ID,
|
||||
1,
|
||||
100,
|
||||
{ actorId, role: "damage", abilityId: "nature-guardian-test-engage" },
|
||||
"physical",
|
||||
);
|
||||
}
|
||||
|
||||
describe("WoW Shaman Nature's Guardian player and companion integration", () => {
|
||||
beforeEach(async () => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
await preloadAbilityCatalogsForClasses(["shaman", "mage"]);
|
||||
useCombatStore.getState().initializeCharacter({ classId: "shaman", role: "damage", level: 80, xp: 0 });
|
||||
useCombatStore.setState({ auras: [], effects: [], combatEvents: [] });
|
||||
usePartyStore.getState().initializeParty(
|
||||
"wow-shaman-natures-guardian-player",
|
||||
80,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("arms on the player's direct hit below 30%, then spends one charge on a Nature damage spell", () => {
|
||||
useCombatStore.setState((state) => ({
|
||||
health: Math.round(state.maxHealth * 0.4),
|
||||
talentRanks: { ...state.talentRanks, "talent-1699": 3 },
|
||||
}));
|
||||
armShamanForHit(PLAYER_AURA_ENTITY_ID);
|
||||
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
|
||||
|
||||
expect(useCombatStore.getState().health).toBeGreaterThan(0);
|
||||
expect(useCombatStore.getState().health).toBeLessThan(useCombatStore.getState().maxHealth * 0.3);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ definition: expect.objectContaining({
|
||||
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
|
||||
}) }),
|
||||
expect.objectContaining({ definition: expect.objectContaining({
|
||||
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG]),
|
||||
}) }),
|
||||
]));
|
||||
|
||||
const lightningBolt = abilitiesForClass("shaman").find((ability) => ability.name === "Lightning Bolt");
|
||||
expect(lightningBolt).toBeDefined();
|
||||
const cast = useCombatStore.getState().castAbility(lightningBolt!.id, { targetActorId: TARGET_ID, now: PLAYER_NEXT_ACTION_AT });
|
||||
expect(cast.ok, cast.reason).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt);
|
||||
|
||||
const damageEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
||||
event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID && event.abilityId === lightningBolt!.id
|
||||
));
|
||||
expect(damageEvent?.critical).toBe(true);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(false);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG)
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("spends the player's charge on an accepted Nature healing spell and guarantees its critical heal", () => {
|
||||
useCombatStore.setState((state) => ({
|
||||
health: Math.round(state.maxHealth * 0.4),
|
||||
talentRanks: { ...state.talentRanks, "talent-1699": 3 },
|
||||
}));
|
||||
armShamanForHit(PLAYER_AURA_ENTITY_ID);
|
||||
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ definition: expect.objectContaining({
|
||||
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
|
||||
}) }),
|
||||
]));
|
||||
|
||||
const healingWave = abilitiesForClass("shaman").find((ability) => ability.name === "Healing Wave");
|
||||
expect(healingWave).toBeDefined();
|
||||
const rejected = useCombatStore.getState().castAbility(healingWave!.id, {
|
||||
targetActorId: "missing-friendly-target",
|
||||
now: PLAYER_NEXT_ACTION_AT,
|
||||
});
|
||||
expect(rejected.ok).toBe(false);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(true);
|
||||
const cast = useCombatStore.getState().castAbility(healingWave!.id, {
|
||||
now: PLAYER_NEXT_ACTION_AT,
|
||||
});
|
||||
expect(cast.ok, cast.reason).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
expect(activeCast, "Healing Wave remains charged while casting").toBeDefined();
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(true);
|
||||
expect(useCombatStore.getState().cancelCast("test interruption", PLAYER_NEXT_ACTION_AT + 500)).toBe(true);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(true);
|
||||
const resolvedCast = useCombatStore.getState().castAbility(healingWave!.id, {
|
||||
now: PLAYER_NEXT_ACTION_AT + 2_000,
|
||||
});
|
||||
expect(resolvedCast.ok, resolvedCast.reason).toBe(true);
|
||||
const resolvedActiveCast = useCombatStore.getState().activeCast;
|
||||
expect(resolvedActiveCast, "the retry begins a new Healing Wave cast").toBeDefined();
|
||||
useCombatStore.getState().tick(0.1, resolvedActiveCast!.completesAt);
|
||||
|
||||
const healingEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
||||
event.kind === "healing" && event.sourceActorId === PLAYER_AGGRO_ID
|
||||
&& event.targetActorId === PLAYER_AURA_ENTITY_ID && event.abilityId === healingWave!.id
|
||||
));
|
||||
expect(healingEvent?.critical).toBe(true);
|
||||
expect(natureGuardianAuras(PLAYER_AURA_ENTITY_ID).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses the companion's own rank for the incoming proc and consumes one charge after its accepted Nature spell", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80, xp: 0 });
|
||||
usePartyStore.getState().initializeParty(
|
||||
"wow-shaman-natures-guardian-companion",
|
||||
80,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
1,
|
||||
);
|
||||
const lightningBolt = abilitiesForClass("shaman").find((ability) => ability.name === "Lightning Bolt");
|
||||
expect(lightningBolt).toBeDefined();
|
||||
const shamanBase = createConfiguredPartyMember({
|
||||
name: "Nature's Guardian Shaman",
|
||||
classId: "shaman",
|
||||
specialization: "Elemental",
|
||||
specializationId: "shaman:elemental",
|
||||
talentTreeId: "shaman-elemental",
|
||||
talentRanks: { "talent-1699": 3 },
|
||||
role: "damage",
|
||||
}, 80, 0, "wow-shaman-natures-guardian-companion");
|
||||
const disabledCooldowns = Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]));
|
||||
const member = {
|
||||
...shamanBase,
|
||||
gearStats: EMPTY_ITEM_STATS,
|
||||
health: Math.round(shamanBase.maxHealth * 0.4),
|
||||
cooldowns: disabledCooldowns,
|
||||
nextActionAt: 1_000_000,
|
||||
activeCast: null,
|
||||
activeCastTargetId: null,
|
||||
status: "ready" as const,
|
||||
};
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: TARGET_ID });
|
||||
useCombatStore.getState().commitCompanionActors([member]);
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
useCombatStore.setState((state) => ({
|
||||
actors: {
|
||||
...state.actors,
|
||||
[member.id]: { ...state.actors[member.id]!, health: member.health, alive: true },
|
||||
},
|
||||
}));
|
||||
armShamanForHit(member.id);
|
||||
updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true);
|
||||
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
|
||||
|
||||
const actorAfterHit = useCombatStore.getState().actors[member.id];
|
||||
expect(actorAfterHit?.health).toBeGreaterThan(0);
|
||||
expect(actorAfterHit?.health).toBeLessThan(actorAfterHit!.maxHealth * 0.3);
|
||||
expect(natureGuardianAuras(member.id)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ definition: expect.objectContaining({
|
||||
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG]),
|
||||
}) }),
|
||||
expect.objectContaining({ definition: expect.objectContaining({
|
||||
utilityTags: expect.arrayContaining([WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG]),
|
||||
}) }),
|
||||
]));
|
||||
|
||||
const readyForBolt = {
|
||||
...usePartyStore.getState().members[0]!,
|
||||
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [
|
||||
ability.id,
|
||||
ability.id === lightningBolt!.id ? 0 : 1_000_000,
|
||||
])),
|
||||
nextActionAt: PLAYER_NEXT_ACTION_AT,
|
||||
activeCast: null,
|
||||
activeCastTargetId: null,
|
||||
};
|
||||
usePartyStore.setState({ members: [readyForBolt] });
|
||||
useCombatStore.getState().commitCompanionActors([readyForBolt]);
|
||||
advanceDungeonPartyCombat(PLAYER_NEXT_ACTION_AT);
|
||||
const started = usePartyStore.getState().members[0]?.activeCast;
|
||||
expect(started, "the companion begins Lightning Bolt").toBeDefined();
|
||||
advanceDungeonPartyCombat(started!.completesAt);
|
||||
|
||||
const damageEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
||||
event.kind === "damage" && event.sourceActorId === member.id && event.abilityId === lightningBolt!.id
|
||||
));
|
||||
expect(damageEvent?.critical).toBe(true);
|
||||
expect(natureGuardianAuras(member.id).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(false);
|
||||
expect(natureGuardianAuras(member.id).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_COOLDOWN_TAG)
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it("spends the companion's charge on its accepted Nature healing spell", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80, xp: 0 });
|
||||
usePartyStore.getState().initializeParty(
|
||||
"wow-shaman-natures-guardian-companion-heal",
|
||||
80,
|
||||
useCombatStore.getState().health,
|
||||
"damage",
|
||||
1,
|
||||
);
|
||||
const healingWave = abilitiesForClass("shaman").find((ability) => ability.name === "Healing Wave");
|
||||
expect(healingWave).toBeDefined();
|
||||
const shamanBase = createConfiguredPartyMember({
|
||||
name: "Nature's Guardian Healing Shaman",
|
||||
classId: "shaman",
|
||||
specialization: "Restoration",
|
||||
specializationId: "shaman:restoration",
|
||||
talentTreeId: "shaman-restoration",
|
||||
talentRanks: { "talent-1699": 3 },
|
||||
role: "healer",
|
||||
}, 80, 0, "wow-shaman-natures-guardian-companion-heal");
|
||||
const disabledCooldowns = Object.fromEntries(abilitiesForClass("shaman").map((ability) => [ability.id, 1_000_000]));
|
||||
const member = {
|
||||
...shamanBase,
|
||||
gearStats: EMPTY_ITEM_STATS,
|
||||
health: Math.round(shamanBase.maxHealth * 0.4),
|
||||
cooldowns: disabledCooldowns,
|
||||
nextActionAt: 1_000_000,
|
||||
activeCast: null,
|
||||
activeCastTargetId: null,
|
||||
status: "ready" as const,
|
||||
};
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: TARGET_ID });
|
||||
useCombatStore.getState().commitCompanionActors([member]);
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
useCombatStore.setState((state) => ({
|
||||
actors: {
|
||||
...state.actors,
|
||||
[member.id]: { ...state.actors[member.id]!, health: member.health, alive: true },
|
||||
},
|
||||
}));
|
||||
armShamanForHit(member.id);
|
||||
updatePartyRuntimeTargetVisibility(member.id, TARGET_ID, true);
|
||||
useCombatStore.getState().advanceMobCombat(ENEMY_HIT_AT);
|
||||
|
||||
const actorAfterHit = useCombatStore.getState().actors[member.id];
|
||||
expect(actorAfterHit?.health).toBeLessThan(actorAfterHit!.maxHealth * 0.3);
|
||||
expect(natureGuardianAuras(member.id).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(true);
|
||||
const readyForHeal = {
|
||||
...usePartyStore.getState().members[0]!,
|
||||
health: actorAfterHit!.health,
|
||||
cooldowns: Object.fromEntries(abilitiesForClass("shaman").map((ability) => [
|
||||
ability.id,
|
||||
ability.id === healingWave!.id ? 0 : 1_000_000,
|
||||
])),
|
||||
nextActionAt: PLAYER_NEXT_ACTION_AT,
|
||||
activeCast: null,
|
||||
activeCastTargetId: null,
|
||||
};
|
||||
usePartyStore.setState({ members: [readyForHeal] });
|
||||
useCombatStore.getState().commitCompanionActors([readyForHeal]);
|
||||
advanceDungeonPartyCombat(PLAYER_NEXT_ACTION_AT);
|
||||
const activeCast = usePartyStore.getState().members[0]?.activeCast;
|
||||
expect(activeCast?.abilityId).toBe(healingWave!.id);
|
||||
expect(natureGuardianAuras(member.id).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(true);
|
||||
advanceDungeonPartyCombat(activeCast!.completesAt);
|
||||
|
||||
const healingEvent = [...useCombatStore.getState().combatEvents].reverse().find((event) => (
|
||||
event.kind === "healing" && event.sourceActorId === member.id
|
||||
&& event.targetActorId === member.id && event.abilityId === healingWave!.id
|
||||
));
|
||||
expect(healingEvent?.critical).toBe(true);
|
||||
expect(natureGuardianAuras(member.id).some((aura) => (
|
||||
aura.definition.utilityTags?.includes(WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG)
|
||||
))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user