forked from phenom/healer-man
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
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user