Release Healer Man 0.1.49

This commit is contained in:
phenom committed 2026-09-17 09:13:15 -04:00
1 parent 39425f2494
commit 612170afd0
91 files changed
+2278 -263

No files matched your search

+10 -4
View File
@@ -10,7 +10,13 @@ import {
type ActionBindingLayer,
} from "../game/actionBindings";
import { useCombatStore } from "../game/combatStore";
import { abilityAtLevel, defaultActionBarForCharacter, type AbilityDefinition } from "../game/abilityCatalog";
import {
abilityAtLevel,
abilityTalentNodeId,
defaultActionBarForCharacter,
learnedAbilityRank,
type AbilityDefinition,
} from "../game/abilityCatalog";
import { useGameStore } from "../game/store";
import { actionKeyLabel, assignedActionBarAbilityIds, assignDesktopAbility, resetDesktopAbilities, type UiLayout } from "../game/uiLayout";
import { currentUiDevice, useUiLayoutStore } from "../game/uiLayoutStore";
@@ -110,14 +116,14 @@ function AbilityBindingsDialog({ onClose }: { onClose?: () => void }) {
const talentRanks = useCombatStore((state) => state.talentRanks);
const tooltipData = (definition: AbilityDefinition | null | undefined): AbilityTooltipData | null => {
if (!definition) return null;
const talentRank = definition.talentEntryId === undefined ? undefined : talentRanks[`coa-entry-${definition.talentEntryId}`] ?? 0;
const talentRank = learnedAbilityRank(definition, talentRanks);
const ability = abilityAtLevel(definition, level, talentRank);
return {
name: ability.name, description: ability.description, target: ability.target,
resourceCost: ability.cost.amount, resourceLabel: ability.cost.resource.replace(/-/g, " "),
cooldownSeconds: ability.cooldownMs / 1_000, castSeconds: ability.castTimeMs / 1_000,
range: ability.range.max, lockedUntilLevel: ability.unlockLevel > level ? ability.unlockLevel : undefined,
talentLocked: ability.talentEntryId !== undefined && !talentRank,
talentLocked: Boolean(abilityTalentNodeId(ability)) && !talentRank,
};
};
const bindings = useCombatStore((state) => state.actionBindings);
@@ -132,7 +138,7 @@ function AbilityBindingsDialog({ onClose }: { onClose?: () => void }) {
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}`]))
&& (!abilityTalentNodeId(ability) || Boolean(learnedAbilityRank(ability, talentRanks)))
&& !assignedIds.has(ability.id);
const assignment = (control: BindingControl) => typeof control === "number"
? pcLayout.bars[bar].slots[control].abilityId : bindings[actionBindingId(layer, control)];
+2 -2
View File
@@ -5,7 +5,7 @@ import { useShellStore } from "../app/shellStore";
import { useGameStore } from "../game/store";
import { useCombatStore } from "../game/combatStore";
import { useArenaStore } from "../game/arenaStore";
import { abilitiesForCharacter, abilityAtLevel } from "../game/abilityCatalog";
import { abilitiesForCharacter, abilityAtLevel, learnedAbilityRank } from "../game/abilityCatalog";
import type { ArenaActorSnapshot, ArenaAuthoritativeSnapshot, ArenaStageManifest } from "../game/arenaTypes";
import { SpellbookPanel } from "./SpellbookPanel";
import { EquipmentPanel } from "./EquipmentPanel";
@@ -26,7 +26,7 @@ export function ArenaPanels({ character, actor, snapshot, stage, cast }: {
const close = () => useGameStore.getState().closeOverlay();
const color = classById(character.classId).color;
const abilities = useMemo(() => abilitiesForCharacter(character.classId, character.secondaryClassId).map((definition) => {
const rank = definition.talentEntryId === undefined ? undefined : actor?.combat?.talentRanks[`coa-entry-${definition.talentEntryId}`];
const rank = learnedAbilityRank(definition, actor?.combat?.talentRanks ?? character.talentRanks);
const ability = abilityAtLevel(definition, actor?.level ?? character.level, rank);
return { id: ability.id, name: ability.name, description: ability.description, level: ability.unlockLevel,
color, sigil: ability.sigil, iconUrl: ability.icon, known: ability.passive || Boolean(actor?.abilityIds?.includes(ability.id)),
+2 -2
View File
@@ -117,8 +117,8 @@ export function DungeonSimulationHud() {
})}
</div>
<p>{simulation.cameraMode === "free-fly"
? "Right-drag to look · WASD fly · E/Space up · Q/Ctrl down · Shift boost"
: "Choose a party member to follow · Right-drag to orbit"}</p>
? "Right-drag/Arrows look · WASD fly · E/Space up · Q/Ctrl down · Shift boost"
: "Choose a party member to follow · Right-drag/Arrows orbit"}</p>
</section>
{simulation.sessionMode === "spectate-replay" && (
+5 -5
View File
@@ -5,6 +5,8 @@ import { useCombatStore } from "../game/combatStore";
import {
abilityAtLevel,
abilityRankForLevel,
abilityTalentNodeId,
learnedAbilityRank,
} from "../game/abilityCatalog";
import { useGameStore } from "../game/store";
import { OptionsPanel } from "./OptionsPanel";
@@ -61,9 +63,7 @@ export function GameplayPanels() {
const spellbookAbilities = useMemo(() => [...abilities]
.sort((left, right) => left.unlockLevel - right.unlockLevel)
.map((catalogAbility) => {
const learnedTalentRank = catalogAbility.talentEntryId === undefined
? undefined
: talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
const learnedTalentRank = learnedAbilityRank(catalogAbility, talentRanks);
const ability = abilityAtLevel(catalogAbility, level, learnedTalentRank);
const activeRank = abilityRankForLevel(catalogAbility, level, learnedTalentRank);
return {
@@ -80,8 +80,8 @@ export function GameplayPanels() {
cooldownSeconds: ability.cooldownMs / 1_000,
castSeconds: ability.castTimeMs / 1_000,
range: ability.range.max,
known: catalogAbility.talentEntryId === undefined || Boolean(learnedTalentRank),
talentLocked: catalogAbility.talentEntryId !== undefined,
known: !abilityTalentNodeId(catalogAbility) || Boolean(learnedTalentRank),
talentLocked: Boolean(abilityTalentNodeId(catalogAbility)),
passive: ability.passive,
rank: activeRank?.rank,
rankCount: catalogAbility.ranks?.length,
+3 -5
View File
@@ -13,7 +13,7 @@ import {
import { useProgress } from "@react-three/drei";
import { classById, raceById } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { abilityAtLevel, resourceProfileForClass } from "../game/abilityCatalog";
import { abilityAtLevel, abilityTalentNodeId, learnedAbilityRank, resourceProfileForClass } from "../game/abilityCatalog";
import { ACTION_CONTROLS, actionBindingId } from "../game/actionBindings";
import { resolvedAbilityCosts, useCombatStore } from "../game/combatStore";
import { partyFightStillActive, respawnDefeatedPlayer } from "../game/deathRespawn";
@@ -260,11 +260,9 @@ export function Hud({ surface = "top", onLayoutChange, completion, onFinish, onR
return abilities.filter((ability) => used.has(ability.id));
}, [abilities, actionBindings, desktopBars, ui.draft, ui.preferences]);
const abilityItems = useMemo<Record<string, ActionBarItem>>(() => Object.fromEntries(assignedAbilities.map((catalogAbility) => {
const learnedTalentRank = catalogAbility.talentEntryId === undefined
? undefined
: talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
const learnedTalentRank = learnedAbilityRank(catalogAbility, talentRanks);
const ability = abilityAtLevel(catalogAbility, level, learnedTalentRank);
const talentLocked = ability.talentEntryId !== undefined && !learnedTalentRank;
const talentLocked = Boolean(abilityTalentNodeId(ability)) && !learnedTalentRank;
const personalRemaining = Math.max(0, (cooldowns[ability.id] ?? 0) - clock);
const globalRemaining = Math.max(0, globalCooldownEndsAt - clock);
const remaining = Math.max(personalRemaining, globalRemaining);
+3 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { useShellStore } from "../app/shellStore";
import { retryCloudSave, useCloudSaveStatus } from "../app/onlineAccountClient";
import { useCombatStore } from "../game/combatStore";
import { abilityAtLevel, defaultActionBarForCharacter } from "../game/abilityCatalog";
import { abilityAtLevel, abilityTalentNodeId, defaultActionBarForCharacter, learnedAbilityRank } from "../game/abilityCatalog";
import { useGameStore } from "../game/store";
import {
BAR_COLUMNS, HUD_ELEMENTS, actionKeyForEvent, bindDesktopSlot, defaultUiLayout, resetDesktopAbilities,
@@ -190,14 +190,14 @@ export function UiLayoutEditor() {
<button type="button" onClick={() => setAbility(null)}>Clear ability</button>
<label>Find ability<input value={search} onChange={(event) => setSearch(event.target.value)} /></label>
<div className="ui-layout-editor__abilities">{abilities.filter((ability) => !ability.passive && ability.name.toLowerCase().includes(search.toLowerCase())).map((definition) => {
const talentRank = definition.talentEntryId === undefined ? undefined : talentRanks[`coa-entry-${definition.talentEntryId}`] ?? 0;
const talentRank = learnedAbilityRank(definition, talentRanks);
const ability = abilityAtLevel(definition, level, talentRank);
return <AbilityTooltipTarget key={ability.id} ability={{
name: ability.name, description: ability.description, target: ability.target,
resourceCost: ability.cost.amount, resourceLabel: ability.cost.resource.replace(/-/g, " "),
cooldownSeconds: ability.cooldownMs / 1_000, castSeconds: ability.castTimeMs / 1_000, range: ability.range.max,
lockedUntilLevel: ability.unlockLevel > level ? ability.unlockLevel : undefined,
talentLocked: ability.talentEntryId !== undefined && !talentRank,
talentLocked: Boolean(abilityTalentNodeId(ability)) && !talentRank,
}}><button type="button" onClick={() => setAbility(ability.id)} aria-pressed={draft.bars[selected.bar].slots[selected.slot].abilityId === ability.id}>{ability.name}</button></AbilityTooltipTarget>;
})}</div>
</div>}