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

+66 -1
View File
@@ -33,6 +33,14 @@ import {
normalizeDungeonRunCheckpoint,
type DungeonRunCheckpoint,
} from "../game/dungeonRunCheckpoint";
import {
isWow335HunterPetFamily,
normalizeWow335HunterPetFamily,
} from "../game/wow335HunterPetFamilySelection";
import {
isCharacterStatRuleset,
normalizeCharacterStatRuleset,
} from "../game/characterStatRuleset";
const DATABASE_KEY = "healer-man.profile-database.v1";
const SESSION_KEY = "healer-man.session.v1";
@@ -123,6 +131,12 @@ function isCharacterProfile(value: unknown): value is CharacterProfile {
function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
const profile = { ...value };
profile.statRuleset = normalizeCharacterStatRuleset(value.statRuleset, value.classId);
if (value.classId === "hunter") {
profile.hunterPetFamily = normalizeWow335HunterPetFamily(value.hunterPetFamily);
} else {
delete profile.hunterPetFamily;
}
// Cards now belong to the owner; legacy fields are consumed before normalization.
delete profile.bossCards;
const legacy = value as CharacterProfile & {
@@ -235,8 +249,13 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
? parsed.bossCardsByOwner : {};
const bossCardsByOwner = Object.fromEntries(Object.entries(rawCards).map(([owner, cards]) => [owner, normalizeBossCardCollection(cards)]));
let cardsMigrated = false;
let statRulesetsMigrated = false;
const rosters = Object.fromEntries(Object.entries(parsed.rosters as Record<string, unknown>).map(([ownerId, roster]) => {
const characters = Array.isArray(roster) ? roster.filter(isCharacterProfile).slice(0, MAX_CHARACTERS) : [];
if (characters.some((character) => (
!isCharacterStatRuleset(character.statRuleset)
|| normalizeCharacterStatRuleset(character.statRuleset, character.classId) !== character.statRuleset
))) statRulesetsMigrated = true;
if (characters.some((character) => character.bossCards !== undefined)) {
const legacyCards = mergeBossCardCollections(characters.map((character) => character.bossCards), "sum");
bossCardsByOwner[ownerId] = mergeBossCardCollections([bossCardsByOwner[ownerId], legacyCards]);
@@ -278,7 +297,11 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
const rawUi = parsed.uiPreferencesByOwner && typeof parsed.uiPreferencesByOwner === "object" ? parsed.uiPreferencesByOwner : {};
const uiPreferencesByOwner = Object.fromEntries(Object.entries(rawUi).map(([owner, value]) => [owner, normalizeUiPreferences(value)]));
const database: ProfileDatabase = { version: 7, accounts, rosters, bossCardsByOwner, presentationSettingsByOwner, uiPreferencesByOwner };
if (parsed.version !== 7 || presentationSettingsMigrated || cardsMigrated) writeDatabase(database, storage);
if (parsed.version !== 7 || presentationSettingsMigrated || cardsMigrated || statRulesetsMigrated) {
// A full or read-only device must still expose its existing roster.
// Retry migration on the next read when persistence is available.
try { writeDatabase(database, storage); } catch { /* Keep the normalized in-memory profiles. */ }
}
return database;
} catch {
return emptyDatabase();
@@ -535,6 +558,7 @@ export function createCharacter(
const now = Date.now();
const character = normalizeCharacterProfile({
...profile,
statRuleset: profile.statRuleset ?? (contentCategoryForClass(profile.classId) === "rom" ? "rom" : "wow"),
id: makeId("character"),
level: 1,
experience: 0,
@@ -553,6 +577,29 @@ export function createCharacter(
return { ok: true, character };
}
export function updateCharacterStatRuleset(
ownerId: string,
characterId: string,
value: unknown,
storage: StorageLike | null = browserLocalStorage(),
): CharacterProfile | null {
if (!storage || !isCharacterStatRuleset(value)) return null;
const database = readDatabase(storage);
let updated: CharacterProfile | null = null;
database.rosters[ownerId] = (database.rosters[ownerId] ?? []).map((character) => {
if (character.id !== characterId || contentCategoryForClass(character.classId) !== "rom") return character;
updated = normalizeCharacterProfile({ ...character, statRuleset: value });
return updated;
});
if (!updated) return null;
try {
writeDatabase(database, storage);
} catch {
return null;
}
return updated;
}
export function touchCharacter(
ownerId: string,
characterId: string,
@@ -788,6 +835,24 @@ export function updateCharacterEquipment(
return updated;
}
export function updateCharacterHunterPetFamily(
ownerId: string,
characterId: string,
family: unknown,
storage: StorageLike | null = browserLocalStorage(),
): CharacterProfile | null {
if (!storage || !isWow335HunterPetFamily(family)) return null;
const database = readDatabase(storage);
let updated: CharacterProfile | null = null;
database.rosters[ownerId] = (database.rosters[ownerId] ?? []).map((character) => {
if (character.id !== characterId || character.classId !== "hunter") return character;
updated = normalizeCharacterProfile({ ...character, hunterPetFamily: family });
return updated;
});
if (updated) writeDatabase(database, storage);
return updated;
}
export function loadAccountBossCards(
ownerId: string,
storage: StorageLike | null = browserLocalStorage(),