Release Healer Man 0.1.4

This commit is contained in:
phenom committed 2026-08-18 12:06:04 -04:00
1 parent 5bb170039a
commit f4c9cf356c
63 files changed
+2694 -81

No files matched your search

+33
View File
@@ -5,6 +5,7 @@ import {
createLocalAccount,
createOfflineSession,
listCharacters,
loadCombatPresentationSettings,
loginLocalAccount,
updateCharacterActionBindings,
updateCharacterEquipment,
@@ -12,9 +13,11 @@ import {
updateCharacterManastormProgress,
updateCharacterProgression,
updateCharacterSettings,
updateCombatPresentationSettings,
type StorageLike,
} from "./accountRepository";
import { DEFAULT_GAMEPLAY_SETTINGS } from "../game/combatStore";
import { DEFAULT_COMBAT_PRESENTATION_SETTINGS } from "../game/combatPresentation";
import { EMPTY_ITEM_STATS } from "../game/itemStats";
import {
MANASTORM_PROGRESS_VERSION,
@@ -124,6 +127,36 @@ describe("local profile repository", () => {
});
});
it("migrates v1/v2 databases and stores combat presentation settings by owner", async () => {
for (const version of [1, 2]) {
const storage = new MemoryStorage();
storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({
version,
accounts: [],
rosters: { "offline-roster": [] },
}));
expect(loadCombatPresentationSettings("offline-roster", storage))
.toEqual(DEFAULT_COMBAT_PRESENTATION_SETTINGS);
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).version).toBe(3);
}
const storage = new MemoryStorage();
const firstOwner = (await createLocalAccount("EffectsOne", "eight-plus", storage)).session!.ownerId;
const secondOwner = (await createLocalAccount("EffectsTwo", "eight-plus", storage)).session!.ownerId;
const customized = updateCombatPresentationSettings(firstOwner, {
...DEFAULT_COMBAT_PRESENTATION_SETTINGS,
vfxQuality: "low",
vfxEnabled: { player: true, party: false, enemy: true },
audioEnabled: { player: true, party: true, enemy: false },
audioVolume: { player: 0.75, party: 0.4, enemy: 0.2 },
}, storage);
expect(loadCombatPresentationSettings(firstOwner, storage)).toEqual(customized);
expect(loadCombatPresentationSettings(secondOwner, storage)).toEqual(DEFAULT_COMBAT_PRESENTATION_SETTINGS);
expect(loadCombatPresentationSettings(createOfflineSession().ownerId, storage))
.toEqual(DEFAULT_COMBAT_PRESENTATION_SETTINGS);
});
it("migrates custom-class profiles into the CoA category", () => {
const storage = new MemoryStorage();
const offline = createOfflineSession();
+45 -5
View File
@@ -12,6 +12,11 @@ import {
import { normalizeInventory, type InventoryItem } from "../game/lootTypes";
import { normalizeEquipment, type EquipmentAssignments } from "../game/equipment";
import type { GameplaySettings } from "../game/combatStore";
import {
DEFAULT_COMBAT_PRESENTATION_SETTINGS,
normalizeCombatPresentationSettings,
type CombatPresentationSettings,
} from "../game/combatPresentation";
import {
createEmptyManastormProgress,
normalizeManastormProgress,
@@ -46,9 +51,10 @@ interface LocalAccount {
}
interface ProfileDatabase {
version: 2;
version: 3;
accounts: LocalAccount[];
rosters: Record<string, CharacterProfile[]>;
presentationSettingsByOwner: Record<string, CombatPresentationSettings>;
}
export interface AccountResult {
@@ -87,7 +93,7 @@ function browserSessionStorage(): StorageLike | null {
}
function emptyDatabase(): ProfileDatabase {
return { version: 2, accounts: [], rosters: {} };
return { version: 3, accounts: [], rosters: {}, presentationSettingsByOwner: {} };
}
function isCharacterProfile(value: unknown): value is CharacterProfile {
@@ -178,8 +184,9 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
version?: number;
accounts?: unknown;
rosters?: unknown;
presentationSettingsByOwner?: unknown;
}) | null;
if (!parsed || (parsed.version !== 1 && parsed.version !== 2) || !Array.isArray(parsed.accounts) || !parsed.rosters || typeof parsed.rosters !== "object") {
if (!parsed || ![1, 2, 3].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(
@@ -197,8 +204,19 @@ function readDatabase(storage: StorageLike | null = browserLocalStorage()): Prof
? roster.filter(isCharacterProfile).map(normalizeCharacterProfile).slice(0, MAX_CHARACTERS)
: [],
]));
const database: ProfileDatabase = { version: 2, accounts, rosters };
if (parsed.version === 1) writeDatabase(database, storage);
const rawPresentationSettings = parsed.presentationSettingsByOwner
&& typeof parsed.presentationSettingsByOwner === "object"
&& !Array.isArray(parsed.presentationSettingsByOwner)
? parsed.presentationSettingsByOwner as Record<string, unknown>
: {};
const presentationSettingsByOwner = Object.fromEntries(
Object.entries(rawPresentationSettings).map(([ownerId, settings]) => [
ownerId,
normalizeCombatPresentationSettings(settings),
]),
);
const database: ProfileDatabase = { version: 3, accounts, rosters, presentationSettingsByOwner };
if (parsed.version !== 3) writeDatabase(database, storage);
return database;
} catch {
return emptyDatabase();
@@ -626,6 +644,28 @@ export function updateCharacterSettings(
return updated;
}
export function loadCombatPresentationSettings(
ownerId: string,
storage: StorageLike | null = browserLocalStorage(),
): CombatPresentationSettings {
if (!storage) return DEFAULT_COMBAT_PRESENTATION_SETTINGS;
return readDatabase(storage).presentationSettingsByOwner[ownerId]
?? DEFAULT_COMBAT_PRESENTATION_SETTINGS;
}
export function updateCombatPresentationSettings(
ownerId: string,
settings: CombatPresentationSettings,
storage: StorageLike | null = browserLocalStorage(),
): CombatPresentationSettings {
const normalized = normalizeCombatPresentationSettings(settings);
if (!storage) return normalized;
const database = readDatabase(storage);
database.presentationSettingsByOwner[ownerId] = normalized;
writeDatabase(database, storage);
return normalized;
}
export function updateCharacterManastormProgress(
ownerId: string,
characterId: string,
+1 -1
View File
@@ -58,7 +58,7 @@ describe("RoM dual-class persistence", () => {
"priest:none": { "primary:face-bottom": "priest-flash-heal" },
},
});
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).version).toBe(2);
expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).version).toBe(3);
});
it("locks selection to level ten and preserves separate bars through atomic swaps", () => {
+40 -2
View File
@@ -5,6 +5,7 @@ import {
createCharacter as persistCharacter,
deleteCharacter as removeCharacter,
listCharacters,
loadCombatPresentationSettings,
restoreSession as restoreStoredSession,
saveSession,
updateCharacterActionBindings,
@@ -13,6 +14,7 @@ import {
updateCharacterManastormProgress,
updateCharacterProgression,
updateCharacterSettings,
updateCombatPresentationSettings as persistCombatPresentationSettings,
swapRomClasses,
} from "./accountRepository";
import {
@@ -36,6 +38,11 @@ import { DEFAULT_DUNGEON_ID } from "../game/dungeonDefaults";
import type { InventoryItem } from "../game/lootTypes";
import type { EquipmentAssignments } from "../game/equipment";
import type { GameplaySettings } from "../game/combatStore";
import {
DEFAULT_COMBAT_PRESENTATION_SETTINGS,
normalizeCombatPresentationSettings,
type CombatPresentationSettings,
} from "../game/combatPresentation";
import type { PartyRole } from "../game/partyRoles";
import type { ManastormPartySize, ManastormProgress } from "../game/manastormProgress";
import { dungeonShellRuntime } from "./dungeonShellRuntimeBridge";
@@ -56,6 +63,7 @@ export interface ShellState {
notice: string;
activeDungeonId: DungeonId;
activeDungeonRole: PartyRole;
combatPresentationSettings: CombatPresentationSettings;
beginSession: (session: PlayerSession) => void;
signOut: () => void;
selectCharacter: (characterId: string) => void;
@@ -89,6 +97,8 @@ export interface ShellState {
saveActiveInventory: (inventory: readonly InventoryItem[]) => void;
saveActiveEquipment: (equipment: EquipmentAssignments) => void;
saveActiveSettings: (settings: GameplaySettings) => void;
updateCombatPresentationSettings: (patch: Partial<CombatPresentationSettings>) => void;
resetCombatPresentationSettings: () => void;
saveActiveManastormProgress: (progress: ManastormProgress) => void;
saveSelectedManastormLoadout: (loadout: readonly string[]) => boolean;
setNotice: (notice: string) => void;
@@ -108,15 +118,22 @@ function defaultDraft(categoryId: ContentCategoryId = "wow"): CharacterDraft {
};
}
function restoredState(): Pick<ShellState, "phase" | "session" | "characters" | "selectedCharacterId"> {
function restoredState(): Pick<ShellState, "phase" | "session" | "characters" | "selectedCharacterId" | "combatPresentationSettings"> {
const session = restoreStoredSession();
if (!session) return { phase: "login", session: null, characters: [], selectedCharacterId: null };
if (!session) return {
phase: "login",
session: null,
characters: [],
selectedCharacterId: null,
combatPresentationSettings: DEFAULT_COMBAT_PRESENTATION_SETTINGS,
};
const characters = listCharacters(session.ownerId);
return {
phase: "characters",
session,
characters,
selectedCharacterId: characters[0]?.id ?? null,
combatPresentationSettings: loadCombatPresentationSettings(session.ownerId),
};
}
@@ -139,6 +156,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
characters,
selectedCharacterId: characters[0]?.id ?? null,
activeCharacter: null,
combatPresentationSettings: loadCombatPresentationSettings(session.ownerId),
notice: session.kind === "offline" ? "Offline roster loaded." : `Welcome, ${session.displayName}.`,
});
},
@@ -154,6 +172,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
activeCharacter: null,
draft: defaultDraft(),
notice: "Signed out.",
combatPresentationSettings: DEFAULT_COMBAT_PRESENTATION_SETTINGS,
});
},
selectCharacter: (selectedCharacterId) => set({ selectedCharacterId, notice: "" }),
@@ -361,6 +380,7 @@ export const useShellStore = create<ShellState>((set, get) => ({
: characters[0]?.id ?? null,
activeCharacter: null,
notice: "",
combatPresentationSettings: loadCombatPresentationSettings(session.ownerId),
});
},
saveActiveProgression: (level, experience, talentRanks) => {
@@ -433,6 +453,24 @@ export const useShellStore = create<ShellState>((set, get) => ({
characters: state.characters.map((character) => character.id === updated.id ? updated : character),
}));
},
updateCombatPresentationSettings: (patch) => {
const { session, combatPresentationSettings } = get();
const next = normalizeCombatPresentationSettings({
...combatPresentationSettings,
...patch,
}, combatPresentationSettings);
const persisted = session
? persistCombatPresentationSettings(session.ownerId, next)
: next;
set({ combatPresentationSettings: persisted });
},
resetCombatPresentationSettings: () => {
const { session } = get();
const settings = session
? persistCombatPresentationSettings(session.ownerId, DEFAULT_COMBAT_PRESENTATION_SETTINGS)
: DEFAULT_COMBAT_PRESENTATION_SETTINGS;
set({ combatPresentationSettings: settings });
},
saveActiveManastormProgress: (progress) => {
const { session, activeCharacter } = get();
if (!session || !activeCharacter) return;