d2d28887b1
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
- Refactored page.tsx (613→252 lines) with GameOverScreen and LeftPanel extracted - Refactored StatsTab.tsx (584→92 lines) with section components - Refactored SkillsTab.tsx (434→54 lines) with sub-components - Created modular structure for GameContext, LootInventory, and other components - All extracted components organized into feature directories
198 lines
8.0 KiB
TypeScript
198 lines
8.0 KiB
TypeScript
'use client';
|
||
|
||
import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
|
||
import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
|
||
import { getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||
import { formatStudyTime, hasMilestoneUpgrade, getSkillDisplayInfo } from './skills-utils';
|
||
|
||
interface SkillRowProps {
|
||
skillId: string;
|
||
onUpgradeClick: (skillId: string, milestone: 5 | 10) => void;
|
||
}
|
||
|
||
export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||
const store = useGameStore();
|
||
const { studySpeedMult, studyCostMult, hasParallelStudy } = useStudyStats();
|
||
|
||
const skillInfo = getSkillDisplayInfo(store, skillId);
|
||
const {
|
||
currentTier,
|
||
tieredSkillId,
|
||
tierMultiplier,
|
||
level,
|
||
maxed,
|
||
isStudying,
|
||
skillDisplayName,
|
||
prereqMet,
|
||
def
|
||
} = skillInfo;
|
||
|
||
// Apply skill modifiers
|
||
const studyEffects = computeEffects(store.skillUpgrades || {}, store.skillTiers || {});
|
||
const effectiveSpeedMult = studySpeedMult * studyEffects.studySpeedMultiplier;
|
||
|
||
const tierStudyTime = def.studyTime * currentTier;
|
||
const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
|
||
|
||
const baseCost = def.base * (level + 1) * currentTier;
|
||
const cost = Math.floor(baseCost * studyCostMult);
|
||
|
||
// Check if any study is in progress (prevent switching topics)
|
||
const isAnyStudyInProgress = store.currentAction === 'study' && store.currentStudyTarget;
|
||
// Can only study if: not maxed, prereqs met, has mana, and either no study in progress or already studying this skill
|
||
const canStudy = !maxed && prereqMet && store.rawMana >= cost && (!isAnyStudyInProgress || isStudying);
|
||
|
||
const milestoneInfo = hasMilestoneUpgrade(store, tieredSkillId, level);
|
||
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
||
const canTierUp = maxed && nextTierSkill;
|
||
|
||
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
||
const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5'));
|
||
const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10'));
|
||
|
||
return (
|
||
<div
|
||
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>
|
||
{currentTier > 1 && (
|
||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x)</Badge>
|
||
)}
|
||
{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}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}</div>
|
||
{!prereqMet && def.req && (
|
||
<div className="text-xs text-red-400 mt-1">
|
||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
||
</div>
|
||
)}
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
<span className={effectiveSpeedMult > 1 ? 'text-green-400' : ''}>
|
||
Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && <span className="text-xs ml-1">({Math.round(effectiveSpeedMult * 100)}% speed)</span>}
|
||
</span>
|
||
{' • '}
|
||
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
|
||
Cost: {fmt(cost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
|
||
</span>
|
||
</div>
|
||
|
||
{milestoneInfo && (
|
||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
||
</div>
|
||
)}
|
||
</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(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||
</div>
|
||
) : milestoneInfo ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-amber-600 hover:bg-amber-700"
|
||
onClick={() => {
|
||
onUpgradeClick(tieredSkillId, milestoneInfo.milestone);
|
||
}}
|
||
>
|
||
Choose Upgrades
|
||
</Button>
|
||
) : canTierUp ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-purple-600 hover:bg-purple-700"
|
||
onClick={() => store.tierUpSkill(tieredSkillId)}
|
||
>
|
||
⬆️ Tier Up
|
||
</Button>
|
||
) : maxed ? (
|
||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||
) : (
|
||
<div className="flex gap-1">
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
size="sm"
|
||
variant={canStudy ? 'default' : 'outline'}
|
||
disabled={!canStudy}
|
||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
||
>
|
||
Study ({fmt(cost)})
|
||
</Button>
|
||
</TooltipTrigger>
|
||
{!canStudy && isAnyStudyInProgress && !isStudying && (
|
||
<TooltipContent>
|
||
<p>Cannot switch topics while studying {SKILLS_DEF[store.currentStudyTarget?.id || '']?.name || 'another skill'}</p>
|
||
</TooltipContent>
|
||
)}
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
{/* Parallel Study button */}
|
||
{hasParallelStudy &&
|
||
store.currentStudyTarget &&
|
||
!store.parallelStudyTarget &&
|
||
store.currentStudyTarget.id !== tieredSkillId &&
|
||
canStudy && (
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
||
>
|
||
⚡
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
<p>Study in parallel (50% speed)</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|