4b7aa82953
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
- 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
161 lines
8.6 KiB
TypeScript
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';
|