Files
Mana-Loop/src/components/game/tabs/SpireTab.tsx
T
Refactoring Agent 837d963b63
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 30m15s
fix: split SpireTab.tsx to 395 lines, remove require() imports, import from data modules; complete store migration
2026-05-04 13:36:10 +02:00

391 lines
18 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 { useMemo } from 'react';
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 { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
import { calcDamage } from '@/lib/game/stores';
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/stores';
import { useGameStore, useManaStore, useSkillStore, useCombatStore, usePrestigeStore } from '@/lib/game/stores';
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
// 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 {
simpleMode?: boolean;
}
// Check if player can enter spire mode
const canEnterSpireMode = (spireMode: boolean): boolean => {
return !spireMode;
};
export function SpireTab({ simpleMode = false }: SpireTabProps) {
// Get state from modular stores
const currentFloor = useCombatStore((s) => s.currentFloor);
const floorHP = useCombatStore((s) => s.floorHP);
const floorMaxHP = useCombatStore((s) => s.floorMaxHP);
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const currentAction = useCombatStore((s) => s.currentAction);
const castProgress = useCombatStore((s) => s.castProgress);
const activeSpell = useCombatStore((s) => s.activeSpell);
const startClimbUp = useCombatStore((s) => s.startClimbUp);
const startClimbDown = useCombatStore((s) => s.startClimbDown);
const enterSpireMode = useGameStore((s) => s.enterSpireMode);
const spireMode = useGameStore((s) => s.spireMode);
const climbDirection = useGameStore((s) => s.climbDirection) || 'up';
const clearedFloors = useGameStore((s) => s.clearedFloors || {});
const currentRoom = useGameStore((s) => s.currentRoom);
const equipmentSpellStates = useGameStore((s) => s.equipmentSpellStates);
const golemancy = useGameStore((s) => s.golemancy);
const activityLog = useGameStore((s) => s.activityLog);
const currentStudyTarget = useGameStore((s) => s.currentStudyTarget);
const parallelStudyTarget = useGameStore((s) => s.parallelStudyTarget);
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const skills = useSkillStore((s) => s.skills);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers);
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const equippedInstances = useGameStore((s) => s.equippedInstances);
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
const designProgress = useGameStore((s) => s.designProgress);
const designProgress2 = useGameStore((s) => s.designProgress2);
const preparationProgress = useGameStore((s) => s.preparationProgress);
const applicationProgress = useGameStore((s) => s.applicationProgress);
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress);
// Derived data
const floorElem = getFloorElement(currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[currentFloor];
const currentGuardian = GUARDIANS[currentFloor];
const isFloorCleared = clearedFloors[currentFloor];
const roomType = currentRoom?.roomType || 'combat';
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
const activeEquipmentSpells = useMemo(
() => getActiveEquipmentSpells(equippedInstances, equipmentInstances),
[equippedInstances, equipmentInstances]
);
const upgradeEffects = useMemo(
() => getUnifiedEffects({
skillUpgrades,
skillTiers,
equippedInstances,
equipmentInstances,
}),
[skillUpgrades, skillTiers, equippedInstances, equipmentInstances]
);
const totalDPS = useMemo(
() => getTotalDPS({ skills, signedPacts, skillUpgrades, skillTiers }, upgradeEffects, floorElem),
[skills, signedPacts, skillUpgrades, skillTiers, upgradeEffects, floorElem]
);
// 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, rawMana, elements);
};
// Climb handler
const handleClimb = (direction: 'up' | 'down') => {
if (direction === 'up') {
startClimbUp();
} else {
startClimbDown();
}
};
const getSkillName = (skillId: string): string => {
return SKILLS_DEF[skillId]?.name || skillId;
};
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={enterSpireMode}
disabled={!canEnterSpireMode(spireMode)}
>
<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={currentFloor}
maxFloorReached={maxFloorReached}
signedPacts={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 = 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">
{calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem)} dmg <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '} {Math.floor(calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem) * (spellDef.castSpeed || 1))} dmg/hr
</div>
{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 && 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 ({golemancy.summonedGolems.length})
</div>
<div className="space-y-2">
{golemancy.summonedGolems.map((summoned) => {
const golemDef = GOLEMS_DEF[summoned.golemId];
if (!golemDef) return null;
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
const damage = getGolemDamage(summoned.golemId, skills);
const attackSpeed = getGolemAttackSpeed(summoned.golemId, 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>
{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={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={floorHP}
floorMaxHP={floorMaxHP}
totalDPS={totalDPS}
currentAction={currentAction}
activeEquipmentSpells={activeEquipmentSpells}
/>
)}
{/* Floor Controls */}
{simpleMode && (
<FloorControls
storeCurrentAction={currentAction}
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}
handleClimb={handleClimb}
formatSpellCost={formatSpellCost}
getSpellCostColor={getSpellCostColor}
/>
)}
{/* Combat Stats Panel */}
{simpleMode && (
<CombatStatsPanel
activeEquipmentSpells={activeEquipmentSpells}
storeCurrentAction={currentAction}
totalDPS={totalDPS}
calcDamage={calcDamage}
formatSpellCost={formatSpellCost}
getSpellCostColor={getSpellCostColor}
SPELLS_DEF={SPELLS_DEF}
upgradeEffects={upgradeEffects}
canCastSpell={canCastSpell}
studySpeedMult={1}
/>
)}
{/* Activity Log - Spire Mode only */}
{simpleMode && <ActivityLog activityLog={activityLog} />}
{/* Study Progress - Normal mode only */}
{!simpleMode && 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(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, (currentStudyTarget.progress / currentStudyTarget.required) * 100)}%` }} />
</div>
{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(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, (parallelStudyTarget.progress / parallelStudyTarget.required) * 100)}%` }} />
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Crafting Progress - Normal mode only */}
{!simpleMode && (designProgress || preparationProgress || applicationProgress) && (
<Card className="bg-gray-900/80 border-cyan-600/50">
<CardContent className="pt-4 pb-4">
{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, (designProgress.progress / designProgress.required) * 100)}%` }} />
</div>
</div>
)}
{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, (preparationProgress.progress / preparationProgress.required) * 100)}%` }} />
</div>
</div>
)}
{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, (applicationProgress.progress / applicationProgress.required) * 100)}%` }} />
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
);
}
SpireTab.displayName = "SpireTab";