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

View File

@@ -1,7 +1,7 @@
'use client';
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';
interface ActionButtonsProps {
@@ -23,7 +23,6 @@ export function ActionButtons({
{ id: 'meditate', label: 'Meditate', icon: Sparkles },
{ id: 'climb', label: 'Climb', icon: Swords },
{ id: 'study', label: 'Study', icon: BookOpen },
{ id: 'convert', label: 'Convert', icon: FlaskConical },
];
const hasDesignProgress = designProgress !== null;
@@ -32,7 +31,7 @@ export function ActionButtons({
return (
<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 }) => (
<Button
key={id}

View File

@@ -1,27 +1,19 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
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 {
store: {
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
skills: Record<string, number>;
convertMana: (element: string, amount: number) => void;
unlockElement: (element: string) => void;
craftComposite: (target: string) => void;
};
}
export function LabTab({ store }: LabTabProps) {
const [convertTarget, setConvertTarget] = useState('fire');
// Unlock cost
const UNLOCK_COST = 500;
// Render elemental mana grid
const renderElementsGrid = () => (
<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)
.map(([id, state]) => {
const def = ELEMENTS[id];
const isSelected = convertTarget === id;
return (
<div
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'}`}
style={{ borderColor: isSelected ? def?.color : undefined }}
onClick={() => setConvertTarget(id)}
className="p-2 rounded border border-gray-700 bg-gray-800/50"
>
<div className="text-lg text-center">{def?.sym}</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>
);
// 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
const renderCompositeCrafting = () => {
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 (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Elemental Mana Display */}
@@ -146,48 +109,6 @@ export function LabTab({ store }: LabTabProps) {
</CardContent>
</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 */}
{renderCompositeCrafting()}
</div>

View File

@@ -8,13 +8,15 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { TooltipProvider } from '@/components/ui/tooltip';
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 { 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 { ComboMeter, CraftingProgress, StudyProgress } from '@/components/game';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
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
function getActiveEquipmentSpells(
@@ -49,30 +51,10 @@ function getActiveEquipmentSpells(
}
interface SpireTabProps {
store: GameState & {
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;
store: GameStore;
}
export function SpireTab({
store,
totalDPS,
studySpeedMult,
incursionStrength,
}: SpireTabProps) {
export function SpireTab({ store }: SpireTabProps) {
const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
@@ -82,9 +64,14 @@ export function SpireTab({
// Check if current floor is cleared (for respawn indicator)
const isFloorCleared = clearedFloors[store.currentFloor];
// Get active equipment spells
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 spell = SPELLS_DEF[spellId];
@@ -140,59 +127,28 @@ export function SpireTab({
<Separator className="bg-gray-700" />
{/* Floor Navigation Controls */}
{/* Floor Navigation - Direction indicator only */}
<div className="space-y-2">
<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">
<Button
variant={climbDirection === 'up' ? 'default' : 'outline'}
size="sm"
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" />
<Badge variant={climbDirection === 'up' ? 'default' : 'outline'}
className={climbDirection === 'up' ? 'bg-green-600' : ''}>
<ChevronUp className="w-3 h-3 mr-1" />
Up
</Button>
<Button
variant={climbDirection === 'down' ? 'default' : 'outline'}
size="sm"
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" />
</Badge>
<Badge variant={climbDirection === 'down' ? 'default' : 'outline'}
className={climbDirection === 'down' ? 'bg-blue-600' : ''}>
<ChevronDown className="w-3 h-3 mr-1" />
Down
</Button>
</Badge>
</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 && (
<div className="text-xs text-amber-400 text-center flex items-center justify-center gap-1">
<RotateCcw className="w-3 h-3" />
Floor will respawn when you leave and return
Floor cleared! Advancing...
</div>
)}
</div>
@@ -239,7 +195,7 @@ export function SpireTab({
</span>
</div>
<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) }}>
{' '}{formatSpellCost(spellDef.cost)}
</span>
@@ -273,16 +229,7 @@ export function SpireTab({
})}
</div>
) : (
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects.</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>
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.</div>
)}
</CardContent>
</Card>
@@ -316,7 +263,7 @@ export function SpireTab({
variant="ghost"
size="sm"
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" />
</Button>
@@ -352,45 +299,6 @@ export function SpireTab({
</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 */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">