forked from phenom/healer-man
Release Healer Man 0.1.29
This commit is contained in:
1 parent
8c9809d3e0
commit
544c1bee91
120 files changed
+4363
-1036
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { useMemo } from "react";
|
||||
import type { CharacterProfile } from "../app/types";
|
||||
import { arenaCharacterAbilities, arenaAbilityIsLearned, createArenaCharacterKit } from "../game/arenaCharacter";
|
||||
import { ACTION_CONTROLS, actionBindingId, type ActionBindingLayer, type ActionBindingControl } from "../game/actionBindings";
|
||||
import type { ArenaActorSnapshot } from "../game/arenaTypes";
|
||||
import { actionKeyLabel } from "../game/uiLayout";
|
||||
import { currentUiDevice, useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { ActionBar, ActionIcon, type ActionBarItem, type ActionBarItems } from "./ActionBar";
|
||||
import { useInputBindingLabels } from "../input/useInputBindingLabels";
|
||||
import { useArenaStore } from "../game/arenaStore";
|
||||
|
||||
const PC_ACTION_CONTROLS = ["face-bottom", "face-left", "face-top", "face-right", "r1", "r2", "dpad-left", "dpad-right"] as const;
|
||||
|
||||
export function ArenaActionBar({ character, actor, layer, inputMode, blocked, onCast, feedback }: {
|
||||
character: CharacterProfile;
|
||||
actor: ArenaActorSnapshot | null;
|
||||
layer: ActionBindingLayer;
|
||||
inputMode: "keyboard" | "gamepad";
|
||||
blocked: boolean;
|
||||
onCast: (abilityId: string) => void;
|
||||
feedback: string;
|
||||
}) {
|
||||
const ui = useUiLayoutStore();
|
||||
const bindingLabel = useInputBindingLabels();
|
||||
const level = actor?.level ?? character.level;
|
||||
const kit = useMemo(() => createArenaCharacterKit(character, level), [character, level]);
|
||||
const abilities = useMemo(() => arenaCharacterAbilities(character, level), [character, level]);
|
||||
const layout = ui.preferences.current[currentUiDevice()];
|
||||
const bindings = actor?.actionBindings ?? kit.actionBindings;
|
||||
const desktopBars = layout.wowBars ? layout.bars.slice(0, layout.barCount).map((bar) => ({ ...bar, slots: bar.slots.map((slot) => ({ ...slot, label: actionKeyLabel(slot.key) })) }))
|
||||
: [{ columns: 8, slots: PC_ACTION_CONTROLS.map((control) => ({ abilityId: bindings[actionBindingId(layer, control)], label: bindingLabel("pc", `ability:${layer}:${control}`) })) }];
|
||||
const items = Object.fromEntries(abilities.map((ability): [string, ActionBarItem] => {
|
||||
const learned = actor?.abilityIds ? actor.abilityIds.includes(ability.id) : arenaAbilityIsLearned(ability, character, level);
|
||||
const remaining = Math.max(actor?.cooldowns[ability.id] ?? 0, actor?.gcdRemainingMs ?? 0) / 1000;
|
||||
return [ability.id, {
|
||||
id: ability.id, name: ability.name, iconUrl: ability.icon, shortLabel: ability.sigil,
|
||||
description: ability.description, target: ability.target,
|
||||
resourceCost: ability.cost.amount, resourceLabel: kit.resource.name,
|
||||
cooldownDuration: Math.max(ability.cooldownMs, ability.gcdMs) / 1000,
|
||||
cooldownRemaining: remaining,
|
||||
disabled: blocked || !actor?.alive || !learned || remaining > 0 || actor.effects.some((effect) => effect.id.startsWith("casting:")),
|
||||
lockedUntilLevel: learned ? undefined : ability.unlockLevel,
|
||||
}];
|
||||
}));
|
||||
const casting = actor?.effects.find((effect) => effect.id.startsWith("casting:"));
|
||||
const castName = casting ? items[casting.id.slice(8)]?.name : null;
|
||||
const castBinding = (control: ActionBindingControl) => {
|
||||
const id = bindings[actionBindingId(layer, control)];
|
||||
if (id) onCast(id);
|
||||
};
|
||||
return <div className="arena-player-hud">
|
||||
<div className="arena-player-status"><strong>{character.name}</strong><span>Level {level} · {kit.classId.replace("rom-", "")} · {kit.resource.name} {actor?.resource ?? 0}/{actor?.maxResource ?? kit.resource.maximum}</span></div>
|
||||
<div className="arena-extra-resources" aria-label="Character resources">
|
||||
{Object.entries(actor?.combat?.resourcePools ?? {}).filter(([type]) => type !== actor?.resourceType).map(([type, amount]) => <span key={type}>{type}: {Math.round(amount ?? 0)}/{actor?.combat?.maxResourcePools[type as keyof typeof actor.combat.maxResourcePools] ?? 0}</span>)}
|
||||
{(actor?.combat?.comboPoints ?? 0) > 0 && <span>Combo points: {actor!.combat!.comboPoints}/5</span>}
|
||||
{actor?.combat?.runes.map((rune) => <span key={rune.slot}>{rune.type}: {Math.max(0, (rune.readyAt - (useArenaStore.getState().snapshot?.serverTime ?? 0)) / 1000).toFixed(1)}s</span>)}
|
||||
{(actor?.combat?.shield ?? 0) > 0 && <span>Absorb: {Math.round(actor!.combat!.shield)}</span>}
|
||||
</div>
|
||||
<div className="arena-aura-strip" aria-label="Your buffs and debuffs">{actor?.effects.filter((effect) => !effect.id.startsWith("casting:")).slice(0, 12).map((effect, index) => <span className={effect.disposition === "debuff" ? "is-debuff" : ""} key={`${effect.id}:${index}`} title={effect.id}>{effect.icon && <img src={effect.icon} alt="" />}{effect.id.replace(/ \(DBC.*$/, "")} {effect.stacks && effect.stacks > 1 ? `×${effect.stacks}` : ""} {effect.remainingMs > 0 ? `${Math.ceil(effect.remainingMs / 1000)}s` : ""}</span>)}</div>
|
||||
{(actor?.combat?.summons.length ?? 0) > 0 && <div className="arena-pet-orders" aria-label="Pet commands">{(["attack", "defend", "stop", "recall"] as const).map((command) => <button key={command} type="button" onClick={() => useArenaStore.getState().intent({ type: "command", command })}>{command}</button>)}</div>}
|
||||
{castName && <div className="arena-cast-status" role="status">{actor?.combat?.activeCast?.mode === "channel" ? "Channeling" : "Casting"} {castName} · {(casting!.remainingMs / 1000).toFixed(1)}s <button type="button" onClick={() => useArenaStore.getState().intent({ type: "command", command: "cancel-cast" })}>Cancel</button></div>}
|
||||
{feedback && <div className="arena-action-feedback" role="status">{feedback}</div>}
|
||||
{currentUiDevice() === "pc" && inputMode !== "gamepad" ? (
|
||||
<nav className="arena-personal-bars" aria-label="Your arena action bars">
|
||||
{desktopBars.map((bar, index) => <section key={index} aria-label={`Action bar ${index + 1}`} style={{ gridTemplateColumns: `repeat(${bar.columns}, minmax(0, 1fr))` }}>
|
||||
{bar.slots.map((slot, slotIndex) => {
|
||||
const item = slot.abilityId ? items[slot.abilityId] : null;
|
||||
return <button type="button" key={slotIndex} disabled={!item || item.disabled}
|
||||
aria-label={`${item?.name ?? "Empty"}, ${slot.label}`} title={item?.description ?? "Empty binding"}
|
||||
onClick={() => item && onCast(item.id)}>
|
||||
<kbd>{slot.label}</kbd><ActionIcon item={item ?? null} /><span>{item?.name ?? "Empty"}</span>
|
||||
{item && (item.cooldownRemaining ?? 0) > 0 && <em>{item.cooldownRemaining!.toFixed(1)}</em>}
|
||||
</button>;
|
||||
})}
|
||||
</section>)}
|
||||
</nav>
|
||||
) : <ActionBar variant="compact" inputMode={inputMode} activeLayer={layer}
|
||||
items={Object.fromEntries(ACTION_CONTROLS.map((control) => [control, items[bindings[actionBindingId(layer, control)] ?? ""] ?? null])) as ActionBarItems}
|
||||
onActivate={castBinding} />}
|
||||
<small className="arena-control-hints">{inputMode === "gamepad"
|
||||
? `Left stick move · Right stick look · ${bindingLabel("controller", "target-next")} enemy · ${bindingLabel("controller", "party-next")} ally`
|
||||
: `${["move-forward", "move-left", "move-backward", "move-right"].map((id) => bindingLabel("pc", id)).join("/")} move · Hold right mouse to look · ${bindingLabel("pc", "target-next")} enemy · Click a team frame to target`}</small>
|
||||
</div>;
|
||||
}
|
||||
Reference in new issue
Block a user