forked from phenom/healer-man
Release Healer Man 0.1.15
This commit is contained in:
1 parent
7b41b102fa
commit
f49e54f35f
43 files changed
+1478
-112
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ClassId } from "../app/characterCatalog";
|
||||
import { classCanEquipItem, equipmentSlotForItem } from "./equipment";
|
||||
import {
|
||||
compareEquipmentUpgrade,
|
||||
resolvedAverageItemLevel,
|
||||
resolvedEquipmentItems,
|
||||
resolvedEquipmentLoadout,
|
||||
type HeirloomContext,
|
||||
} from "./heirloomEquipment";
|
||||
import { itemStatsFor } from "./itemStats";
|
||||
import { LOOT_CATALOG, scaledLootItemLevel, type LootCatalogItem } from "./lootCatalog";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
import {
|
||||
SPECIALIZATION_DEFINITIONS,
|
||||
type SpecializationDefinition,
|
||||
} from "./specializationCatalog";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { normalizeInventory } from "./lootTypes";
|
||||
|
||||
const templates = LOOT_CATALOG.bosses.flatMap((boss) => boss.items);
|
||||
|
||||
function context(profile: SpecializationDefinition, level: number, ownerKind: "player" | "party" = "player"): HeirloomContext {
|
||||
return {
|
||||
ownerId: ownerKind === "player" ? "player" : `party-${profile.id}`,
|
||||
ownerKind,
|
||||
classId: profile.classId,
|
||||
specializationId: profile.id,
|
||||
role: profile.role,
|
||||
level,
|
||||
};
|
||||
}
|
||||
|
||||
function catalogItem(template: LootCatalogItem, itemLevel: number, suffix = "test"): InventoryItem {
|
||||
return {
|
||||
instanceId: `${template.itemId}:${itemLevel}:${suffix}`,
|
||||
itemId: template.itemId,
|
||||
itemKey: template.itemKey,
|
||||
name: template.itemName,
|
||||
quality: template.quality,
|
||||
inventorySlot: template.inventorySlot,
|
||||
armorType: template.armorType,
|
||||
weaponType: template.weaponType,
|
||||
itemLevel,
|
||||
sourceItemLevel: template.sourceItemLevel,
|
||||
stats: itemStatsFor({ ...template, itemLevel }),
|
||||
quantity: 1,
|
||||
sourceBossKey: "test-boss",
|
||||
sourceBossName: "Test Boss",
|
||||
acquiredAt: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function compatibleItems(classId: ClassId, level: number): InventoryItem[] {
|
||||
return templates
|
||||
.map((template, index) => catalogItem(template, level, String(index)))
|
||||
.filter((item) => classCanEquipItem(classId, item));
|
||||
}
|
||||
|
||||
describe("specialization-aware heirlooms", () => {
|
||||
it("caps player heirlooms at 79 and party heirlooms at 80", () => {
|
||||
const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!;
|
||||
expect(new Set(resolvedEquipmentItems({}, [], context(profile, 80)).map((item) => item.itemLevel))).toEqual(new Set([79]));
|
||||
expect(new Set(resolvedEquipmentItems({}, [], context(profile, 80, "party")).map((item) => item.itemLevel))).toEqual(new Set([80]));
|
||||
expect(resolvedAverageItemLevel({}, [], context(profile, 40))).toBe(40);
|
||||
});
|
||||
|
||||
it("fills all applicable slots and uses the specialization weapon layout", () => {
|
||||
const protection = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "warrior:protection")!;
|
||||
const loadout = resolvedEquipmentLoadout({}, [], context(protection, 40));
|
||||
expect(loadout["head"]?.quality).toBe("heirloom");
|
||||
expect(loadout["finger"]?.quality).toBe("heirloom");
|
||||
expect(loadout["trinket"]?.quality).toBe("heirloom");
|
||||
expect(loadout["main-hand"]?.inventorySlot).toBe("one-hand");
|
||||
expect(loadout["off-hand"]?.armorType).toBe("shield");
|
||||
});
|
||||
|
||||
it("allocates only useful archetype stats", () => {
|
||||
const protection = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "warrior:protection")!;
|
||||
const holy = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!;
|
||||
const combat = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "rogue:combat")!;
|
||||
const protectionStats = resolvedEquipmentItems({}, [], context(protection, 40)).find((item) => item.inventorySlot === "chest")!.stats;
|
||||
const holyStats = resolvedEquipmentItems({}, [], context(holy, 40)).find((item) => item.inventorySlot === "chest")!.stats;
|
||||
const combatStats = resolvedEquipmentItems({}, [], context(combat, 40)).find((item) => item.inventorySlot === "chest")!.stats;
|
||||
expect(protectionStats).toMatchObject({ stamina: expect.any(Number), defenseRating: expect.any(Number), blockRating: expect.any(Number), spellPower: 0 });
|
||||
expect(holyStats).toMatchObject({ intellect: expect.any(Number), spirit: expect.any(Number), spellPower: expect.any(Number), mp5: expect.any(Number), attackPower: 0 });
|
||||
expect(combatStats).toMatchObject({ agility: expect.any(Number), attackPower: expect.any(Number), hitRating: expect.any(Number), spellPower: 0 });
|
||||
expect(protectionStats.blockRating).toBeGreaterThan(0);
|
||||
expect(holyStats.spellPower).toBeGreaterThan(0);
|
||||
expect(combatStats.agility).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each([1, 40, 79])("keeps compatible same-level boss items below heirlooms at level %i", (level) => {
|
||||
for (const profile of SPECIALIZATION_DEFINITIONS) {
|
||||
const loadout = resolvedEquipmentLoadout({}, [], context(profile, level));
|
||||
for (const item of compatibleItems(profile.classId, level)) {
|
||||
expect(compareEquipmentUpgrade(item, loadout, profile, context(profile, level)).isUpgrade, `${profile.id}: ${item.name}`).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("opens a real item-level-85 upgrade path for every gear profile at level 80", () => {
|
||||
expect(scaledLootItemLevel(79)).toBe(79);
|
||||
expect(scaledLootItemLevel(80)).toBe(85);
|
||||
for (const profile of SPECIALIZATION_DEFINITIONS) {
|
||||
const loadout = resolvedEquipmentLoadout({}, [], context(profile, 80));
|
||||
const upgrades = compatibleItems(profile.classId, 85)
|
||||
.filter((item) => compareEquipmentUpgrade(item, loadout, profile, context(profile, 80)).isUpgrade);
|
||||
expect(upgrades.length, profile.id).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("lets earned assignments override a slot and restores its heirloom when removed", () => {
|
||||
const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!;
|
||||
const ownerContext = context(profile, 80);
|
||||
const item = compatibleItems(profile.classId, 85).find((candidate) => equipmentSlotForItem(candidate) === "head")
|
||||
?? compatibleItems(profile.classId, 85)[0];
|
||||
const slot = equipmentSlotForItem(item)!;
|
||||
const equipped = resolvedEquipmentLoadout({ player: { [slot]: item.instanceId } }, [item], ownerContext);
|
||||
expect(equipped[slot]).toBe(item);
|
||||
expect(resolvedEquipmentLoadout({}, [item], ownerContext)[slot]?.quality).toBe("heirloom");
|
||||
});
|
||||
|
||||
it("preserves player health and resource percentages across gear maximum changes", () => {
|
||||
const profile = SPECIALIZATION_DEFINITIONS.find((definition) => definition.id === "priest:holy")!;
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "priest",
|
||||
specializationId: profile.id,
|
||||
role: profile.role,
|
||||
level: 40,
|
||||
});
|
||||
const item = compatibleItems("priest", 85).find((candidate) => equipmentSlotForItem(candidate) === "head")
|
||||
?? compatibleItems("priest", 85)[0];
|
||||
const before = useCombatStore.getState();
|
||||
useCombatStore.setState({
|
||||
health: before.maxHealth * 0.4,
|
||||
resource: before.maxResource * 0.25,
|
||||
inventory: [item],
|
||||
});
|
||||
expect(useCombatStore.getState().equipItem(item.instanceId)).toBe(true);
|
||||
expect(useCombatStore.getState().health / useCombatStore.getState().maxHealth).toBeCloseTo(0.4, 5);
|
||||
expect(useCombatStore.getState().resource / useCombatStore.getState().maxResource).toBeCloseTo(0.25, 5);
|
||||
});
|
||||
|
||||
it("raises persisted item safety to 255 without admitting virtual heirlooms", () => {
|
||||
const persisted = { ...compatibleItems("priest", 85)[0], itemLevel: 999 };
|
||||
expect(normalizeInventory([persisted])[0].itemLevel).toBe(255);
|
||||
expect(normalizeInventory([{ ...persisted, quality: "heirloom" }])).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user