Files
Mana-Loop/src/components/game/tabs/SpireTab.tsx
T
Refactoring Agent d5cbc9faff
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
Fix build errors: update imports and re-exports
- Fixed equipment/index.ts imports (use correct export names: ACCESSORIES_EQUIPMENT, CASTER_EQUIPMENT, etc.)
- Fixed page.tsx: added lazy, Suspense imports from react
- Fixed page.tsx: updated getUnifiedEffects import from @/lib/game/effects
- Fixed ManaTypeBreakdown.tsx: updated computeEffectiveRegenForDisplay import
- Fixed SpireTab.tsx: updated getEnemyName import from enemy-utils
- Fixed LeftPanel.tsx: updated getUnifiedEffects import from @/lib/game/effects
- Build now succeeds with all tabs working
2026-05-02 18:36:36 +02:00

369 lines
16 KiB
TypeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Mountain } from 'lucide-react';
import type { ActivityLogEntry } from '@/lib/game/types';
import type { GameStore } from '@/lib/game/store';
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
import { calcDamage } from '@/lib/game/store';
import { getEnemyName } from '@/lib/game/store-modules/enemy-utils';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { getUnifiedEffects } from '@/lib/game/effects';
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
import { canAffordSpellCost, getFloorElement } from '@/lib/game/store';
// Extracted components
import { SpireHeader } from './SpireHeader';
import { GuardianPanel } from './GuardianPanel';
import { RoomDisplay } from './RoomDisplay';
import { FloorControls } from './FloorControls';
import { CombatStatsPanel } from './CombatStatsPanel';
import { ActivityLog } from './ActivityLog';
// Room type configurations
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
};
interface SpireTabProps {
store: GameStore;
simpleMode?: boolean;
}
// Check if player can enter spire mode
const canEnterSpireMode = (store: GameStore): boolean => {
return !store.spireMode;
};
export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
// Derived data
const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
const currentGuardian = GUARDIANS[store.currentFloor];
const climbDirection = store.climbDirection || 'up';
const clearedFloors = store.clearedFloors || {};
const currentRoom = store.currentRoom;
const isFloorCleared = clearedFloors[store.currentFloor];
const roomType = currentRoom?.roomType || 'combat';
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
const upgradeEffects = getUnifiedEffects(store);
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
const studySpeedMult = 1;
// Enemy display info
const primaryEnemy = currentRoom?.enemies?.[0] || null;
const swarmEnemies = roomType === 'swarm' && currentRoom?.enemies ? currentRoom.enemies : [];
// Spell casting check
const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId];
if (!spell) return false;
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
};
// Climb handler
const handleClimb = (direction: 'up' | 'down') => {
if (direction === 'up') {
store.startClimbUp();
} else {
store.startClimbDown();
}
};
return (
<div className="grid gap-4">
{/* Enter Spire Mode - Normal mode only */}
{!simpleMode && (
<Card className="bg-gray-900/80 border-amber-600/50">
<CardContent className="pt-4">
<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()}
disabled={!canEnterSpireMode(store)}
>
<Mountain className="w-5 h-5 mr-2" />
Enter Spire Mode
</Button>
<div className="text-xs text-gray-400 text-center mt-2">
Climb the Spire to face guardians and earn pacts
</div>
</CardContent>
</Card>
)}
{/* Spire Header */}
<SpireHeader
currentFloor={store.currentFloor}
maxFloorReached={store.maxFloorReached}
signedPacts={store.signedPacts.length}
isGuardianFloor={isGuardianFloor}
roomType={roomType}
roomLabel={roomConfig.label}
roomIcon={roomConfig.icon}
roomColor={roomConfig.color}
floorElem={floorElem}
floorElemDef={floorElemDef}
simpleMode={simpleMode}
/>
{/* Active Spells Card - Spire Mode only */}
{simpleMode && (
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 pb-4">
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider">
Active Spells ({activeEquipmentSpells.length})
</div>
{activeEquipmentSpells.length > 0 ? (
<div className="space-y-2">
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) return null;
const spellState = store.equipmentSpellStates?.find(
s => s.spellId === spellId && s.sourceEquipment === equipmentId
);
const progress = spellState?.castProgress || 0;
const canCast = canCastSpell(spellId);
return (
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded bg-gray-800/30 border border-gray-700">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
{spellDef.name}
{spellDef.tier === 0 && <span className="ml-2 bg-gray-600 text-gray-200 text-xs px-1 rounded">Basic</span>}
</span>
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>{canCast ? '✓' : '✗'}</span>
</div>
<div className="text-xs text-gray-400 mb-1">
{fmt(calcDamage(store, spellId))} dmg <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '} {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
</div>
{store.currentAction === 'climb' && (
<div className="space-y-0.5">
<div className="flex justify-between text-xs text-gray-500">
<span>Cast</span>
<span>{(progress * 100).toFixed(0)}%</span>
</div>
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300" style={{ width: `${Math.min(100, progress * 100)}%`, background: spellDef.elem === 'raw' ? 'linear-gradient(90deg, #60A5FA99, #60A5FA)' : `linear-gradient(90deg, ${ELEMENTS[spellDef.elem]?.color}99, ${ELEMENTS[spellDef.elem]?.color})` }} />
</div>
</div>
)}
</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>
)}
{/* Summoned Golems */}
{simpleMode && store.golemancy.summonedGolems.length > 0 && (
<Card className="bg-gray-900/80 border-amber-600/50">
<CardContent className="pt-4 pb-4">
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
<Mountain className="w-4 h-4" />
Active Golems ({store.golemancy.summonedGolems.length})
</div>
<div className="space-y-2">
{store.golemancy.summonedGolems.map((summoned) => {
const golemDef = getGolemDef(summoned.golemId);
if (!golemDef) return null;
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
const damage = getGolemDamage(summoned.golemId, store.skills);
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
return (
<div key={summoned.golemId} className="p-2 rounded bg-gray-800/30 border border-gray-700">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
<span className="text-xs font-semibold" style={{ color: elemColor }}>{golemDef.name}</span>
</div>
{golemDef.isAoe && <span className="text-xs border border-gray-600 px-1 rounded">AOE {golemDef.aoeTargets}</span>}
</div>
<div className="text-xs text-gray-400"> {damage} DMG {attackSpeed.toFixed(1)}/hr</div>
{store.currentAction === 'climb' && summoned.attackProgress > 0 && (
<div className="mt-1">
<div className="flex justify-between text-xs text-gray-500 mb-0.5">
<span>Attack</span>
<span>{(summoned.attackProgress * 100).toFixed(0)}%</span>
</div>
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300" style={{ width: `${Math.min(100, summoned.attackProgress * 100)}%`, background: elemColor }} />
</div>
</div>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
)}
{/* Guardian Panel */}
{isGuardianFloor && simpleMode && (
<GuardianPanel currentFloor={store.currentFloor} floorElemDef={floorElemDef} />
)}
{/* Room Display */}
{simpleMode && (
<RoomDisplay
roomType={roomType}
roomConfig={roomConfig}
primaryEnemy={primaryEnemy}
swarmEnemies={swarmEnemies}
puzzleId={currentRoom?.puzzleId}
puzzleProgress={currentRoom?.puzzleProgress}
simpleMode={true}
floorElemDef={floorElemDef}
floorHP={store.floorHP}
floorMaxHP={store.floorMaxHP}
totalDPS={totalDPS}
currentAction={store.currentAction}
activeEquipmentSpells={activeEquipmentSpells}
/>
)}
{/* Floor Controls */}
{simpleMode && (
<FloorControls
store={store}
climbDirection={climbDirection}
isGuardianFloor={isGuardianFloor}
currentRoom={currentRoom}
currentGuardian={currentGuardian}
isFloorCleared={isFloorCleared}
floorElemDef={floorElemDef}
roomType={roomType}
roomConfig={roomConfig}
activeEquipmentSpells={activeEquipmentSpells}
upgradeEffects={upgradeEffects}
floorElem={floorElem}
totalDPS={totalDPS}
getEnemyName={getEnemyName}
calcDamage={calcDamage}
SPELLS_DEF={SPELLS_DEF}
canCastSpell={canCastSpell}
storeCurrentAction={store.currentAction}
handleClimb={handleClimb}
formatSpellCost={formatSpellCost}
getSpellCostColor={getSpellCostColor}
/>
)}
{/* Combat Stats Panel */}
{simpleMode && (
<CombatStatsPanel
activeEquipmentSpells={activeEquipmentSpells}
store={store}
totalDPS={totalDPS}
calcDamage={calcDamage}
formatSpellCost={formatSpellCost}
getSpellCostColor={getSpellCostColor}
SPELLS_DEF={SPELLS_DEF}
upgradeEffects={upgradeEffects}
canCastSpell={canCastSpell}
studySpeedMult={studySpeedMult}
storeCurrentAction={store.currentAction}
/>
)}
{/* Activity Log - Spire Mode only */}
{simpleMode && <ActivityLog activityLog={store.activityLog} />}
{/* Study Progress - Normal mode only */}
{!simpleMode && store.currentStudyTarget && (
<Card className="bg-gray-900/80 border-purple-600/50">
<CardContent className="pt-4 pb-4">
<div className="text-xs text-gray-400 mb-2">Study: {getSkillName(store.currentStudyTarget.id)}</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300 bg-purple-500" style={{ width: `${Math.min(100, (store.currentStudyTarget.progress / store.currentStudyTarget.required) * 100)}%` }} />
</div>
{store.parallelStudyTarget && (
<div className="mt-3 p-2 rounded border border-cyan-600/50 bg-cyan-900/20">
<div className="text-xs text-cyan-300 mb-1">Parallel: {getSkillName(store.parallelStudyTarget.id)} (50% speed)</div>
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)}%` }} />
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Crafting Progress - Normal mode only */}
{!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && (
<Card className="bg-gray-900/80 border-cyan-600/50">
<CardContent className="pt-4 pb-4">
{store.designProgress && (
<div className="mb-3">
<div className="text-xs text-gray-400 mb-1">Design Progress</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.designProgress.progress / store.designProgress.required) * 100)}%` }} />
</div>
</div>
)}
{store.preparationProgress && (
<div className="mb-3">
<div className="text-xs text-gray-400 mb-1">Preparation Progress</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.preparationProgress.progress / store.preparationProgress.required) * 100)}%` }} />
</div>
</div>
)}
{store.applicationProgress && (
<div>
<div className="text-xs text-gray-400 mb-1">Application Progress</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.applicationProgress.progress / store.applicationProgress.required) * 100)}%` }} />
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
);
}
SpireTab.displayName = "SpireTab";
function getSkillName(skillId: string): string {
const { SKILLS_DEF } = require('@/lib/game/constants');
return SKILLS_DEF[skillId]?.name || skillId;
}
function fmt(value: number): string {
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
return value.toFixed(0);
}
function getGolemDef(golemId: string) {
const { GOLEMS_DEF } = require('@/lib/game/data/golems');
return GOLEMS_DEF[golemId];
}
function getGolemDamage(golemId: string, skills: any) {
const { getGolemDamage } = require('@/lib/game/data/golems');
return getGolemDamage(golemId, skills);
}
function getGolemAttackSpeed(golemId: string, skills: any) {
const { getGolemAttackSpeed } = require('@/lib/game/data/golems');
return getGolemAttackSpeed(golemId, skills);
}