Files
Mana-Loop/src/components/game/tabs/golemancy/GolemDesignBuilder.tsx
T
n8n-gitea 4b7aa82953
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
feat(golemancy): Phase 1 - Component-based construction system data definitions
- Add new golem component types (Core, Frame, MindCircuit, Enchantment)
- Create 4 Core tiers, 7 Frames, 4 Mind Circuits, 8 Enchantments
- Rewrite golem utils for component-based stat computation
- Update GolemancyState with new fields (golemDesigns, golemLoadout, activeGolems)
- Update combat store, actions, and pipelines for new golem system
- Rewrite GolemancyTab with component selection UI
- Update fabricator discipline perks for new system
- Add comprehensive tests for component registries and utilities
- All files under 400 lines, all 743 tests passing
2026-06-06 16:50:26 +02:00

161 lines
8.6 KiB
TypeScript

'use client';
import React from 'react';
import {
CORES, ALL_CORES, FRAMES, ALL_FRAMES,
MIND_CIRCUITS, ALL_MIND_CIRCUITS,
GOLEM_ENCHANTMENTS, ALL_GOLEM_ENCHANTMENTS,
} from '@/lib/game/data/golems';
import type {
CoreDefinition, FrameDefinition, MindCircuitDefinition,
GolemEnchantmentDefinition, ComputedGolemStats,
} from '@/lib/game/data/golems/types';
import { ELEMENTS } from '@/lib/game/constants/elements';
import { ScrollArea } from '@/components/ui/scroll-area';
import clsx from 'clsx';
import { formatRequirement } from './golemancy-utils';
import { ComponentSelector, StatsPreview } from './GolemancySharedComponents';
export interface GolemDesignBuilderProps {
selectedCoreId: string | null;
selectedFrameId: string | null;
selectedCircuitId: string | null;
selectedEnchantmentIds: string[];
designName: string;
selectedManaTypes: string[];
unlockedCoreIds: Set<string>;
unlockedFrameIds: Set<string>;
unlockedCircuitIds: Set<string>;
computedStats: ComputedGolemStats | null;
affordability: { canAfford: boolean; missing: string };
enchantmentCapacity: number;
usedEnchantmentCapacity: number;
golemSlots: number;
enabledCount: number;
onSelectCore: (id: string) => void;
onSelectFrame: (id: string) => void;
onSelectCircuit: (id: string) => void;
onToggleEnchantment: (id: string) => void;
onDesignNameChange: (name: string) => void;
onSaveDesign: () => void;
}
export const GolemDesignBuilder: React.FC<GolemDesignBuilderProps> = ({
selectedCoreId, selectedFrameId, selectedCircuitId,
selectedEnchantmentIds, designName, selectedManaTypes,
unlockedCoreIds, unlockedFrameIds, unlockedCircuitIds,
computedStats, affordability, enchantmentCapacity, usedEnchantmentCapacity,
onSelectCore, onSelectFrame, onSelectCircuit, onToggleEnchantment,
onDesignNameChange, onSaveDesign,
}) => {
const canSaveDesign = selectedCoreId && selectedFrameId && selectedCircuitId && affordability.canAfford;
const selectedCore = selectedCoreId ? CORES[selectedCoreId] ?? null : null;
const selectedFrame = selectedFrameId ? FRAMES[selectedFrameId] ?? null : null;
const selectedCircuit = selectedCircuitId ? MIND_CIRCUITS[selectedCircuitId] ?? null : null;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<ScrollArea className="h-[560px] rounded border border-gray-700 p-3">
<div className="space-y-4">
<div className="space-y-1">
<label className="text-xs text-gray-400">Design Name</label>
<input type="text" value={designName} onChange={e => onDesignNameChange(e.target.value)}
placeholder="Enter a name for this golem..."
className="w-full rounded bg-gray-800 border border-gray-700 px-3 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500" />
</div>
<ComponentSelector label="1. Core (Power Source)" items={ALL_CORES} selectedId={selectedCoreId} unlockedIds={unlockedCoreIds} onSelect={onSelectCore}
renderItem={(core: CoreDefinition, unlocked: boolean) => (
<div>
<div className="flex items-center justify-between">
<span className="font-medium">{core.name}</span>
<span className="text-gray-500">T{core.tier}</span>
</div>
<p className="text-gray-500 mt-0.5">{core.description}</p>
<div className="flex gap-2 mt-1 text-gray-500">
<span>Cap: {core.manaCapacity}</span><span>Regen: {core.manaRegen}/h</span><span>Duration: {core.maxRoomDuration}r</span>
</div>
{!unlocked && <p className="text-red-400 mt-0.5">🔒 {formatRequirement(core.unlockRequirement)}</p>}
</div>
)} />
<ComponentSelector label="2. Frame (Combat Body)" items={ALL_FRAMES} selectedId={selectedFrameId} unlockedIds={unlockedFrameIds} onSelect={onSelectFrame}
renderItem={(frame: FrameDefinition, unlocked: boolean) => (
<div>
<div className="flex items-center justify-between">
<span className="font-medium">{frame.name}</span>
{frame.element && <span style={{ color: ELEMENTS[frame.element]?.color ?? '#888' }}>{ELEMENTS[frame.element]?.sym ?? ''} {frame.element}</span>}
</div>
<p className="text-gray-500 mt-0.5">{frame.description}</p>
<div className="flex gap-2 mt-1 text-gray-500 flex-wrap">
<span>DMG: {frame.baseDamage}</span><span>SPD: {frame.attackSpeed}/h</span>
<span>AP: {Math.round(frame.armorPierce * 100)}%</span><span>MA: {Math.round(frame.magicAffinity * 100)}%</span>
{frame.aoeTargets > 1 && <span>AoE: {frame.aoeTargets}</span>}
{frame.specialEffect !== 'none' && <span className="text-purple-400 capitalize">{frame.specialEffect}</span>}
</div>
{!unlocked && <p className="text-red-400 mt-0.5">🔒 {formatRequirement(frame.unlockRequirement)}</p>}
</div>
)} />
<ComponentSelector label="3. Mind Circuit (Behavior)" items={ALL_MIND_CIRCUITS} selectedId={selectedCircuitId} unlockedIds={unlockedCircuitIds} onSelect={onSelectCircuit}
renderItem={(circuit: MindCircuitDefinition, unlocked: boolean) => (
<div>
<div className="flex items-center justify-between">
<span className="font-medium">{circuit.name}</span>
<span className="text-gray-500">Slots: {circuit.spellSlots}</span>
</div>
<p className="text-gray-500 mt-0.5">{circuit.description}</p>
<div className="flex gap-2 mt-1 text-gray-500"><span className="capitalize">Behavior: {circuit.behavior}</span></div>
{!unlocked && <p className="text-red-400 mt-0.5">🔒 {formatRequirement(circuit.unlockRequirement)}</p>}
</div>
)} />
{selectedCore && selectedFrame && selectedCircuit && (
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-300">4. Enchantments (Optional)
<span className="text-gray-500 font-normal ml-2">{usedEnchantmentCapacity}/{Math.round(enchantmentCapacity)} capacity</span>
</h4>
{usedEnchantmentCapacity > enchantmentCapacity && <p className="text-xs text-red-400">Over capacity! Remove enchantments to save design.</p>}
<div className="grid grid-cols-1 gap-1.5">
{ALL_GOLEM_ENCHANTMENTS.map(enchant => {
const isSelected = selectedEnchantmentIds.includes(enchant.id);
const canAdd = !isSelected && usedEnchantmentCapacity + enchant.capacityCost <= enchantmentCapacity;
return (
<button key={enchant.id} onClick={() => onToggleEnchantment(enchant.id)} disabled={!isSelected && !canAdd}
className={clsx('text-left rounded px-3 py-2 text-xs transition-colors',
isSelected ? 'bg-purple-600/30 border border-purple-500 text-purple-200'
: canAdd ? 'bg-gray-800/60 border border-gray-700 text-gray-300 hover:bg-gray-700/60'
: 'bg-gray-900/40 border border-gray-800 text-gray-600 cursor-not-allowed')}>
<div className="flex items-center justify-between">
<span className="font-medium">{enchant.name}</span>
<span className="text-gray-500">Cost: {enchant.capacityCost}</span>
</div>
<p className="text-gray-500 mt-0.5">{enchant.description}</p>
<p className="text-gray-600 mt-0.5">Effect: {enchant.effect}</p>
</button>
);
})}
</div>
</div>
)}
<div className="pt-2 border-t border-gray-700">
<button onClick={onSaveDesign} disabled={!canSaveDesign}
className={clsx('w-full rounded px-4 py-2 text-sm font-medium transition-colors',
canSaveDesign ? 'bg-blue-600 text-white hover:bg-blue-500' : 'bg-gray-700 text-gray-500 cursor-not-allowed')}>
{canSaveDesign ? 'Save Design' : 'Select all required components'}
</button>
</div>
</div>
</ScrollArea>
<div className="rounded border border-gray-700 p-4">
<h3 className="text-sm font-semibold text-gray-200 mb-3">Stats Preview</h3>
<StatsPreview stats={computedStats} canAfford={affordability} />
</div>
</div>
);
};
GolemDesignBuilder.displayName = 'GolemDesignBuilder';