forked from phenom/healer-man
Release Healer Man 0.1.4
This commit is contained in:
1 parent
5bb170039a
commit
f4c9cf356c
63 files changed
+2694
-81
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user