Release Healer Man 0.1.15

This commit is contained in:
phenom committed 2026-09-05 15:33:06 -04:00
1 parent 7b41b102fa
commit f49e54f35f
43 files changed
+1478 -112

No files matched your search

+150
View File
@@ -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([]);
});
});