refactor: extract components from SkillsTab.tsx to reduce below 400 lines
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m10s

This commit is contained in:
Refactoring Agent
2026-05-01 16:41:29 +02:00
parent 86683fe288
commit f0ab3ca3ce
8 changed files with 422 additions and 232 deletions
+44 -211
View File
@@ -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<string, number>,
skillUpgrades: Record<string, string[]>
): { 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<string | null>(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 (
<div className="space-y-4">
{/* 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) {
</Card>
)}
{/* 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 (
<Card key={cat.id} className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2 cursor-pointer" onClick={() => toggleCategory(cat.id)}>
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
<span>
{cat.icon} {cat.name}
</span>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{skillsInCat.length} skills
</Badge>
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</div>
</CardTitle>
</CardHeader>
{!isCollapsed && (
<CardContent>
<div className="space-y-2">
{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 (
<SkillRow
key={id}
skillId={tieredSkillId}
def={def}
level={level}
maxed={maxed}
isStudying={isStudying}
tierMultiplier={tierMultiplier}
skillDisplayName={skillDisplayName}
selectedUpgrades={selectedUpgrades}
selectedL5={selectedL5}
selectedL10={selectedL10}
prereqMet={prereqMet}
canStudy={canStudy}
isParallelStudy={isParallelStudy}
canParallelStudy={canParallelStudy}
canTierUp={canTierUp}
hasInsufficientMana={hasInsufficientMana}
currentStudyTarget={store.currentStudyTarget}
milestoneInfo={milestoneInfo}
upgradeEffects={upgradeEffects}
cost={cost}
additionalCost={additionalCost}
effectiveStudyTime={effectiveStudyTime}
costMult={costMult}
speedMult={speedMult}
onStudy={handleStartStudying}
onParallelStudy={handleParallelStudy}
onCancelStudy={() => {
if (store.currentStudyTarget?.id === tieredSkillId) {
handleCancelStudy();
}
}}
onUpgradeDialogOpen={(skillId, milestone) => {
setUpgradeDialogSkill(skillId);
setUpgradeDialogMilestone(milestone);
setPendingSelections([]);
}}
onTierUp={(skillId) => store.tierUpSkill(skillId)}
/>
);
})}
</div>
</CardContent>
)}
</Card>
);
});
})()}
{/* Skill Categories */}
{SKILL_CATEGORIES.filter((cat) => availableCategories.includes(cat.id)).map((cat) => (
<CategorySkillsList
key={cat.id}
category={cat}
availableCategories={availableCategories}
isCollapsed={collapsedCategories.has(cat.id)}
onToggleCategory={toggleCategory}
store={store}
studySpeedMult={studySpeedMult}
upgradeEffects={upgradeEffects}
currentStudyTarget={store.currentStudyTarget}
onStartStudying={handleStartStudying}
onParallelStudy={handleParallelStudy}
onCancelStudy={handleCancelStudy}
onOpenUpgradeDialog={(skillId, milestone) => {
setUpgradeDialogSkill(skillId);
setUpgradeDialogMilestone(milestone);
setPendingSelections([]);
}}
onTierUp={(skillId) => store.tierUpSkill(skillId)}
pendingSelections={pendingSelections}
setPendingSelections={setPendingSelections}
/>
))}
</div>
);
}