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:
@@ -1,23 +1,43 @@
|
||||
// ─── Skills Tab ────────────────────────────
|
||||
// SkillsTab - Displays all skills organized by category
|
||||
// Refactored: uses SkillRow component for per-skill rendering (reduced from 469 lines)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SKILLS_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import {
|
||||
SKILLS_DEF,
|
||||
SKILL_CATEGORIES,
|
||||
getStudySpeedMultiplier,
|
||||
getStudyCostMultiplier,
|
||||
} from '@/lib/game/constants';
|
||||
import {
|
||||
SKILL_EVOLUTION_PATHS,
|
||||
getUpgradesForSkillAtMilestone,
|
||||
getNextTierSkill,
|
||||
getTierMultiplier,
|
||||
} from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import type { SkillUpgradeChoice, GameStore } 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';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { StudyProgress } from './StudyProgress';
|
||||
import { UpgradeDialog } from './UpgradeDialog';
|
||||
import { ConfirmDialog } from '@/components/game/ConfirmDialog';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { SkillRow } from './SkillRow';
|
||||
import { useSkillUpgradeSelection } from '@/lib/game/hooks/useSkillUpgradeSelection';
|
||||
|
||||
export interface SkillsTabProps {
|
||||
store: GameStore;
|
||||
@@ -25,33 +45,33 @@ export interface SkillsTabProps {
|
||||
|
||||
// Check if skill has milestone available
|
||||
function hasMilestoneUpgrade(
|
||||
skillId: string,
|
||||
level: number,
|
||||
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'));
|
||||
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'));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -59,16 +79,27 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
const showToast = useGameToast();
|
||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||||
const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState<string[]>([]);
|
||||
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
|
||||
const [cancelStudyConfirm, setCancelStudyConfirm] = useState<{ skillId: string; skillName: string } | null>(null);
|
||||
|
||||
const [cancelStudyConfirm, setCancelStudyConfirm] = useState<{
|
||||
skillId: string;
|
||||
skillName: string;
|
||||
} | null>(null);
|
||||
|
||||
const studySpeedMult = getStudySpeedMultiplier(store.skills);
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
|
||||
|
||||
// Upgrade selection hook
|
||||
const {
|
||||
pendingSelections,
|
||||
setPendingSelections,
|
||||
toggleUpgrade,
|
||||
handleConfirm: hookHandleConfirm,
|
||||
handleCancel: hookHandleCancel,
|
||||
} = useSkillUpgradeSelection();
|
||||
|
||||
// Toggle category collapse
|
||||
const toggleCategory = (categoryId: string) => {
|
||||
setCollapsedCategories(prev => {
|
||||
setCollapsedCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(categoryId)) {
|
||||
newSet.delete(categoryId);
|
||||
@@ -78,39 +109,30 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Get upgrade choices for dialog
|
||||
const getUpgradeChoices = () => {
|
||||
if (!upgradeDialogSkill) return { available: [] as SkillUpgradeChoice[], selected: [] as string[] };
|
||||
if (!upgradeDialogSkill)
|
||||
return { available: [] as SkillUpgradeChoice[], selected: [] as string[] };
|
||||
return store.getSkillUpgradeChoices(upgradeDialogSkill, upgradeDialogMilestone);
|
||||
};
|
||||
|
||||
|
||||
const { available, selected: alreadySelected } = getUpgradeChoices();
|
||||
|
||||
// Toggle selection
|
||||
const toggleUpgrade = (upgradeId: string) => {
|
||||
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
||||
if (currentSelections.includes(upgradeId)) {
|
||||
setPendingUpgradeSelections(currentSelections.filter(id => id !== upgradeId));
|
||||
} else if (currentSelections.length < 2) {
|
||||
setPendingUpgradeSelections([...currentSelections, upgradeId]);
|
||||
}
|
||||
};
|
||||
|
||||
// Commit selections and close
|
||||
|
||||
// Handle upgrade dialog confirm
|
||||
const handleConfirm = () => {
|
||||
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
||||
if (currentSelections.length === 2 && upgradeDialogSkill) {
|
||||
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
|
||||
}
|
||||
setPendingUpgradeSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
hookHandleConfirm(
|
||||
upgradeDialogSkill,
|
||||
upgradeDialogMilestone,
|
||||
(skillId, selections, milestone) =>
|
||||
store.commitSkillUpgrades(skillId, selections, milestone),
|
||||
() => setUpgradeDialogSkill(null)
|
||||
);
|
||||
};
|
||||
|
||||
// Cancel and close
|
||||
|
||||
// Handle upgrade dialog cancel
|
||||
const handleCancel = () => {
|
||||
setPendingUpgradeSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
hookHandleCancel(() => setUpgradeDialogSkill(null));
|
||||
};
|
||||
|
||||
// Handle study start with toast
|
||||
@@ -132,9 +154,9 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
const currentTarget = store.currentStudyTarget;
|
||||
if (currentTarget?.type === 'skill') {
|
||||
const skillDef = SKILLS_DEF[currentTarget.id.includes('_t') ? currentTarget.id.split('_t')[0] : currentTarget.id];
|
||||
setCancelStudyConfirm({
|
||||
skillId: currentTarget.id,
|
||||
skillName: skillDef?.name || 'Unknown Skill'
|
||||
setCancelStudyConfirm({
|
||||
skillId: currentTarget.id,
|
||||
skillName: skillDef?.name || 'Unknown Skill',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -142,11 +164,15 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
const confirmCancelStudy = () => {
|
||||
if (cancelStudyConfirm) {
|
||||
store.cancelStudy();
|
||||
showToast('warning', 'Study Cancelled', `${cancelStudyConfirm.skillName} study cancelled. Progress will be partially saved based on your Knowledge Retention skill.`);
|
||||
showToast(
|
||||
'warning',
|
||||
'Study Cancelled',
|
||||
`${cancelStudyConfirm.skillName} study cancelled. Progress will be partially saved based on your Knowledge Retention skill.`
|
||||
);
|
||||
setCancelStudyConfirm(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Upgrade Selection Dialog */}
|
||||
@@ -154,7 +180,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
open={!!upgradeDialogSkill}
|
||||
skillId={upgradeDialogSkill}
|
||||
milestone={upgradeDialogMilestone}
|
||||
pendingSelections={pendingUpgradeSelections}
|
||||
pendingSelections={pendingSelections.length > 0 ? pendingSelections : alreadySelected}
|
||||
available={available}
|
||||
alreadySelected={alreadySelected}
|
||||
onToggle={toggleUpgrade}
|
||||
@@ -162,12 +188,12 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setPendingUpgradeSelections([]);
|
||||
setPendingSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{/* Cancel Study Confirmation Dialog */}
|
||||
{cancelStudyConfirm && (
|
||||
<ConfirmDialog
|
||||
@@ -180,7 +206,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
onConfirm={confirmCancelStudy}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Current Study Progress */}
|
||||
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
@@ -194,269 +220,181 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
</CardContent>
|
||||
</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]) => {
|
||||
// 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;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
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={costMult < 1 ? 'text-green-400' : ''}>
|
||||
Cost: {fmt(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 {fmt(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(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||||
</div>
|
||||
) : milestoneInfo ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
onClick={() => {
|
||||
setUpgradeDialogSkill(tieredSkillId);
|
||||
setUpgradeDialogMilestone(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">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canStudy ? 'default' : 'outline'}
|
||||
disabled={!canStudy}
|
||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||
onClick={() => {
|
||||
if (store.rawMana < cost) {
|
||||
const deficit = cost - store.rawMana;
|
||||
showToast('error', 'Insufficient Mana', `Need ${fmt(deficit)} more mana to study ${skillDisplayName}`);
|
||||
return;
|
||||
}
|
||||
handleStartStudying(tieredSkillId);
|
||||
}}
|
||||
>
|
||||
Study ({fmt(cost)}{additionalCost && additionalCost.type === 'element' && ` + ${additionalCost.amount} ${ELEMENTS[additionalCost.element]?.sym}`}
|
||||
</Button>
|
||||
{/* Parallel Study button */}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||||
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={() => {
|
||||
if (store.rawMana < cost) {
|
||||
const deficit = cost - store.rawMana;
|
||||
showToast('error', 'Insufficient Mana', `Need ${fmt(deficit)} more mana for parallel study`);
|
||||
return;
|
||||
}
|
||||
handleParallelStudy(tieredSkillId);
|
||||
}}
|
||||
>
|
||||
⚡
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Study in parallel (50% speed)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
// 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>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkillsTab.displayName = "SkillsTab";
|
||||
|
||||
Reference in New Issue
Block a user