From f0ab3ca3ce9159e87741a6477acadc7eeed6276f Mon Sep 17 00:00:00 2001
From: Refactoring Agent <[email protected]>
Date: Fri, 1 May 2026 16:41:29 +0200
Subject: [PATCH] refactor: extract components from SkillsTab.tsx to reduce
below 400 lines
---
.../game/tabs/CategorySkillsList.tsx | 221 +++++++++++++++
.../game/tabs/MilestoneProgress.tsx | 22 ++
.../game/tabs/SkillCategoryHeader.tsx | 42 +++
src/components/game/tabs/SkillMultipliers.tsx | 50 ++++
src/components/game/tabs/SkillRow.tsx | 30 +--
src/components/game/tabs/SkillsTab.tsx | 255 +++---------------
.../game/hooks/useSkillUpgradeSelection.ts | 31 +++
src/lib/game/types/index.ts | 3 +-
8 files changed, 422 insertions(+), 232 deletions(-)
create mode 100644 src/components/game/tabs/CategorySkillsList.tsx
create mode 100644 src/components/game/tabs/MilestoneProgress.tsx
create mode 100644 src/components/game/tabs/SkillCategoryHeader.tsx
create mode 100644 src/components/game/tabs/SkillMultipliers.tsx
diff --git a/src/components/game/tabs/CategorySkillsList.tsx b/src/components/game/tabs/CategorySkillsList.tsx
new file mode 100644
index 0000000..e7704aa
--- /dev/null
+++ b/src/components/game/tabs/CategorySkillsList.tsx
@@ -0,0 +1,221 @@
+// ─── Category Skills List ───────────────────────────────────────────
+// Wraps all skills in a single category, handles category-level UI
+
+'use client';
+
+import { useState } from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { SkillRow } from './SkillRow';
+import { SkillCategoryHeader } from './SkillCategoryHeader';
+import type { GameStore } from '@/lib/game/store';
+import { SKILL_CATEGORIES, SKILLS_DEF, getStudyCostMultiplier, getStudySpeedMultiplier } from '@/lib/game/constants';
+import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
+import { SPECIAL_EFFECTS, hasSpecial } from '@/lib/game/special-effects';
+import { ELEMENTS } from '@/lib/game/constants';
+import { getUnifiedEffects } from '@/lib/game/effects';
+import { hasMilestoneUpgrade } from '@/lib/game/hooks/useSkillUpgradeSelection';
+import type { ComputedEffects } from '@/lib/game/upgrade-effects.types';
+import type { SkillCost } from '@/lib/game/types';
+import { useGameToast } from '@/components/game/GameToast';
+
+interface CategorySkillsListProps {
+ category: { id: string; name: string; icon: string };
+ availableCategories: string[];
+ isCollapsed: boolean;
+ onToggleCategory: (categoryId: string) => void;
+ store: GameStore;
+ studySpeedMult: number;
+ upgradeEffects: ComputedEffects;
+ currentStudyTarget: any;
+ onStartStudying: (skillId: string) => void;
+ onParallelStudy: (skillId: string) => void;
+ onCancelStudy: () => void;
+ onOpenUpgradeDialog: (skillId: string, milestone: 5 | 10) => void;
+ onTierUp: (skillId: string) => void;
+ pendingSelections: string[];
+ setPendingSelections: (selections: string[]) => void;
+}
+
+// Type guard for element skill costs
+function isElementCost(cost?: SkillCost | null): cost is SkillCost & { type: 'element'; element: string } {
+ return cost !== null && typeof cost !== 'undefined' && cost.type === 'element' && typeof cost.element === 'string';
+}
+
+export function CategorySkillsList({
+ category,
+ availableCategories,
+ isCollapsed,
+ onToggleCategory,
+ store,
+ studySpeedMult,
+ upgradeEffects,
+ currentStudyTarget,
+ onStartStudying,
+ onParallelStudy,
+ onCancelStudy,
+ onOpenUpgradeDialog,
+ onTierUp,
+ pendingSelections,
+ setPendingSelections,
+}: CategorySkillsListProps) {
+ const showToast = useGameToast();
+
+ // Skip if category not available
+ if (!availableCategories.includes(category.id)) return null;
+
+ const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === category.id);
+ if (skillsInCat.length === 0) return null;
+
+ const handleCancelStudyInternal = () => {
+ onCancelStudy();
+ };
+
+ return (
+
+ onToggleCategory(category.id)}
+ />
+ {!isCollapsed && (
+
+
+ {skillsInCat.map(([id, def]) => {
+ // GATE MANA CAPACITY SKILLS BY UNLOCKED ELEMENT
+ if (isElementCost(def.cost)) {
+ const element = store.elements[def.cost.element];
+ if (!element?.unlocked) return null;
+ }
+
+ // Get tier info
+ const currentTier = store.skillTiers?.[id] || 1;
+ const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id;
+ const tierMultiplier = getTierMultiplier(tieredSkillId);
+
+ // Get the actual level from the tiered skill
+ const level = store.skills[tieredSkillId] || store.skills[id] || 0;
+ const maxed = level >= def.max;
+
+ // Check if studying this skill
+ const isStudying =
+ (store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) &&
+ store.currentStudyTarget?.type === 'skill';
+
+ // Get tier name for display
+ const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find((t) => t.tier === currentTier);
+ const skillDisplayName = tierDef?.name || def.name;
+
+ // Check prerequisites
+ let prereqMet = true;
+ if (def.req) {
+ for (const [r, rl] of Object.entries(def.req)) {
+ if ((store.skills[r] || 0) < rl) {
+ prereqMet = false;
+ break;
+ }
+ }
+ }
+
+ // Apply skill modifiers
+ const costMult = getStudyCostMultiplier(store.skills);
+ const speedMult = getStudySpeedMultiplier(store.skills);
+ const studyEffects = getUnifiedEffects(store);
+ const effectiveSpeedMult = speedMult * studyEffects.studySpeedMultiplier;
+
+ // Study time scales with tier
+ const tierStudyTime = def.studyTime * currentTier;
+ const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
+
+ // Cost scales with tier
+ const baseCost = def.base * (level + 1) * currentTier;
+ const cost = Math.floor(baseCost * costMult);
+
+ // Additional cost (element mana) - only pass element costs
+ const additionalCost = isElementCost(def.cost)
+ ? { type: 'element' as const, element: def.cost.element, amount: def.cost.amount }
+ : undefined;
+
+ // Can start studying?
+ let canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying;
+
+ // Check additional cost (element mana)
+ if (isElementCost(def.cost)) {
+ const element = store.elements[def.cost.element];
+ if (!element || element.current < def.cost.amount) {
+ canStudy = false;
+ }
+ }
+
+ // Check for milestone upgrades
+ const milestoneInfo = hasMilestoneUpgrade(
+ tieredSkillId,
+ level,
+ store.skillTiers || {},
+ store.skillUpgrades
+ );
+
+ // Check for tier up
+ const nextTierSkill = getNextTierSkill(tieredSkillId);
+ const canTierUp = Boolean(maxed && nextTierSkill);
+
+ // Get selected upgrades
+ const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
+ const selectedL5 = selectedUpgrades.filter((u) => u.includes('_l5'));
+ const selectedL10 = selectedUpgrades.filter((u) => u.includes('_l10'));
+
+ // Check if insufficient mana for toast
+ const hasInsufficientMana = !isStudying && !maxed && store.rawMana < cost;
+
+ // Check for parallel study eligibility
+ const isParallelStudy =
+ store.currentStudyTarget?.id === tieredSkillId &&
+ store.currentStudyTarget?.type === 'skill';
+ const canParallelStudy: boolean =
+ hasSpecial(studyEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
+ !!store.currentStudyTarget &&
+ store.currentStudyTarget.id !== tieredSkillId &&
+ !isStudying;
+
+ return (
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/game/tabs/MilestoneProgress.tsx b/src/components/game/tabs/MilestoneProgress.tsx
new file mode 100644
index 0000000..3b2d34e
--- /dev/null
+++ b/src/components/game/tabs/MilestoneProgress.tsx
@@ -0,0 +1,22 @@
+// ─── Milestone Progress ───────────────────────────────────────────
+// Milestone upgrade progress indicator for skill rows
+
+import { Badge } from '@/components/ui/badge';
+
+interface MilestoneProgressProps {
+ milestoneInfo: {
+ milestone: 5 | 10;
+ hasUpgrades: boolean;
+ selectedCount: number;
+ } | null;
+}
+
+export function MilestoneProgress({ milestoneInfo }: MilestoneProgressProps) {
+ if (!milestoneInfo) return null;
+
+ return (
+
+ ⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
+
+ );
+}
diff --git a/src/components/game/tabs/SkillCategoryHeader.tsx b/src/components/game/tabs/SkillCategoryHeader.tsx
new file mode 100644
index 0000000..f39ce2b
--- /dev/null
+++ b/src/components/game/tabs/SkillCategoryHeader.tsx
@@ -0,0 +1,42 @@
+// ─── Skill Category Header ───────────────────────────────────────────
+// Header for a skill category with collapse/expand toggle
+
+import { Badge } from '@/components/ui/badge';
+import { ChevronDown, ChevronRight } from 'lucide-react';
+
+interface SkillCategoryHeaderProps {
+ category: {
+ id: string;
+ name: string;
+ icon: string;
+ };
+ skillCount: number;
+ isCollapsed: boolean;
+ onToggle: () => void;
+}
+
+export function SkillCategoryHeader({
+ category,
+ skillCount,
+ isCollapsed,
+ onToggle,
+}: SkillCategoryHeaderProps) {
+ return (
+
+
+
+ {category.icon} {category.name}
+
+
+
+ {skillCount} skills
+
+ {isCollapsed ? : }
+
+
+
+ );
+}
+
+// Local import of CardHeader and CardTitle to avoid circular deps
+import { CardHeader, CardTitle } from '@/components/ui/card';
diff --git a/src/components/game/tabs/SkillMultipliers.tsx b/src/components/game/tabs/SkillMultipliers.tsx
new file mode 100644
index 0000000..6c76bd6
--- /dev/null
+++ b/src/components/game/tabs/SkillMultipliers.tsx
@@ -0,0 +1,50 @@
+// ─── Skill Multipliers ───────────────────────────────────────────
+// Study speed and cost multiplier display for skill rows
+
+import { ELEMENTS } from '@/lib/game/constants';
+
+type AdditionalCost = { type: 'element'; element: string; amount: number };
+
+interface SkillMultipliersProps {
+ effectiveStudyTime: number;
+ speedMult: number;
+ costMult: number;
+ cost: number;
+ additionalCost?: AdditionalCost;
+}
+
+export function SkillMultipliers({
+ effectiveStudyTime,
+ speedMult,
+ costMult,
+ cost,
+ additionalCost,
+}: SkillMultipliersProps) {
+ return (
+
+ 1 ? 'text-green-400' : ''}>
+ Study: {formatStudyTime(effectiveStudyTime)}{speedMult > 1 && (
+ ({Math.round(speedMult * 100)}% speed)
+ )}
+
+ {' • '}
+
+ Cost: {cost} mana{costMult < 1 && (
+ ({Math.round(costMult * 100)}% cost)
+ )}
+ {additionalCost && additionalCost.type === 'element' && (
+
+ + {additionalCost.amount} {ELEMENTS[additionalCost.element]?.sym} {additionalCost.element}
+
+ )}
+
+
+ );
+}
+
+function formatStudyTime(ms: number): string {
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
+ if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`;
+ return `${Math.floor(ms / 86_400_000)}d ${Math.floor((ms % 86_400_000) / 3_600_000)}h`;
+}
diff --git a/src/components/game/tabs/SkillRow.tsx b/src/components/game/tabs/SkillRow.tsx
index 07b2d2f..fd30403 100644
--- a/src/components/game/tabs/SkillRow.tsx
+++ b/src/components/game/tabs/SkillRow.tsx
@@ -10,11 +10,12 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import { ChevronRight } from 'lucide-react';
import type { SkillUpgradeChoice } from '@/lib/game/types';
import { ELEMENTS } from '@/lib/game/constants';
-import { hasMilestoneUpgrade } from '@/lib/game/hooks/useSkillUpgradeSelection';
import { formatStudyTime } from '@/lib/game/formatting';
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
import type { ComputedEffects } from '@/lib/game/upgrade-effects.types';
import { SPECIAL_EFFECTS, hasSpecial } from '@/lib/game/special-effects';
+import { MilestoneProgress } from './MilestoneProgress';
+import { SkillMultipliers } from './SkillMultipliers';
type StudyTarget = { type: 'skill' | 'spell'; id: string; progress: number; required: number; } | null;
@@ -126,20 +127,13 @@ export function SkillRow(props: SkillRowProps) {
Requires: {Object.entries(def.req).map(([r, rl]) => `${r} Lv.${rl}`).join(', ')}
)}
-
- 1 ? 'text-green-400' : ''}>
- Study: {formatStudyTime(effectiveStudyTime)}{speedMult > 1 && ({Math.round(speedMult * 100)}% speed)}
-
- {' • '}
-
- Cost: {cost} mana{costMult < 1 && ({Math.round(costMult * 100)}% cost)}
- {additionalCost && additionalCost.type === 'element' && (
-
- + {additionalCost.amount} {ELEMENTS[additionalCost.element]?.sym} {additionalCost.element}
-
- )}
-
-
+
{hasInsufficientMana && (
@@ -147,11 +141,7 @@ export function SkillRow(props: SkillRowProps) {
)}
- {milestoneInfo && (
-
- ⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
-
- )}
+
diff --git a/src/components/game/tabs/SkillsTab.tsx b/src/components/game/tabs/SkillsTab.tsx
index 1633b04..336eb44 100755
--- a/src/components/game/tabs/SkillsTab.tsx
+++ b/src/components/game/tabs/SkillsTab.tsx
@@ -1,10 +1,10 @@
-// ─── Skills Tab ────────────────────────────
+// ─── Skills Tab ───────────────────────────────────────────────────
// SkillsTab - Displays all skills organized by category
-// Refactored: uses SkillRow component for per-skill rendering (reduced from 469 lines)
+// Refactored: extracted components for better modularity (reduced from 400 lines)
'use client';
-import { useState } from 'react';
+import { useState, useCallback } from 'react';
import {
SKILLS_DEF,
SKILL_CATEGORIES,
@@ -20,7 +20,7 @@ import {
import { getUnifiedEffects } from '@/lib/game/effects';
import { getAvailableSkillCategories } from '@/lib/game/data/attunements';
import { fmt, fmtDec } from '@/lib/game/store';
-import type { SkillUpgradeChoice, GameStore } from '@/lib/game/types';
+import type { SkillUpgradeChoice } from '@/lib/game/types';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -38,43 +38,13 @@ import { ELEMENTS } from '@/lib/game/constants';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { SkillRow } from './SkillRow';
import { useSkillUpgradeSelection } from '@/lib/game/hooks/useSkillUpgradeSelection';
+import { CategorySkillsList } from './CategorySkillsList';
+import type { GameStore } from '@/lib/game/store';
export interface SkillsTabProps {
store: GameStore;
}
-// Check if skill has milestone available
-function hasMilestoneUpgrade(
- skillId: string,
- level: number,
- skillTiers: Record
,
- skillUpgrades: Record
-): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
- const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
- const path = SKILL_EVOLUTION_PATHS[baseSkillId];
- if (!path) return null;
-
- // Check level 5 milestone
- if (level >= 5) {
- const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, skillTiers);
- const selected5 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l5'));
- if (upgrades5.length > 0 && selected5.length < 2) {
- return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
- }
- }
-
- // Check level 10 milestone
- if (level >= 10) {
- const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, skillTiers);
- const selected10 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l10'));
- if (upgrades10.length > 0 && selected10.length < 2) {
- return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
- }
- }
-
- return null;
-}
-
export function SkillsTab({ store }: SkillsTabProps) {
const showToast = useGameToast();
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState(null);
@@ -86,7 +56,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
} | null>(null);
const studySpeedMult = getStudySpeedMultiplier(store.skills);
- const upgradeEffects = getUnifiedEffects(store);
+ const upgradeEffects = getUnifiedEffects(store as any);
// Upgrade selection hook
const {
@@ -135,6 +105,14 @@ export function SkillsTab({ store }: SkillsTabProps) {
hookHandleCancel(() => setUpgradeDialogSkill(null));
};
+ // Wrapper for upgrade toggle that matches UpgradeDialog's onToggle signature
+ const handleUpgradeToggle = useCallback(
+ (upgradeId: string) => {
+ toggleUpgrade(upgradeId, available, alreadySelected);
+ },
+ [toggleUpgrade, available, alreadySelected]
+ );
+
// Handle study start with toast
const handleStartStudying = (skillId: string) => {
const skillDef = SKILLS_DEF[skillId.includes('_t') ? skillId.split('_t')[0] : skillId];
@@ -173,6 +151,9 @@ export function SkillsTab({ store }: SkillsTabProps) {
}
};
+ // Get available skill categories based on attunements
+ const availableCategories = getAvailableSkillCategories(store.attunements || {});
+
return (
{/* Upgrade Selection Dialog */}
@@ -183,7 +164,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
pendingSelections={pendingSelections.length > 0 ? pendingSelections : alreadySelected}
available={available}
alreadySelected={alreadySelected}
- onToggle={toggleUpgrade}
+ onToggle={handleUpgradeToggle}
onConfirm={handleConfirm}
onCancel={handleCancel}
onOpenChange={(open) => {
@@ -221,179 +202,31 @@ export function SkillsTab({ store }: SkillsTabProps) {
)}
- {/* Get available skill categories based on attunements */}
- {(() => {
- const availableCategories = getAvailableSkillCategories(store.attunements || {});
-
- return SKILL_CATEGORIES.filter((cat) => availableCategories.includes(cat.id)).map((cat) => {
- const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
- if (skillsInCat.length === 0) return null;
-
- const isCollapsed = collapsedCategories.has(cat.id);
-
- return (
-
- toggleCategory(cat.id)}>
-
-
- {cat.icon} {cat.name}
-
-
-
- {skillsInCat.length} skills
-
- {isCollapsed ? : }
-
-
-
- {!isCollapsed && (
-
-
- {skillsInCat.map(([id, def]) => {
- // GATE MANA CAPACITY SKILLS BY UNLOCKED ELEMENT
- if (def.cost?.type === 'element') {
- const element = store.elements[def.cost.element];
- if (!element?.unlocked) {
- return null; // Don't render this skill
- }
- }
- // Get tier info
- const currentTier = store.skillTiers?.[id] || 1;
- const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id;
- const tierMultiplier = getTierMultiplier(tieredSkillId);
-
- // Get the actual level from the tiered skill
- const level = store.skills[tieredSkillId] || store.skills[id] || 0;
- const maxed = level >= def.max;
-
- // Check if studying this skill
- const isStudying =
- (store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) &&
- store.currentStudyTarget?.type === 'skill';
-
- // Get tier name for display
- const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find((t) => t.tier === currentTier);
- const skillDisplayName = tierDef?.name || def.name;
-
- // Check prerequisites
- let prereqMet = true;
- if (def.req) {
- for (const [r, rl] of Object.entries(def.req)) {
- if ((store.skills[r] || 0) < rl) {
- prereqMet = false;
- break;
- }
- }
- }
-
- // Apply skill modifiers
- const costMult = getStudyCostMultiplier(store.skills);
- const speedMult = getStudySpeedMultiplier(store.skills);
- const studyEffects = getUnifiedEffects(store);
- const effectiveSpeedMult = speedMult * studyEffects.studySpeedMultiplier;
-
- // Study time scales with tier
- const tierStudyTime = def.studyTime * currentTier;
- const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
-
- // Cost scales with tier
- const baseCost = def.base * (level + 1) * currentTier;
- const cost = Math.floor(baseCost * costMult);
-
- // Additional cost (element mana)
- const additionalCost = def.cost;
-
- // Can start studying?
- let canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying;
-
- // Check additional cost (element mana)
- if (def.cost && def.cost.type === 'element') {
- const element = store.elements[def.cost.element];
- if (!element || element.current < def.cost.amount) {
- canStudy = false;
- }
- }
-
- // Check for milestone upgrades
- const milestoneInfo = hasMilestoneUpgrade(
- tieredSkillId,
- level,
- store.skillTiers || {},
- store.skillUpgrades
- );
-
- // Check for tier up
- const nextTierSkill = getNextTierSkill(tieredSkillId);
- const canTierUp = maxed && nextTierSkill;
-
- // Get selected upgrades
- const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
- const selectedL5 = selectedUpgrades.filter((u) => u.includes('_l5'));
- const selectedL10 = selectedUpgrades.filter((u) => u.includes('_l10'));
-
- // Check if insufficient mana for toast
- const hasInsufficientMana = !isStudying && !maxed && store.rawMana < cost;
-
- // Check for parallel study eligibility
- const isParallelStudy =
- store.parallelStudyTarget?.id === tieredSkillId &&
- store.parallelStudyTarget?.type === 'skill';
- const canParallelStudy =
- hasSpecial(studyEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
- store.currentStudyTarget &&
- store.currentStudyTarget.id !== tieredSkillId &&
- !isStudying;
-
- return (
- {
- if (store.currentStudyTarget?.id === tieredSkillId) {
- handleCancelStudy();
- }
- }}
- onUpgradeDialogOpen={(skillId, milestone) => {
- setUpgradeDialogSkill(skillId);
- setUpgradeDialogMilestone(milestone);
- setPendingSelections([]);
- }}
- onTierUp={(skillId) => store.tierUpSkill(skillId)}
- />
- );
- })}
-
-
- )}
-
- );
- });
- })()}
+ {/* Skill Categories */}
+ {SKILL_CATEGORIES.filter((cat) => availableCategories.includes(cat.id)).map((cat) => (
+
{
+ setUpgradeDialogSkill(skillId);
+ setUpgradeDialogMilestone(milestone);
+ setPendingSelections([]);
+ }}
+ onTierUp={(skillId) => store.tierUpSkill(skillId)}
+ pendingSelections={pendingSelections}
+ setPendingSelections={setPendingSelections}
+ />
+ ))}
);
}
diff --git a/src/lib/game/hooks/useSkillUpgradeSelection.ts b/src/lib/game/hooks/useSkillUpgradeSelection.ts
index 944b83a..41bd90d 100644
--- a/src/lib/game/hooks/useSkillUpgradeSelection.ts
+++ b/src/lib/game/hooks/useSkillUpgradeSelection.ts
@@ -3,6 +3,7 @@
import { useState, useMemo, useCallback, SetStateAction, Dispatch } from 'react';
import type { SkillUpgradeChoice } from '@/lib/game/types';
+import { getUpgradesForSkillAtMilestone } from '@/lib/game/skill-evolution';
export interface UseSkillUpgradeSelectionResult {
pendingSelections: string[];
@@ -12,6 +13,36 @@ export interface UseSkillUpgradeSelectionResult {
handleCancel: (onClose: () => void) => void;
}
+/**
+ * Check if skill has milestone available (also exported for use in SkillRow/CategorySkillsList)
+ */
+export function hasMilestoneUpgrade(
+ skillId: string,
+ level: number,
+ skillTiers: Record,
+ skillUpgrades: Record
+): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
+ // Check level 5 milestone
+ if (level >= 5) {
+ const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, skillTiers);
+ const selected5 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l5'));
+ if (upgrades5.length > 0 && selected5.length < 2) {
+ return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
+ }
+ }
+
+ // Check level 10 milestone
+ if (level >= 10) {
+ const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, skillTiers);
+ const selected10 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l10'));
+ if (upgrades10.length > 0 && selected10.length < 2) {
+ return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
+ }
+ }
+
+ return null;
+}
+
/**
* Hook for managing skill upgrade selection state in the SkillsTab milestone upgrade dialog.
* Manages pending selections across the dialog open/close cycle.
diff --git a/src/lib/game/types/index.ts b/src/lib/game/types/index.ts
index 2c7b1e6..842d37e 100644
--- a/src/lib/game/types/index.ts
+++ b/src/lib/game/types/index.ts
@@ -20,7 +20,8 @@ export type {
SkillTierDef,
SkillPerkChoice,
SkillUpgradeChoice,
- PrestigeDef
+ PrestigeDef,
+ SkillCost
} from './skills';
// Equipment types