Update Healer Man server
This commit is contained in:
1 parent
4fb7a0b9c8
commit
4c768fbec6
24 files changed
+654
-22
No files matched your search
@@ -39,6 +39,7 @@ import {
|
||||
} from "../game/manastormProgress";
|
||||
import { abilitiesForClass } from "../game/abilityCatalog";
|
||||
import { DUNGEON_RUN_CHECKPOINT_VERSION } from "../game/dungeonRunCheckpoint";
|
||||
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "../game/wow335GlyphCatalog";
|
||||
|
||||
class MemoryStorage implements StorageLike {
|
||||
private values = new Map<string, string>();
|
||||
@@ -48,6 +49,43 @@ class MemoryStorage implements StorageLike {
|
||||
}
|
||||
|
||||
describe("local profile repository", () => {
|
||||
it("defaults new glyph loadouts to empty and persists explicit glyph choices", () => {
|
||||
const storage = new MemoryStorage();
|
||||
const character = createCharacter({
|
||||
ownerId: "offline-roster",
|
||||
name: "Leafkeeper",
|
||||
categoryId: "wow",
|
||||
raceId: "night-elf",
|
||||
classId: "druid",
|
||||
specializationId: "druid:restoration",
|
||||
gender: "female",
|
||||
appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 },
|
||||
}, storage).character!;
|
||||
|
||||
expect(character.glyphIds).toEqual([]);
|
||||
|
||||
const legacyProfile = { ...character };
|
||||
delete (legacyProfile as Partial<typeof character>).glyphIds;
|
||||
const [migrated] = replaceCharactersForOwner("offline-roster", [legacyProfile], storage);
|
||||
expect(migrated?.glyphIds).toEqual([]);
|
||||
|
||||
const updated = updateCharacterProgression("offline-roster", migrated.id, {
|
||||
level: migrated.level,
|
||||
experience: migrated.experience,
|
||||
talentRanks: migrated.talentRanks,
|
||||
glyphIds: [WOW335_GLYPH_OF_SWIFTMEND_ID, WOW335_GLYPH_OF_SWIFTMEND_ID, " "],
|
||||
}, storage)!;
|
||||
expect(updated.glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
expect(listCharacters("offline-roster", storage)[0]?.glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
|
||||
const progressionOnly = updateCharacterProgression("offline-roster", migrated.id, {
|
||||
level: updated.level,
|
||||
experience: updated.experience,
|
||||
talentRanks: updated.talentRanks,
|
||||
}, storage)!;
|
||||
expect(progressionOnly.glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
});
|
||||
|
||||
it("persists a safe dungeon checkpoint per character", () => {
|
||||
const storage = new MemoryStorage();
|
||||
const character = createCharacter({
|
||||
|
||||
@@ -129,6 +129,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
|
||||
actionBindings?: unknown;
|
||||
experience?: unknown;
|
||||
talentRanks?: unknown;
|
||||
glyphIds?: unknown;
|
||||
level?: unknown;
|
||||
inventory?: unknown;
|
||||
equipment?: unknown;
|
||||
@@ -150,6 +151,11 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
|
||||
.filter(([, rank]) => typeof rank === "number" && Number.isFinite(rank) && rank > 0)
|
||||
.map(([id, rank]) => [id, Math.floor(rank as number)]))
|
||||
: {};
|
||||
const glyphIds = Array.isArray(legacy.glyphIds)
|
||||
? [...new Set(legacy.glyphIds.filter((glyphId): glyphId is string => (
|
||||
typeof glyphId === "string" && glyphId.trim().length > 0
|
||||
)))]
|
||||
: [];
|
||||
const actionBindings = legacy.actionBindings && typeof legacy.actionBindings === "object" && !Array.isArray(legacy.actionBindings)
|
||||
? Object.fromEntries(Object.entries(legacy.actionBindings as Record<string, unknown>)
|
||||
.filter(([, abilityId]) => abilityId === null || typeof abilityId === "string")
|
||||
@@ -188,6 +194,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
|
||||
level,
|
||||
experience,
|
||||
talentRanks,
|
||||
glyphIds,
|
||||
actionBindings,
|
||||
categoryId,
|
||||
specializationId,
|
||||
@@ -513,7 +520,7 @@ export function replaceCharactersForOwner(
|
||||
}
|
||||
|
||||
export function createCharacter(
|
||||
profile: Omit<CharacterProfile, "id" | "createdAt" | "lastPlayedAt" | "level" | "experience" | "talentRanks" | "actionBindings" | "inventory" | "equipment" | "manastormProgress" | "location" | "specializationId"> & {
|
||||
profile: Omit<CharacterProfile, "id" | "createdAt" | "lastPlayedAt" | "level" | "experience" | "talentRanks" | "glyphIds" | "actionBindings" | "inventory" | "equipment" | "manastormProgress" | "location" | "specializationId"> & {
|
||||
specializationId?: string | null;
|
||||
},
|
||||
storage: StorageLike | null = browserLocalStorage(),
|
||||
@@ -532,6 +539,7 @@ export function createCharacter(
|
||||
level: 1,
|
||||
experience: 0,
|
||||
talentRanks: {},
|
||||
glyphIds: [],
|
||||
actionBindings: {},
|
||||
inventory: [],
|
||||
equipment: {},
|
||||
@@ -565,7 +573,7 @@ export function touchCharacter(
|
||||
export function updateCharacterProgression(
|
||||
ownerId: string,
|
||||
characterId: string,
|
||||
progression: Pick<CharacterProfile, "level" | "experience" | "talentRanks">,
|
||||
progression: Pick<CharacterProfile, "level" | "experience" | "talentRanks"> & Partial<Pick<CharacterProfile, "glyphIds">>,
|
||||
storage: StorageLike | null = browserLocalStorage(),
|
||||
): CharacterProfile | null {
|
||||
if (!storage) return null;
|
||||
@@ -578,6 +586,9 @@ export function updateCharacterProgression(
|
||||
level: progression.level,
|
||||
experience: progression.experience,
|
||||
talentRanks: { ...progression.talentRanks },
|
||||
glyphIds: progression.glyphIds === undefined
|
||||
? [...(character.glyphIds ?? [])]
|
||||
: [...progression.glyphIds],
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { beginMainMenuCharacterEditing } from "./mainMenuCharacterEditing";
|
||||
import { selectedCharacter, useShellStore } from "./shellStore";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { talentNodesForTree } from "../game/talentCatalog";
|
||||
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "../game/wow335GlyphCatalog";
|
||||
|
||||
class MemoryStorage implements StorageLike {
|
||||
values = new Map<string, string>();
|
||||
@@ -65,6 +66,33 @@ describe("main-menu character editing", () => {
|
||||
expect(listCharacters("offline-roster", storage)[0]).toMatchObject({ level: 17, experience: 123 });
|
||||
});
|
||||
|
||||
it("persists a glyph toggle and reloads it when the editor is reopened", () => {
|
||||
const druid = createCharacter({
|
||||
ownerId: "offline-roster", name: "Leafkeeper", classId: "druid",
|
||||
raceId: "night-elf", gender: "female", appearance: createDefaultAppearance(),
|
||||
specializationId: "druid:restoration",
|
||||
}, storage).character!;
|
||||
useShellStore.setState({
|
||||
selectedCharacterId: druid.id,
|
||||
characters: [druid],
|
||||
});
|
||||
stop = beginMainMenuCharacterEditing(storage);
|
||||
|
||||
expect(useCombatStore.getState().glyphIds).not.toContain(WOW335_GLYPH_OF_SWIFTMEND_ID);
|
||||
expect(useCombatStore.getState().classId).toBe("druid");
|
||||
expect(useCombatStore.getState().setGlyphActive(WOW335_GLYPH_OF_SWIFTMEND_ID, true)).toBe(true);
|
||||
expect(useCombatStore.getState().glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
expect(listCharacters("offline-roster", storage).find((character) => character.id === druid.id)?.glyphIds)
|
||||
.toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
expect(selectedCharacter(useShellStore.getState())?.glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
|
||||
stop!();
|
||||
stop = beginMainMenuCharacterEditing(storage);
|
||||
expect(useCombatStore.getState().glyphIds).toEqual([WOW335_GLYPH_OF_SWIFTMEND_ID]);
|
||||
useCombatStore.getState().setGlyphActive(WOW335_GLYPH_OF_SWIFTMEND_ID, false);
|
||||
expect(listCharacters("offline-roster", storage).find((character) => character.id === druid.id)?.glyphIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("persists controller assignments and reloads them without touching progression or equipment", () => {
|
||||
const profile = selectedCharacter(useShellStore.getState())!;
|
||||
stop = beginMainMenuCharacterEditing(storage);
|
||||
|
||||
@@ -24,16 +24,19 @@ export function beginMainMenuCharacterEditing(storage?: StorageLike): (() => voi
|
||||
const profile = selectedCharacter(current);
|
||||
if (current.phase !== "main-menu" || current.session?.ownerId !== ownerId
|
||||
|| profile?.id !== characterId || profile.ownerId !== ownerId) return;
|
||||
if (combat.talentRanks === previous.talentRanks && combat.actionBindings === previous.actionBindings) return;
|
||||
if (combat.talentRanks === previous.talentRanks
|
||||
&& combat.glyphIds === previous.glyphIds
|
||||
&& combat.actionBindings === previous.actionBindings) return;
|
||||
|
||||
try {
|
||||
let updated = profile;
|
||||
if (combat.talentRanks !== previous.talentRanks) {
|
||||
if (combat.talentRanks !== previous.talentRanks || combat.glyphIds !== previous.glyphIds) {
|
||||
const saved = updateCharacterProgression(ownerId, characterId, {
|
||||
// Menu edits cannot award experience or alter the character's level.
|
||||
level: profile.level,
|
||||
experience: profile.experience,
|
||||
talentRanks: combat.talentRanks,
|
||||
glyphIds: combat.glyphIds,
|
||||
}, storage);
|
||||
if (!saved) throw new Error("Character storage is unavailable.");
|
||||
updated = saved;
|
||||
|
||||
@@ -114,7 +114,7 @@ export interface ShellState {
|
||||
/** Compatibility alias for the original single-dungeon entry point. */
|
||||
enterWorld: () => boolean;
|
||||
returnToCharacters: () => void;
|
||||
saveActiveProgression: (level: number, experience: number, talentRanks: Record<string, number>) => void;
|
||||
saveActiveProgression: (level: number, experience: number, talentRanks: Record<string, number>, glyphIds?: readonly string[]) => void;
|
||||
saveActiveActionBindings: (actionBindings: Readonly<Record<string, string | null>>) => void;
|
||||
saveActiveInventory: (inventory: readonly InventoryItem[]) => void;
|
||||
saveAccountBossCards: (bossCards: BossCardCollection) => void;
|
||||
@@ -533,13 +533,14 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
ownerId: session.ownerId, characterId: selectedCharacterId, phase: "characters",
|
||||
});
|
||||
},
|
||||
saveActiveProgression: (level, experience, talentRanks) => {
|
||||
saveActiveProgression: (level, experience, talentRanks, glyphIds) => {
|
||||
const { session, activeCharacter } = get();
|
||||
if (!session || !activeCharacter) return;
|
||||
const updated = updateCharacterProgression(session.ownerId, activeCharacter.id, {
|
||||
level,
|
||||
experience,
|
||||
talentRanks,
|
||||
glyphIds,
|
||||
});
|
||||
if (!updated) return;
|
||||
set((state) => ({
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface CharacterProfile {
|
||||
level: number;
|
||||
experience: number;
|
||||
talentRanks: Record<string, number>;
|
||||
/** Active WoW glyph IDs in the character's saved build; normalized legacy profiles may omit this field. */
|
||||
glyphIds?: readonly string[];
|
||||
actionBindings: Record<string, string | null>;
|
||||
/** Bindings keyed by ordered `primary:secondary` class pair. */
|
||||
actionLoadouts?: Record<string, Record<string, string | null>>;
|
||||
|
||||
Reference in new issue
Block a user