Release Healer Man 0.1.7

This commit is contained in:
phenom committed 2026-08-22 15:53:43 -04:00
1 parent b12fb84859
commit eca6b5db9b
99 files changed
+1931 -150

No files matched your search

+58 -11
View File
@@ -4,14 +4,18 @@ import {
learnedPartyTalentSelections,
upcomingPartyTalentSelections,
type PartyTalentSelection,
} from "../game/partyTalents";
} from "../game/partyTalentUi";
import {
PARTY_ROLE_LABELS,
usePartyStore,
type PartyCommand,
type PartyMember,
} from "../game/partyStore";
import { talentTreeById } from "../game/talentCatalog";
import {
loadTalentUiCatalog,
talentUiTreeById,
type TalentUiCatalog,
} from "../game/talentUiCatalog";
import { useGameStore } from "../game/store";
import { useMenuController, type MenuAction } from "../input/useMenuController";
import { ControllerButton } from "./ControllerButton";
@@ -70,6 +74,8 @@ function PartyManagementDialog() {
const command = usePartyStore((state) => state.command);
const openOverlay = useGameStore((state) => state.openOverlay);
const [inspectedMemberId, setInspectedMemberId] = useState(() => members[0]?.id ?? "");
const [talentCatalog, setTalentCatalog] = useState<TalentUiCatalog | null>(null);
const [talentCatalogErrorClassId, setTalentCatalogErrorClassId] = useState<string | null>(null);
const close = useCallback(() => openOverlay("pause"), [openOverlay]);
useEffect(() => {
@@ -79,13 +85,45 @@ function PartyManagementDialog() {
}, [inspectedMemberId, members]);
const inspectedMember = members.find((member) => member.id === inspectedMemberId) ?? members[0] ?? null;
const learned = useMemo(() => inspectedMember
? learnedPartyTalentSelections(inspectedMember.classId, inspectedMember.specialization, inspectedMember.level)
: [], [inspectedMember]);
useEffect(() => {
const classId = inspectedMember?.classId;
if (!classId || classId.startsWith("rom-")) {
setTalentCatalog(null);
setTalentCatalogErrorClassId(null);
return;
}
let active = true;
setTalentCatalogErrorClassId(null);
void loadTalentUiCatalog(classId).then((catalog) => {
if (active) setTalentCatalog(catalog);
}).catch(() => {
if (!active) return;
setTalentCatalog(null);
setTalentCatalogErrorClassId(classId);
});
return () => { active = false; };
}, [inspectedMember?.classId]);
const activeTalentCatalog = inspectedMember && talentCatalog?.classId === inspectedMember.classId
? talentCatalog
: null;
const learned = useMemo(() => inspectedMember && activeTalentCatalog
? learnedPartyTalentSelections(
activeTalentCatalog,
inspectedMember.classId,
inspectedMember.specialization,
inspectedMember.level,
)
: [], [activeTalentCatalog, inspectedMember]);
const currentTalents = useMemo(() => currentTalentRanks(learned), [learned]);
const upcomingTalents = useMemo(() => inspectedMember
? upcomingPartyTalentSelections(inspectedMember.classId, inspectedMember.specialization, inspectedMember.level, 3)
: [], [inspectedMember]);
const upcomingTalents = useMemo(() => inspectedMember && activeTalentCatalog
? upcomingPartyTalentSelections(
activeTalentCatalog,
inspectedMember.classId,
inspectedMember.specialization,
inspectedMember.level,
3,
)
: [], [activeTalentCatalog, inspectedMember]);
const actions = useMemo<MenuAction[]>(() => [
...members.map((member) => ({
@@ -106,7 +144,9 @@ function PartyManagementDialog() {
const memberClass = inspectedMember ? classById(inspectedMember.classId) : null;
const memberRace = inspectedMember ? raceById(inspectedMember.raceId) : null;
const primaryTree = inspectedMember ? talentTreeById(inspectedMember.talentTreeId) : null;
const primaryTree = inspectedMember && activeTalentCatalog
? talentUiTreeById(activeTalentCatalog, inspectedMember.talentTreeId)
: null;
const primaryPoints = inspectedMember
? learned.filter((selection) => selection.treeId === inspectedMember.talentTreeId).length
: 0;
@@ -210,7 +250,14 @@ function PartyManagementDialog() {
</header>
<div className="party-management__talent-list">
{currentTalents.map((selection) => <TalentSelectionCard key={selection.nodeId} selection={selection} />)}
{!currentTalents.length && (
{!activeTalentCatalog && (
<p className="party-management__empty-talents">
{talentCatalogErrorClassId === inspectedMember.classId
? "Talent details are temporarily unavailable."
: "Preparing this class's talent catalog…"}
</p>
)}
{activeTalentCatalog && !currentTalents.length && (
<p className="party-management__empty-talents">
Talent training begins at level 10. This build will advance automatically with you.
</p>
@@ -227,7 +274,7 @@ function PartyManagementDialog() {
{upcomingTalents.map((selection) => (
<TalentSelectionCard key={`${selection.level}-${selection.nodeId}`} selection={selection} upcoming />
))}
{!upcomingTalents.length && (
{activeTalentCatalog && !upcomingTalents.length && (
<p className="party-management__empty-talents">This party member has completed the level 80 talent plan.</p>
)}
</div>
+15 -17
View File
@@ -1,14 +1,12 @@
import { useMemo } from "react";
import { classById } from "../app/characterCatalog";
import { use, useMemo } from "react";
import { classById, type CoaClassId } from "../app/characterCatalog";
import { useCombatStore } from "../game/combatStore";
import {
spentTalentPointsInTree,
talentNodesForTree,
talentTreesForClass,
unspentCoaEssence,
unspentTalentPoints,
} from "../game/talentCatalog";
import { isCoaClassId } from "../game/coaLiveCatalog";
runtimeSpentTalentPointsInTree,
runtimeUnspentCoaEssence,
runtimeUnspentTalentPoints,
} from "../game/talentRuntimeCatalog";
import { loadTalentUiCatalog, talentUiNodesForTree } from "../game/talentUiCatalog";
import { TalentsPanel } from "./TalentsPanel";
function treeSigil(name: string): string {
@@ -20,19 +18,20 @@ export default function TalentGameplayPanel() {
const level = useCombatStore((state) => state.level);
const talentRanks = useCombatStore((state) => state.talentRanks);
const classDefinition = classById(classId);
const talentTrees = useMemo(() => talentTreesForClass(classId).map((tree) => ({
const catalog = use(loadTalentUiCatalog(classId));
const talentTrees = useMemo(() => catalog.trees.map((tree) => ({
id: tree.id,
name: tree.name,
description: tree.description,
points: spentTalentPointsInTree(talentRanks, tree.id),
points: runtimeSpentTalentPointsInTree(talentRanks, tree.id),
color: classDefinition.color,
sigil: treeSigil(tree.name),
iconUrl: tree.icon,
backgroundUrl: tree.background,
nodes: talentNodesForTree(tree.id),
})), [classDefinition.color, classId, talentRanks]);
const coaEssence = isCoaClassId(classId)
? unspentCoaEssence(level, talentRanks, classId)
nodes: talentUiNodesForTree(catalog, tree.id),
})), [catalog, classDefinition.color, talentRanks]);
const coaEssence = classDefinition.mode === "conquest"
? runtimeUnspentCoaEssence(level, talentRanks, classId as CoaClassId)
: null;
return <TalentsPanel
@@ -40,11 +39,10 @@ export default function TalentGameplayPanel() {
ranks={talentRanks}
classId={classId}
playerLevel={level}
availablePoints={unspentTalentPoints(level, talentRanks, classId)}
availablePoints={runtimeUnspentTalentPoints(level, talentRanks, classId)}
abilityEssence={coaEssence?.ability}
talentEssence={coaEssence?.talent}
onInvest={(nodeId) => { useCombatStore.getState().allocateTalent(nodeId); }}
onReset={() => useCombatStore.getState().resetTalents()}
/>;
}
+18 -16
View File
@@ -1,11 +1,13 @@
import { useEffect, useMemo, useState, type CSSProperties } from "react";
import type { ClassId } from "../app/characterCatalog";
import { runtimeTalentAllocationFailure } from "../game/talentRuntimeCatalog";
import {
talentDescriptionForRank,
talentAllocationFailure,
type TalentAllocationFailure,
type TalentNodeDefinition,
type TalentRanks,
talentUiDescriptionForRank,
type TalentUiNode,
} from "../game/talentUiCatalog";
import type {
TalentAllocationFailure,
TalentRanks,
} from "../game/talentCatalog";
import { useGameStore } from "../game/store";
import { useMenuController, type MenuAction, type MenuDirection } from "../input/useMenuController";
@@ -20,7 +22,7 @@ export interface TalentTreeView {
sigil: string;
iconUrl?: string;
backgroundUrl: string;
nodes: readonly TalentNodeDefinition[];
nodes: readonly TalentUiNode[];
}
export interface TalentsPanelProps {
@@ -53,10 +55,10 @@ function TalentImage({ src, fallback }: { src?: string; fallback: string }) {
}
function nearestNode(
nodes: readonly TalentNodeDefinition[],
source: TalentNodeDefinition,
nodes: readonly TalentUiNode[],
source: TalentUiNode,
direction: MenuDirection,
): TalentNodeDefinition | null {
): TalentUiNode | null {
const vertical = direction === "up" || direction === "down";
const candidates = nodes.filter((candidate) => {
if (direction === "up") return candidate.row < source.row;
@@ -77,9 +79,9 @@ function nearestNode(
function failureText(
failure: TalentAllocationFailure | null,
node: TalentNodeDefinition,
node: TalentUiNode,
tree: TalentTreeView,
prerequisite: TalentNodeDefinition | null,
prerequisite: TalentUiNode | null,
): string {
if (!failure) return "Ready to learn";
if (failure === "max-rank") return "Maximum rank learned";
@@ -125,7 +127,7 @@ function TalentsDialog({
const nodeStateById = useMemo(() => new Map(allNodes.map((node) => {
const rank = Math.max(0, Math.min(node.maxRank, Math.trunc(ranks[node.id] ?? 0)));
const tree = treeByNodeId.get(node.id);
const rawFailure = talentAllocationFailure(ranks, classId, node.id, playerLevel);
const rawFailure = runtimeTalentAllocationFailure(ranks, classId, node.id, playerLevel);
const failure: TalentAllocationFailure | null = rank >= node.maxRank
? "max-rank"
: node.system !== "coa" && tree && tree.points < (node.row - 1) * 5
@@ -215,7 +217,7 @@ function TalentsDialog({
setDetailNodeId(nodeId);
};
const activateNode = (treeId: string, node: TalentNodeDefinition) => {
const activateNode = (treeId: string, node: TalentUiNode) => {
inspectNode(treeId, node.id);
if (!nodeStateById.get(node.id)?.failure) onInvest(node.id);
};
@@ -333,7 +335,7 @@ function TalentsDialog({
aria-label={label}
aria-disabled={state.failure !== null}
aria-describedby={detailNode?.id === node.id ? "talent-node-details" : undefined}
title={`${label} — ${talentDescriptionForRank(node, state.rank || 1)}`}
title={`${label} — ${talentUiDescriptionForRank(node, state.rank || 1)}`}
onFocus={() => inspectNode(tree.id, node.id)}
onPointerEnter={() => inspectNode(tree.id, node.id)}
onClick={() => activateNode(tree.id, node)}
@@ -368,11 +370,11 @@ function TalentsDialog({
<small className="talent-details__effect-rank">
{detailState.rank > 0 ? `Rank ${detailState.rank} effect` : "Rank 1 effect"}
</small>
<p>{talentDescriptionForRank(detailNode, detailState.rank || 1)}</p>
<p>{talentUiDescriptionForRank(detailNode, detailState.rank || 1)}</p>
{detailState.rank > 0 && detailState.rank < detailNode.maxRank && (
<>
<small className="talent-details__effect-rank">Next rank</small>
<p>{talentDescriptionForRank(detailNode, detailState.rank + 1)}</p>
<p>{talentUiDescriptionForRank(detailNode, detailState.rank + 1)}</p>
</>
)}
{(detailNode.row > 1 || prerequisite || detailNode.system === "coa") && (