Redesign skill system with upgrade trees and tier progression
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled
Major changes: - Created docs/skills.md with comprehensive skill system documentation - Rewrote skill-evolution.ts with new upgrade tree structure: - Upgrades organized in branching paths with prerequisites - Each choice can lead to upgraded versions at future milestones - Support for upgrade children and requirement chains - Added getBaseSkillId and generateTierSkillDef helper functions - Fixed getFloorElement to use FLOOR_ELEM_CYCLE.length - Updated test files to match current skill definitions - Removed tests for non-existent skills Skill system now supports: - Levels 1-10 for most skills, level 5 caps for specialized, level 1 for research - Tier up system: Tier N Level 1 = Tier N-1 Level 10 in power - Milestone upgrades at levels 5 and 10 with branching upgrade trees - Attunement requirements for skill access and tier up - Study costs and time for leveling
This commit is contained in:
126
src/components/game/shared/UpgradeDialog.tsx
Executable file
126
src/components/game/shared/UpgradeDialog.tsx
Executable file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useGameContext } from '../GameContext';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
|
||||
interface UpgradeDialogProps {
|
||||
skillId: string | null;
|
||||
milestone: 5 | 10;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function UpgradeDialog({ skillId, milestone, onClose }: UpgradeDialogProps) {
|
||||
const { store } = useGameContext();
|
||||
|
||||
const skillDef = skillId ? SKILLS_DEF[skillId] : null;
|
||||
const { available, selected: alreadySelected } = skillId
|
||||
? store.getSkillUpgradeChoices(skillId, milestone)
|
||||
: { available: [], selected: [] };
|
||||
|
||||
// Use local state for selections within this dialog session
|
||||
const [pendingSelections, setPendingSelections] = useState<string[]>(() => [...alreadySelected]);
|
||||
|
||||
const toggleUpgrade = (upgradeId: string) => {
|
||||
setPendingSelections((prev) => {
|
||||
if (prev.includes(upgradeId)) {
|
||||
return prev.filter((id) => id !== upgradeId);
|
||||
} else if (prev.length < 2) {
|
||||
return [...prev, upgradeId];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDone = () => {
|
||||
if (pendingSelections.length === 2 && skillId) {
|
||||
store.commitSkillUpgrades(skillId, pendingSelections);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setPendingSelections([...alreadySelected]);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render if no skill selected
|
||||
if (!skillId) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={!!skillId} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">Choose Upgrade - {skillDef?.name || skillId}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Level {milestone} Milestone - Select 2 upgrades ({pendingSelections.length}/2 chosen)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 mt-4">
|
||||
{available.map((upgrade) => {
|
||||
const isSelected = pendingSelections.includes(upgrade.id);
|
||||
const canToggle = pendingSelections.length < 2 || isSelected;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-amber-500 bg-amber-900/30'
|
||||
: canToggle
|
||||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
||||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (canToggle) {
|
||||
toggleUpgrade(upgrade.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
||||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||
{upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs text-green-400 mt-1">
|
||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs text-blue-400 mt-1">
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs text-cyan-400 mt-1">⚡ {upgrade.desc || 'Special effect'}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setPendingSelections([...alreadySelected]);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" onClick={handleDone} disabled={pendingSelections.length !== 2}>
|
||||
{pendingSelections.length < 2 ? `Select ${2 - pendingSelections.length} more` : 'Confirm'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user