forked from phenom/healer-man
Release Healer Man 0.1.7
This commit is contained in:
1 parent
b12fb84859
commit
eca6b5db9b
99 files changed
+1931
-150
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
@@ -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") && (
|
||||
|
||||
Reference in new issue
Block a user