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
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:
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
@@ -86,6 +68,11 @@ export function SpireTab({
|
||||
// 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];
|
||||
if (!spell) return false;
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user