'use client'; import type { ReactElement } from 'react'; import { parseRevivalExpression, REVIVAL_EXPRESSION_MAX_DEPTH, REVIVAL_EXPRESSION_MAX_LEAVES, type RevivalExpression, } from '@/game/revival-expression-promises'; import { Button } from './ui/button'; import { Input } from './ui/input'; type LeaderChoice = { id: string; name: string }; const newLeaf = (): RevivalExpression => ({ kind: 'forces', counter: 'total', minimum: 1 }); export function revivalExpressionInputError(value: unknown): string | null { try { parseRevivalExpression(value); return null; } catch (error) { return error instanceof Error ? error.message : 'Choose a valid ordinary Revival expression.'; } } // Numeric drafts remain visible and repairable. Only structural controls use // this detached placeholder tree; the user's original numbers are retained. const structuralDraft = (node: RevivalExpression): RevivalExpression => node.kind === 'and' || node.kind === 'or' ? { ...node, terms: node.terms.map(structuralDraft) } : node.kind === 'forces' ? { ...node, minimum: 1 } : node; const inputSize = (node: RevivalExpression): { leaves: number; depth: number } => { if (node.kind !== 'and' && node.kind !== 'or') return { leaves: 1, depth: 0 }; const sizes = node.terms.map(inputSize); return { leaves: sizes.reduce((sum, child) => sum + child.leaves, 0), depth: 1 + Math.max(0, ...sizes.map(child => child.depth)) }; }; /** Hook-free controlled tree, including invalid numeric drafts. Eligibility * and the whole-expression answer remain authoritative on the server. */ export function RevivalExpressionFields({ value, leaders, disabled, onChange }: { value: RevivalExpression; leaders: readonly LeaderChoice[]; disabled: boolean; onChange: (expression: RevivalExpression) => void; }) { const size = inputSize(value), error = revivalExpressionInputError(value); const commit = (next: RevivalExpression) => { if (!disabled && !revivalExpressionInputError(structuralDraft(next))) onChange(next); }; const render = (node: RevivalExpression, path: string, replace: (next: RevivalExpression) => RevivalExpression, remove?: () => RevivalExpression): ReactElement => { const group = node.kind === 'and' || node.kind === 'or'; const id = `truth-revival-expression-${path}`; const allowed = (next: RevivalExpression) => !disabled && !revivalExpressionInputError(structuralDraft(replace(next))); const change = (next: RevivalExpression) => commit(replace(next)); const wrap = (kind: 'and' | 'or'): RevivalExpression => ({ kind, terms: [node, newLeaf()] }); return
{group ? `Revival group ${path} · ${node.kind.toUpperCase()}` : `Revival condition ${path}`} {group ? <> {node.terms.map((term, index) => render(term, `${path}.${index + 1}`, next => replace({ ...node, terms: node.terms.map((old, i) => i === index ? next : old) }), () => { const terms = node.terms.filter((_, i) => i !== index); return replace(terms.length === 1 ? terms[0] : { ...node, terms }); }))}
{(['and', 'or'] as const).map(kind => { const next: RevivalExpression = { ...node, terms: [...node.terms, { kind, terms: [newLeaf(), newLeaf()] }] }; return ; })}
: <> {node.kind === 'forces' ? <> change({ ...node, minimum: event.currentTarget.valueAsNumber })} /> : node.kind === 'leader' ? <> : null} }
{(['and', 'or'] as const).map(kind => )} {remove && }
; }; return
Ordinary Revival expression

AND requires every condition; OR requires at least one. No makes the whole expression false, not necessarily every condition. Legal alternatives remain available; the question does not pin a fixed branch. Count only future ordinary native force and named-leader returns after the answer this turn.

App input limits: {REVIVAL_EXPRESSION_MAX_LEAVES} conditions and {REVIVAL_EXPRESSION_MAX_DEPTH} levels of grouping. Groups do not count as conditions. These are input limits, not additional printed rules.

{size.leaves} / {REVIVAL_EXPRESSION_MAX_LEAVES} conditions · {size.depth} / {REVIVAL_EXPRESSION_MAX_DEPTH} levels of grouping

{error &&

{error}

} {render(value, '1', next => next)}
; }