'use client'; import { ShipmentClaimFields, invalidShipmentClause, type ShipmentClauseInput } from './shipment-claim-fields'; import { BattleClaimFields } from './battle-promises'; import { CardCountFields } from './truthtrance-card-count'; import { KnowledgeFactFields } from './truthtrance-knowledge'; import { ForceCountFields, forceCountInputError } from './truthtrance-force-count'; import { isKnowledgeFact, knowledgeFactInputError } from '@/game/truthtrance-knowledge'; import type { PlanClaim } from '@/game/battle-promises'; import { useId, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CHEAP_HERO_TRAITOR } from '@/game/traitors'; import { shipmentPromiseModeSupported } from '@/game/shipment-promises'; import { TRUTH_CARD_NAMES, truthQuestionText, type TruthFact, type TruthQuestion, } from '@/game/truthtrance'; import type { Action, GameView } from '@/game/engine'; import { canUseAsTruthtranceRole } from '@/game/shrine'; export function Truthtrance({ game: g, act, busy, }: { game: GameView; act: (a: Action) => void; busy: boolean; }) { const w = g.truthtrance!; const me = g.players.find((p) => p.id === g.me)!; const current = w.queue[0]; const truthCards = me.hand?.filter((card) => canUseAsTruthtranceRole(g, me, card)) ?? []; const declaration = (cards: typeof truthCards): Action => ({ type: 'card', card: cards[0].id, ...(cards.length > 1 ? { cards: cards.slice(1).map((card) => card.id) } : {}), ...(cards.some((card) => card.effect === 'karama') ? { shrineTruthtrance: cards .filter((card) => card.effect === 'karama') .map((card) => card.id), } : {}), }); const [target, setTarget] = useState( g.players.find((p) => p.id !== me.id)!.id, ); const [kind, setKind] = useState< 'fact' | 'freeform' | 'battlePlan' | 'shipment' >('fact'); const [shipmentJoin, setShipmentJoin] = useState<'single' | 'and' | 'or'>('single'); const [shipmentClauses, setShipmentClauses] = useState([ { territory: 'carthag', minimum: 6 }, { territory: 'arrakeen', minimum: 4 }, ]); const invalidShipment = shipmentClauses.slice(0, shipmentJoin === 'single' ? 1 : 2).some(invalidShipmentClause); const shipmentAvailable = shipmentPromiseModeSupported(g) && g.phase === 5 && g.active === target && !g.players.find((p) => p.id === target)?.shipped && !g.response && !g.decision && !g.phaseOpening; const [planClauses, setPlanClauses] = useState([ { kind: 'dial', compare: 'gte', value: 1 }, { kind: 'weapon', name: null }, ]); const [scope, setScope] = useState<'fact' | 'currentTurn'>('fact'); const [text, setText] = useState(''); const [join, setJoin] = useState<'single' | 'and' | 'or'>('single'); const [clauses, setClauses] = useState([ { kind: 'hand', name: 'Shield' }, { kind: 'hand', name: 'Snooper' }, ]); const factId = useId(); const invalidSpice = (fact: TruthFact) => (fact.kind === 'spice' || fact.kind === 'handCount' || fact.kind === 'handInventory') && (!Number.isSafeInteger(fact.value) || fact.value < 0); const invalidFact = clauses .slice(0, join === 'single' ? 1 : 2) .some((fact) => invalidSpice(fact) || (isKnowledgeFact(fact) && !!knowledgeFactInputError(fact)) || (fact.kind === 'forceCount' && !!forceCountInputError(fact, g, g.players.find(p => p.id === target)!))); const leaderName = (id: string) => id === CHEAP_HERO_TRAITOR ? 'Cheap Hero / Heroine' : (g.allLeaders.find((l) => l.id === id)?.name ?? id); const name = (id: string) => g.players.find((p) => p.id === id)!.name; const question: TruthQuestion = kind === 'fact' ? { kind, target, fact: join === 'single' ? clauses[0] : { kind: join, terms: clauses }, } : kind === 'battlePlan' ? { kind, target, territory: g.battle?.territory ?? '', claim: join === 'single' ? planClauses[0] : { kind: join, terms: planClauses }, } : kind === 'shipment' ? { kind, target, ...(shipmentJoin === 'single' ? shipmentClauses[0] : { claim: { op: shipmentJoin, terms: shipmentClauses } }), } : { kind, target, text, scope }; const button = (label: string, action: Action, disabled = false) => ( ); return (

Truthtrance

Question rules · The table resumes its previous decision after these questions.

{w.stage === 'priority' ? ( <>

A Truthtrance has been declared. Declare yours now or pass; questions then follow storm order.

    {g.players.map((p) => (
  • {p.name} ·{' '} {w.queue.some((q) => q.player === p.id) ? 'Declared' : w.passed.includes(p.id) ? 'Passed' : 'Choosing'}
  • ))}
{!w.passed.includes(me.id) && ( <> {truthCards.map((c) => (
{button( c.effect === 'karama' ? 'Use my Karama as Truthtrance' : 'Declare my Truthtrance', declaration([c]), )}
))} {truthCards.length > 1 && button( `Declare all ${truthCards.length} as Truthtrance`, declaration(truthCards), )} {button('Pass Truthtrance priority', { type: 'truthPass' })} )} ) : ( <>

{name(current.player)} holds the question · {w.queue.length} pending

{w.question && (

{truthQuestionText(w.question, leaderName)}

Asked of {name(w.question.target)}

)} {w.stage === 'answer' ? ( w.question!.target === me.id ? ( <>

Your answer is public. Answer truthfully about the game; a promise can only bind decisions in this turn.

{w.question!.kind === 'shipment' ? ( <>

A definite answer binds your shipment from reserves this turn. Available preparation includes your Ghola, Karama and recoverable allied funding. Yes leaves the exact count, sector and payment yours to choose within the complete statement. No requires that statement to be false; for AND, at least one condition must be false; for OR, every condition must be false.

{(g.truthShipmentAnswers ?? []).map((answer) => (
{button( answer === 'unknown' ? 'I don’t know yet' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer }, )}
))} ) : w.question!.kind === 'battlePlan' ? ( <>

Choose an answer you can honor in a legal plan, including available Ghola preparation. Earlier Truthtrance answers, Voice and revealed prescience elements apply. An answer about a sealed plan is already fixed.

{(g.truthBattleAnswers ?? []).map((answer) => (
{button( answer === 'unknown' ? 'I don’t know yet' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer }, )}
))}

A definite answer constrains your plan. Other plan elements remain yours to choose.

) : w.question!.kind === 'fact' ? ( <>

Your current game information establishes:{' '} {g.truthAnswer === 'unknown' ? 'I don’t know yet' : g.truthAnswer === 'yes' ? 'Yes' : 'No'} . Only you see this until you answer.

{button('Publish my answer', { type: 'truthAnswer', answer: g.truthAnswer, })} ) : ( <> {button('Answer Yes', { type: 'truthAnswer', answer: 'yes', })} {button('Answer No', { type: 'truthAnswer', answer: 'no' })} {button('I don’t know', { type: 'truthAnswer', answer: 'unknown', })}

Use “I don’t know” for something you cannot know or decide. The holder can ask a different question or save the card.

)} ) : ( Waiting for {name(w.question!.target)} to answer. ) ) : current.player !== me.id ? ( Waiting for {name(current.player)} to{' '} {w.stage === 'unknown' ? 'ask again or save the card' : 'ask a question'} . ) : ( <> {w.stage === 'unknown' && ( <>

The answer could not be known. Ask a different question or keep Truthtrance for later.

{button('Save card for later', { type: 'truthSave' })} )} {kind === 'shipment' ? ( <>

Includes Fremen reinforcements and Guild transport from southern reserves. Ground movement and transport of forces already on the board do not count.

{!shipmentAvailable && (

Automatic enforcement currently supports the active player’s unused shipment in base Basic games and base Advanced games without Guild or optional modules. Select that player and finish pending decisions first. Earlier questions and other rules configurations remain unfinished.

)}

{invalidShipment ? 'Enter a whole minimum of one to twenty forces.' : truthQuestionText(question, leaderName)}

) : kind === 'battlePlan' ? ( <> {planClauses .slice(0, join === 'single' ? 1 : 2) .map((c, index) => (
Condition {index + 1} setPlanClauses( planClauses.map((old, n) => n === index ? next : old, ), ) } />
))}

{truthQuestionText(question, leaderName)}

{g.battle && ![g.battle.attacker, g.battle.defender].includes( target, ) &&

Select one of the combatants for this question.

} ) : kind === 'fact' ? ( <> {clauses .slice(0, join === 'single' ? 1 : 2) .map((c, index) => (
Fact {index + 1} {c.kind === 'hand' ? ( ) : c.kind === 'handCount' || c.kind === 'handInventory' ? ( setClauses( clauses.map((old, n) => n === index ? next : old, ), ) } /> ) : c.kind === 'forceCount' ? ( p.id === target)!} onChange={next => setClauses(clauses.map((old, n) => n === index ? next : old))} /> ) : c.kind === 'prediction' || c.kind === 'stormDial' || c.kind === 'stormForecast' ? ( setClauses(clauses.map((old, n) => n === index ? next : old)) } /> ) : c.kind === 'spice' ? ( <>

Counts spice the player personally holds now. Excludes pledged aid and incoming payments. This asks about the current balance; it does not promise future holdings or spending.

{invalidSpice(c) && ( )} ) : ( c.kind === 'traitor' && ( ) )}
))}

{invalidFact ? 'Resolve the marked fields before asking this question.' : truthQuestionText(question, leaderName)}

) : ( <>