Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+202
View File
@@ -9,6 +9,7 @@ import {
loadAccountBossCards,
saveAccountBossCards,
replaceCharactersForOwner,
setRosterChangeListener,
deleteCharacter,
loadCombatPresentationSettings,
loginLocalAccount,
@@ -16,10 +17,12 @@ import {
saveSession,
updateCharacterActionBindings,
updateCharacterEquipment,
updateCharacterHunterPetFamily,
updateCharacterDungeonRunCheckpoint,
updateCharacterInventory,
updateCharacterManastormProgress,
updateCharacterProgression,
updateCharacterStatRuleset,
updateCharacterSettings,
updateCharacterSpecialization,
updateCombatPresentationSettings,
@@ -48,7 +51,206 @@ class MemoryStorage implements StorageLike {
removeItem(key: string) { this.values.delete(key); }
}
class FailingWriteStorage extends MemoryStorage {
failWrites = false;
setItem(key: string, value: string) {
if (this.failWrites) throw new Error("Storage quota exceeded.");
super.setItem(key, value);
}
}
describe("local profile repository", () => {
it("normalizes legacy Hunter family data to Wolf and persists implemented family choices", () => {
const storage = new MemoryStorage();
const hunter = createCharacter({
ownerId: "offline-roster",
name: "Shellguard",
categoryId: "wow",
raceId: "human",
classId: "hunter",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
expect(hunter.hunterPetFamily).toBe("wolf");
const cat = updateCharacterHunterPetFamily(hunter.ownerId, hunter.id, "cat", storage)!;
expect(cat.hunterPetFamily).toBe("cat");
expect(listCharacters(hunter.ownerId, storage)[0]?.hunterPetFamily).toBe("cat");
expect(updateCharacterHunterPetFamily(hunter.ownerId, hunter.id, "bear", storage)).toBeNull();
expect(listCharacters(hunter.ownerId, storage)[0]?.hunterPetFamily).toBe("cat");
const invalidStoredFamily = { ...cat, hunterPetFamily: "bear" } as unknown as typeof cat;
const [normalizedLegacy] = replaceCharactersForOwner(hunter.ownerId, [invalidStoredFamily], storage);
expect(normalizedLegacy?.hunterPetFamily).toBe("wolf");
});
it("defaults legacy saved characters to WoW stat rules and new RoM characters to native RoM", () => {
const storage = new MemoryStorage();
const legacyRom = {
id: "legacy-rom",
ownerId: "offline-roster",
name: "Old Healer",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-priest",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
level: 4,
location: "Dungeon Entrance",
createdAt: 1,
lastPlayedAt: 1,
};
storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({
version: 7,
accounts: [],
rosters: { "offline-roster": [legacyRom] },
bossCardsByOwner: {},
presentationSettingsByOwner: {},
}));
expect(listCharacters("offline-roster", storage)[0]?.statRuleset).toBe("wow");
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).rosters["offline-roster"][0].statRuleset)
.toBe("wow");
const created = createCharacter({
ownerId: "offline-roster",
name: "New Healer",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-priest",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
expect(created.statRuleset).toBe("rom");
});
it("keeps a legacy roster readable when its ruleset migration cannot be written", () => {
const storage = new FailingWriteStorage();
const character = createCharacter({
ownerId: "offline-roster", name: "Legacy Healer", classId: "rom-priest",
raceId: "rom-human", gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
const raw = JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!);
delete raw.rosters[character.ownerId][0].statRuleset;
const legacySave = JSON.stringify(raw);
storage.setItem(PROFILE_STORAGE_KEYS.database, legacySave);
storage.failWrites = true;
expect(listCharacters(character.ownerId, storage)[0]).toMatchObject({
id: character.id, statRuleset: "wow",
});
expect(updateCharacterStatRuleset(character.ownerId, character.id, "rom", storage)).toBeNull();
expect(storage.getItem(PROFILE_STORAGE_KEYS.database)).toBe(legacySave);
expect(listCharacters(character.ownerId, storage)[0]?.statRuleset).toBe("wow");
storage.failWrites = false;
expect(listCharacters(character.ownerId, storage)[0]?.statRuleset).toBe("wow");
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).rosters[character.ownerId][0].statRuleset)
.toBe("wow");
});
it("switches one RoM profile while preserving its build and other characters", () => {
const storage = new MemoryStorage();
const first = createCharacter({
ownerId: "offline-roster",
name: "First",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-priest",
secondaryClassId: "rom-mage",
actionLoadouts: { "rom-priest:rom-mage": { "primary:face-top": "rom-priest-heal" } },
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
const item = {
instanceId: "kept-item",
itemId: 10412,
itemKey: "belt-of-the-fang",
name: "Belt of the Fang",
quality: "rare" as const,
inventorySlot: "waist",
armorType: "leather",
weaponType: null,
itemLevel: 17,
sourceItemLevel: 21,
stats: { ...EMPTY_ITEM_STATS, agility: 4, stamina: 4, armor: 15 },
quantity: 1,
sourceBossKey: "lady-anacondra",
sourceBossName: "Lady Anacondra",
acquiredAt: 5_000,
};
updateCharacterActionBindings(first.ownerId, first.id, { "primary:face-top": "rom-priest-heal" }, storage);
updateCharacterInventory(first.ownerId, first.id, [item], storage);
const second = createCharacter({
ownerId: "offline-roster",
name: "Second",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-mage",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
const wowCharacter = createCharacter({
ownerId: "offline-roster",
name: "WoW Character",
categoryId: "wow",
raceId: "human",
classId: "priest",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
let cloudSnapshot: ReadonlyArray<ReturnType<typeof listCharacters>[number]> = [];
setRosterChangeListener((ownerId, characters) => {
if (ownerId === first.ownerId) cloudSnapshot = characters;
});
const updated = updateCharacterStatRuleset(first.ownerId, first.id, "wow", storage)!;
setRosterChangeListener(null);
expect(updated).toMatchObject({
statRuleset: "wow",
secondaryClassId: "rom-mage",
actionBindings: { "primary:face-top": "rom-priest-heal" },
actionLoadouts: { "rom-priest:rom-mage": { "primary:face-top": "rom-priest-heal" } },
inventory: [item],
});
expect(listCharacters(first.ownerId, storage).find((entry) => entry.id === first.id)?.statRuleset).toBe("wow");
expect(listCharacters(second.ownerId, storage).find((entry) => entry.id === second.id)?.statRuleset).toBe("rom");
expect(updateCharacterStatRuleset(first.ownerId, first.id, "rom", storage)?.statRuleset).toBe("rom");
expect(cloudSnapshot.find((entry) => entry.id === first.id)?.statRuleset).toBe("wow");
expect(updateCharacterDungeonRunCheckpoint(first.ownerId, first.id, {
version: DUNGEON_RUN_CHECKPOINT_VERSION,
dungeonId: "wailing-caverns",
difficultyId: "normal",
checkpointId: "entrance",
spawn: { footPosition: [-158, -82, 123], yaw: 0 },
encounters: [],
savedAt: 42,
}, storage)?.statRuleset).toBe("rom");
expect(updateCharacterStatRuleset(first.ownerId, second.id, "invalid", storage)).toBeNull();
expect(updateCharacterStatRuleset(first.ownerId, wowCharacter.id, "rom", storage)).toBeNull();
expect(updateCharacterStatRuleset(first.ownerId, "missing", "wow", storage)).toBeNull();
});
it("leaves the saved stat ruleset unchanged when storage rejects a switch", () => {
const storage = new FailingWriteStorage();
const character = createCharacter({
ownerId: "offline-roster",
name: "Steady",
categoryId: "rom",
raceId: "rom-human",
classId: "rom-priest",
gender: "female",
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
}, storage).character!;
storage.failWrites = true;
expect(updateCharacterStatRuleset(character.ownerId, character.id, "wow", storage)).toBeNull();
storage.failWrites = false;
expect(listCharacters(character.ownerId, storage).find((entry) => entry.id === character.id)?.statRuleset)
.toBe("rom");
});
it("defaults new glyph loadouts to empty and persists explicit glyph choices", () => {
const storage = new MemoryStorage();
const character = createCharacter({