'use client'; import { TERRITORIES } from '@/game/board'; import { PHASES } from '@/game/catalog'; import type { GameView } from '@/game/engine'; import type { MixedActionExpression } from '@/game/mixed-action-question'; import { parseBattleActionClaim } from '@/game/mixed-battle-question'; import { battlePowerOf, nativeBattlePowerOpportunity, type BattlePowerClaim } from '@/game/battle-power-question'; import { battleCardLabel, VOICE_KINDS } from '@/game/battle-cards'; import { WINDOW_CARD_EFFECTS, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play'; import { parseTurnActionLeaf, TURN_ACTION_MAX_DEPTH, TURN_ACTION_MAX_LEAVES, type TurnActionLeaf } from '@/game/turn-action-promises'; import { BattleClaimFields } from './battle-promises'; import { MixedActionFields, mixedActionInputError, type MixedActionConfig } from './mixed-action-fields'; import { Input } from './ui/input'; type TurnActionExpression = MixedActionExpression; const recipients = (game: GameView, target: string) => { const player = game.players.find(player => player.id === target); return player ? game.players.filter(recipient => recipient.id !== target && recipient.id !== player.ally) : []; }; /** Public faction names only; casualties, custody and native eligibility stay * on the server. Advanced Atreides may name their Kwisatz Haderach. */ const leaders = (game: GameView, target: string) => { const faction = game.players.find(player => player.id === target)?.faction; return [...game.allLeaders.filter(leader => !!faction && leader.faction === faction), ...(game.advanced && faction === 'atreides' ? [{ id: 'kwisatz', name: 'Kwisatz Haderach' }] : [])]; }; const currentLot = (game: GameView) => game.bidPromiseLot?.turn === game.turn ? game.bidPromiseLot : null; const partners = (game: GameView, target: string) => game.players.some(player => player.id === target) ? game.players.filter(player => player.id !== target) : []; const currentNexus = (game: GameView, target: string) => game.status === 'playing' && game.phase === 1 && game.nexus && !game.spiceWindow && game.alliancePromiseNexus?.turn === game.turn && game.truthMixedAllianceTargets.includes(target) && !game.response && !game.decision && !game.phaseOpening && !game.automaticContinuationPending; const currentBattle = (game: GameView, target: string, kind: 'battlePlan' | 'traitorCall') => { const battle = game.battle; if (game.status !== 'playing' || game.phase !== 6 || !battle || !battle.event || game.response || game.decision || game.phaseOpening || game.automaticContinuationPending) return null; if (kind === 'battlePlan') return game.truthMixedBattleTargets.includes(target) && [battle.attacker, battle.defender].includes(target) ? battle : null; return battle.revealed && game.truthMixedTraitorTargets.includes(target) && battle.traitorVoters.includes(target) && !battle.traitorSubmitted.includes(target) ? battle : null; }; const powerOpportunity = (game: GameView, target: string, claim: BattlePowerClaim) => { try { return nativeBattlePowerOpportunity(game, target, battlePowerOf(claim)); } catch { return null; } }; const newPowerClaim = (game: GameView, target: string): BattlePowerClaim => nativeBattlePowerOpportunity(game, target, 'voice') ? { kind: 'voice', use: true } : nativeBattlePowerOpportunity(game, target, 'prescience') ? { kind: 'prescience', use: true } : { kind: 'voice', use: true }; const windowRoleNames: Record = { weather: 'Weather Control', atomics: 'Family Atomics', harvester: 'Harvester', karamaAuction: 'Spend a printed Karama for this current ordinary lot', karamaShipment: 'Spend a printed Karama for the current shipment-price opportunity', thumper: 'Play Thumper before this turn’s first spice reveal', amal: 'Play Amal in this original phase opening' }; const config: MixedActionConfig = { prefix: 'turn-action', legend: 'Current battle and this-turn action conditions, with optional current facts', actionLabel: 'native battle power, printed card play, battle, alliance, bid, Revival, payment, shipment or ground-movement condition', actionTiming: 'Current battle or future native action · this turn', actionHelp: <>Ordinary payment conditions sum future real bribes to the same recipient after the answer this turn. Earlier payments, gifts and ally aid do not count. The sender pays immediately; the recipient collects at the next Mentat opening. Payments made during Mentat Pause wait until next turn's Mentat opening; collection never counts twice. Ordinary bid conditions use the highest accepted future bid declaration in the named current lot, never the sum of smaller bids. A qualifying bid counts even if another player wins; it does not promise a purchase. Winning still requires the original payment. Ordinary Revival conditions count cumulative future normal and elite physical force returns, with each counter counted once, or an actual future return of a named native leader. Smaller ordinary force returns add together. Earlier returns, requests alone, Ghola, foreign leaders and special card returns do not count. Alliance conditions concern actual final membership when this current Nexus closes. Offers alone are not alliances. A remaining alliance condition settles at the actual close; forming an alliance does not fulfill it early. No can allow temporary membership followed by a break before the close. You cannot promise another player's consent. Battle-plan conditions concern one actual native plan in this current battle, including a plan already sealed or revealed. An open plan commitment settles when both plans are revealed, not merely when your own plan is sealed. Traitor conditions concern one original Reveal or decline declaration in the current revealed battle. The declaration counts before a later Karama or betrayal response can prevent its effect. Native Voice and Prescience conditions concern your original use or explicit decline, exact Voice command, or requested Prescience field in this battle. They count before later Karama prevention. A requested field is not the opponent's answer or inspected value. Borrowed Nexus and special powers do not count. These conditions close at the actual native choice or when its original source expires. Printed-card conditions ask you to play Hajr, Tleilaxu Ghola, Nullentropy Box, Distrans, Juice of Sapho, Semuta Drug, Ornithopter or Residual Poison in its printed role after this answer this turn. An earlier play, discard, sale, Karama role or recovery alone does not count; an actual later replay after Box recovery does. Hajr counts when played even if its extra movement is unused. Tleilaxu Ghola is a card play, separate from ordinary Revival. Hajr conditions close at your own Movement turn end; Tleilaxu Ghola conditions close at the actual turn end. Nullentropy Box counts when its printed activation is accepted and you pay 2 spice to begin the private search. Earlier activation or recovery alone does not count. The selected recovered card stays private and is not promised by this condition. Box conditions close at the actual turn end; complete the paid search with the existing native controls. Distrans and Juice of Sapho conditions count an actual later printed use, not a particular recipient, transferred card or order outcome. Their native controls retain those choices. The deadline is the actual turn end, including for a whole No answer. Semuta Drug counts only after its native transfer and disposal complete after this answer. Committing to inspect multiple candidates, an earlier play or recovery alone does not count; the selected card is not promised. A committed private choice cannot be canceled. The condition, including a whole No answer, has the actual turn-end deadline. Ornithopter counts its actual physical flight declaration after this answer, when the card enters its played zone. Later groups and disposal do not count again. This condition closes at your own Movement turn end; it names no flight mode or destination. Residual Poison counts the actual printed play, random leader death and disposal after this answer, not a selected victim. Its deadline, including for a whole No answer, is the original turn end. Earlier play, disposal or recovery alone does not count. Weather Control, Family Atomics and Harvester conditions count an actual accepted future printed-role play after this answer in the original open Storm or fresh spice-blow window. Earlier plays, disposal, other roles and recovery alone do not count. They do not ask for a selected storm distance or resulting spice amount. Each condition closes only when its original native window closes. A current-lot Karama condition requires spending a printed Karama for this original ordinary lot, through immediate purchase or its winning invoice. Other Karama roles, Worthless conversion and a successful bid alone do not count. Recovery alone does not count. This condition closes with the original ordinary lot. A shipment-price Karama condition requires spending a printed Karama to grant the named active recipient this original unused native shipment price. The giver may be another player. The price grant counts separately from an actual force shipment; it does not require the recipient to ship forces. Other roles, Worthless conversion and recovery alone do not count. This condition closes when that original shipment-price opportunity ends. Thumper conditions count an actual accepted future printed play before this turn's original first spice reveal. Earlier plays, disposal, other roles and recovery alone do not count. A later reveal is a different opportunity. This condition closes when the actual first spice draw begins. Amal conditions count an actual accepted future printed play in this original phase opening. Earlier plays, disposal, other roles and recovery alone do not count. The condition closes with this opening. Amal halves available cash before ordinary payments become legal; preserve enough remaining cash for any payment condition. Shipment conditions concern the single future original reserve shipment after the answer. Ground-movement conditions use the maximum physical forces in one actual future move to that destination; smaller moves never add together. Bid conditions close with their named ordinary lot; Revival conditions close with your native Revival opportunity. Shipment and movement close at your own Movement turn end, while payment conditions last until the actual turn ends. AND requires every condition; OR keeps legal alternatives. No makes the whole expression false, not every condition separately. Original costs, timing, cards, preparation and action rights still apply; this claim grants no extra action., fallbackError: 'Choose a current battle or future native action condition, with optional current facts.', maxLeaves: TURN_ACTION_MAX_LEAVES, maxDepth: TURN_ACTION_MAX_DEPTH, requireFact: false, newAction: () => ({ kind: 'ship', territory: 'carthag', minimum: 1 }), parseAction: (input, game, target) => { const action = parseTurnActionLeaf(input); if (action.kind === 'payment' && !recipients(game, target).some(recipient => recipient.id === action.recipient)) throw new Error('Choose a public non-allied recipient for the future ordinary payment.'); if (action.kind === 'bid' && currentLot(game)?.lot !== action.lot) throw new Error('Choose the current public ordinary lot for the future bid.'); if (action.kind === 'leader' && !leaders(game, target).some(leader => leader.id === action.leader)) throw new Error('Choose a public native leader for the respondent’s faction.'); if (action.kind === 'alliance' && (!currentNexus(game, target) || !partners(game, target).some(partner => partner.id === action.partner))) throw new Error('Choose a public other partner during the current open Nexus.'); if (action.kind === 'battlePlan' || action.kind === 'traitorCall') { const battle = currentBattle(game, target, action.kind); if (!battle || action.battle !== battle.event || action.territory !== battle.territory) throw new Error('Choose this player’s current native battle or unsubmitted Traitor declaration.'); if (action.kind === 'battlePlan') parseBattleActionClaim(action.claim, game.allLeaders.map(leader => leader.id)); } if (action.kind === 'battlePower') { const opportunity = powerOpportunity(game, target, action.claim); if (!opportunity || action.battle !== opportunity.event || action.territory !== opportunity.territory) throw new Error('Choose this player’s original native Voice or Prescience opportunity in the current battle.'); } if (action.kind === 'windowCardPlay') { const opportunity = windowCardOpportunity(game, action.effect); if (!opportunity || opportunity.turn !== game.turn || action.source !== opportunity.source) throw new Error('Choose the original current native window for this printed role, including the first spice reveal or active shipment price.'); } return action; }, renderAction: ({ value, id, game, target, disabled, onChange }) => { const choices = recipients(game, target), leaderChoices = leaders(game, target), lot = currentLot(game), partnerChoices = partners(game, target); const currentPower = value.kind === 'battlePower' ? powerOpportunity(game, target, value.claim) : null; const currentWindow = value.kind === 'windowCardPlay' ? windowCardOpportunity(game, value.effect) : null; return <> {value.kind === 'cardPlay' ? <> : value.kind === 'windowCardPlay' ? <> {!currentWindow &&

This printed role has no current original open native window.

} {value.effect === 'thumper' && game.turn === 1 &&

On turn 1, playing Thumper still discards the card, but its worm encounter is ignored and the normal Spice Card draw continues.

} : value.kind === 'battlePower' ? <> {value.claim.kind === 'voice' || value.claim.kind === 'prescience' ? <> : value.claim.kind === 'voiceCommand' ? <> : value.claim.kind === 'prescienceField' ? <> : null} {!currentPower &&

This respondent has no original native opportunity for this power in the current battle.

} : value.kind === 'battlePlan' || value.kind === 'traitorCall' ? <> {value.kind === 'battlePlan' ?
{ if (!disabled && !('terms' in claim)) onChange({ ...value, claim }); }} />
: <> } : value.kind === 'alliance' ? <> {!currentNexus(game, target) &&

Alliance conditions require the current open Nexus.

} : value.kind === 'bid' ? <> {!lot &&

No public ordinary lot is currently available. Special auctions do not count.

} : value.kind === 'forces' ? <> : value.kind === 'leader' ? <> : value.kind === 'payment' ? <> : <> } {'minimum' in value && <> 20} onChange={event => onChange({ ...value, minimum: event.currentTarget.valueAsNumber })} /> } ; }, }; /** Public schema choices only; the native engine owns eligibility and funding. */ export function turnActionInputError(value: unknown, game: GameView, target: string): string | null { return mixedActionInputError(value, game, target, config); } export function TurnActionFields(props: { value: TurnActionExpression; game: GameView; target: string; disabled: boolean; onChange: (expression: TurnActionExpression) => void; }) { return MixedActionFields({ ...props, config }); }