forked from phenom/healer-man
Release Healer Man 0.1.24
This commit is contained in:
1 parent
c4346751a9
commit
bac0da9300
118 files changed
+857
-289
No files matched your search
@@ -19,7 +19,7 @@ import {
|
||||
} from "./combatStore";
|
||||
import { TALENT_NODES, talentNodesForTree } from "./talentCatalog";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { abilitiesForClass, defaultActionBarForClass } from "./abilityCatalog";
|
||||
import { abilitiesForClass, abilityAtLevel, defaultActionBarForClass } from "./abilityCatalog";
|
||||
import { defaultActionBindings } from "./actionBindings";
|
||||
import { deriveCharacterStats, MANA_POOL_MULTIPLIER } from "./wotlkStats";
|
||||
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
|
||||
@@ -93,14 +93,12 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().mobs.ravager.health).toBe(100);
|
||||
useCombatStore.getState().tick(0.1, 2500);
|
||||
expect(useCombatStore.getState().mobs.ravager.health).toBeLessThan(100);
|
||||
expect(useCombatStore.getState().resource).toBe(
|
||||
startingResource - Math.round(startingResource / MANA_POOL_MULTIPLIER * 0.14),
|
||||
);
|
||||
expect(useCombatStore.getState().resource).toBe(startingResource - 17);
|
||||
expect(useCombatStore.getState().activeCast).toBeNull();
|
||||
expect(useCombatStore.getState().mobs.ravager.engaged).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the displayed primary mana and resolved percentage cost for affordability", () => {
|
||||
it("uses the displayed primary mana and fixed rank cost for affordability", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
||||
const initial = useCombatStore.getState();
|
||||
const frostbolt = initial.abilities.find((ability) => ability.id === "mage-frostbolt");
|
||||
@@ -108,7 +106,7 @@ describe("combat store", () => {
|
||||
const [cost] = resolvedAbilityCosts(frostbolt!, initial.talentModifiers, initial);
|
||||
expect(cost).toEqual({
|
||||
resource: "mana",
|
||||
amount: Math.round(initial.maxResource / MANA_POOL_MULTIPLIER * 0.14),
|
||||
amount: 17,
|
||||
});
|
||||
registerTarget("mana-target", [10, 0, 0]);
|
||||
|
||||
@@ -129,6 +127,31 @@ describe("combat store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not increase a classic spell's mana cost until a new rank is learned", () => {
|
||||
const pain = abilitiesForClass("priest").find((ability) => ability.id === "priest-shadow-word-pain");
|
||||
expect(pain).toBeDefined();
|
||||
|
||||
const resolvedAtLevel = (level: number) => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level });
|
||||
const state = useCombatStore.getState();
|
||||
const ranked = abilityAtLevel(pain!, level);
|
||||
return {
|
||||
dbcSpellId: ranked.dbcSpellId,
|
||||
maximumMana: state.maxResource,
|
||||
cost: resolvedAbilityCosts(ranked, state.talentModifiers, state)[0],
|
||||
};
|
||||
};
|
||||
|
||||
const rankFourAt26 = resolvedAtLevel(26);
|
||||
const rankFourAt32 = resolvedAtLevel(32);
|
||||
const rankFiveAt34 = resolvedAtLevel(34);
|
||||
|
||||
expect(rankFourAt26).toMatchObject({ dbcSpellId: 992, cost: { resource: "mana", amount: 133 } });
|
||||
expect(rankFourAt32).toMatchObject({ dbcSpellId: 992, cost: { resource: "mana", amount: 133 } });
|
||||
expect(rankFourAt32.maximumMana).toBeGreaterThan(rankFourAt26.maximumMana);
|
||||
expect(rankFiveAt34).toMatchObject({ dbcSpellId: 2767, cost: { resource: "mana", amount: 188 } });
|
||||
});
|
||||
|
||||
it("emits exactly one cast and release event and cancels presentation with the cast", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
||||
registerTarget("presentation-target", [10, 0, 0]);
|
||||
@@ -1080,13 +1103,16 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().talentModifiers).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "resource-cost-percent", amount: -15, sourceTalentName: "Convection" }),
|
||||
]));
|
||||
const lightningBolt = abilityAtLevel(
|
||||
abilitiesForClass("shaman").find((ability) => ability.id === "shaman-lightning-bolt")!,
|
||||
80,
|
||||
);
|
||||
const expectedLightningBoltCost = Math.round(lightningBolt.cost.amount * 0.85);
|
||||
registerTarget("mana-dummy", [10, 0, 0], 10_000, 0);
|
||||
expect(useCombatStore.getState().castSlot(0, undefined, 10_000).ok).toBe(true);
|
||||
const boltCompletion = useCombatStore.getState().activeCast!.completesAt;
|
||||
useCombatStore.getState().tick(0.1, boltCompletion);
|
||||
expect(useCombatStore.getState().resource).toBe(
|
||||
startingMana - Math.round(startingMana / MANA_POOL_MULTIPLIER * 0.1 * 0.85),
|
||||
);
|
||||
expect(useCombatStore.getState().resource).toBe(startingMana - expectedLightningBoltCost);
|
||||
});
|
||||
|
||||
it("routes a cast-time direct heal to the selected living party member", () => {
|
||||
|
||||
@@ -157,7 +157,6 @@ import {
|
||||
equipmentAttackPower,
|
||||
hastedDuration,
|
||||
healthFromTotalStamina,
|
||||
MANA_POOL_MULTIPLIER,
|
||||
ratingPercent,
|
||||
type CharacterStats,
|
||||
type WotlkAttackKind,
|
||||
@@ -1611,10 +1610,7 @@ export function resolvedAbilityCosts(
|
||||
: cost.resource === activeResourceType
|
||||
? state.maxResource
|
||||
: state.maxResourcePools[cost.resource] ?? defaultResourceMaximum(cost.resource);
|
||||
const percentageMaximum = cost.resource === "mana" && ability.source === "wow335-progression"
|
||||
? maximum / MANA_POOL_MULTIPLIER
|
||||
: maximum;
|
||||
const baseAmount = cost.percentage ? percentageMaximum * cost.amount / 100 : cost.amount;
|
||||
const baseAmount = cost.percentage ? maximum * cost.amount / 100 : cost.amount;
|
||||
const rawAmount = Math.max(0, Math.round(applyTalentModifierValue(
|
||||
baseAmount,
|
||||
modifiers,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { resolvedAbilityCosts, useCombatStore } from "./combatStore";
|
||||
import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog";
|
||||
import { respawnDefeatedPlayer } from "./deathRespawn";
|
||||
import { useGameStore } from "./store";
|
||||
import { useWailingEncounterStore } from "./wailingCavernsEncounter";
|
||||
import { MANA_POOL_MULTIPLIER } from "./wotlkStats";
|
||||
|
||||
describe("death respawn", () => {
|
||||
beforeEach(() => {
|
||||
@@ -32,10 +32,17 @@ describe("death respawn", () => {
|
||||
useCombatStore.getState().damageMob("naralex-event:ravager:1", 75, 950);
|
||||
useCombatStore.getState().selectMob("respawn-pack:a");
|
||||
useCombatStore.getState().damageMob("respawn-pack:a", 1, 1000);
|
||||
expect(useCombatStore.getState().castAbility("priest-psychic-scream", [10, -50, 20], 2000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resource).toBe(
|
||||
startingResource - Math.round(startingResource / MANA_POOL_MULTIPLIER * 0.15),
|
||||
const psychicScream = abilityAtLevel(
|
||||
abilitiesForClass("priest").find((ability) => ability.id === "priest-psychic-scream")!,
|
||||
14,
|
||||
);
|
||||
const psychicScreamCost = resolvedAbilityCosts(
|
||||
psychicScream,
|
||||
useCombatStore.getState().talentModifiers,
|
||||
useCombatStore.getState(),
|
||||
)[0].amount;
|
||||
expect(useCombatStore.getState().castAbility("priest-psychic-scream", [10, -50, 20], 2000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().resource).toBe(startingResource - psychicScreamCost);
|
||||
|
||||
const revisionBefore = useGameStore.getState().resetRevision;
|
||||
useWailingEncounterStore.setState({
|
||||
|
||||
@@ -102,8 +102,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "kalins-shrine-visual.glb",
|
||||
"checksum": "769accb48e23c9f80e28697e3e8454ef4148243a69ce247ed06ba4eb35bb0da7",
|
||||
"size": 8747072,
|
||||
"checksum": "023c43a56253e97fa0526185c61f76e0e9fa8398b7a35771728566c331cd22a9",
|
||||
"size": 8851724,
|
||||
"triangleCount": 202382
|
||||
}
|
||||
],
|
||||
@@ -133,7 +133,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
"navigation": {
|
||||
"id": "navigation",
|
||||
"fileName": "kalins-shrine-navigation.glb",
|
||||
"checksum": "37fd7f8efac36f83279efbfb33df37646453af214db29597bd14c8e9f19ddc04",
|
||||
"checksum": "6b77496f7b46890d401f955f9254ae37d25d9a1287a3bc9e787e9a254f9d9071",
|
||||
"size": 327792,
|
||||
"triangleCount": 6811
|
||||
}
|
||||
@@ -291,8 +291,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "hoto-visual.glb",
|
||||
"checksum": "0d12b9c6dc477704d2833c81f76113d340da7f07f5885a40bafd1321524e455e",
|
||||
"size": 6667208,
|
||||
"checksum": "beff3a180c55166d5ebd7e3610e65b59f8104edfd7596aa22ac3a49c578fcdc0",
|
||||
"size": 6710184,
|
||||
"triangleCount": 189855
|
||||
}
|
||||
],
|
||||
@@ -322,7 +322,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
"navigation": {
|
||||
"id": "navigation",
|
||||
"fileName": "hoto-navigation.glb",
|
||||
"checksum": "2cd78e6d8f52cb6488747a1f87cb688eedb77ccf412fb50674c25f70306477fd",
|
||||
"checksum": "a2bde2e4b8ed0d0bac8c4669a05192ff0a35ae9c64df0c0a29fe3735b65cbf4b",
|
||||
"size": 482260,
|
||||
"triangleCount": 10029
|
||||
}
|
||||
@@ -480,8 +480,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "mystic-altar-visual.glb",
|
||||
"checksum": "765dd1b15bbe79fe1ffc2104e10caece19acaf86c919f0327419cb3609dbf600",
|
||||
"size": 5552872,
|
||||
"checksum": "c37f27643598c8fe6401828be2950bc08f299dfe491df37736fda4662c70a96e",
|
||||
"size": 5552892,
|
||||
"triangleCount": 174521
|
||||
}
|
||||
],
|
||||
@@ -663,8 +663,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "kal-turok-hive-visual.glb",
|
||||
"checksum": "012867deefc4dc5a18c3e1a507309aa48f119cf4c2e88968fdd2787936ed7c24",
|
||||
"size": 1777616,
|
||||
"checksum": "75c0c596218cec07003e2def1d74c8e1f5ca3010ed370db558a1786b003f4f1c",
|
||||
"size": 1777728,
|
||||
"triangleCount": 24026
|
||||
}
|
||||
],
|
||||
@@ -838,8 +838,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "paspers-shrine-visual.glb",
|
||||
"checksum": "82ea6cafd19c0a323c994795699223a07193807da29b300eb12dac41146f1034",
|
||||
"size": 5625952,
|
||||
"checksum": "bcf6443e8adb8b7f7dd81f4a97b79aac1c99756a83289bbada31853d2d9adb7c",
|
||||
"size": 5674908,
|
||||
"triangleCount": 169935
|
||||
}
|
||||
],
|
||||
@@ -862,7 +862,7 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
"navigation": {
|
||||
"id": "navigation",
|
||||
"fileName": "paspers-shrine-navigation.glb",
|
||||
"checksum": "e05fb0b7a3c86bba3ac46606365169dea6c575281b2bb15707addcedb279f108",
|
||||
"checksum": "4ebba3fb97a99df49c7301dbfcdb8154634cf4e56a85c5ed587c7515221d0a10",
|
||||
"size": 152356,
|
||||
"triangleCount": 3156
|
||||
}
|
||||
@@ -1020,8 +1020,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "treasure-trove-visual.glb",
|
||||
"checksum": "f4c4dc12df65f8afc3c696308db17cb42b6a5894086dbe870dae2e21e1258448",
|
||||
"size": 7433280,
|
||||
"checksum": "c56d429187fd2ffbe20e11d5e7bbfd2128a779c9251fa2ab4b12286caf0de0b6",
|
||||
"size": 7433780,
|
||||
"triangleCount": 182387
|
||||
}
|
||||
],
|
||||
@@ -1210,8 +1210,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "ystra-labyrinth-boss1-visual.glb",
|
||||
"checksum": "c8ca72a5fdf487fa71a1aecd1e66ea821183f476540be9a89444b409be957c25",
|
||||
"size": 2709352,
|
||||
"checksum": "5e35eeccf5e207477bfb3a38dee75d53a5c5467433a90270b79a9ba8619a6260",
|
||||
"size": 2709464,
|
||||
"triangleCount": 64284
|
||||
}
|
||||
],
|
||||
@@ -1399,8 +1399,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "barren-caves-visual.glb",
|
||||
"checksum": "d0f733de4b26c0f432c5e160614765f320bf1753a829be269740b340e7415a12",
|
||||
"size": 1710152,
|
||||
"checksum": "e6dc49e6aa6fd20cee1b2afa43324a714f8f8284a6becef8dc514509b432f9bb",
|
||||
"size": 1710188,
|
||||
"triangleCount": 47084
|
||||
}
|
||||
],
|
||||
@@ -1574,8 +1574,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "ystra-labyrinth-boss2-visual.glb",
|
||||
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
|
||||
"size": 2682688,
|
||||
"checksum": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a",
|
||||
"size": 2682800,
|
||||
"triangleCount": 21428
|
||||
}
|
||||
],
|
||||
@@ -1749,8 +1749,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "ystra-labyrinth-boss3-visual.glb",
|
||||
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
|
||||
"size": 2682688,
|
||||
"checksum": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a",
|
||||
"size": 2682800,
|
||||
"triangleCount": 21428
|
||||
}
|
||||
],
|
||||
@@ -1924,8 +1924,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "ice-dwarf-kingdom-visual.glb",
|
||||
"checksum": "a6b902e85bb6026f18281d77efa235ec880ffc2fc13ea6bfdc431d24af768e00",
|
||||
"size": 9880192,
|
||||
"checksum": "41cef8ce1bf6dbf7df0c4e2797a325e4d50a3870d435d46684600894d4aa92e7",
|
||||
"size": 9880304,
|
||||
"triangleCount": 285517
|
||||
}
|
||||
],
|
||||
@@ -2120,8 +2120,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "the-origin-visual.glb",
|
||||
"checksum": "d1ded0316b2cbaa07458c56864c47c01afaa85e4f33dd13f5d95984d7305fb92",
|
||||
"size": 28683180,
|
||||
"checksum": "1f96c2b57a24470a003834a8c91cb0ceda5b01824d5bfa31904afd247c404e88",
|
||||
"size": 28684736,
|
||||
"triangleCount": 1176881
|
||||
}
|
||||
],
|
||||
@@ -2394,8 +2394,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "hall-of-survivors-visual.glb",
|
||||
"checksum": "18b9d116da599159719ad76eeee45a4d89056ee8c61cc4f21cab9a1d9228fff7",
|
||||
"size": 15216768,
|
||||
"checksum": "96b91b24462b8428e8808295dcea29d26de9be8431ea6667dde92cdded4ddf2a",
|
||||
"size": 15216936,
|
||||
"triangleCount": 380192
|
||||
}
|
||||
],
|
||||
@@ -2597,8 +2597,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "cave-water-dragon-visual.glb",
|
||||
"checksum": "cc57c0285de77690c4a1cdabaecd8b192791cbd05aca38aa21ed96462688ca31",
|
||||
"size": 8893112,
|
||||
"checksum": "dd1887f88d6ee9102da0e34ea2b4087742e37a2b8ce779c722bc124effe1faca",
|
||||
"size": 8893204,
|
||||
"triangleCount": 249082
|
||||
}
|
||||
],
|
||||
@@ -2786,8 +2786,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "lair-demon-dragon-visual.glb",
|
||||
"checksum": "d28481504802203e800985836f2b0907d67f6d6d0495c67500914e749d771ffe",
|
||||
"size": 4551300,
|
||||
"checksum": "b371465e96ef978915c9a515846ecf9dccb98607fd549617a1c71f45468fa5a7",
|
||||
"size": 4551356,
|
||||
"triangleCount": 64688
|
||||
}
|
||||
],
|
||||
@@ -2961,8 +2961,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "zurhidon-stronghold-visual.glb",
|
||||
"checksum": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4",
|
||||
"size": 17189492,
|
||||
"checksum": "58be5be365b14e42772a4560c5c1d10fceb8aa8abb2c1771cc21ba2b2e02872a",
|
||||
"size": 17189904,
|
||||
"triangleCount": 364765
|
||||
}
|
||||
],
|
||||
@@ -3340,8 +3340,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "demon-stronghold-visual.glb",
|
||||
"checksum": "9368984b3c183a8db6e8b7e18b7818edcf41b8edd45ffeca1d1df28aadc592a2",
|
||||
"size": 15166984,
|
||||
"checksum": "664181dc790b9e68cdc7451dbd0b1170a033a71ca19375743b6e66ca25889723",
|
||||
"size": 15167544,
|
||||
"triangleCount": 261609
|
||||
}
|
||||
],
|
||||
@@ -3530,8 +3530,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "zurhidon-stronghold-124-visual.glb",
|
||||
"checksum": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4",
|
||||
"size": 17189492,
|
||||
"checksum": "58be5be365b14e42772a4560c5c1d10fceb8aa8abb2c1771cc21ba2b2e02872a",
|
||||
"size": 17189904,
|
||||
"triangleCount": 364765
|
||||
}
|
||||
],
|
||||
@@ -3733,8 +3733,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "demon-stronghold-125-visual.glb",
|
||||
"checksum": "ecbacfe274e7eecb104c5807eedf3cc51cd4b7a628967c6331fc93b86396b842",
|
||||
"size": 15134500,
|
||||
"checksum": "e2106115dcc216999d2e37d26eeb52844ce4f41b44e19594699f37a54d7f19b9",
|
||||
"size": 15135060,
|
||||
"triangleCount": 259377
|
||||
}
|
||||
],
|
||||
@@ -3922,8 +3922,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "dungeon-of-dalanis-visual.glb",
|
||||
"checksum": "4c584ef34779e516f8e6ff3a4907eaab1c7c8d49d4c302de66e17b09222d8829",
|
||||
"size": 11936864,
|
||||
"checksum": "45cad819bc348dec8926f7de237c625e9d18697b4200804e0440dd16b8976959",
|
||||
"size": 11937088,
|
||||
"triangleCount": 288949
|
||||
}
|
||||
],
|
||||
@@ -4118,8 +4118,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "warnorken-arena-visual.glb",
|
||||
"checksum": "c452a843c2e4123d4fe0d20da41558f5b5a1906548b3d091dc691ffc42ed19d9",
|
||||
"size": 3752136,
|
||||
"checksum": "ee81c9e7c94d624977143297ca6c070c40799fdda1c51b92044c70dff53987ea",
|
||||
"size": 3752248,
|
||||
"triangleCount": 60751
|
||||
}
|
||||
],
|
||||
@@ -4293,8 +4293,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "raksha-temple-visual.glb",
|
||||
"checksum": "3a61098e772a0c656aa25d88aee357f78b84c738e45150fca6fd5059c2b675f7",
|
||||
"size": 23554528,
|
||||
"checksum": "1fad5149acfce870be822f5355369f15dd76fba280bbeda31ff7f371c442082f",
|
||||
"size": 23556584,
|
||||
"triangleCount": 605820
|
||||
}
|
||||
],
|
||||
@@ -4511,8 +4511,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "kawaks-tomb-visual.glb",
|
||||
"checksum": "de54d223725e2c3dfb0c47a97c208a212ef9423860bd0ac390f1482b302d3a32",
|
||||
"size": 8963188,
|
||||
"checksum": "3f1755780540c54a1ba9b528e646ebd39b12ab127e41fa2d497fd7749d202018",
|
||||
"size": 8963376,
|
||||
"triangleCount": 161613
|
||||
}
|
||||
],
|
||||
@@ -4693,8 +4693,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "grafu-castle-visual.glb",
|
||||
"checksum": "ba4552484ef9cddc1c00607d0dfd4c8fb0bae5b3cd3f992d215a580dac1e96ed",
|
||||
"size": 11149716,
|
||||
"checksum": "b775778f457d639e3f21982aaa6bd3ee1b0eba951c711d1ba319b0588e24a38f",
|
||||
"size": 11150124,
|
||||
"triangleCount": 207925
|
||||
}
|
||||
],
|
||||
@@ -4882,8 +4882,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "sardo-castle-visual.glb",
|
||||
"checksum": "16d759af9f8a3f98281e89362c7fb52dceff6543bbd837a7c6f89015920cdd94",
|
||||
"size": 7756568,
|
||||
"checksum": "191396625e301a41afab38e168848a198a9be5918a071dadda841683a7c6261e",
|
||||
"size": 7756828,
|
||||
"triangleCount": 147586
|
||||
}
|
||||
],
|
||||
@@ -5064,8 +5064,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "tomb-seven-heroes-visual.glb",
|
||||
"checksum": "d9f39da6b61c9f841c5d075e4985bc67e533ce422313fdf38d5f6c99dbb304c1",
|
||||
"size": 10910804,
|
||||
"checksum": "09250a1fbabf8a03f922c9a3950aee3daa1edc54215cd03ddc9bf5c96e1429bd",
|
||||
"size": 10911104,
|
||||
"triangleCount": 155422
|
||||
}
|
||||
],
|
||||
@@ -5246,8 +5246,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "aeternal-circle-visual.glb",
|
||||
"checksum": "42fdcff1b25b58688943a030f69fd14ea839247a7baabbc31b5f6f7faaf49785",
|
||||
"size": 4963912,
|
||||
"checksum": "de1ba06b7e3a9237fcff75aff2b00093287975e755cc7e98e77de1b7af6a9c14",
|
||||
"size": 4963968,
|
||||
"triangleCount": 61639
|
||||
}
|
||||
],
|
||||
@@ -5422,8 +5422,8 @@ export const RUNEWAKER_DUNGEON_DEFINITIONS = [
|
||||
{
|
||||
"id": "visual-000",
|
||||
"fileName": "kulech-bones-nest-visual.glb",
|
||||
"checksum": "2d912dcf7c0c8e59e36362fd947df1d5554c5dd5060590e1abb112e4c39f606f",
|
||||
"size": 12146892,
|
||||
"checksum": "b06b42b3c7f19abf9d645eec6be78f868cf6966e0b472e2ca4a1c36d3cf4d2d1",
|
||||
"size": 12147008,
|
||||
"triangleCount": 168192
|
||||
}
|
||||
],
|
||||
|
||||
@@ -2,12 +2,17 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { abilitiesForClass, abilityAtLevel, resourceProfileForClass } from "./abilityCatalog";
|
||||
import { aiHintsForAbility } from "./abilityRules";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { commitPartyAbilityUse, choosePartyAbility, partyAbilityIsReady } from "./partyAbilityAi";
|
||||
import { usePartyStore, type PartyMember } from "./partyStore";
|
||||
import { deriveCharacterStats } from "./wotlkStats";
|
||||
|
||||
function memberFor(classId: PartyMember["classId"], role: PartyMember["role"]): PartyMember {
|
||||
const base = usePartyStore.getState().members[0];
|
||||
const profile = resourceProfileForClass(classId);
|
||||
const maximum = profile.type === "mana"
|
||||
? deriveCharacterStats(classId, 80, EMPTY_ITEM_STATS).mana
|
||||
: profile.maximum;
|
||||
return {
|
||||
...base,
|
||||
classId,
|
||||
@@ -15,9 +20,9 @@ function memberFor(classId: PartyMember["classId"], role: PartyMember["role"]):
|
||||
level: 80,
|
||||
resourceType: profile.type,
|
||||
resourceName: profile.name,
|
||||
resource: profile.maximum,
|
||||
maxResource: profile.maximum,
|
||||
resourcePools: { [profile.type]: profile.maximum },
|
||||
resource: maximum,
|
||||
maxResource: maximum,
|
||||
resourcePools: { [profile.type]: maximum },
|
||||
cooldowns: {},
|
||||
globalCooldownEndsAt: 0,
|
||||
nextActionAt: 0,
|
||||
|
||||
@@ -61,6 +61,18 @@ describe("dungeon party composition", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("gives mana-using companions real pools that can pay fixed spell-rank costs", () => {
|
||||
const party = createDungeonParty("fixed-rank-mana", 32, "damage");
|
||||
const manaUsers = party.filter((member) => member.resourceType === "mana");
|
||||
|
||||
expect(manaUsers.length).toBeGreaterThan(0);
|
||||
for (const member of manaUsers) {
|
||||
expect(member.maxResource).toBeGreaterThan(100);
|
||||
expect(member.resource).toBe(member.maxResource);
|
||||
expect(member.resourcePools.mana).toBe(member.maxResource);
|
||||
}
|
||||
});
|
||||
|
||||
it("is deterministic per character and scales every ally to the player level", () => {
|
||||
const first = createDungeonParty("character-42", 18);
|
||||
const second = createDungeonParty("character-42", 18);
|
||||
|
||||
+42
-3
@@ -23,6 +23,7 @@ import {
|
||||
} from "./itemStats";
|
||||
import {
|
||||
damageAfterEquipmentMitigation,
|
||||
deriveCharacterStats,
|
||||
} from "./wotlkStats";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
|
||||
@@ -195,6 +196,18 @@ function memberMaxHealth(
|
||||
return base + healthFromStamina(gearStats.stamina);
|
||||
}
|
||||
|
||||
function memberMaxResource(
|
||||
classId: ClassId,
|
||||
level: number,
|
||||
raceId: RaceId,
|
||||
gearStats: ItemStats = EMPTY_ITEM_STATS,
|
||||
): number {
|
||||
const profile = resourceProfileForClass(classId);
|
||||
return profile.type === "mana"
|
||||
? deriveCharacterStats(classId, level, gearStats, raceId).mana
|
||||
: profile.maximum;
|
||||
}
|
||||
|
||||
function withMemberGearStats(member: PartyMember, gearStats: ItemStats): PartyMember {
|
||||
const maxHealth = memberMaxHealth(
|
||||
member.level,
|
||||
@@ -206,7 +219,19 @@ function withMemberGearStats(member: PartyMember, gearStats: ItemStats): PartyMe
|
||||
const health = member.health <= 0
|
||||
? 0
|
||||
: Math.max(1, Math.round(member.health / Math.max(1, member.maxHealth) * maxHealth));
|
||||
return { ...member, gearStats, health, maxHealth };
|
||||
const maxResource = memberMaxResource(member.classId, member.level, member.raceId, gearStats);
|
||||
const resource = Math.max(0, Math.round(
|
||||
member.resource / Math.max(1, member.maxResource) * maxResource,
|
||||
));
|
||||
return {
|
||||
...member,
|
||||
gearStats,
|
||||
health,
|
||||
maxHealth,
|
||||
resource,
|
||||
maxResource,
|
||||
resourcePools: Object.freeze({ ...member.resourcePools, [member.resourceType]: resource }),
|
||||
};
|
||||
}
|
||||
|
||||
function createMember(
|
||||
@@ -242,8 +267,10 @@ function createMember(
|
||||
});
|
||||
const maxHealth = memberMaxHealth(level, role, gearStats, template.classId, race.id);
|
||||
const resourceProfile = resourceProfileForClass(template.classId);
|
||||
const maxResource = resourceProfile.maximum;
|
||||
const resource = resourceProfile.initial;
|
||||
const maxResource = memberMaxResource(template.classId, level, race.id, gearStats);
|
||||
const resource = Math.round(
|
||||
maxResource * (resourceProfile.maximum > 0 ? resourceProfile.initial / resourceProfile.maximum : 0),
|
||||
);
|
||||
return {
|
||||
id,
|
||||
name: template.name,
|
||||
@@ -452,11 +479,23 @@ export const usePartyStore = create<PartyState>((set, get) => ({
|
||||
const health = member.health <= 0
|
||||
? 0
|
||||
: Math.max(1, Math.round(member.health / Math.max(1, member.maxHealth) * maxHealth));
|
||||
const maxResource = memberMaxResource(
|
||||
member.classId,
|
||||
safeLevel,
|
||||
member.raceId,
|
||||
member.gearStats,
|
||||
);
|
||||
const resource = Math.max(0, Math.round(
|
||||
member.resource / Math.max(1, member.maxResource) * maxResource,
|
||||
));
|
||||
return {
|
||||
...member,
|
||||
level: safeLevel,
|
||||
health,
|
||||
maxHealth,
|
||||
resource,
|
||||
maxResource,
|
||||
resourcePools: Object.freeze({ ...member.resourcePools, [member.resourceType]: resource }),
|
||||
talentRanks: runtimePartyTalentRanksForLevel(member.classId, member.specialization, safeLevel),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
|
||||
import { abilityAtLevel, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
|
||||
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
||||
|
||||
function byId(id: string): AbilityDefinition {
|
||||
@@ -60,12 +60,40 @@ describe("WoW 3.3.5 executable effect mapping", () => {
|
||||
expect(byId("wow335-death-knight-death-coil").ranks?.map((rank) => rank.spellId)).not.toContain(47632);
|
||||
});
|
||||
|
||||
it("keeps percentage costs and form resources source-correct", () => {
|
||||
expect(byId("wow335-mage-frostbolt").cost).toMatchObject({ resource: "mana", amount: 14, percentage: true });
|
||||
it("freezes mana costs per learned rank while keeping form resources source-correct", () => {
|
||||
expect(byId("wow335-mage-frostbolt").cost).toEqual({ resource: "mana", amount: 17 });
|
||||
|
||||
const pain = byId("wow335-priest-shadow-word-pain");
|
||||
expect(abilityAtLevel(pain, 26)).toMatchObject({
|
||||
dbcSpellId: 992,
|
||||
cost: { resource: "mana", amount: 133 },
|
||||
});
|
||||
expect(abilityAtLevel(pain, 32)).toMatchObject({
|
||||
dbcSpellId: 992,
|
||||
cost: { resource: "mana", amount: 133 },
|
||||
});
|
||||
expect(abilityAtLevel(pain, 34)).toMatchObject({
|
||||
dbcSpellId: 2767,
|
||||
cost: { resource: "mana", amount: 188 },
|
||||
});
|
||||
|
||||
expect(byId("wow335-druid-claw").cost).toMatchObject({ resource: "energy", amount: 45 });
|
||||
expect(byId("wow335-druid-maul").cost).toMatchObject({ resource: "rage", amount: 15 });
|
||||
});
|
||||
|
||||
it("never leaves an imported player mana cost tied to maximum mana", () => {
|
||||
const manaRanks = Object.values(WOW335_ABILITIES_BY_CLASS)
|
||||
.flat()
|
||||
.flatMap((ability) => ability.ranks ?? [])
|
||||
.filter((rank) => rank.cost.resource === "mana");
|
||||
|
||||
expect(manaRanks.length).toBeGreaterThan(100);
|
||||
for (const rank of manaRanks) {
|
||||
expect(rank.cost.percentage, `spell ${rank.spellId} rank ${rank.rank}`).not.toBe(true);
|
||||
expect(Number.isFinite(rank.cost.amount), `spell ${rank.spellId} rank ${rank.rank}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("provides mechanics for previously silent class scripts and hidden proc spells", () => {
|
||||
expect(effectsOfKind(byId("wow335-warrior-slam"), "damage")).not.toEqual([]);
|
||||
expect(effectsOfKind(byId("wow335-death-knight-death-grip"), "pull")).not.toEqual([]);
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
} from "./combatAuras";
|
||||
import generatedData from "./wow335AbilityData.generated.json";
|
||||
import { playerCombatPower } from "./progression";
|
||||
import { baseResourcesFor } from "./wotlkStats";
|
||||
|
||||
export const BASE_CLASS_STARTER_SOURCE_LEVEL = 10;
|
||||
|
||||
@@ -1164,8 +1165,19 @@ function targetForRank(rank: Wow335Rank, effects: readonly AbilityEffect[]): Abi
|
||||
return "self";
|
||||
}
|
||||
|
||||
function costForRank(resource: ResourceType, rank: Wow335Rank): number {
|
||||
if (rank.powerCostPercentage > 0) return rank.powerCostPercentage;
|
||||
function costForRank(classId: BaseClassId, resource: ResourceType, rank: Wow335Rank): number {
|
||||
if (rank.powerCostPercentage > 0) {
|
||||
if (resource === "mana") {
|
||||
// Freeze DBC percent-of-base-mana costs at the level where this rank is
|
||||
// learned. Advancing levels or equipping Intellect must never make an
|
||||
// already-known rank more expensive; learning a later rank still can.
|
||||
const sourceLevel = Math.max(1, rank.sourceLevel || rank.level);
|
||||
return Math.max(0, Math.round(
|
||||
baseResourcesFor(classId, sourceLevel).mana * rank.powerCostPercentage / 100,
|
||||
));
|
||||
}
|
||||
return rank.powerCostPercentage;
|
||||
}
|
||||
if (resource === "rage" || resource === "runic-power") return Math.round(rank.powerCost / 10);
|
||||
return rank.powerCost;
|
||||
}
|
||||
@@ -1205,8 +1217,8 @@ function rankDefinition(
|
||||
gcdMs: rank.gcdMs,
|
||||
cost: {
|
||||
resource: rankResource,
|
||||
amount: costForRank(rankResource, rank),
|
||||
...(rank.powerCostPercentage > 0 ? { percentage: true } : {}),
|
||||
amount: costForRank(classId, rankResource, rank),
|
||||
...(rank.powerCostPercentage > 0 && rankResource !== "mana" ? { percentage: true } : {}),
|
||||
},
|
||||
effects,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user