Release Healer Man 0.1.44

This commit is contained in:
phenom committed 2026-09-14 22:12:49 -04:00
1 parent ccc01396cd
commit 756ee4f64a
24 files changed
+575 -46

No files matched your search

+46 -1
View File
@@ -36,6 +36,8 @@ export interface UiTemplate { name: string; layout: UiLayout }
export interface UiPreferences {
version: 1;
keybinds: InputPreferences;
/** Ability assignments belong to a character and ordered class pair; geometry and keys stay shared. */
desktopAbilityLoadouts: Record<string, (string | null)[]>;
current: Record<UiDevice, UiLayout>;
slots: Record<UiDevice, (UiTemplate | null)[]>;
}
@@ -139,6 +141,12 @@ export function normalizeUiPreferences(value: unknown): UiPreferences {
return {
version: 1,
keybinds: normalizeInputPreferences(source.keybinds),
desktopAbilityLoadouts: Object.fromEntries(Object.entries(record(source.desktopAbilityLoadouts))
.filter(([key, value]) => key.length <= 300 && Array.isArray(value))
.map(([key, value]) => [key, Array.from({ length: 60 }, (_, index) => {
const id = (value as unknown[])[index];
return typeof id === "string" && id.length <= 200 ? id : null;
})])),
current: { mobile: normalizeUiLayout(current.mobile), pc: normalizeUiLayout(current.pc) },
slots: Object.fromEntries(UI_DEVICES.map((device) => [device, Array.from({ length: 3 }, (_, index) => {
const slot = record(Array.isArray(slots[device]) ? slots[device][index] : null);
@@ -162,11 +170,41 @@ export function assignDesktopAbility(layout: UiLayout, barIndex: number, slotInd
}
/** Restore abilities only; key chords and the arrangement belong to the UI template. */
export function resetDesktopAbilities(layout: UiLayout, abilityIds: readonly string[], barIndex?: number): UiLayout {
export function resetDesktopAbilities(layout: UiLayout, abilityIds: readonly (string | null)[], barIndex?: number): UiLayout {
return { ...layout, bars: layout.bars.map((bar, b) => barIndex !== undefined && b !== barIndex ? bar : {
...bar, slots: bar.slots.map((slot, s) => ({ ...slot, abilityId: abilityIds[b * 12 + s] ?? null })),
}) };
}
/** Use the same class defaults as mobile, while preserving an existing character's empty slots. */
export function activateDesktopAbilityLoadout(
preferences: UiPreferences, key: string, defaults: readonly string[], availableIds: readonly string[],
): UiPreferences {
const layout = preferences.current.pc;
const available = new Set(availableIds);
const legacy = layout.bars.flatMap((bar) => bar.slots.map((slot) => slot.abilityId));
const saved = preferences.desktopAbilityLoadouts[key];
// Adopt the old account-wide assignments once, only for a compatible character.
const abilities = saved ?? (Object.keys(preferences.desktopAbilityLoadouts).length === 0
&& legacy.some((id) => id && available.has(id))
? legacy.map((id) => id && available.has(id) ? id : null)
: defaults);
const pc = resetDesktopAbilities(layout, abilities);
return { ...preferences, current: { ...preferences.current, pc },
desktopAbilityLoadouts: { ...preferences.desktopAbilityLoadouts,
[key]: pc.bars.flatMap((bar) => bar.slots.map((slot) => slot.abilityId)) },
};
}
/** Modifier layers count together; inactive PC bars cannot be used until enabled. */
export function assignedActionBarAbilityIds(
layout: UiLayout, bindings: Readonly<Record<string, string | null>>, desktop: boolean,
): ReadonlySet<string> {
const ids = desktop
? layout.bars.slice(0, layout.barCount).flatMap((bar) => bar.slots.map((slot) => slot.abilityId))
: Object.values(bindings);
return new Set(ids.filter((id): id is string => Boolean(id)));
}
export function desktopSlotForKey(layout: UiLayout, key: string | null): DesktopActionSlot | null {
if (!layout.wowBars || !key) return null;
// Hidden bars retain their bindings, like WoW; reducing the bar count deactivates them.
@@ -180,6 +218,13 @@ export function importUiPreferences(text: string): UiPreferences {
let source: Record<string, unknown>;
try { source = record(JSON.parse(text)); } catch { throw new Error("This file is not valid JSON."); }
if (source.format !== "healer-man-ui" || source.version !== 1) throw new Error("Unsupported UI file format or version.");
if (source.desktopAbilityLoadouts !== undefined) {
if (!source.desktopAbilityLoadouts || typeof source.desktopAbilityLoadouts !== "object" || Array.isArray(source.desktopAbilityLoadouts)) throw new Error("Invalid character action bars.");
for (const [key, ids] of Object.entries(record(source.desktopAbilityLoadouts))) {
if (key.length > 300 || !Array.isArray(ids) || ids.length !== 60
|| ids.some((id) => id !== null && (typeof id !== "string" || id.length > 200))) throw new Error("Invalid character action bars.");
}
}
const validateLayout = (value: unknown) => {
const layout = record(value);
if (layout.dpsMeterDisplay !== undefined && !["auto", "main", "companion"].includes(String(layout.dpsMeterDisplay))) throw new Error("Invalid DPS meter display.");