Update Healer Man server

This commit is contained in:
phenom committed 2026-09-30 13:25:55 -04:00
1 parent 4fb7a0b9c8
commit 4c768fbec6
24 files changed
+654 -22

No files matched your search

+38
View File
@@ -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({
+13 -2
View File
@@ -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;
});
+28
View File
@@ -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);
+5 -2
View File
@@ -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;
+3 -2
View File
@@ -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) => ({
+2
View File
@@ -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>>;