import { useState, useMemo } from 'react'; import { toast } from 'sonner'; import { type ZodType, z } from 'zod'; import { PRIMITIVE_REGISTRY } from '../modifiers/primitives/index.js'; import type { EffectPrimitiveNode, PrimitiveKind, EffectPrimitive } from '../modifiers/primitives/types.js'; import type { CustomModifierDescriptor } from '../modifiers/custom/types.js'; import { saveToCustomModifierLibrary, loadCustomModifierLibrary, type SavedCustomModifier, } from '../modifiers/custom/library.js'; import { validateCustomDescriptor } from '../modifiers/custom/validate.js'; import { asCustomModifierId } from '../modifiers/custom/types.js'; interface Props { isOpen: boolean; onClose: () => void; } const CATEGORIES: Record = { State: [ 'seed-attribute', 'add-to-attribute', 'multiply-attribute', 'add-direction', 'set-capture-flag', ], Mechanic: [ 'absorb-damage-with-attribute', 'reflect-damage', 'modify-movement-range', 'block-move-type', 'override-promotion', ], Advanced: [ 'add-aura', 'on-turn-start', 'on-capture', 'on-damaged', 'conditional', ], }; function generateId(): string { return `custom-${Math.random().toString(36).substring(2, 9)}`; } function makeBlankDescriptor(): CustomModifierDescriptor { return { type: 'data', id: asCustomModifierId(generateId()), name: 'New Custom Modifier', description: '', version: 1, primitives: [], targetAttrs: [], // Will be auto-computed based on primitives uiForm: 'primitive-composer', source: 'custom', }; } /** Fallback to default params based on primitive schema */ function generateDefaultParams(schema: ZodType): unknown { try { if (schema instanceof z.ZodDefault) { return (schema as unknown as { _def: { defaultValue: () => unknown } })._def.defaultValue(); } if (schema instanceof z.ZodObject) { const shape = schema.shape as Record>; const result: Record = {}; for (const [key, subSchema] of Object.entries(shape)) { if (subSchema instanceof z.ZodDefault) { result[key] = (subSchema as unknown as { _def: { defaultValue: () => unknown } })._def.defaultValue(); } else if (subSchema instanceof z.ZodNumber) { result[key] = 0; } else if (subSchema instanceof z.ZodString) { result[key] = ''; } else if (subSchema instanceof z.ZodBoolean) { result[key] = false; } else if (subSchema instanceof z.ZodEnum) { result[key] = (subSchema as unknown as { _def: { values: unknown[] } })._def.values[0]; } else if (subSchema instanceof z.ZodArray) { result[key] = []; } else if (subSchema instanceof z.ZodOptional || subSchema instanceof z.ZodNullable) { // Leave it undefined } } return result; } if (schema instanceof z.ZodNumber) return 0; if (schema instanceof z.ZodString) return ''; if (schema instanceof z.ZodBoolean) return false; if (schema instanceof z.ZodEnum) return (schema as unknown as { _def: { values: unknown[] } })._def.values[0]; if (schema instanceof z.ZodArray) return []; } catch (e) { console.error('Failed to generate default params', e); } return {}; // Fallback } export function CustomModifierEditor({ isOpen, onClose }: Props) { const [descriptor, setDescriptor] = useState(makeBlankDescriptor()); const [selectedIndex, setSelectedIndex] = useState(null); // Library state for the "Load" dialog const [showLibrary, setShowLibrary] = useState(false); const [libraryItems, setLibraryItems] = useState([]); // Open library const openLibrary = () => { setLibraryItems(loadCustomModifierLibrary()); setShowLibrary(true); }; const validationResult = useMemo(() => validateCustomDescriptor(descriptor), [descriptor]); const updateDescriptor = (updater: (prev: CustomModifierDescriptor) => CustomModifierDescriptor) => { setDescriptor((prev) => updater(prev)); }; const handleSave = () => { const res = saveToCustomModifierLibrary(descriptor); if (res.ok) { toast.success('Custom modifier saved to library'); } else { toast.error(`Save failed: ${res.reason}`); } }; if (!isOpen) return null; return (
{/* Header */}

Primitive Composer

updateDescriptor((p) => ({ ...p, name: e.target.value }))} placeholder="Modifier Name" className="px-3 py-1 text-sm font-semibold border border-neutral-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" maxLength={40} /> updateDescriptor((p) => ({ ...p, description: e.target.value }))} placeholder="Description (optional)" className="px-3 py-1 text-xs border border-neutral-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" maxLength={200} />
ID: {descriptor.id}
{/* Main Body (3 Columns) */}
{/* Left: Palette */} {/* Center: Tree View */}

Effect Sequence ({descriptor.primitives.length})

{descriptor.primitives.length === 0 ? (
Add primitives from the palette to build the modifier.
) : (
{descriptor.primitives.map((node, i) => { const isSelected = selectedIndex === i; const primitive = PRIMITIVE_REGISTRY.get(node.kind); return (
setSelectedIndex(i)} className={` flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${isSelected ? 'border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-500/20' : 'border-neutral-200 hover:border-neutral-300 bg-white hover:bg-neutral-50'} `} >
{i + 1}
{primitive?.label ?? node.kind}
{JSON.stringify(node.params).substring(0, 60)} {JSON.stringify(node.params).length > 60 ? '...' : ''}
); })}
)}
{/* Right: Inspector */}
{/* Footer: Validation Status */}
{validationResult.ok ? (
Valid Custom Descriptor
) : (
{validationResult.errors.length} Error{validationResult.errors.length !== 1 ? 's' : ''}
)}
{!validationResult.ok && (
{validationResult.errors.map(e => `[${e.path.join('.')}] ${e.message}`).join(' | ')}
)}
{/* Load Modal Overlay */} {showLibrary && (

Load from Library

{libraryItems.length === 0 ? (
No custom modifiers saved yet.
) : (
{libraryItems.map(item => (
{ setDescriptor(item.descriptor); setSelectedIndex(null); setShowLibrary(false); }} >
{item.descriptor.name}
{item.descriptor.primitives.length} primitives
Load
))}
)}
)}
); } /** * Sub-component for rendering the parameter form based on Zod schema introspection. * Since fully parsing arbitrary Zod schemas into UI is complex, we use a hybrid approach: * basic types get inputs, complex types get a JSON textarea fallback. */ function PrimitiveInspector({ node, primitive, onChange }: { node: EffectPrimitiveNode; primitive?: EffectPrimitive; onChange: (params: unknown) => void; }) { if (!primitive) { return
Unknown primitive kind: {node.kind}
; } // Generic fallback for complex schemas (nested arrays, etc) const isComplex = primitive.paramsSchema instanceof z.ZodObject === false; if (isComplex) { const isArray = primitive.paramsSchema instanceof z.ZodArray; return (