'use client'; import type { ReactElement, ReactNode } from 'react'; import type { GameView } from '@/game/engine'; import { mixedActionExpressionSize, parseMixedActionExpression, type MixedActionExpression } from '@/game/mixed-action-question'; import type { TruthFact } from '@/game/truthtrance'; import { truthFactExpressionError } from '@/game/truth-fact-expression'; import { TruthFactFields, invalidTruthFact } from './mixed-shipment-fields'; import { Button } from './ui/button'; export type MixedActionConfig = { prefix: string; legend: string; actionLabel: string; actionTiming: string; actionHelp: ReactNode; fallbackError: string; maxLeaves: number; maxDepth: number; maxNodes?: number; maxTerms?: number; requireFact?: boolean; newAction: () => A; parseAction: (value: unknown, game: GameView, target: string) => A; renderAction: (props: { value: A; id: string; game: GameView; target: string; disabled: boolean; onChange: (action: A) => void }) => ReactNode; }; const newFact = (): MixedActionExpression => ({ kind: 'fact', fact: { kind: 'hand', name: 'Shield' } }); const newFactLeaf = (): TruthFact => ({ kind: 'hand', name: 'Shield' }); function structuralError(node: MixedActionExpression, config: MixedActionConfig): string | null { const size = mixedActionExpressionSize(node); if (size.leaves > config.maxLeaves || size.depth > config.maxDepth) return 'Use at most sixteen conditions and four levels of grouping.'; if (config.maxNodes !== undefined && size.nodes > config.maxNodes) return `Use at most ${config.maxNodes} total clauses, including groups.`; if (config.maxTerms !== undefined && size.maxTerms > config.maxTerms) return `Use at most ${config.maxTerms} conditions in each AND/OR group.`; if (node.kind === 'fact') return truthFactExpressionError(node.fact); if (node.kind === 'action') return null; if (node.terms.length < 2) return 'Join two or more conditions with AND or OR.'; for (const term of node.terms) { const error = structuralError(term, config); if (error) return error; } return null; } /** Current-fact eligibility and each action family's parser share one envelope. */ export function mixedActionInputError(value: unknown, game: GameView, target: string, config: MixedActionConfig): string | null { try { parseMixedActionExpression(value, input => { const fact = input as TruthFact; const error = truthFactExpressionError(fact); if (error) throw new Error(error); if (invalidTruthFact(fact, game, target)) throw new Error('Resolve the marked current-fact fields.'); return fact; }, input => config.parseAction(input, game, target), { maxDepth: config.maxDepth, maxLeaves: config.maxLeaves, ...(config.maxNodes === undefined ? {} : { maxNodes: config.maxNodes }), ...(config.maxTerms === undefined ? {} : { maxTerms: config.maxTerms }), ...(config.requireFact === undefined ? {} : { requireFact: config.requireFact }) }); return null; } catch (error) { return error instanceof Error ? error.message : config.fallbackError; } } /** Preserve the complete original tree, including incomplete numeric drafts. * Structural changes never evaluate current facts or select a future branch. */ export function MixedActionFields({ value, game, target, disabled, onChange, config }: { value: MixedActionExpression; game: GameView; target: string; disabled: boolean; onChange: (expression: MixedActionExpression) => void; config: MixedActionConfig; }) { const newAction = (): MixedActionExpression => ({ kind: 'action', action: config.newAction() }); const attrs = (suffix: string, value: string) => ({ [`data-${config.prefix}-${suffix}`]: value }); const size = mixedActionExpressionSize(value), error = mixedActionInputError(value, game, target, config); const commit = (next: MixedActionExpression) => { if (!disabled && !structuralError(next, config)) onChange(next); }; const renderFact = (fact: TruthFact, path: string, replace: (next: TruthFact) => MixedActionExpression, remove?: () => MixedActionExpression): ReactElement => { const allowed = (next: TruthFact) => !disabled && !structuralError(replace(next), config); const change = (next: TruthFact) => commit(replace(next)); const wrap = (kind: 'and' | 'or'): TruthFact => ({ kind, terms: [fact, newFactLeaf()] }); return
; }; const render = (node: MixedActionExpression, path: string, replace: (next: MixedActionExpression) => MixedActionExpression, remove?: () => MixedActionExpression): ReactElement => { const id = `truth-${config.prefix}-${path}`; const allowed = (next: MixedActionExpression) => !disabled && !structuralError(replace(next), config); const change = (next: MixedActionExpression) => commit(replace(next)); const wrap = (kind: 'and' | 'or'): MixedActionExpression => ({ kind, terms: [node, node.kind === 'fact' ? newAction() : newFact()] }); return ; }; return ; }