Task 2: SpireTab Overhaul - add Climb the Spire button, implement Spire Mode with exit condition

This commit is contained in:
Refactoring Agent
2026-04-26 13:44:09 +02:00
parent 9f029d93e1
commit 50ce70efdd
6 changed files with 426 additions and 294 deletions
+124 -61
View File
@@ -1,86 +1,149 @@
'use client';
import { Button } from '@/components/ui/button';
import { Sparkles, Swords, BookOpen, Target, FlaskConical } from 'lucide-react';
import { Sparkles, Swords, BookOpen, Target, FlaskConical, Cog, Hammer } from 'lucide-react';
import type { GameAction } from '@/lib/game/types';
interface ActionButtonsProps {
currentAction: GameAction;
currentStudyTarget: { type: 'skill' | 'spell'; id: string; progress: number; required: number } | null;
designProgress: { progress: number; required: number } | null;
designProgress2: { progress: number; required: number } | null;
preparationProgress: { progress: number; required: number } | null;
applicationProgress: { progress: number; required: number } | null;
setAction: (action: GameAction) => void;
equipmentCraftingProgress: { progress: number; required: number } | null;
}
// Map action IDs to labels and icons
const ACTION_CONFIG: Record<string, { label: string; icon: typeof Sparkles; color: string }> = {
meditate: { label: 'Meditating', icon: Sparkles, color: 'text-blue-400' },
climb: { label: 'Climbing', icon: Swords, color: 'text-green-400' },
study: { label: 'Studying', icon: BookOpen, color: 'text-yellow-400' },
design: { label: 'Designing Enchantment', icon: Target, color: 'text-purple-400' },
prepare: { label: 'Preparing Equipment', icon: FlaskConical, color: 'text-purple-400' },
enchant: { label: 'Enchanting', icon: Sparkles, color: 'text-purple-400' },
craft: { label: 'Crafting Equipment', icon: Hammer, color: 'text-orange-400' },
convert: { label: 'Converting Mana', icon: Cog, color: 'text-cyan-400' },
};
function ProgressBar({ progress, required, label }: { progress: number; required: number; label?: string }) {
const percentage = Math.min(100, (progress / required) * 100);
return (
<div className="mt-1">
{label && <div className="text-xs text-gray-400 mb-0.5">{label}</div>}
<div className="w-full bg-gray-700 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
export function ActionButtons({
currentAction,
currentStudyTarget,
designProgress,
designProgress2,
preparationProgress,
applicationProgress,
setAction,
equipmentCraftingProgress,
}: ActionButtonsProps) {
const actions: { id: GameAction; label: string; icon: typeof Swords }[] = [
{ id: 'meditate', label: 'Meditate', icon: Sparkles },
{ id: 'climb', label: 'Climb', icon: Swords },
{ id: 'study', label: 'Study', icon: BookOpen },
];
const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' };
const Icon = config.icon;
const hasDesignProgress = designProgress !== null;
const hasPrepProgress = preparationProgress !== null;
const hasAppProgress = applicationProgress !== null;
// Calculate additional info for specific actions
const getActionDetails = () => {
switch (currentAction) {
case 'study':
if (currentStudyTarget) {
const progress = currentStudyTarget.progress;
const required = currentStudyTarget.required;
const percentage = Math.min(100, (progress / required) * 100);
return (
<ProgressBar
progress={progress}
required={required}
label={`${currentStudyTarget.type === 'skill' ? 'Skill' : 'Spell'}: ${percentage.toFixed(0)}%`}
/>
);
}
break;
case 'design':
if (designProgress) {
return (
<ProgressBar
progress={designProgress.progress}
required={designProgress.required}
label="Design progress"
/>
);
}
break;
case 'prepare':
if (preparationProgress) {
return (
<ProgressBar
progress={preparationProgress.progress}
required={preparationProgress.required}
label="Preparation progress"
/>
);
}
break;
case 'enchant':
if (applicationProgress) {
return (
<ProgressBar
progress={applicationProgress.progress}
required={applicationProgress.required}
label="Enchantment progress"
/>
);
}
break;
case 'craft':
if (equipmentCraftingProgress) {
return (
<ProgressBar
progress={equipmentCraftingProgress.progress}
required={equipmentCraftingProgress.required}
label="Crafting progress"
/>
);
}
break;
}
return null;
};
return (
<div className="space-y-2">
<div className="grid grid-cols-3 gap-2">
{actions.map(({ id, label, icon: Icon }) => (
<Button
key={id}
variant={currentAction === id ? 'default' : 'outline'}
size="sm"
className={`h-9 ${currentAction === id ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
onClick={() => setAction(id)}
>
<Icon className="w-4 h-4 mr-1" />
{label}
</Button>
))}
</div>
{/* Crafting actions row - shown when there's active crafting progress */}
{(hasDesignProgress || hasPrepProgress || hasAppProgress) && (
<div className="grid grid-cols-3 gap-2">
<Button
variant={currentAction === 'design' ? 'default' : 'outline'}
size="sm"
disabled={!hasDesignProgress}
className={`h-9 ${currentAction === 'design' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
onClick={() => hasDesignProgress && setAction('design')}
>
<Target className="w-4 h-4 mr-1" />
Design
</Button>
<Button
variant={currentAction === 'prepare' ? 'default' : 'outline'}
size="sm"
disabled={!hasPrepProgress}
className={`h-9 ${currentAction === 'prepare' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
onClick={() => hasPrepProgress && setAction('prepare')}
>
<FlaskConical className="w-4 h-4 mr-1" />
Prepare
</Button>
<Button
variant={currentAction === 'enchant' ? 'default' : 'outline'}
size="sm"
disabled={!hasAppProgress}
className={`h-9 ${currentAction === 'enchant' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
onClick={() => hasAppProgress && setAction('enchant')}
>
<Sparkles className="w-4 h-4 mr-1" />
Enchant
</Button>
<div className="bg-gray-800/50 rounded-lg p-3 border border-gray-700">
<div className="flex items-center gap-2">
<Icon className={`w-4 h-4 ${config.color}`} />
<span className="text-sm font-medium text-gray-200">Current Activity</span>
</div>
)}
<div className={`text-lg font-semibold mt-1 ${config.color}`}>
{config.label}
</div>
{getActionDetails()}
{/* Show second design slot if active */}
{designProgress2 && (
<div className="mt-2 pt-2 border-t border-gray-700">
<div className="flex items-center gap-2">
<Target className="w-3 h-3 text-purple-400" />
<span className="text-xs text-gray-400">Second Design Slot</span>
</div>
<ProgressBar
progress={designProgress2.progress}
required={designProgress2.required}
label="Design progress"
/>
</div>
)}
</div>
</div>
);
}
+63 -56
View File
@@ -19,9 +19,10 @@ import { getUnifiedEffects } from '@/lib/game/effects';
interface SpireTabProps {
store: GameStore;
simpleMode?: boolean; // When true, only show essential Spire info (for Spire Mode)
}
export function SpireTab({ store }: SpireTabProps) {
export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
@@ -48,7 +49,7 @@ export function SpireTab({ store }: SpireTabProps) {
return (
<TooltipProvider>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className={`grid gap-4 ${simpleMode ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'}`}>
{/* Current Floor Card */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
@@ -92,35 +93,39 @@ export function SpireTab({ store }: SpireTabProps) {
</div>
</div>
<Separator className="bg-gray-700" />
{/* Floor Navigation - Direction indicator only */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Direction</span>
<div className="flex gap-1">
<Badge variant={climbDirection === 'up' ? 'default' : 'outline'}
className={climbDirection === 'up' ? 'bg-green-600' : ''}>
<ChevronUp className="w-3 h-3 mr-1" />
Up
</Badge>
<Badge variant={climbDirection === 'down' ? 'default' : 'outline'}
className={climbDirection === 'down' ? 'bg-blue-600' : ''}>
<ChevronDown className="w-3 h-3 mr-1" />
Down
</Badge>
{!simpleMode && (
<>
<Separator className="bg-gray-700" />
{/* Floor Navigation - Direction indicator only */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Direction</span>
<div className="flex gap-1">
<Badge variant={climbDirection === 'up' ? 'default' : 'outline'}
className={climbDirection === 'up' ? 'bg-green-600' : ''}>
<ChevronUp className="w-3 h-3 mr-1" />
Up
</Badge>
<Badge variant={climbDirection === 'down' ? 'default' : 'outline'}
className={climbDirection === 'down' ? 'bg-blue-600' : ''}>
<ChevronDown className="w-3 h-3 mr-1" />
Down
</Badge>
</div>
</div>
{isFloorCleared && (
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
<RotateCcw className="w-3 h-3" />
Floor cleared! Advancing...
</div>
)}
</div>
</div>
{isFloorCleared && (
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
<RotateCcw className="w-3 h-3" />
Floor cleared! Advancing...
</div>
)}
</div>
<Separator className="bg-gray-700" />
<Separator className="bg-gray-700" />
</>
)}
<div className="text-sm text-gray-400">
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong>
@@ -205,8 +210,8 @@ export function SpireTab({ store }: SpireTabProps) {
</CardContent>
</Card>
{/* Summoned Golems Card */}
{store.golemancy.summonedGolems.length > 0 && (
{/* Summoned Golems Card - Always show in simple mode, conditional in normal mode */}
{(simpleMode || store.golemancy.summonedGolems.length > 0) && (
<Card className="bg-gray-900/80 border-amber-600/50">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
@@ -257,8 +262,8 @@ export function SpireTab({ store }: SpireTabProps) {
</Card>
)}
{/* Current Study (if any) */}
{store.currentStudyTarget && (
{/* Current Study (if any) - Only show in normal mode */}
{!simpleMode && store.currentStudyTarget && (
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
<CardContent className="pt-4 space-y-3">
<StudyProgress
@@ -299,8 +304,8 @@ export function SpireTab({ store }: SpireTabProps) {
</Card>
)}
{/* Crafting Progress (if any) */}
{(store.designProgress || store.preparationProgress || store.applicationProgress) && (
{/* Crafting Progress (if any) - Only show in normal mode */}
{!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && (
<Card className="bg-gray-900/80 border-cyan-600/50 lg:col-span-2">
<CardContent className="pt-4">
<CraftingProgress
@@ -319,26 +324,28 @@ export function SpireTab({ store }: SpireTabProps) {
</Card>
)}
{/* Activity Log */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-32">
<div className="space-y-1">
{store.log.slice(0, 20).map((entry, i) => (
<div
key={i}
className={`text-sm ${i === 0 ? 'text-gray-200' : 'text-gray-500'} italic`}
>
{entry}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
{/* Activity Log - Only show in normal mode */}
{!simpleMode && (
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-32">
<div className="space-y-1">
{store.log.slice(0, 20).map((entry, i) => (
<div
key={i}
className={`text-sm ${i === 0 ? 'text-gray-200' : 'text-gray-500'} italic`}
>
{entry}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</div>
</TooltipProvider>
);