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"; import { AbilityTooltipTarget } from "./AbilityTooltip"; 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, cooldownSeconds: ability.cooldownMs / 1000, castSeconds: ability.castTimeMs / 1000, range: ability.range.max, 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
{character.name}Level {level} · {kit.classId.replace("rom-", "")} · {kit.resource.name} {actor?.resource ?? 0}/{actor?.maxResource ?? kit.resource.maximum}
{Object.entries(actor?.combat?.resourcePools ?? {}).filter(([type]) => type !== actor?.resourceType).map(([type, amount]) => {type}: {Math.round(amount ?? 0)}/{actor?.combat?.maxResourcePools[type as keyof typeof actor.combat.maxResourcePools] ?? 0})} {(actor?.combat?.comboPoints ?? 0) > 0 && Combo points: {actor!.combat!.comboPoints}/5} {actor?.combat?.runes.map((rune) => {rune.type}: {Math.max(0, (rune.readyAt - (useArenaStore.getState().snapshot?.serverTime ?? 0)) / 1000).toFixed(1)}s)} {(actor?.combat?.shield ?? 0) > 0 && Absorb: {Math.round(actor!.combat!.shield)}}
{actor?.effects.filter((effect) => !effect.id.startsWith("casting:")).slice(0, 12).map((effect, index) => {effect.icon && }{effect.id.replace(/ \(DBC.*$/, "")} {effect.stacks && effect.stacks > 1 ? `×${effect.stacks}` : ""} {effect.remainingMs > 0 ? `${Math.ceil(effect.remainingMs / 1000)}s` : ""})}
{(actor?.combat?.summons.length ?? 0) > 0 &&
{(["attack", "defend", "stop", "recall"] as const).map((command) => )}
} {castName &&
{actor?.combat?.activeCast?.mode === "channel" ? "Channeling" : "Casting"} {castName} · {(casting!.remainingMs / 1000).toFixed(1)}s
} {feedback &&
{feedback}
} {currentUiDevice() === "pc" && inputMode !== "gamepad" ? ( ) : [control, items[bindings[actionBindingId(layer, control)] ?? ""] ?? null])) as ActionBarItems} onActivate={castBinding} />} {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`}
; }