feat: add prestige system and skill upgrades with comprehensive documentation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
// ─── 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 { 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';
|
||||
|
||||
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>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<span className={speedMult > 1 ? 'text-green-400' : ''}>
|
||||
Study: {formatStudyTime(effectiveStudyTime)}{speedMult > 1 && <span className="text-xs ml-1">({Math.round(speedMult * 100)}% speed)</span>}
|
||||
</span>
|
||||
{' • '}
|
||||
<span className={costMult < 1 ? 'text-green-400' : ''}>
|
||||
Cost: {cost} mana{costMult < 1 && <span className="text-xs ml-1">({Math.round(costMult * 100)}% cost)</span>}
|
||||
{additionalCost && additionalCost.type === 'element' && (
|
||||
<span className="ml-2" style={{ color: ELEMENTS[additionalCost.element]?.color }}>
|
||||
+ {additionalCost.amount} {ELEMENTS[additionalCost.element]?.sym} {additionalCost.element}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hasInsufficientMana && (
|
||||
<div className="text-xs text-red-400 mt-1">
|
||||
Insufficient mana! Need {cost} mana to study.
|
||||
</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(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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user