318 lines
12 KiB
TypeScript
Executable File
318 lines
12 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { GameCard, StatRow, ElementBadge, ActionButton } from '@/components/ui';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
Mountain, Zap, Clock, Swords, Sparkles, Lock, Check, X,
|
|
Info, HelpCircle
|
|
} from 'lucide-react';
|
|
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
|
import { ELEMENTS } from '@/lib/game/constants';
|
|
import { useManaStore, useSkillStore, useCombatStore, useAttunementStore } from '@/lib/game/stores';
|
|
|
|
export function GolemancyTab() {
|
|
const attunements = useAttunementStore((s) => s.attunements);
|
|
const elements = useManaStore((s) => s.elements);
|
|
const skills = useSkillStore((s) => s.skills);
|
|
const golemancy = useCombatStore((s) => s.golemancy);
|
|
const currentFloor = useCombatStore((s) => s.currentFloor);
|
|
const currentRoom = useCombatStore((s) => s.currentRoom);
|
|
const toggleGolem = useCombatStore((s) => s.toggleGolem);
|
|
const rawMana = useManaStore((s) => s.rawMana);
|
|
|
|
// 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 elementId = golem.baseManaType;
|
|
|
|
if (!isUnlocked) {
|
|
// Locked golem card
|
|
return (
|
|
<GameCard key={golemId} variant="sunken" className="opacity-60">
|
|
<div className="pb-2">
|
|
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
<Lock className="w-4 h-4" />
|
|
<span className="text-[var(--text-muted)]">???</span>
|
|
</h3>
|
|
</div>
|
|
<div className="text-xs text-[var(--text-muted)]">
|
|
{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>
|
|
)}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<GameCard
|
|
key={golemId}
|
|
variant={isEnabled ? "default" : "sunken"}
|
|
className={`transition-all cursor-pointer border-2 ${
|
|
isEnabled
|
|
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
|
: 'border-[var(--border-subtle)] hover:border-[var(--border-default)]'
|
|
}`}
|
|
onClick={() => toggleGolem(golemId)}
|
|
aria-label={`${isEnabled ? 'Disable' : 'Enable'} ${golem.name}`}
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<div className="pb-2">
|
|
<h3 className="text-sm font-semibold flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Mountain className="w-4 h-4" style={{ color: `var(--mana-${elementId})` }} />
|
|
<span style={{ color: `var(--mana-${elementId})` }}>{golem.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{golem.isAoe && (
|
|
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
AOE {golem.aoeTargets}
|
|
</span>
|
|
)}
|
|
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
{golem.tier}
|
|
</span>
|
|
{isEnabled ? (
|
|
<Check className="w-4 h-4 text-[var(--color-success)]" />
|
|
) : (
|
|
<X className="w-4 h-4 text-[var(--text-muted)]" />
|
|
)}
|
|
</div>
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-[var(--text-secondary)]">{golem.description}</p>
|
|
|
|
<Separator className="bg-[var(--border-subtle)]" />
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<StatRow label="DMG:" value={damage.toString()} />
|
|
<StatRow label="Speed:" value={`${attackSpeed.toFixed(1)}/hr`} />
|
|
<StatRow label="Pierce:" value={`${Math.floor(golem.armorPierce * 100)}%`} />
|
|
<StatRow label="Duration:" value={`${floorDuration} floor(s)`} />
|
|
</div>
|
|
|
|
<Separator className="bg-[var(--border-subtle)]" />
|
|
|
|
{/* Summon Cost */}
|
|
<div>
|
|
<div className="text-xs text-[var(--text-secondary)] 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'
|
|
? rawMana
|
|
: elements[cost.element || '']?.current || 0;
|
|
const canAfford = available >= cost.amount;
|
|
|
|
return (
|
|
<span
|
|
key={idx}
|
|
className={`text-xs px-1.5 py-0.5 border rounded ${
|
|
canAfford
|
|
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
|
: 'border-[var(--color-danger)] text-[var(--color-danger)]'
|
|
}`}
|
|
>
|
|
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
|
{' '}{cost.amount}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Maintenance Cost */}
|
|
<div>
|
|
<div className="text-xs text-[var(--text-secondary)] mb-1">Maintenance/hr:</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{golem.maintenanceCost.map((cost, idx) => {
|
|
return (
|
|
<span key={idx} className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
|
{' '}{cost.amount}/hr
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
{isSelected && (
|
|
<div className="mt-2 text-xs text-[var(--color-success)] flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" />
|
|
Active on Floor {currentFloor}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<GameCard>
|
|
<div className="pb-2">
|
|
<h2 className="text-lg font-[var(--font-heading)] font-semibold flex items-center gap-2 text-[var(--text-primary)]">
|
|
<Mountain className="w-5 h-5 text-[var(--mana-earth)]" />
|
|
Golemancy
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{!hasGolemancy ? (
|
|
<div className="text-center text-[var(--text-secondary)] 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>
|
|
) : (
|
|
<>
|
|
<StatRow
|
|
label="Golem Slots:"
|
|
value={`${golemancy.enabledGolems.length} / ${maxSlots}`}
|
|
highlight={golemancy.enabledGolems.length > 0 ? 'success' : undefined}
|
|
/>
|
|
<StatRow
|
|
label="Fabricator Level:"
|
|
value={fabricatorLevel.toString()}
|
|
highlight="warning"
|
|
/>
|
|
<StatRow
|
|
label="Floor Duration:"
|
|
value={`${getGolemFloorDuration(skills)} floor(s)`}
|
|
/>
|
|
<StatRow
|
|
label="Status:"
|
|
value={inCombat ? 'Combat Active' : 'Puzzle Room (No Golems)'}
|
|
highlight={inCombat ? 'success' : 'warning'}
|
|
/>
|
|
|
|
<p className="text-xs text-[var(--text-muted)] 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>
|
|
</GameCard>
|
|
|
|
{/* Active Golems - Empty State */}
|
|
{hasGolemancy && golemancy.summonedGolems.length === 0 && (
|
|
<GameCard variant="sunken">
|
|
<div className="text-center py-4 text-[var(--text-muted)]">
|
|
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">No golems summoned</p>
|
|
<p className="text-xs mt-1">Enable golems below to summon them at the start of combat</p>
|
|
</div>
|
|
</GameCard>
|
|
)}
|
|
|
|
{/* Active Golems */}
|
|
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
|
<GameCard variant="default" className="border-[var(--color-success)]">
|
|
<div className="pb-2">
|
|
<h3 className="text-sm font-semibold flex items-center gap-2 text-[var(--color-success)]">
|
|
<Sparkles className="w-4 h-4" />
|
|
Active Golems ({golemancy.summonedGolems.length})
|
|
</h3>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{golemancy.summonedGolems.map(sg => {
|
|
const golem = GOLEMS_DEF[sg.golemId];
|
|
if (!golem) return null;
|
|
|
|
return (
|
|
<span key={sg.golemId} className="text-xs px-2 py-1 border border-[var(--border-default)] rounded">
|
|
<Mountain className="w-3 h-3 inline mr-1" style={{ color: `var(--mana-${golem.baseManaType})` }} />
|
|
{golem.name}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</GameCard>
|
|
)}
|
|
|
|
{/* Golem Selection */}
|
|
{hasGolemancy && (
|
|
<GameCard>
|
|
<div className="pb-2">
|
|
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Select Golems to Summon</h3>
|
|
</div>
|
|
<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>
|
|
</GameCard>
|
|
)}
|
|
|
|
{/* Golemancy Skills Info */}
|
|
<GameCard>
|
|
<div className="pb-2">
|
|
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Golemancy Skills</h3>
|
|
</div>
|
|
<div className="space-y-1 text-xs">
|
|
<StatRow label="Golem Mastery:" value={`+${(skills.golemMastery || 0) * 10}% damage`} />
|
|
<StatRow label="Golem Efficiency:" value={`+${(skills.golemEfficiency || 0) * 5}% attack speed`} />
|
|
<StatRow label="Golem Longevity:" value={`+${skills.golemLongevity || 0} floor duration`} />
|
|
<StatRow label="Golem Siphon:" value={`-${(skills.golemSiphon || 0) * 10}% maintenance`} />
|
|
</div>
|
|
</GameCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
GolemancyTab.displayName = "GolemancyTab";
|