Fix UI issues: Equipment tab, remove manual floor navigation and element conversion
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s

- Fix EquipmentTab props interface
- Fix ActionButtons to receive correct props
- Remove manual ascend/descend floor buttons - floor changes automatically after clearing
- Remove Element Conversion section from LabTab
- Remove Unlock Elements section from LabTab
- Remove Convert action from ActionButtons
- Simplify SpireTab to be self-contained with just store prop
This commit is contained in:
2026-03-26 16:47:50 +00:00
parent 5416b327af
commit a5e37b9b24
4 changed files with 51 additions and 241 deletions

View File

@@ -19,7 +19,6 @@ import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
export default function ManaLoopGame() { export default function ManaLoopGame() {
const [activeTab, setActiveTab] = useState('spire'); const [activeTab, setActiveTab] = useState('spire');
const [convertTarget, setConvertTarget] = useState('fire');
const [isGathering, setIsGathering] = useState(false); const [isGathering, setIsGathering] = useState(false);
// Game store // Game store
@@ -188,20 +187,11 @@ export default function ManaLoopGame() {
{/* Action Buttons */} {/* Action Buttons */}
<ActionButtons <ActionButtons
store={store} currentAction={store.currentAction}
isClimbing={store.isClimbing} designProgress={store.designProgress}
currentStudyTarget={store.currentStudyTarget} preparationProgress={store.preparationProgress}
currentFloor={store.currentFloor} applicationProgress={store.applicationProgress}
currentGuardian={currentGuardian} setAction={store.setAction}
isGuardianFloor={isGuardianFloor}
floorElem={floorElem}
floorElemDef={floorElemDef}
activeSpell={store.activeSpell}
convertTarget={convertTarget}
setConvertTarget={setConvertTarget}
canCastSpell={canCastSpell}
activeEquipmentSpells={activeEquipmentSpells}
totalDPS={totalDPS}
/> />
{/* Calendar */} {/* Calendar */}
@@ -250,15 +240,7 @@ export default function ManaLoopGame() {
</TabsList> </TabsList>
<TabsContent value="spire"> <TabsContent value="spire">
<SpireTab <SpireTab store={store} />
store={store}
floorElem={floorElem}
isGuardianFloor={isGuardianFloor}
currentGuardian={currentGuardian}
activeEquipmentSpells={activeEquipmentSpells}
totalDPS={totalDPS}
upgradeEffects={upgradeEffects}
/>
</TabsContent> </TabsContent>
<TabsContent value="skills"> <TabsContent value="skills">

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sparkles, Swords, BookOpen, FlaskConical, Target } from 'lucide-react'; import { Sparkles, Swords, BookOpen, Target, FlaskConical } from 'lucide-react';
import type { GameAction } from '@/lib/game/types'; import type { GameAction } from '@/lib/game/types';
interface ActionButtonsProps { interface ActionButtonsProps {
@@ -23,7 +23,6 @@ export function ActionButtons({
{ id: 'meditate', label: 'Meditate', icon: Sparkles }, { id: 'meditate', label: 'Meditate', icon: Sparkles },
{ id: 'climb', label: 'Climb', icon: Swords }, { id: 'climb', label: 'Climb', icon: Swords },
{ id: 'study', label: 'Study', icon: BookOpen }, { id: 'study', label: 'Study', icon: BookOpen },
{ id: 'convert', label: 'Convert', icon: FlaskConical },
]; ];
const hasDesignProgress = designProgress !== null; const hasDesignProgress = designProgress !== null;
@@ -32,7 +31,7 @@ export function ActionButtons({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-3 gap-2">
{actions.map(({ id, label, icon: Icon }) => ( {actions.map(({ id, label, icon: Icon }) => (
<Button <Button
key={id} key={id}

View File

@@ -1,27 +1,19 @@
'use client'; 'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ELEMENTS, MANA_PER_ELEMENT } from '@/lib/game/constants'; import { ELEMENTS } from '@/lib/game/constants';
interface LabTabProps { interface LabTabProps {
store: { store: {
rawMana: number; rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>; elements: Record<string, { current: number; max: number; unlocked: boolean }>;
skills: Record<string, number>; skills: Record<string, number>;
convertMana: (element: string, amount: number) => void;
unlockElement: (element: string) => void;
craftComposite: (target: string) => void; craftComposite: (target: string) => void;
}; };
} }
export function LabTab({ store }: LabTabProps) { export function LabTab({ store }: LabTabProps) {
const [convertTarget, setConvertTarget] = useState('fire');
// Unlock cost
const UNLOCK_COST = 500;
// Render elemental mana grid // Render elemental mana grid
const renderElementsGrid = () => ( const renderElementsGrid = () => (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2"> <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
@@ -29,13 +21,10 @@ export function LabTab({ store }: LabTabProps) {
.filter(([, state]) => state.unlocked) .filter(([, state]) => state.unlocked)
.map(([id, state]) => { .map(([id, state]) => {
const def = ELEMENTS[id]; const def = ELEMENTS[id];
const isSelected = convertTarget === id;
return ( return (
<div <div
key={id} key={id}
className={`p-2 rounded border cursor-pointer transition-all ${isSelected ? 'border-blue-500 bg-blue-900/20' : 'border-gray-700 bg-gray-800/50 hover:border-gray-600'}`} className="p-2 rounded border border-gray-700 bg-gray-800/50"
style={{ borderColor: isSelected ? def?.color : undefined }}
onClick={() => setConvertTarget(id)}
> >
<div className="text-lg text-center">{def?.sym}</div> <div className="text-lg text-center">{def?.sym}</div>
<div className="text-xs font-semibold text-center" style={{ color: def?.color }}>{def?.name}</div> <div className="text-xs font-semibold text-center" style={{ color: def?.color }}>{def?.name}</div>
@@ -46,47 +35,6 @@ export function LabTab({ store }: LabTabProps) {
</div> </div>
); );
// Render locked elements (can be unlocked)
const renderLockedElements = () => {
const lockedElements = Object.entries(store.elements)
.filter(([, state]) => !state.unlocked)
.filter(([id]) => !ELEMENTS[id]?.recipe); // Only show base elements (no recipe = base)
if (lockedElements.length === 0) return null;
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Unlock Elements</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{lockedElements.map(([id, state]) => {
const def = ELEMENTS[id];
const canUnlock = store.rawMana >= UNLOCK_COST;
return (
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">{def?.sym}</span>
<span className="text-sm" style={{ color: def?.color }}>{def?.name}</span>
</div>
<Button
size="sm"
variant="outline"
disabled={!canUnlock}
onClick={() => store.unlockElement(id)}
>
{UNLOCK_COST}
</Button>
</div>
);
})}
</div>
</CardContent>
</Card>
);
};
// Render composite crafting // Render composite crafting
const renderCompositeCrafting = () => { const renderCompositeCrafting = () => {
const compositeElements = Object.entries(ELEMENTS) const compositeElements = Object.entries(ELEMENTS)
@@ -134,6 +82,21 @@ export function LabTab({ store }: LabTabProps) {
); );
}; };
// Check if there are any unlocked elements
const hasUnlockedElements = Object.values(store.elements).some(e => e.unlocked);
if (!hasUnlockedElements) {
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-6">
<div className="text-center text-gray-500">
No elemental mana available. Elements are unlocked through gameplay.
</div>
</CardContent>
</Card>
);
}
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Elemental Mana Display */} {/* Elemental Mana Display */}
@@ -146,48 +109,6 @@ export function LabTab({ store }: LabTabProps) {
</CardContent> </CardContent>
</Card> </Card>
{/* Element Conversion */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm">Element Conversion</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-400 mb-3">
Convert raw mana to elemental mana (100:1 ratio)
</p>
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="outline"
onClick={() => store.convertMana(convertTarget, 1)}
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT}
>
+1 ({MANA_PER_ELEMENT})
</Button>
<Button
size="sm"
variant="outline"
onClick={() => store.convertMana(convertTarget, 10)}
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT * 10}
>
+10 ({MANA_PER_ELEMENT * 10})
</Button>
<Button
size="sm"
variant="outline"
onClick={() => store.convertMana(convertTarget, 100)}
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT * 100}
>
+100 ({MANA_PER_ELEMENT * 100})
</Button>
</div>
</CardContent>
</Card>
{/* Unlock Elements */}
{renderLockedElements()}
{/* Composite Crafting */} {/* Composite Crafting */}
{renderCompositeCrafting()} {renderCompositeCrafting()}
</div> </div>

View File

@@ -8,13 +8,15 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X } from 'lucide-react'; import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X } from 'lucide-react';
import type { GameState, GameAction, EquipmentSpellState, StudyTarget } from '@/lib/game/types'; import type { GameStore } from '@/lib/game/types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
import { fmt, fmtDec, getFloorElement, calcDamage, canAffordSpellCost } from '@/lib/game/store'; import { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store';
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
import { ComboMeter, CraftingProgress, StudyProgress } from '@/components/game'; import { ComboMeter, CraftingProgress, StudyProgress } from '@/components/game';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { getUnifiedEffects } from '@/lib/game/effects';
import { getTotalDPS } from '@/lib/game/store';
// Helper to get active spells from equipped caster weapons // Helper to get active spells from equipped caster weapons
function getActiveEquipmentSpells( function getActiveEquipmentSpells(
@@ -49,30 +51,10 @@ function getActiveEquipmentSpells(
} }
interface SpireTabProps { interface SpireTabProps {
store: GameState & { store: GameStore;
setAction: (action: GameAction) => void;
setSpell: (spellId: string) => void;
cancelStudy: () => void;
cancelParallelStudy: () => void;
setClimbDirection: (direction: 'up' | 'down') => void;
changeFloor: (direction: 'up' | 'down') => void;
cancelDesign?: () => void;
cancelPreparation?: () => void;
pauseApplication?: () => void;
resumeApplication?: () => void;
cancelApplication?: () => void;
};
totalDPS: number;
studySpeedMult: number;
incursionStrength: number;
} }
export function SpireTab({ export function SpireTab({ store }: SpireTabProps) {
store,
totalDPS,
studySpeedMult,
incursionStrength,
}: SpireTabProps) {
const floorElem = getFloorElement(store.currentFloor); const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem]; const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor]; const isGuardianFloor = !!GUARDIANS[store.currentFloor];
@@ -86,6 +68,11 @@ export function SpireTab({
// Get active equipment spells // Get active equipment spells
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances); const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
// Get upgrade effects and DPS
const upgradeEffects = getUnifiedEffects(store);
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
const studySpeedMult = 1; // Base study speed
const canCastSpell = (spellId: string): boolean => { const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId]; const spell = SPELLS_DEF[spellId];
if (!spell) return false; if (!spell) return false;
@@ -140,59 +127,28 @@ export function SpireTab({
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
{/* Floor Navigation Controls */} {/* Floor Navigation - Direction indicator only */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-gray-400">Auto-Direction</span> <span className="text-xs text-gray-400">Direction</span>
<div className="flex gap-1"> <div className="flex gap-1">
<Button <Badge variant={climbDirection === 'up' ? 'default' : 'outline'}
variant={climbDirection === 'up' ? 'default' : 'outline'} className={climbDirection === 'up' ? 'bg-green-600' : ''}>
size="sm" <ChevronUp className="w-3 h-3 mr-1" />
className={`h-7 px-2 ${climbDirection === 'up' ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-800/50'}`}
onClick={() => store.setClimbDirection('up')}
>
<ChevronUp className="w-4 h-4 mr-1" />
Up Up
</Button> </Badge>
<Button <Badge variant={climbDirection === 'down' ? 'default' : 'outline'}
variant={climbDirection === 'down' ? 'default' : 'outline'} className={climbDirection === 'down' ? 'bg-blue-600' : ''}>
size="sm" <ChevronDown className="w-3 h-3 mr-1" />
className={`h-7 px-2 ${climbDirection === 'down' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-800/50'}`}
onClick={() => store.setClimbDirection('down')}
>
<ChevronDown className="w-4 h-4 mr-1" />
Down Down
</Button> </Badge>
</div> </div>
</div> </div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1 h-8 bg-gray-800/50 hover:bg-gray-700/50 disabled:opacity-50"
disabled={store.currentFloor <= 1}
onClick={() => store.changeFloor('down')}
>
<ChevronDown className="w-4 h-4 mr-1" />
Descend
</Button>
<Button
variant="outline"
size="sm"
className="flex-1 h-8 bg-gray-800/50 hover:bg-gray-700/50 disabled:opacity-50"
disabled={store.currentFloor >= 100}
onClick={() => store.changeFloor('up')}
>
<ChevronUp className="w-4 h-4 mr-1" />
Ascend
</Button>
</div>
{isFloorCleared && ( {isFloorCleared && (
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1"> <div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
<RotateCcw className="w-3 h-3" /> <RotateCcw className="w-3 h-3" />
Floor will respawn when you leave and return Floor cleared! Advancing...
</div> </div>
)} )}
</div> </div>
@@ -239,7 +195,7 @@ export function SpireTab({
</span> </span>
</div> </div>
<div className="text-xs text-gray-400 game-mono mb-1"> <div className="text-xs text-gray-400 game-mono mb-1">
{fmt(calcDamage(store, spellId, floorElem))} dmg {fmt(totalDPS)} DPS
<span style={{ color: getSpellCostColor(spellDef.cost) }}> <span style={{ color: getSpellCostColor(spellDef.cost) }}>
{' '}{formatSpellCost(spellDef.cost)} {' '}{formatSpellCost(spellDef.cost)}
</span> </span>
@@ -273,16 +229,7 @@ export function SpireTab({
})} })}
</div> </div>
) : ( ) : (
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects.</div> <div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.</div>
)}
{incursionStrength > 0 && (
<div className="p-2 bg-red-900/20 border border-red-800/50 rounded">
<div className="text-xs text-red-400 game-panel-title mb-1">LABYRINTH INCURSION</div>
<div className="text-sm text-gray-300">
-{Math.round(incursionStrength * 100)}% mana regen
</div>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -316,7 +263,7 @@ export function SpireTab({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 text-gray-400 hover:text-white" className="h-6 w-6 p-0 text-gray-400 hover:text-white"
onClick={() => store.cancelParallelStudy()} onClick={() => store.cancelParallelStudy?.()}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</Button> </Button>
@@ -352,45 +299,6 @@ export function SpireTab({
</Card> </Card>
)} )}
{/* Spells Available */}
<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">Known Spells</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{Object.entries(store.spells)
.filter(([, state]) => state.learned)
.map(([id, state]) => {
const def = SPELLS_DEF[id];
if (!def) return null;
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
const isActive = store.activeSpell === id;
const canCast = canCastSpell(id);
return (
<Button
key={id}
variant="outline"
className={`h-auto py-2 px-3 flex flex-col items-start ${isActive ? 'border-amber-500 bg-amber-900/20' : canCast ? 'border-gray-600 bg-gray-800/50 hover:bg-gray-700/50' : 'border-gray-700 bg-gray-800/30 opacity-60'}`}
onClick={() => store.setSpell(id)}
>
<div className="text-sm font-semibold" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
{def.name}
</div>
<div className="text-xs text-gray-400 game-mono">
{fmt(calcDamage(store, id))} dmg
</div>
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
{formatSpellCost(def.cost)}
</div>
</Button>
);
})}
</div>
</CardContent>
</Card>
{/* Activity Log */} {/* Activity Log */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2"> <Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2"> <CardHeader className="pb-2">