232 lines
7.8 KiB
TypeScript
232 lines
7.8 KiB
TypeScript
// ─── Skill Row Component ───────────────────────────────────────────────────
|
||
// Individual skill row for the Skills tab - extracted from SkillsTab for modularity
|
||
|
||
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||
import { ChevronRight } from 'lucide-react';
|
||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||
import { ELEMENTS } from '@/lib/game/constants';
|
||
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;
|
||
|
||
interface SkillRowProps {
|
||
skillId: string;
|
||
def: {
|
||
name: string;
|
||
desc: string;
|
||
cat: string;
|
||
max: number;
|
||
studyTime: number;
|
||
base: number;
|
||
cost?: { type: 'element'; element: string; amount: number } | { type: 'raw'; amount: number };
|
||
req?: Record<string, number>;
|
||
};
|
||
level: number;
|
||
maxed: boolean;
|
||
isStudying: boolean;
|
||
tierMultiplier: number;
|
||
skillDisplayName: string;
|
||
selectedUpgrades: string[];
|
||
selectedL5: string[];
|
||
selectedL10: string[];
|
||
prereqMet: boolean;
|
||
canStudy: boolean;
|
||
isParallelStudy: boolean;
|
||
canParallelStudy: boolean;
|
||
canTierUp: boolean;
|
||
hasInsufficientMana: boolean;
|
||
currentStudyTarget: StudyTarget;
|
||
milestoneInfo: { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null;
|
||
upgradeEffects: ComputedEffects;
|
||
// Costs and times
|
||
cost: number;
|
||
additionalCost?: { type: 'element'; element: string; amount: number };
|
||
effectiveStudyTime: number;
|
||
costMult: number;
|
||
speedMult: number;
|
||
// Callbacks
|
||
onStudy: (skillId: string) => void;
|
||
onParallelStudy: (skillId: string) => void;
|
||
onCancelStudy: (skillId: string) => void;
|
||
onUpgradeDialogOpen: (skillId: string, milestone: 5 | 10) => void;
|
||
onTierUp: (skillId: string) => void;
|
||
onShowToast: (type: 'info' | 'error', title: string, description: string) => void;
|
||
tierUpLabel?: string;
|
||
}
|
||
|
||
export function SkillRow(props: SkillRowProps) {
|
||
const {
|
||
skillId,
|
||
def,
|
||
level,
|
||
maxed,
|
||
isStudying,
|
||
tierMultiplier,
|
||
skillDisplayName,
|
||
selectedUpgrades,
|
||
selectedL5,
|
||
selectedL10,
|
||
prereqMet,
|
||
canStudy,
|
||
isParallelStudy,
|
||
canParallelStudy,
|
||
canTierUp,
|
||
hasInsufficientMana,
|
||
currentStudyTarget,
|
||
milestoneInfo,
|
||
upgradeEffects,
|
||
cost,
|
||
additionalCost,
|
||
effectiveStudyTime,
|
||
costMult,
|
||
speedMult,
|
||
onStudy,
|
||
onParallelStudy,
|
||
onCancelStudy,
|
||
onUpgradeDialogOpen,
|
||
onTierUp,
|
||
} = props;
|
||
|
||
return (
|
||
<div
|
||
key={skillId}
|
||
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
||
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||
'border-gray-700 bg-gray-800/30'
|
||
}`}
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
||
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
||
{selectedUpgrades.length > 0 && (
|
||
<div className="flex gap-1">
|
||
{selectedL5.length > 0 && (
|
||
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
||
)}
|
||
{selectedL10.length > 0 && (
|
||
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="text-xs text-gray-400 italic">{def.desc}{level > 0 && tierMultiplier !== 1 && ` (Tier ${tierMultiplier}x effect)`}</div>
|
||
{!prereqMet && def.req && (
|
||
<div className="text-xs text-red-400 mt-1">
|
||
Requires: {Object.entries(def.req).map(([r, rl]) => `${r} Lv.${rl}`).join(', ')}
|
||
</div>
|
||
)}
|
||
<SkillMultipliers
|
||
effectiveStudyTime={effectiveStudyTime}
|
||
speedMult={speedMult}
|
||
costMult={costMult}
|
||
cost={cost}
|
||
additionalCost={additionalCost}
|
||
/>
|
||
|
||
{hasInsufficientMana && (
|
||
<div className="text-xs text-red-400 mt-1">
|
||
Insufficient mana! Need {cost} mana to study.
|
||
</div>
|
||
)}
|
||
|
||
<MilestoneProgress milestoneInfo={milestoneInfo} />
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||
{/* Level dots */}
|
||
<div className="flex gap-1 shrink-0">
|
||
{Array.from({ length: def.max }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className={`w-2 h-2 rounded-full border ${
|
||
i < level ? 'bg-purple-500 border-purple-400' :
|
||
i === 4 || i === 9 ? 'border-amber-500' :
|
||
'border-gray-600'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{isStudying ? (
|
||
<div className="text-xs text-purple-400">
|
||
{formatStudyTime(currentStudyTarget?.progress || 0)}/{formatStudyTime(def.studyTime * (level > 1 ? level : 1))}
|
||
</div>
|
||
) : milestoneInfo ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-amber-600 hover:bg-amber-700"
|
||
onClick={() => onUpgradeDialogOpen(skillId, milestoneInfo.milestone)}
|
||
>
|
||
Choose Upgrades
|
||
</Button>
|
||
) : canTierUp ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-purple-600 hover:bg-purple-700"
|
||
onClick={() => onTierUp(skillId)}
|
||
>
|
||
⬆️ Tier Up
|
||
</Button>
|
||
) : maxed ? (
|
||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||
) : (
|
||
<div className="flex gap-1">
|
||
<Button
|
||
size="sm"
|
||
variant={canStudy ? 'default' : 'outline'}
|
||
disabled={!canStudy}
|
||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||
onClick={() => {
|
||
if (cost > 0) {
|
||
onStudy(skillId);
|
||
}
|
||
}}
|
||
>
|
||
Study ({cost}{additionalCost && additionalCost.type === 'element' && ` + ${additionalCost.amount} ${ELEMENTS[additionalCost.element]?.sym}`}
|
||
</Button>
|
||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||
currentStudyTarget &&
|
||
!isParallelStudy &&
|
||
canParallelStudy &&
|
||
canStudy && (
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||
onClick={() => {
|
||
if (cost > 0) {
|
||
onParallelStudy(skillId);
|
||
}
|
||
}}
|
||
>
|
||
⚡
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
<p>Study in parallel (50% speed)</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|