342 lines
14 KiB
TypeScript
Executable File
342 lines
14 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
Golem, Zap, Clock, Swords, Shield, Target, Sparkles, Lock, Check, X
|
|
} from 'lucide-react';
|
|
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
|
import { ELEMENTS } from '@/lib/game/constants';
|
|
import { fmt } from '@/lib/game/store';
|
|
import type { GameStore } from '@/lib/game/store';
|
|
import type { GolemancyState, AttunementState, ElementState } from '@/lib/game/types';
|
|
|
|
export interface GolemancyTabProps {
|
|
store: GameStore;
|
|
}
|
|
|
|
export function GolemancyTab({ store }: GolemancyTabProps) {
|
|
const attunements = store.attunements;
|
|
const elements = store.elements;
|
|
const skills = store.skills;
|
|
const golemancy = store.golemancy;
|
|
const currentFloor = store.currentFloor;
|
|
const currentRoom = store.currentRoom;
|
|
const toggleGolem = store.toggleGolem;
|
|
|
|
// Get Fabricator level and golem slots
|
|
const fabricatorLevel = attunements.fabricator?.level || 0;
|
|
const fabricatorActive = attunements.fabricator?.active || false;
|
|
const maxSlots = getGolemSlots(fabricatorLevel);
|
|
|
|
// Get unlocked elements
|
|
const unlockedElements = Object.entries(elements)
|
|
.filter(([, e]) => e.unlocked)
|
|
.map(([id]) => id);
|
|
|
|
// Get all unlocked golems
|
|
const unlockedGolems = Object.values(GOLEMS_DEF).filter(golem =>
|
|
isGolemUnlocked(golem.id, attunements, unlockedElements)
|
|
);
|
|
|
|
// Check if golemancy is available
|
|
const hasGolemancy = fabricatorActive && fabricatorLevel >= 2;
|
|
|
|
// Check if currently in combat (not puzzle)
|
|
const inCombat = currentRoom.roomType !== 'puzzle';
|
|
|
|
// Get element info helper
|
|
const getElementInfo = (elementId: string) => {
|
|
return ELEMENTS[elementId];
|
|
};
|
|
|
|
// Render a golem card
|
|
const renderGolemCard = (golemId: string, isUnlocked: boolean) => {
|
|
const golem = GOLEMS_DEF[golemId];
|
|
if (!golem) return null;
|
|
|
|
const isEnabled = golemancy.enabledGolems.includes(golemId);
|
|
const isSelected = golemancy.summonedGolems.some(g => g.golemId === golemId);
|
|
|
|
// Calculate effective stats
|
|
const damage = getGolemDamage(golemId, skills);
|
|
const attackSpeed = getGolemAttackSpeed(golemId, skills);
|
|
const floorDuration = getGolemFloorDuration(skills);
|
|
|
|
// Get element color
|
|
const primaryElement = getElementInfo(golem.baseManaType);
|
|
const elementColor = primaryElement?.color || '#888';
|
|
|
|
if (!isUnlocked) {
|
|
// Locked golem card
|
|
return (
|
|
<Card key={golemId} className="bg-gray-900/80 border-gray-700 opacity-50">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Lock className="w-4 h-4" />
|
|
<span className="text-gray-500">???</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-xs text-gray-500">
|
|
{golem.unlockCondition.type === 'attunement_level' && (
|
|
<div>Requires Fabricator Level {golem.unlockCondition.level}</div>
|
|
)}
|
|
{golem.unlockCondition.type === 'mana_unlocked' && (
|
|
<div>Requires {ELEMENTS[golem.unlockCondition.manaType || '']?.name || golem.unlockCondition.manaType} Mana</div>
|
|
)}
|
|
{golem.unlockCondition.type === 'dual_attunement' && (
|
|
<div>Requires Enchanter & Fabricator Level 5</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
key={golemId}
|
|
className={`bg-gray-900/80 border-2 transition-all cursor-pointer ${
|
|
isEnabled
|
|
? 'border-green-500 bg-green-900/10'
|
|
: 'border-gray-700 hover:border-gray-600'
|
|
}`}
|
|
onClick={() => toggleGolem(golemId)}
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Golem className="w-4 h-4" style={{ color: elementColor }} />
|
|
<span style={{ color: elementColor }}>{golem.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{golem.isAoe && (
|
|
<Badge variant="outline" className="text-xs">AOE {golem.aoeTargets}</Badge>
|
|
)}
|
|
<Badge variant="outline" className="text-xs">T{golem.tier}</Badge>
|
|
{isEnabled ? (
|
|
<Check className="w-4 h-4 text-green-400" />
|
|
) : (
|
|
<X className="w-4 h-4 text-gray-500" />
|
|
)}
|
|
</div>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<p className="text-xs text-gray-400">{golem.description}</p>
|
|
|
|
<Separator className="bg-gray-700" />
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div className="flex items-center gap-1">
|
|
<Swords className="w-3 h-3 text-red-400" />
|
|
<span className="text-gray-400">DMG:</span>
|
|
<span className="text-white">{damage}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Zap className="w-3 h-3 text-yellow-400" />
|
|
<span className="text-gray-400">Speed:</span>
|
|
<span className="text-white">{attackSpeed.toFixed(1)}/hr</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Target className="w-3 h-3 text-blue-400" />
|
|
<span className="text-gray-400">Pierce:</span>
|
|
<span className="text-white">{Math.floor(golem.armorPierce * 100)}%</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3 text-purple-400" />
|
|
<span className="text-gray-400">Duration:</span>
|
|
<span className="text-white">{floorDuration} floor(s)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="bg-gray-700" />
|
|
|
|
{/* Summon Cost */}
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-1">Summon Cost:</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{golem.summonCost.map((cost, idx) => {
|
|
const elem = getElementInfo(cost.element || '');
|
|
const available = cost.type === 'raw'
|
|
? store.rawMana
|
|
: elements[cost.element || '']?.current || 0;
|
|
const canAfford = available >= cost.amount;
|
|
|
|
return (
|
|
<Badge
|
|
key={idx}
|
|
variant="outline"
|
|
className={`text-xs ${canAfford ? 'border-green-500' : 'border-red-500'}`}
|
|
style={{ borderColor: canAfford ? undefined : '#ef4444' }}
|
|
>
|
|
<span style={{ color: elem?.color }}>{elem?.sym || '💎'}</span>
|
|
{' '}{cost.amount}
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Maintenance Cost */}
|
|
<div>
|
|
<div className="text-xs text-gray-500 mb-1">Maintenance/hr:</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{golem.maintenanceCost.map((cost, idx) => {
|
|
const elem = getElementInfo(cost.element || '');
|
|
|
|
return (
|
|
<Badge key={idx} variant="outline" className="text-xs">
|
|
<span style={{ color: elem?.color }}>{elem?.sym || '💎'}</span>
|
|
{' '}{cost.amount}/hr
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
{isSelected && (
|
|
<div className="mt-2 text-xs text-green-400 flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" />
|
|
Active on Floor {currentFloor}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Golem className="w-5 h-5 text-amber-500" />
|
|
Golemancy
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!hasGolemancy ? (
|
|
<div className="text-center text-gray-400 py-4">
|
|
<Lock className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|
<p>Unlock the Fabricator attunement and reach Level 2 to summon golems.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-400">Golem Slots:</span>
|
|
<span className="text-sm font-semibold">
|
|
<span className="text-amber-400">{golemancy.enabledGolems.length}</span>
|
|
<span className="text-gray-500"> / {maxSlots}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-400">Fabricator Level:</span>
|
|
<span className="text-sm font-semibold text-amber-400">{fabricatorLevel}</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-400">Floor Duration:</span>
|
|
<span className="text-sm font-semibold">{getGolemFloorDuration(skills)} floor(s)</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-gray-400">Status:</span>
|
|
<span className={`text-sm ${inCombat ? 'text-green-400' : 'text-yellow-400'}`}>
|
|
{inCombat ? '⚔️ Combat Active' : '🧩 Puzzle Room (No Golems)'}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Golems are automatically summoned at the start of each combat floor.
|
|
They cost mana to maintain and will be dismissed if you run out.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Active Golems */}
|
|
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
|
<Card className="bg-gray-900/80 border-green-600">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm text-green-400 flex items-center gap-2">
|
|
<Sparkles className="w-4 h-4" />
|
|
Active Golems ({golemancy.summonedGolems.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-2">
|
|
{golemancy.summonedGolems.map(sg => {
|
|
const golem = GOLEMS_DEF[sg.golemId];
|
|
const elem = getElementInfo(golem?.baseManaType || '');
|
|
|
|
return (
|
|
<Badge key={sg.golemId} variant="outline" className="text-sm py-1 px-2">
|
|
<Golem className="w-3 h-3 mr-1" style={{ color: elem?.color }} />
|
|
{golem?.name}
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Golem Selection */}
|
|
{hasGolemancy && (
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Select Golems to Summon</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-96">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
|
{/* Unlocked Golems */}
|
|
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
|
|
|
{/* Locked Golems */}
|
|
{Object.values(GOLEMS_DEF)
|
|
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
|
.map(golem => renderGolemCard(golem.id, false))}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Golemancy Skills Info */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm">Golemancy Skills</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xs text-gray-400 space-y-1">
|
|
<div className="flex justify-between">
|
|
<span>Golem Mastery:</span>
|
|
<span className="text-white">+{skills.golemMastery || 0}0% damage</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Golem Efficiency:</span>
|
|
<span className="text-white">+{(skills.golemEfficiency || 0) * 5}% attack speed</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Golem Longevity:</span>
|
|
<span className="text-white">+{skills.golemLongevity || 0} floor duration</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Golem Siphon:</span>
|
|
<span className="text-white">-{(skills.golemSiphon || 0) * 10}% maintenance</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|