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

+66 -6
View File
@@ -22,6 +22,11 @@ import {
normalizeManastormProgress,
type ManastormProgress,
} from "../game/manastormProgress";
import { abilitiesForClass } from "../game/abilityCatalog";
import {
normalizeSpecializationId,
specializationById,
} from "../game/specializationCatalog";
const DATABASE_KEY = "healer-man.profile-database.v1";
const SESSION_KEY = "healer-man.session.v1";
@@ -51,7 +56,7 @@ interface LocalAccount {
}
interface ProfileDatabase {
version: 3;
version: 4;
accounts: LocalAccount[];
rosters: Record<string, CharacterProfile[]>;
presentationSettingsByOwner: Record<string, CombatPresentationSettings>;
@@ -93,7 +98,7 @@ function browserSessionStorage(): StorageLike | null {
}
function emptyDatabase(): ProfileDatabase {
return { version: 3, accounts: [], rosters: {}, presentationSettingsByOwner: {} };
return { version: 4, accounts: [], rosters: {}, presentationSettingsByOwner: {} };
}
function isCharacterProfile(value: unknown): value is CharacterProfile {
@@ -120,6 +125,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
settings?: unknown;
secondaryClassId?: unknown;
actionLoadouts?: unknown;
specializationId?: unknown;
};
const level = typeof legacy.level === "number" && Number.isFinite(legacy.level)
? Math.max(1, Math.min(80, Math.floor(legacy.level)))
@@ -138,6 +144,9 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
.map(([bindingId, abilityId]) => [bindingId, abilityId as string | null]))
: {};
const categoryId = contentCategoryForClass(value.classId);
const specializationId = categoryId === "rom"
? null
: normalizeSpecializationId(value.classId, legacy.specializationId);
const secondaryClassId = categoryId === "rom"
&& isRomClassId(value.classId)
&& typeof legacy.secondaryClassId === "string"
@@ -168,6 +177,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
talentRanks,
actionBindings,
categoryId,
specializationId,
secondaryClassId,
actionLoadouts,
inventory,
@@ -186,7 +196,7 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
rosters?: unknown;
presentationSettingsByOwner?: unknown;
}) | null;
if (!parsed || ![1, 2, 3].includes(parsed.version ?? 0) || !Array.isArray(parsed.accounts) || !parsed.rosters || typeof parsed.rosters !== "object") {
if (!parsed || ![1, 2, 3, 4].includes(parsed.version ?? 0) || !Array.isArray(parsed.accounts) || !parsed.rosters || typeof parsed.rosters !== "object") {
return emptyDatabase();
}
const accounts = parsed.accounts.filter((account): account is LocalAccount => Boolean(
@@ -226,8 +236,8 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
return [ownerId, normalized];
}),
);
const database: ProfileDatabase = { version: 3, accounts, rosters, presentationSettingsByOwner };
if (parsed.version !== 3 || presentationSettingsMigrated) writeDatabase(database, storage);
const database: ProfileDatabase = { version: 4, accounts, rosters, presentationSettingsByOwner };
if (parsed.version !== 4 || presentationSettingsMigrated) writeDatabase(database, storage);
return database;
} catch {
return emptyDatabase();
@@ -421,7 +431,9 @@ export function replaceCharactersForOwner(
}
export function createCharacter(
profile: Omit<CharacterProfile, "id" | "createdAt" | "lastPlayedAt" | "level" | "experience" | "talentRanks" | "actionBindings" | "inventory" | "equipment" | "manastormProgress" | "location">,
profile: Omit<CharacterProfile, "id" | "createdAt" | "lastPlayedAt" | "level" | "experience" | "talentRanks" | "actionBindings" | "inventory" | "equipment" | "manastormProgress" | "location" | "specializationId"> & {
specializationId?: string | null;
},
storage: StorageLike | null = browserLocalStorage(),
): CharacterResult {
if (!storage) return { ok: false, error: "Local storage is unavailable on this device." };
@@ -491,6 +503,54 @@ export function updateCharacterProgression(
return updated;
}
export function updateCharacterSpecialization(
ownerId: string,
characterId: string,
specializationId: string,
resetTalents: boolean,
storage: StorageLike | null = browserLocalStorage(),
): CharacterResult {
if (!storage) return { ok: false, error: "Local storage is unavailable on this device." };
const database = readDatabase(storage);
let result: CharacterResult = { ok: false, error: "That character is no longer available." };
database.rosters[ownerId] = (database.rosters[ownerId] ?? []).map((character) => {
if (character.id !== characterId) return character;
if (character.categoryId === "rom") {
result = { ok: false, error: "RoM gear follows the active primary class and activity role." };
return character;
}
const specialization = specializationById(specializationId);
if (!specialization || specialization.classId !== character.classId) {
result = { ok: false, error: "That specialization is not available to this class." };
return character;
}
const talentAbilityIds = resetTalents
? new Set(abilitiesForClass(character.classId)
.filter((ability) => ability.talentEntryId !== undefined || ability.source === "coa-tree")
.map((ability) => ability.id))
: new Set<string>();
const actionBindings = resetTalents
? Object.fromEntries(Object.entries(character.actionBindings)
.map(([bindingId, abilityId]) => [bindingId, abilityId && talentAbilityIds.has(abilityId) ? null : abilityId]))
: character.actionBindings;
const loadoutKey = orderedClassLoadoutKey(character.classId, character.secondaryClassId);
const updated = normalizeCharacterProfile({
...character,
specializationId: specialization.id,
talentRanks: resetTalents ? {} : character.talentRanks,
actionBindings,
actionLoadouts: {
...(character.actionLoadouts ?? {}),
[loadoutKey]: { ...actionBindings },
},
});
result = { ok: true, character: updated };
return updated;
});
if (result.ok) writeDatabase(database, storage);
return result;
}
export function updateCharacterActionBindings(
ownerId: string,
characterId: string,