forked from phenom/healer-man
Release Healer Man 0.1.44
This commit is contained in:
1 parent
ccc01396cd
commit
756ee4f64a
24 files changed
+575
-46
No files matched your search
@@ -12,7 +12,7 @@ import {
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { abilityAtLevel, defaultActionBarForCharacter, type AbilityDefinition } from "../game/abilityCatalog";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { actionKeyLabel, assignDesktopAbility, resetDesktopAbilities, type UiLayout } from "../game/uiLayout";
|
||||
import { actionKeyLabel, assignedActionBarAbilityIds, assignDesktopAbility, resetDesktopAbilities, type UiLayout } from "../game/uiLayout";
|
||||
import { currentUiDevice, useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { getControllerHeldTokens, subscribeControllerToken } from "../input/controller";
|
||||
import { INPUT_ACTIONS, controllerBindingMatches, inputKeyForEvent } from "../input/keybinds";
|
||||
@@ -130,6 +130,10 @@ function AbilityBindingsDialog({ onClose }: { onClose?: () => void }) {
|
||||
const [notice, setNotice] = useState("Choose a button, then choose the ability it should use.");
|
||||
const classColor = classById(classId).color;
|
||||
const isWow = layout === "wow";
|
||||
const assignedIds = assignedActionBarAbilityIds(pcLayout, bindings, isWow);
|
||||
const isMissing = (ability: AbilityDefinition) => ability.unlockLevel <= level
|
||||
&& (ability.talentEntryId === undefined || Boolean(talentRanks[`coa-entry-${ability.talentEntryId}`]))
|
||||
&& !assignedIds.has(ability.id);
|
||||
const assignment = (control: BindingControl) => typeof control === "number"
|
||||
? pcLayout.bars[bar].slots[control].abilityId : bindings[actionBindingId(layer, control)];
|
||||
const controlLabel = (control: BindingControl) => typeof control === "number"
|
||||
@@ -490,10 +494,11 @@ function AbilityBindingsDialog({ onClose }: { onClose?: () => void }) {
|
||||
<>
|
||||
<p className="eyebrow">Assign {controlLabel(editingControl)}</p>
|
||||
<h3>Choose an ability</h3>
|
||||
<p className="action-bar-missing-legend">Light blue: missing from action bars</p>
|
||||
<div className="ability-bindings__choices">
|
||||
{pickerAbilities.map((ability) => (
|
||||
<AbilityTooltipTarget key={ability.id} ability={tooltipData(ability)}>
|
||||
<ControllerButton controlId={choiceControlId(ability.id)} selectedId={controller.selectedId} select={controller.select} type="button" className={`${ability.id === editingAbilityId ? "is-current" : ""} ${ability.unlockLevel > level ? "is-locked" : ""}`} onClick={() => assign(ability.id)}>
|
||||
<ControllerButton controlId={choiceControlId(ability.id)} selectedId={controller.selectedId} select={controller.select} type="button" className={`${ability.id === editingAbilityId ? "is-current" : ""} ${ability.unlockLevel > level ? "is-locked" : ""} ${isMissing(ability) ? "is-missing-from-bars" : ""}`} aria-label={`${ability.name}${isMissing(ability) ? ", missing from action bars" : ""}`} onClick={() => assign(ability.id)}>
|
||||
<AbilityBindingIcon icon={ability.icon} sigil={ability.sigil} color={classColor} />
|
||||
<span><strong>{ability.name}</strong><small>{ability.unlockLevel > level ? `Unlocks at level ${ability.unlockLevel}` : ability.id === editingAbilityId ? "Current assignment" : ability.target === "friendly" ? "Friendly / self" : ability.target}</small></span>
|
||||
</ControllerButton>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { assignedActionBarAbilityIds } from "../game/uiLayout";
|
||||
import { currentUiDevice, useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
import { AbilityTooltipTarget } from "./AbilityTooltip";
|
||||
@@ -80,6 +83,11 @@ function SpellbookDialog({
|
||||
unlockedOnly,
|
||||
onUnlockedOnlyChange,
|
||||
}: SpellbookDialogProps) {
|
||||
const bindings = useCombatStore((state) => state.actionBindings);
|
||||
const ui = useUiLayoutStore();
|
||||
const device = currentUiDevice();
|
||||
const layout = ui.draft ?? ui.preferences.current[device];
|
||||
const assignedIds = assignedActionBarAbilityIds(layout, bindings, device === "pc" && layout.wowBars);
|
||||
const orderedAbilities = useMemo(
|
||||
() => spellbookAbilitiesForDisplay(abilities, playerLevel, false),
|
||||
[abilities, playerLevel],
|
||||
@@ -131,6 +139,7 @@ function SpellbookDialog({
|
||||
</header>
|
||||
|
||||
<div className="spellbook-panel__toolbar">
|
||||
<span className="action-bar-missing-legend">Light blue: missing from action bars</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`spellbook-panel__filter ${unlockedOnly ? "is-active" : ""}`}
|
||||
@@ -146,6 +155,7 @@ function SpellbookDialog({
|
||||
<div className="spellbook-panel__list" aria-label={unlockedOnly ? "Unlocked abilities" : "Known and locked abilities"}>
|
||||
{visibleAbilities.map((ability) => {
|
||||
const locked = !ability.known || ability.level > playerLevel;
|
||||
const missing = !locked && !ability.passive && !assignedIds.has(ability.id);
|
||||
const lockLabel = ability.talentLocked && !ability.known
|
||||
? "Learn in talents"
|
||||
: `Unlocks at level ${ability.level}`;
|
||||
@@ -163,9 +173,9 @@ function SpellbookDialog({
|
||||
setInspectedId(ability.id);
|
||||
}}
|
||||
type="button"
|
||||
className={`spellbook-ability ${locked ? "spellbook-ability--locked" : ""}`}
|
||||
className={`spellbook-ability ${locked ? "spellbook-ability--locked" : ""} ${missing ? "is-missing-from-bars" : ""}`}
|
||||
aria-disabled={locked}
|
||||
aria-label={locked ? `${ability.name}, ${lockLabel.toLowerCase()}` : `${ability.name}, unlocked`}
|
||||
aria-label={locked ? `${ability.name}, ${lockLabel.toLowerCase()}` : `${ability.name}, unlocked${missing ? ", missing from action bars" : ""}`}
|
||||
onFocus={() => setInspectedId(ability.id)}
|
||||
onClick={() => {
|
||||
setInspectedId(ability.id);
|
||||
@@ -214,6 +224,7 @@ function SpellbookDialog({
|
||||
? "Learn in talents"
|
||||
: `Unlocks at level ${inspected.level}`
|
||||
: inspected.passive ? "Passive" : "Unlocked"}</dd></div>
|
||||
{!inspectedLocked && !inspected.passive && <div><dt>Action bars</dt><dd>{assignedIds.has(inspected.id) ? "Assigned" : "Missing from action bars"}</dd></div>}
|
||||
{inspected.rank !== undefined && <div><dt>Current rank</dt><dd>{inspected.rank} / {inspected.rankCount ?? inspected.rank}</dd></div>}
|
||||
{inspected.resourceCost !== undefined && <div><dt>Cost</dt><dd>{inspected.resourceCost} {inspected.resourceLabel}</dd></div>}
|
||||
{inspected.cooldownSeconds !== undefined && <div><dt>Cooldown</dt><dd>{inspected.cooldownSeconds || "None"}{inspected.cooldownSeconds ? " sec" : ""}</dd></div>}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCombatStore } from "../game/combatStore";
|
||||
import { abilityAtLevel, defaultActionBarForCharacter } from "../game/abilityCatalog";
|
||||
import { useGameStore } from "../game/store";
|
||||
import {
|
||||
BAR_COLUMNS, HUD_ELEMENTS, actionKeyForEvent, bindDesktopSlot, defaultUiLayout,
|
||||
BAR_COLUMNS, HUD_ELEMENTS, actionKeyForEvent, bindDesktopSlot, defaultUiLayout, resetDesktopAbilities,
|
||||
exportUiPreferences, importUiPreferences, type HudElementId, type UiDevice,
|
||||
resolveHudScreen, type HudScreen,
|
||||
} from "../game/uiLayout";
|
||||
@@ -176,7 +176,7 @@ export function UiLayoutEditor() {
|
||||
const combat = useCombatStore.getState();
|
||||
const defaults = defaultActionBarForCharacter(combat.classId, combat.secondaryClassId, combat.level);
|
||||
const empty = layout.bars.every((bar) => bar.slots.every((button) => !button.abilityId));
|
||||
return { ...layout, wowBars: event.target.checked, bars: empty && event.target.checked ? layout.bars.map((bar, b) => ({ ...bar, slots: bar.slots.map((button, s) => ({ ...button, abilityId: defaults[b * 12 + s] ?? null })) })) : layout.bars };
|
||||
return { ...(empty && event.target.checked ? resetDesktopAbilities(layout, defaults) : layout), wowBars: event.target.checked };
|
||||
})} />Use WoW action bars</label>
|
||||
{draft.wowBars && <>
|
||||
<label>Visible bar count<select value={draft.barCount} onChange={(event) => { ui.updateDraft((layout) => ({ ...layout, barCount: Number(event.target.value) })); useUiLayoutStore.setState({ selectedButton: null }); }}>{[1, 2, 3, 4, 5].map((count) => <option key={count}>{count}</option>)}</select></label>
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
SpellbookPanel,
|
||||
spellbookAbilitiesForDisplay,
|
||||
type SpellbookAbilityView,
|
||||
} from "./SpellbookPanel";
|
||||
|
||||
const barState = vi.hoisted(() => ({ pcIds: [] as (string | null)[], controllerIds: [] as string[], wowBars: true }));
|
||||
vi.mock("../game/uiLayoutStore", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../game/uiLayoutStore")>();
|
||||
const { normalizeUiPreferences } = await import("../game/uiLayout");
|
||||
return { ...actual, currentUiDevice: () => "pc", useUiLayoutStore: () => {
|
||||
const preferences = normalizeUiPreferences(null);
|
||||
preferences.current.pc.wowBars = barState.wowBars;
|
||||
preferences.current.pc.bars[0].slots.forEach((slot, index) => { slot.abilityId = barState.pcIds[index] ?? null; });
|
||||
return { preferences, draft: null };
|
||||
} };
|
||||
});
|
||||
vi.mock("../game/combatStore", () => ({ useCombatStore: (selector: (state: unknown) => unknown) => selector({
|
||||
actionBindings: Object.fromEntries(barState.controllerIds.map((id, index) => [`layer:${index}`, id])),
|
||||
}) }));
|
||||
|
||||
const abilities: readonly SpellbookAbilityView[] = [
|
||||
{
|
||||
id: "known-one",
|
||||
@@ -47,6 +62,29 @@ const abilities: readonly SpellbookAbilityView[] = [
|
||||
];
|
||||
|
||||
describe("spellbook ability progression", () => {
|
||||
it("highlights only usable spells missing from the active layout and updates after assignment", () => {
|
||||
barState.wowBars = true;
|
||||
barState.pcIds = [null, null, null, null, "known-one"];
|
||||
barState.controllerIds = ["known-two"];
|
||||
try {
|
||||
const render = () => renderToStaticMarkup(createElement(SpellbookPanel, {
|
||||
abilities: [...abilities, { ...abilities[1], id: "passive", passive: true }],
|
||||
playerLevel: 1, onActivate: () => undefined, open: true, onClose: () => undefined,
|
||||
}));
|
||||
let markup = render();
|
||||
expect(markup).toContain("Known Two, unlocked, missing from action bars");
|
||||
expect(markup).not.toContain("Known One, unlocked, missing from action bars");
|
||||
expect(markup.match(/is-missing-from-bars/g)).toHaveLength(1);
|
||||
barState.pcIds[11] = "known-two";
|
||||
markup = render();
|
||||
expect(markup).not.toContain("is-missing-from-bars");
|
||||
barState.wowBars = false;
|
||||
markup = render();
|
||||
expect(markup).toContain("Known One, unlocked, missing from action bars");
|
||||
expect(markup).not.toContain("Known Two, unlocked, missing from action bars");
|
||||
expect(markup.match(/is-missing-from-bars/g)).toHaveLength(1);
|
||||
} finally { barState.pcIds = []; barState.controllerIds = []; barState.wowBars = true; }
|
||||
});
|
||||
it("renders future abilities as inspectable locked buttons with level requirements", () => {
|
||||
const markup = renderToStaticMarkup(createElement(SpellbookPanel, {
|
||||
abilities,
|
||||
|
||||
Reference in new issue
Block a user