'use client'; import { MovementClaimFields } from './movement-claim-fields'; import { TraitorDecisionAnswers } from './traitor-decision-promises'; import { validMovementClaim, type MovementClaim } from '@/game/movement-promises'; import { TruthShipmentQuestion } from './truth-shipment-question'; import { TruthBattlePlanQuestion } from './truth-battle-plan-question'; import type { ShipmentExpression } from '@/game/shipment-promises'; import { MixedShipmentFields, mixedShipmentInputError, } from './mixed-shipment-fields'; import { TruthFactQuestion } from './truth-fact-question'; import type { MixedShipmentExpression } from '@/game/mixed-shipment-question'; import type { PlanClaim } from '@/game/battle-promises'; import { useId, useState } from 'react'; import { Button } from '@/components/ui/button'; import { CHEAP_HERO_TRAITOR } from '@/game/traitors'; import { 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' | 'mixedShipment' | 'movement' | 'traitorCall' >('fact'); const [movement, setMovement] = useState({ territory: 'carthag', minimum: 6 }); const [shipment, setShipment] = useState({ territory: 'carthag', minimum: 6 }); const [mixed, setMixed] = useState({ kind: 'and', terms: [ { kind: 'fact', fact: { kind: 'hand', name: 'Shield' } }, { kind: 'shipment', territory: 'carthag', minimum: 6 }, ], }); const invalidMixed = mixedShipmentInputError(mixed, g, target); const shipmentAvailable = g.truthShipmentTargets.includes(target); const [planClaim, setPlanClaim] = useState({ kind: 'dial', compare: 'gte', value: 1 }); const [scope, setScope] = useState<'fact' | 'currentTurn'>('fact'); const [text, setText] = useState(''); const [fact, setFact] = useState({ kind: 'hand', name: 'Shield' }); const factId = useId(); 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 locationName = (id: string) => g.combatLocations.find(place => place.id === id)!.name; const question: TruthQuestion = kind === 'fact' ? { kind, target, fact, } : kind === 'battlePlan' ? { kind, target, territory: g.battle?.territory ?? '', claim: planClaim, } : kind === 'traitorCall' ? { kind, target, battle: g.battle?.event ?? '', territory: g.battle?.territory ?? '' } : kind === 'movement' ? { kind, target, ...movement } : kind === 'shipment' ? { kind, target, claim: shipment, } : kind === 'mixedShipment' ? { kind, target, mixed } : { 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, locationName)}

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' || w.question!.kind === 'mixedShipment' ? ( <>

A definite answer binds the whole claim about your shipment from reserves this turn. Any current facts are fixed privately when you answer; they create no later card, spice or force holding obligation. Available preparation includes your Ghola, Karama and recoverable allied funding. Count, sector and payment remain yours to choose while honoring the whole claim. AND requires all conditions; OR requires at least one. No makes the whole claim false. A Yes answer may allow skipping shipment if a current-fact branch already satisfies it.

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

Yes requires one matching ground movement before your turn ends, while possible. No forbids every matching move through later Hajr or Ornithopter opportunities. Separate smaller groups do not add together. The No-Field marker itself is not a physical force counter.

{(g.truthMovementAnswers ?? []).map(answer => (
{button(answer === 'unknown' ? 'I don’t know yet' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer })}
))} ) : w.question!.kind === 'traitorCall' ? ( ) : 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 === 'traitorCall' ? ( <>

Choose a still-unsubmitted voter in the current revealed battle. A definite answer binds that player’s original declaration, without reopening any sealed vote or promising that a later response will allow it.

{!g.truthTraitorTargets.includes(target) &&

Reveal both plans and choose a voter who has not submitted their Traitor decision.

} {g.battle?.revealed &&

{truthQuestionText(question, leaderName, locationName)}

} ) : kind === 'movement' ? ( <>

One ground movement of the chosen physical minimum, from now until the respondent’s Movement turn ends. Shipment, HMS relocation and Ambassador relocation do not count. Owned Hajr, Ornithopter, native Kulon and paid Box recovery of a known discard can prepare a later move. No stays binding after earlier nonmatching moves.

{g.phase < 5 &&

Movement comes later this round. Keep the forces, spice and cards needed by your answer; available Charity, your own CHOAM opening income or Revival may prepare the route. Current auction commitments reserve their actual funding. Current preparation appears when it is available.

} {g.advanced &&

Advanced movement counts physical forces, including each elite once. Hajr remains available only immediately after a completed movement and its required arrival decisions.

}

An owned No-Field may prepare a route through its normal shipment, movement or voluntary reveal. The concealed marker does not count toward the promised physical minimum; revelation uses only available reserves.

{!g.truthMovementTargets.includes(target) &&

Choose a supported Basic or Advanced respondent with declared native families and an unused turn this round, and finish pending decisions. Optional modules remain unfinished.

}

{truthQuestionText(question, leaderName, locationName)}

) : kind === 'shipment' || kind === 'mixedShipment' ? ( <> {kind === 'mixedShipment' ? ( ) : question.kind === 'shipment' && ( )}

Includes Fremen reinforcements and Guild transport from southern reserves. Ground movement and transport of forces already on the board do not count. {' '}Guild interception and Karama rate responses resolve before delivery. A stopped shipment can release an impossible answer; an unfinished response does not fulfill it.

{g.phase < 5 &&

Your shipment comes later this turn. Answers use the current board and preparations you can make yourself. A change outside your control can release an answer that becomes impossible. Keep playing the current phase; shipping waits for your turn.

} {!shipmentAvailable && (

Automatic enforcement supports a later shipment this turn under Basic or Advanced rules with selected native faction decks and no optional modules, including the Guild. Active or waiting turns are eligible during Shipment and Movement; existing optional profiles retain their separate scope. Select an eligible player and finish pending decisions first. Native No-Field shipments and other rules configurations remain unfinished.

)} {kind === 'mixedShipment' &&

{invalidMixed ?? truthQuestionText(question, leaderName, locationName)}

} ) : kind === 'battlePlan' ? ( question.kind === 'battlePlan' && ( ) ) : kind === 'fact' ? ( question.kind === 'fact' && ( ) ) : ( <>