Compare commits

...

7 Commits

Author SHA1 Message Date
Refactoring Agent 563e41dbe3 WIP: Task 1 ActionButtons Rework - investigating approach
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m47s
2026-04-26 17:43:58 +02:00
Refactoring Agent c2dd846f63 Task 2: System Integrity - Fix Show Component Names for all components 2026-04-26 16:22:01 +02:00
Refactoring Agent c8baea4346 Task 2: Crafting - disable Prepare for non-enchanted items, limit Design to owned gear types 2026-04-26 15:42:32 +02:00
Refactoring Agent 9bf6e911f4 Task 2: Fix Combat UI Casting Bar progress animation 2026-04-26 15:16:19 +02:00
Refactoring Agent 50ce70efdd Task 2: SpireTab Overhaul - add Climb the Spire button, implement Spire Mode with exit condition 2026-04-26 13:44:45 +02:00
Refactoring Agent 9f029d93e1 Task 2: DebugTab Update - add Invoker Debugging Buttons for Pacts 2026-04-26 12:52:48 +02:00
Refactoring Agent 229cb16c5d Task 2: Research Locking - prevent switching topics while study in progress 2026-04-26 10:56:39 +02:00
55 changed files with 1059 additions and 315 deletions
+26 -13
View File
@@ -1,7 +1,7 @@
# Task 2 Progress Tracking
**Last Updated**: 2026-04-26 10:35:00
**Current Status**: In Progress
**Last Updated**: 2026-04-26 16:00:00
**Current Status**: Nearly Complete (11/12 tasks done)
## Completed Tasks
- [2026-04-25] Task 9: Remove 'Transference' mana from LootTab essence list ✓
@@ -9,26 +9,39 @@
- [2026-04-25] Task 4: Equipment System - 2-Handed Weapons ✓
- [2026-04-25] Task 12: StatsTab - Lock Fire/Water/Air/Earth at start ✓
- [2026-04-26] Task 7: CRITICAL BUG FIX - Mana Well 'Deep Basin' upgrade ✓
- [2026-04-26] Task 2: Research Locking - Prevent switching topics while study in progress ✓
- [2026-04-26] Task 5: DebugTab Update - Invoker Debugging Buttons ✓
- [2026-04-26] Task 3: SpireTab Overhaul - Implement Spire Mode with exit condition ✓
- [2026-04-26] Task 8: Bug Fix: Combat UI - Casting Bar progress animation ✓
- [2026-04-26] Task 10: Bug Fix: Crafting - Disable Prepare for enchanted items; limit Design to owned gear ✓
- [2026-04-26] Task 6: System Integrity - Fix 'Show Component Names' ✓
## In Progress Tasks
None currently
## Pending Tasks
1. Task 1: ActionButtons Rework [FAILED - sub-agent context too long]
2. Task 2: Research Locking - SkillsTab [Pending]
3. Task 3: SpireTab Overhaul [FAILED - sub-agent context too long]
4. Task 5: DebugTab Update - Invoker Debugging Buttons [Pending]
5. Task 6: System Integrity - Fix 'Show Component Names' [In Progress - investigating]
6. Task 8: Bug Fix: Combat UI - Casting Bar animation [FAILED - sub-agent context too long]
7. Task 10: Bug Fix: Crafting - Prepare/Design limits [Pending]
## Remaining Tasks (1/12)
1. Task 1: ActionButtons Rework - **BLOCKED** (sub-agent context length error: 2.4M tokens > 262k limit due to conversation history)
## Blocked Tasks
- Task 1, 3, 8: Sub-agent attempts failed due to context length limits (603k+ tokens)
- Task 1: Sub-agent fails due to framework bug (entire 2.4M token conversation history is passed to sub-agent, exceeding 262k limit)
## Commit History
## Commit History (Task 2)
- 65b0f96: Remove Transference from LootTab, delete Ascension skills
- 7c05bea: Update task2 progress
- 5e0bee8: Equipment System - 2-handed weapons, staves block offhand
- 2355be6: StatsTab - Lock Fire/Water/Air/Earth at start
- f61ed00: FIX: Skill perks multiplier values (Deep Basin + others)
- a6ce36b: WIP: Task 2 progress - investigating Show Component Names debug option
- a6ce36b: WIP: Task 2 progress - investigating Show Component Names
- 4193718: WIP: Task 2 - completed 5/12 tasks, investigating remaining
- fc9e4c8: Add context files for Task 2 sub-agents
- 229cb16: Task 2: Research Locking - prevent switching topics while study in progress
- 9f029d9: Task 2: DebugTab Update - add Invoker Debugging Buttons for Pacts
- 50ce70e: Task 2: SpireTab Overhaul - add Climb the Spire button, implement Spire Mode
- 9bf6e91: Task 2: Fix Combat UI Casting Bar progress animation
- c8baea4: Task 2: Crafting - disable Prepare for enchanted items, limit Design to owned gear types
- c2dd846: Task 2: System Integrity - Fix Show Component Names for all components
## Summary
**11/12 tasks completed successfully!** Only Task 1 (ActionButtons Rework) remains but is blocked due to a framework bug where the entire 2.4M token conversation history is passed to sub-agents, exceeding the 262k token limit.
**All completed work has been committed and pushed to gitea (master branch).**
+178 -134
View File
@@ -12,7 +12,7 @@ import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { RotateCcw } from 'lucide-react';
import { RotateCcw, Mountain, ChevronDown } from 'lucide-react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { DebugName } from '@/lib/game/debug-context';
// Non-tab component imports
@@ -181,7 +181,7 @@ export default function ManaLoopGame() {
<TimeDisplay
day={store.day}
hour={store.hour}
isPaused={store.isPaused}
isPaused={store.paused}
togglePause={store.togglePause}
/>
</div>
@@ -207,16 +207,34 @@ export default function ManaLoopGame() {
/>
</DebugName>
{/* Action Buttons */}
<DebugName name="ActionButtons">
<ActionButtons
currentAction={store.currentAction}
designProgress={store.designProgress}
preparationProgress={store.preparationProgress}
applicationProgress={store.applicationProgress}
setAction={store.setAction}
/>
</DebugName>
{/* Climb the Spire Button - only show when not in Spire Mode */}
{!store.spireMode && (
<DebugName name="ClimbSpireButton">
<Button
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
size="lg"
onClick={() => store.enterSpireMode()}
>
<Mountain className="w-5 h-5 mr-2" />
Climb the Spire
</Button>
</DebugName>
)}
{/* Action Buttons - only show when not in Spire Mode */}
{!store.spireMode && (
<DebugName name="ActionButtons">
<ActionButtons
currentAction={store.currentAction}
currentStudyTarget={store.currentStudyTarget}
designProgress={store.designProgress}
designProgress2={store.designProgress2}
preparationProgress={store.preparationProgress}
applicationProgress={store.applicationProgress}
equipmentCraftingProgress={store.equipmentCraftingProgress}
/>
</DebugName>
)}
{/* Calendar */}
<DebugName name="CalendarDisplay">
@@ -230,140 +248,166 @@ export default function ManaLoopGame() {
{/* Loot and Achievements moved to tabs */}
</div>
{/* Right Panel - Tabs */}
<div className="flex-1 min-w-0">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
<TabsTrigger value="spire" className="text-xs px-2 py-1"> Spire</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attune</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡 Gear</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
<TabsTrigger value="debug" className="text-xs px-2 py-1">🔧 Debug</TabsTrigger>
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
</TabsList>
{/* Right Panel - Conditional rendering based on Spire Mode */}
{store.spireMode ? (
/* Spire Mode - Simplified UI */
<div className="flex-1 min-w-0 space-y-4">
<DebugName name="SpireModeUI">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold game-title text-amber-400">
🏔 Spire Mode
</h2>
<Button
variant="outline"
className="border-blue-600/50 text-blue-400 hover:bg-blue-900/20"
onClick={() => store.exitSpireMode()}
>
<ChevronDown className="w-4 h-4 mr-2" />
Climb Down
</Button>
</div>
<Suspense fallback={<TabLoadingFallback />}>
<SpireTab store={store} simpleMode={true} />
</Suspense>
</DebugName>
</div>
) : (
/* Normal Mode - Tabs */
<div className="flex-1 min-w-0">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
<TabsTrigger value="spire" className="text-xs px-2 py-1"> Spire</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attune</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡 Gear</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
<TabsTrigger value="debug" className="text-xs px-2 py-1">🔧 Debug</TabsTrigger>
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
</TabsList>
<TabsContent value="spire">
<DebugName name="SpireTab">
<Suspense fallback={<TabLoadingFallback />}>
<SpireTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="spire">
<DebugName name="SpireTab">
<Suspense fallback={<TabLoadingFallback />}>
<SpireTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="attunements">
<DebugName name="AttunementsTab">
<Suspense fallback={<TabLoadingFallback />}>
<AttunementsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="attunements">
<DebugName name="AttunementsTab">
<Suspense fallback={<TabLoadingFallback />}>
<AttunementsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="golemancy">
<DebugName name="GolemancyTab">
<Suspense fallback={<TabLoadingFallback />}>
<GolemancyTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="golemancy">
<DebugName name="GolemancyTab">
<Suspense fallback={<TabLoadingFallback />}>
<GolemancyTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="skills">
<DebugName name="SkillsTab">
<Suspense fallback={<TabLoadingFallback />}>
<SkillsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="skills">
<DebugName name="SkillsTab">
<Suspense fallback={<TabLoadingFallback />}>
<SkillsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="spells">
<DebugName name="SpellsTab">
<Suspense fallback={<TabLoadingFallback />}>
<SpellsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="spells">
<DebugName name="SpellsTab">
<Suspense fallback={<TabLoadingFallback />}>
<SpellsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="equipment">
<DebugName name="EquipmentTab">
<Suspense fallback={<TabLoadingFallback />}>
<EquipmentTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="equipment">
<DebugName name="EquipmentTab">
<Suspense fallback={<TabLoadingFallback />}>
<EquipmentTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="crafting">
<DebugName name="CraftingTab">
<Suspense fallback={<TabLoadingFallback />}>
<CraftingTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="loot">
<DebugName name="LootTab">
<Suspense fallback={<TabLoadingFallback />}>
<LootTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="crafting">
<DebugName name="CraftingTab">
<Suspense fallback={<TabLoadingFallback />}>
<CraftingTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="loot">
<DebugName name="LootTab">
<Suspense fallback={<TabLoadingFallback />}>
<LootTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="achievements">
<DebugName name="AchievementsTab">
<Suspense fallback={<TabLoadingFallback />}>
<AchievementsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="achievements">
<DebugName name="AchievementsTab">
<Suspense fallback={<TabLoadingFallback />}>
<AchievementsTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="lab">
<DebugName name="LabTab">
<Suspense fallback={<TabLoadingFallback />}>
<LabTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="lab">
<DebugName name="LabTab">
<Suspense fallback={<TabLoadingFallback />}>
<LabTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="stats">
<DebugName name="StatsTab">
<Suspense fallback={<TabLoadingFallback />}>
<StatsTab
store={store}
upgradeEffects={upgradeEffects}
maxMana={maxMana}
baseRegen={baseRegen}
clickMana={clickMana}
meditationMultiplier={meditationMultiplier}
effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus}
studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult}
/>
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="stats">
<DebugName name="StatsTab">
<Suspense fallback={<TabLoadingFallback />}>
<StatsTab
store={store}
upgradeEffects={upgradeEffects}
maxMana={maxMana}
baseRegen={baseRegen}
clickMana={clickMana}
meditationMultiplier={meditationMultiplier}
effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus}
studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult}
/>
</Suspense>
</DebugName>
</TabsContent>
<TabsContent value="grimoire">
<DebugName name="GrimoireTab">
{renderGrimoireTab()}
</DebugName>
</TabsContent>
<TabsContent value="grimoire">
<DebugName name="GrimoireTab">
{renderGrimoireTab()}
</DebugName>
</TabsContent>
<TabsContent value="debug">
<DebugName name="DebugTab">
<Suspense fallback={<TabLoadingFallback />}>
<DebugTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
</Tabs>
</div>
<TabsContent value="debug">
<DebugName name="DebugTab">
<Suspense fallback={<TabLoadingFallback />}>
<DebugTab store={store} />
</Suspense>
</DebugName>
</TabsContent>
</Tabs>
</div>
)}
</main>
</div>
</TooltipProvider>
@@ -171,3 +171,5 @@ export function AchievementsDisplay({ achievements, gameState }: AchievementsPro
</Card>
);
}
AchievementsDisplay.displayName = "AchievementsDisplay";
+127 -61
View File
@@ -1,86 +1,152 @@
'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>
);
}
ActionButtons.displayName = "ActionButtons";
ProgressBar.displayName = "ProgressBar";
+152
View File
@@ -0,0 +1,152 @@
'use client';
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;
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,
equipmentCraftingProgress,
}: ActionButtonsProps) {
const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' };
const Icon = config.icon;
// 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="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>
);
}
ActionButtons.displayName = "ActionButtons";
ProgressBar.displayName = "ProgressBar";
+3
View File
@@ -48,3 +48,6 @@ export function CalendarDisplay({ day }: CalendarDisplayProps) {
</div>
);
}
CalendarDisplay.displayName = "CalendarDisplay";
CalendarDisplay.displayName = "CalendarDisplay";
+2
View File
@@ -159,3 +159,5 @@ export function CraftingProgress({
</div>
) : null;
}
CraftingProgress.displayName = "CraftingProgress";
+2
View File
@@ -422,5 +422,7 @@ export function useGameContext() {
return context;
}
GameProvider.displayName = "GameProvider";
// Re-export useGameLoop for convenience
export { useGameLoop };
+2
View File
@@ -169,3 +169,5 @@ export function LabTab() {
</div>
);
}
LabTab.displayName = "LabTab";
+2
View File
@@ -459,3 +459,5 @@ export function LootInventoryDisplay({
</>
);
}
LootInventoryDisplay.displayName = "LootInventoryDisplay";
+2
View File
@@ -121,3 +121,5 @@ export function ManaDisplay({
</Card>
);
}
ManaDisplay.displayName = "ManaDisplay";
+26 -10
View File
@@ -267,7 +267,10 @@ export function SkillsTab() {
const baseCost = def.base * (level + 1) * currentTier;
const cost = Math.floor(baseCost * studyCostMult);
const canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying;
// 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(tieredSkillId, level);
const nextTierSkill = getNextTierSkill(tieredSkillId);
@@ -369,15 +372,26 @@ export function SkillsTab() {
<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={() => store.startStudyingSkill(tieredSkillId)}
>
Study ({fmt(cost)})
</Button>
<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 &&
@@ -416,3 +430,5 @@ export function SkillsTab() {
</div>
);
}
SkillsTab.displayName = "SkillsTab";
+2
View File
@@ -164,3 +164,5 @@ export function SpellsTab() {
</div>
);
}
SpellsTab.displayName = "SpellsTab";
+2
View File
@@ -318,3 +318,5 @@ export function SpireTab() {
</div>
);
}
SpireTab.displayName = "SpireTab";
+2
View File
@@ -580,3 +580,5 @@ export function StatsTab() {
</div>
);
}
StatsTab.displayName = "StatsTab";
+2
View File
@@ -55,3 +55,5 @@ export function StudyProgress({
</div>
);
}
StudyProgress.displayName = "StudyProgress";
+2
View File
@@ -49,3 +49,5 @@ export function TimeDisplay({
</div>
);
}
TimeDisplay.displayName = "TimeDisplay";
+2
View File
@@ -113,3 +113,5 @@ export function UpgradeDialog({
</Dialog>
);
}
UpgradeDialog.displayName = "UpgradeDialog";
@@ -209,3 +209,5 @@ export function EnchantmentApplier({
</div>
);
}
EnchantmentApplier.displayName = "EnchantmentApplier";
@@ -11,6 +11,7 @@ import { Wand2, Scroll, Trash2, Plus, Minus } from 'lucide-react';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import { CRAFTING_RECIPES } from '@/lib/game/data/crafting-recipes';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import { fmt, type GameStore } from '@/lib/game/store';
@@ -138,6 +139,34 @@ export function EnchantmentDesigner({
);
};
// Get equipment types that the player has blueprints for
const getOwnedEquipmentTypes = () => {
const ownedBlueprints = store.lootInventory.blueprints || [];
// Map blueprint IDs to equipment type IDs
const ownedEquipmentTypeIds = new Set<string>();
for (const blueprintId of ownedBlueprints) {
const recipe = CRAFTING_RECIPES[blueprintId];
if (recipe) {
ownedEquipmentTypeIds.add(recipe.equipmentTypeId);
}
}
// Also include the starting equipment types (basicStaff, civilianShirt, civilianShoes)
// These are the types the player starts with, so they should be able to design for them
ownedEquipmentTypeIds.add('basicStaff');
ownedEquipmentTypeIds.add('civilianShirt');
ownedEquipmentTypeIds.add('civilianShoes');
ownedEquipmentTypeIds.add('apprenticeWand');
ownedEquipmentTypeIds.add('clothHood');
ownedEquipmentTypeIds.add('civilianGloves');
ownedEquipmentTypeIds.add('copperRing');
return Object.values(EQUIPMENT_TYPES).filter(type => ownedEquipmentTypeIds.has(type.id));
};
const ownedEquipmentTypes = getOwnedEquipmentTypes();
// Render design stage
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
@@ -162,7 +191,7 @@ export function EnchantmentDesigner({
) : (
<ScrollArea className="h-64">
<div className="grid grid-cols-2 gap-2">
{Object.values(EQUIPMENT_TYPES).map(type => (
{ownedEquipmentTypes.map(type => (
<div
key={type.id}
className={`p-2 rounded border cursor-pointer transition-all ${
@@ -177,6 +206,11 @@ export function EnchantmentDesigner({
</div>
))}
</div>
{ownedEquipmentTypes.length === 0 && (
<div className="text-center text-gray-400 py-4 text-sm">
No equipment blueprints owned. Craft or find equipment blueprints first.
</div>
)}
</ScrollArea>
)}
</CardContent>
@@ -354,3 +388,5 @@ export function EnchantmentDesigner({
</div>
);
}
EnchantmentDesigner.displayName = "EnchantmentDesigner";
@@ -202,3 +202,5 @@ export function EnchantmentPreparer({
</div>
);
}
EnchantmentPreparer.displayName = "EnchantmentPreparer";
@@ -198,3 +198,5 @@ export function EquipmentCrafter({ store }: EquipmentCrafterProps) {
</div>
);
}
EquipmentCrafter.displayName = "EquipmentCrafter";
@@ -85,3 +85,5 @@ export function AttunementDebug({ store }: AttunementDebugProps) {
</Card>
);
}
AttunementDebug.displayName = "AttunementDebug";
@@ -86,3 +86,5 @@ export function ElementDebug({ store }: ElementDebugProps) {
</Card>
);
}
ElementDebug.displayName = "ElementDebug";
@@ -269,3 +269,5 @@ export function GameStateDebug({ store }: GameStateDebugProps) {
</div>
);
}
GameStateDebug.displayName = "GameStateDebug";
+2
View File
@@ -25,3 +25,5 @@ export function GolemDebug({ store }: GolemDebugProps) {
</Card>
);
}
GolemDebug.displayName = "GolemDebug";
+211
View File
@@ -0,0 +1,211 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Bug } from 'lucide-react';
import { useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
export function PactDebug() {
// Get state from the main game store where pacts are stored
const store = useGameStore();
const signedPacts = useGameStore((s) => s.signedPacts);
const signedPactDetails = useGameStore((s) => s.signedPactDetails);
const elements = useGameStore((s) => s.elements);
const prestigeUpgrades = useGameStore((s) => s.prestigeUpgrades);
// Get all guardian floors
const guardianFloors = Object.keys(GUARDIANS).map(Number).sort((a, b) => a - b);
// Helper to add log messages
const addLog = (message: string) => {
store.log.unshift(message);
};
// Force sign a pact with a guardian (bypass costs and time)
const forcePact = (floor: number) => {
const guardian = GUARDIANS[floor];
if (!guardian) return;
// Check if already signed
if (signedPacts.includes(floor)) {
addLog(`⚠️ Already signed pact with ${guardian.name}!`);
return;
}
// Check max pacts
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
if (signedPacts.length >= maxPacts) {
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
return;
}
// Force sign the pact
const newSignedPacts = [...signedPacts, floor];
// Add pact details
const newSignedPactDetails = {
...signedPactDetails,
[floor]: {
floor,
guardianId: guardian.element,
signedAt: { day: store.day, hour: store.hour },
skillLevels: {} as Record<string, number>,
},
};
// Unlock mana types
let newElements = { ...elements };
for (const elemId of guardian.unlocksMana) {
if (newElements[elemId]) {
newElements = {
...newElements,
[elemId]: { ...newElements[elemId], unlocked: true },
};
}
}
// Check for compound element unlocks
const unlockedSet = new Set(
Object.entries(newElements)
.filter(([, e]) => e.unlocked)
.map(([id]) => id)
);
for (const [elemId, elemDef] of Object.entries(ELEMENTS)) {
if (elemDef.recipe && !newElements[elemId]?.unlocked) {
const canUnlock = elemDef.recipe.every((comp: string) => unlockedSet.has(comp));
if (canUnlock) {
newElements = {
...newElements,
[elemId]: { ...newElements[elemId], unlocked: true },
};
addLog(`🔮 ${elemDef.name} mana unlocked through component synergy!`);
}
}
}
addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed! ${guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')} mana unlocked!`);
// Update store
store.setState({
signedPacts: newSignedPacts,
signedPactDetails: newSignedPactDetails,
elements: newElements,
});
};
// Remove a pact
const removePact = (floor: number) => {
const guardian = GUARDIANS[floor];
const newSignedPacts = signedPacts.filter(f => f !== floor);
const newSignedPactDetails = { ...signedPactDetails };
delete newSignedPactDetails[floor];
addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`);
store.setState({
signedPacts: newSignedPacts,
signedPactDetails: newSignedPactDetails,
});
};
// Clear all pacts
const clearAllPacts = () => {
addLog(`📜 DEBUG: Cleared all pacts!`);
store.setState({
signedPacts: [],
signedPactDetails: {},
});
};
return (
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-orange-400 text-sm flex items-center gap-2">
<Bug className="w-4 h-4" />
Pact Debug
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<p className="text-xs text-gray-400 mb-2">
Force sign pacts with guardians (bypasses mana costs and signing time)
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{guardianFloors.map((floor) => {
const guardian = GUARDIANS[floor];
const isSigned = signedPacts.includes(floor);
return (
<div
key={floor}
className={`p-2 rounded border flex items-center justify-between ${
isSigned ? 'border-green-600/50 bg-green-900/20' : 'border-gray-700'
}`}
style={{ borderColor: isSigned ? undefined : guardian.color, borderWidth: '1px' }}
>
<div>
<div className="text-sm font-semibold" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} | {guardian.pact}x multiplier
</div>
<div className="text-xs text-gray-500">
Unlocks: {guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')}
</div>
</div>
<div className="flex gap-1">
{isSigned ? (
<Button
size="sm"
variant="destructive"
onClick={() => removePact(floor)}
className="text-xs"
>
Remove
</Button>
) : (
<Button
size="sm"
variant="default"
onClick={() => forcePact(floor)}
className="text-xs bg-amber-600 hover:bg-amber-700"
>
Force Sign
</Button>
)}
</div>
</div>
);
})}
</div>
{/* Clear All Button */}
{signedPacts.length > 0 && (
<div className="pt-2 border-t border-gray-700">
<Button
size="sm"
variant="destructive"
onClick={clearAllPacts}
className="w-full text-xs"
>
Clear All Pacts ({signedPacts.length})
</Button>
</div>
)}
{/* Status */}
<div className="text-xs text-gray-400 pt-2 border-t border-gray-700">
Signed Pacts: {signedPacts.length} |
Max Pacts: {1 + (prestigeUpgrades?.pactCapacity || 0)}
</div>
</div>
</CardContent>
</Card>
);
}
PactDebug.displayName = "PactDebug";
+2
View File
@@ -291,3 +291,5 @@ export function SkillDebug({ store }: SkillDebugProps) {
</Card>
);
}
SkillDebug.displayName = "SkillDebug";
+1
View File
@@ -3,3 +3,4 @@ export { SkillDebug } from './SkillDebug';
export { ElementDebug } from './ElementDebug';
export { AttunementDebug } from './AttunementDebug';
export { GolemDebug } from './GolemDebug';
export { PactDebug } from './PactDebug';
@@ -204,3 +204,5 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
</Card>
);
}
MemorySlotPicker.displayName = "MemorySlotPicker";
@@ -58,3 +58,5 @@ export function StudyProgress({ target, showCancel = true, speedLabel }: StudyPr
</div>
);
}
StudyProgress.displayName = "StudyProgress";
@@ -124,3 +124,5 @@ export function UpgradeDialog({ skillId, milestone, onClose }: UpgradeDialogProp
</Dialog>
);
}
UpgradeDialog.displayName = "UpgradeDialog";
@@ -62,3 +62,5 @@ export function CombatStatsSection({ store }: CombatStatsSectionProps) {
</Card>
);
}
CombatStatsSection.displayName = "CombatStatsSection";
@@ -255,3 +255,5 @@ export function ManaStatsSection({
</Card>
);
}
ManaStatsSection.displayName = "ManaStatsSection";
@@ -53,3 +53,5 @@ export function StudyStatsSection({ store, studySpeedMult, studyCostMult }: Stud
</Card>
);
}
StudyStatsSection.displayName = "StudyStatsSection";
@@ -80,3 +80,5 @@ export function UpgradeEffectsSection({ store }: UpgradeEffectsSectionProps) {
</Card>
);
}
UpgradeEffectsSection.displayName = "UpgradeEffectsSection";
@@ -41,3 +41,5 @@ export function AchievementsTab({ store }: AchievementsTabProps) {
</div>
);
}
AchievementsTab.displayName = "AchievementsTab";
@@ -265,3 +265,5 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
</div>
);
}
AttunementsTab.displayName = "AttunementsTab";
+2
View File
@@ -160,3 +160,5 @@ export function CraftingTab({ store }: CraftingTabProps) {
</div>
);
}
CraftingTab.displayName = "CraftingTab";
+5 -1
View File
@@ -6,7 +6,8 @@ import {
SkillDebug,
ElementDebug,
AttunementDebug,
GolemDebug
GolemDebug,
PactDebug
} from '@/components/game/debug';
interface DebugTabProps {
@@ -25,6 +26,9 @@ export function DebugTab({ store }: DebugTabProps) {
<SkillDebug store={store} />
<GolemDebug store={store} />
<PactDebug />
</div>
);
}
DebugTab.displayName = "DebugTab";
@@ -431,3 +431,5 @@ export function EquipmentTab({ store }: EquipmentTabProps) {
</div>
);
}
EquipmentTab.displayName = "EquipmentTab";
@@ -336,3 +336,5 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
</div>
);
}
GolemancyTab.displayName = "GolemancyTab";
+2
View File
@@ -114,3 +114,5 @@ export function LabTab({ store }: LabTabProps) {
</div>
);
}
LabTab.displayName = "LabTab";
+2
View File
@@ -44,3 +44,5 @@ export function LootTab({ store }: LootTabProps) {
</div>
);
}
LootTab.displayName = "LootTab";
+2
View File
@@ -367,3 +367,5 @@ export function SkillsTab({ store }: SkillsTabProps) {
</div>
);
}
SkillsTab.displayName = "SkillsTab";
+2
View File
@@ -178,3 +178,5 @@ export function SpellsTab({ store }: SpellsTabProps) {
</div>
);
}
SpellsTab.displayName = "SpellsTab";
+65 -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,27 +324,31 @@ 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>
);
}
SpireTab.displayName = "SpireTab";
+2
View File
@@ -247,3 +247,5 @@ export function StatsTab({
</div>
);
}
StatsTab.displayName = "StatsTab";
@@ -71,3 +71,5 @@ export function StudyProgress({
</div>
);
}
StudyProgress.displayName = "StudyProgress";
@@ -113,3 +113,5 @@ export function UpgradeDialog({
</Dialog>
);
}
UpgradeDialog.displayName = "UpgradeDialog";
+3
View File
@@ -422,6 +422,9 @@ export function createCraftingSlice(
const instance = state.equipmentInstances[equipmentInstanceId];
if (!instance) return false;
// Don't allow preparing enchanted items - they need to be disenchanted first
if (instance.enchantments.length > 0) return false;
const prepTime = calculatePrepTime(instance.totalCapacity);
const manaCost = calculatePrepManaCost(instance.totalCapacity);
+2
View File
@@ -65,3 +65,5 @@ export function DebugName({ name, children }: DebugNameProps) {
</div>
);
}
DebugName.displayName = "DebugName";
+127 -34
View File
@@ -2,7 +2,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType } from './types';
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType, EquipmentSpellState } from './types';
import {
ELEMENTS,
GUARDIANS,
@@ -45,6 +45,7 @@ import {
getSpellsFromEquipment,
type CraftingActions
} from './crafting-slice';
import { getActiveEquipmentSpells, type ActiveEquipmentSpell } from './utils/combat-utils';
import { EQUIPMENT_TYPES } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { ATTUNEMENTS_DEF, getTotalAttunementRegen, getAttunementConversionRate, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
@@ -749,6 +750,9 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
// Mana Well Effects (Phase 4)
manaHeartBonus: manaHeartBonus, // Cumulative +10% max mana per loop from MANA_HEART
// Spire Mode - simplified UI for climbing
spireMode: false,
};
}
@@ -801,6 +805,10 @@ interface GameStore extends GameState, CraftingActions {
getMeditationMultiplier: () => number;
canCastSpell: (spellId: string) => boolean;
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
// Spire Mode actions
enterSpireMode: () => void;
exitSpireMode: () => void;
}
export const useGameStore = create<GameStore>()(
@@ -833,6 +841,9 @@ export const useGameStore = create<GameStore>()(
// Compute unified effects (includes skill upgrades AND equipment enchantments)
const effects = getUnifiedEffects(state);
// Track current action for potential auto-transitions
let currentAction = state.currentAction;
const maxMana = computeMaxMana(state, effects);
const baseRegen = computeRegen(state, effects);
@@ -878,7 +889,7 @@ export const useGameStore = create<GameStore>()(
let meditateTicks = state.meditateTicks;
let meditationMultiplier = 1;
if (state.currentAction === 'meditate') {
if (currentAction === 'meditate') {
meditateTicks++;
meditationMultiplier = getMeditationBonus(meditateTicks, state.skills);
@@ -973,7 +984,7 @@ export const useGameStore = create<GameStore>()(
let unlockedEffects = state.unlockedEffects;
let consecutiveStudyHours = state.consecutiveStudyHours;
if (state.currentAction === 'study' && currentStudyTarget) {
if (currentAction === 'study' && currentStudyTarget) {
// Calculate base study speed
let studySpeedMult = getStudySpeedMultiplier(skills);
@@ -1076,11 +1087,13 @@ export const useGameStore = create<GameStore>()(
log = [`📖 ${SPELLS_DEF[spellId]?.name} learned!`, ...log.slice(0, 49)];
}
currentStudyTarget = null;
// Auto-transition to meditate when study completes
currentAction = 'meditate';
}
}
// Parallel Study processing (PARALLEL_STUDY special effect)
let parallelStudyTarget = state.parallelStudyTarget;
if (parallelStudyTarget && state.currentAction === 'study') {
if (parallelStudyTarget && currentAction === 'study') {
// Parallel study progresses at 50% speed
const parallelProgressGain = HOURS_PER_TICK * 0.5;
parallelStudyTarget = {
@@ -1101,7 +1114,7 @@ export const useGameStore = create<GameStore>()(
}
// Convert action - auto convert mana
if (state.currentAction === 'convert') {
if (currentAction === 'convert') {
const unlockedElements = Object.entries(elements)
.filter(([, e]) => e.unlocked && e.current < e.max);
@@ -1130,7 +1143,7 @@ export const useGameStore = create<GameStore>()(
const floorElement = getFloorElement(currentFloor);
// Handle puzzle rooms separately
if (state.currentAction === 'climb' && currentRoom.roomType === 'puzzle') {
if (currentAction === 'climb' && currentRoom.roomType === 'puzzle') {
const progressSpeed = getPuzzleProgressSpeed(
currentRoom.puzzleId || '',
state.attunements
@@ -1154,7 +1167,7 @@ export const useGameStore = create<GameStore>()(
maxFloorReached = Math.max(maxFloorReached, currentFloor);
castProgress = 0;
}
} else if (state.currentAction === 'climb') {
} else if (currentAction === 'climb') {
const spellId = state.activeSpell;
const spellDef = SPELLS_DEF[spellId];
@@ -1349,6 +1362,87 @@ export const useGameStore = create<GameStore>()(
}
}
// ─── Equipment Spell Processing ────────────────────────────────────────
// Process casting for spells from equipped weapons
const activeEquipSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
// Update equipmentSpellStates with current progress and process casts
const updatedSpellStates: EquipmentSpellState[] = [];
for (const { spellId, equipmentId } of activeEquipSpells) {
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) continue;
// Find or create spell state for this spell + equipment combo
let spellState = state.equipmentSpellStates.find(
s => s.spellId === spellId && s.sourceEquipment === equipmentId
);
if (!spellState) {
spellState = {
spellId,
sourceEquipment: equipmentId,
castProgress: 0,
};
}
// Only process if climbing
if (currentAction === 'climb') {
// Calculate progress per tick for this spell
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
const spellCastSpeed = spellDef.castSpeed || 1;
const lightningBonus = spellDef.elem === 'lightning' ? 0.3 : 0;
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed * (1 + lightningBonus);
// Accumulate progress
let equipCastProgress = (spellState.castProgress || 0) + progressPerTick;
// Process complete casts
while (equipCastProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
// Deduct cost
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
totalManaGathered += spellDef.cost.amount;
// Calculate damage
const floorElement = getFloorElement(currentFloor);
let baseDmg = calcDamage(
{ ...state, skills: state.skills, signedPacts: state.signedPacts },
spellId,
floorElement
);
baseDmg = baseDmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
// Apply damage to enemies
const aliveEnemies = currentRoom.enemies.filter(e => e.hp > 0);
if (aliveEnemies.length > 0) {
const target = aliveEnemies[0]; // Simple: target first alive enemy
const armorPierceEffect = spellDef.effects?.find(e => e.type === 'armor_pierce');
const armorPierce = armorPierceEffect?.value || 0;
const effectiveArmor = Math.max(0, target.armor - armorPierce);
const dmg = Math.floor(baseDmg * (1 - effectiveArmor));
target.hp = Math.max(0, target.hp - dmg);
}
equipCastProgress -= 1;
}
// Update spell state with new progress
spellState = { ...spellState, castProgress: equipCastProgress };
}
updatedSpellStates.push(spellState);
}
// Keep spell states for equipment that's no longer active (they'll be cleaned up elsewhere)
const inactiveStates = state.equipmentSpellStates.filter(
s => !activeEquipSpells.some(es => es.spellId === s.spellId && es.equipmentId === s.sourceEquipment)
);
const equipmentSpellStates = [...updatedSpellStates, ...inactiveStates];
// ─── Golemancy Processing ─────────────────────────────────────────────────
let golemancy = state.golemancy;
const fabricatorLevel = state.attunements.fabricator?.level || 0;
@@ -1358,7 +1452,7 @@ export const useGameStore = create<GameStore>()(
const floorChanged = currentFloor !== golemancy.lastSummonFloor;
const inCombatRoom = currentRoom.roomType !== 'puzzle';
if (state.currentAction === 'climb' && inCombatRoom && floorChanged && maxGolemSlots > 0) {
if (currentAction === 'climb' && inCombatRoom && floorChanged && maxGolemSlots > 0) {
// Determine which golems should be summoned
const unlockedElementIds = Object.entries(elements)
.filter(([, e]) => e.unlocked)
@@ -1406,7 +1500,7 @@ export const useGameStore = create<GameStore>()(
}
// Process golem maintenance and attacks each tick
if (golemancy.summonedGolems.length > 0 && state.currentAction === 'climb' && inCombatRoom) {
if (golemancy.summonedGolems.length > 0 && currentAction === 'climb' && inCombatRoom) {
const floorDuration = getGolemFloorDuration(skills);
const survivingGolems: typeof golemancy.summonedGolems = [];
let anyGolemDismissed = false;
@@ -1525,7 +1619,7 @@ export const useGameStore = create<GameStore>()(
}
// Unsummon golems when not climbing or in puzzle room
if ((state.currentAction !== 'climb' || !inCombatRoom) && golemancy.summonedGolems.length > 0) {
if ((currentAction !== 'climb' || !inCombatRoom) && golemancy.summonedGolems.length > 0) {
log = [`🗿 Golems returned to the earth.`, ...log.slice(0, 49)];
golemancy = {
...golemancy,
@@ -1559,31 +1653,10 @@ export const useGameStore = create<GameStore>()(
// Apply crafting updates
if (craftingUpdates.rawMana !== undefined) rawMana = craftingUpdates.rawMana;
if (craftingUpdates.log !== undefined) log = craftingUpdates.log;
// If crafting slice set currentAction (e.g., auto-transition to meditate), use it
if (craftingUpdates.currentAction !== undefined) {
set({
...craftingUpdates,
day,
hour,
rawMana,
meditateTicks,
totalManaGathered,
currentFloor,
floorHP,
floorMaxHP,
maxFloorReached,
signedPacts,
currentRoom,
incursionStrength,
currentStudyTarget,
skills,
skillProgress,
spells,
elements,
log,
castProgress,
golemancy,
});
return;
currentAction = craftingUpdates.currentAction;
}
set({
@@ -1599,6 +1672,7 @@ export const useGameStore = create<GameStore>()(
signedPacts,
currentRoom,
incursionStrength,
currentAction,
currentStudyTarget,
parallelStudyTarget,
skills,
@@ -1608,6 +1682,7 @@ export const useGameStore = create<GameStore>()(
unlockedEffects,
log,
castProgress,
equipmentSpellStates,
golemancy,
flowSurgeEndTime,
comboHitCount,
@@ -1929,6 +2004,24 @@ export const useGameStore = create<GameStore>()(
set(newState);
},
// Spire Mode - enter simplified UI for climbing
enterSpireMode: () => {
set((state) => ({
spireMode: true,
currentAction: 'climb',
log: ['🏔️ Entered Spire Mode! The climb begins...', ...state.log.slice(0, 49)],
}));
},
// Exit Spire Mode - return to normal game UI
exitSpireMode: () => {
set((state) => ({
spireMode: false,
currentAction: 'meditate',
log: ['⬇️ Climbed down from the Spire. Returning to normal view.', ...state.log.slice(0, 49)],
}));
},
togglePause: () => {
set((state) => ({ paused: !state.paused }));
},
+3
View File
@@ -212,6 +212,9 @@ export interface GameState {
// Loop insight (earned at end of current loop)
loopInsight: number;
// Spire Mode - simplified UI for climbing
spireMode: boolean;
}
// ─── Action Types for Store ─────────────────────────────────────────────
+15 -5
View File
@@ -221,13 +221,19 @@ export function deductSpellCost(
// ─── Equipment Spell Helpers ──────────────────────────────────────────────────
// Return type for active equipment spells with source equipment
export interface ActiveEquipmentSpell {
spellId: string;
equipmentId: string;
}
// Get active spells from equipped equipment
export function getActiveEquipmentSpells(
equippedInstances: Record<string, string | null>,
equipmentInstances: Record<string, EquipmentInstance>
): string[] {
): ActiveEquipmentSpell[] {
const equippedIds = Object.values(equippedInstances).filter((id): id is string => id !== null);
const spells: string[] = [];
const spells: ActiveEquipmentSpell[] = [];
for (const id of equippedIds) {
const instance = equipmentInstances[id];
@@ -236,12 +242,16 @@ export function getActiveEquipmentSpells(
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
// Check if we already have this spell from this equipment
const exists = spells.some(s => s.spellId === effectDef.effect.spellId && s.equipmentId === id);
if (!exists) {
spells.push({ spellId: effectDef.effect.spellId, equipmentId: id });
}
}
}
}
return [...new Set(spells)];
return spells;
}
// ─── DPS Calculation ──────────────────────────────────────────────────────────
@@ -258,7 +268,7 @@ export function getTotalDPS(
const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
// Calculate DPS for each active spell
for (const spellId of activeSpells) {
for (const { spellId } of activeSpells) {
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) continue;