Connect printed flight and poison plays to Truthtrance
This commit is contained in:
1 parent
b02839ce2a
commit
6a0fba860d
17 files changed
+963
-41
No files matched your search
@@ -76,7 +76,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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 or Semuta Drug in its printed role after this answer this turn.
|
||||
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.
|
||||
@@ -88,6 +88,10 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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.
|
||||
@@ -174,7 +178,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
if (kind === 'ship' || kind === 'move') onChange({ kind,
|
||||
territory: value.kind === 'ship' || value.kind === 'move' ? value.territory : 'carthag', minimum });
|
||||
}}>
|
||||
<option value="cardPlay">Play Hajr, Tleilaxu Ghola, Nullentropy Box, Distrans, Juice of Sapho or Semuta Drug in its printed role</option>
|
||||
<option value="cardPlay">Use a named printed card role after this answer</option>
|
||||
<option value="windowCardPlay">Use a printed card in this original native window or shipment-price opportunity</option>
|
||||
<option value="battlePower">Declare native Voice or Prescience in this battle</option>
|
||||
<option value="battlePlan">Use an actual current battle-plan choice</option>
|
||||
@@ -190,9 +194,9 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
{value.kind === 'cardPlay' ? <>
|
||||
<label htmlFor={`${id}-effect`}>Printed card role after this answer</label>
|
||||
<select id={`${id}-effect`} value={value.effect} disabled={disabled}
|
||||
aria-invalid={!['hajr', 'ghola', 'nullentropyBox', 'distrans', 'juiceOfSapho', 'semutaDrug'].includes(value.effect)} onChange={event => {
|
||||
aria-invalid={!['hajr', 'ghola', 'nullentropyBox', 'distrans', 'juiceOfSapho', 'semutaDrug', 'ornithopter', 'residualPoison'].includes(value.effect)} onChange={event => {
|
||||
const effect = event.currentTarget.value;
|
||||
if (effect === 'hajr' || effect === 'ghola' || effect === 'nullentropyBox' || effect === 'distrans' || effect === 'juiceOfSapho' || effect === 'semutaDrug') onChange({ kind: 'cardPlay', effect });
|
||||
if (effect === 'hajr' || effect === 'ghola' || effect === 'nullentropyBox' || effect === 'distrans' || effect === 'juiceOfSapho' || effect === 'semutaDrug' || effect === 'ornithopter' || effect === 'residualPoison') onChange({ kind: 'cardPlay', effect });
|
||||
}}>
|
||||
<option value="hajr">Play Hajr in its printed role after this answer this turn</option>
|
||||
<option value="ghola">Play Tleilaxu Ghola in its printed role after this answer this turn</option>
|
||||
@@ -200,6 +204,8 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
<option value="distrans">Play Distrans in its printed role after this answer this turn</option>
|
||||
<option value="juiceOfSapho">Play Juice of Sapho in its printed role after this answer this turn</option>
|
||||
<option value="semutaDrug">Complete a printed Semuta Drug transfer and disposal after this answer this turn</option>
|
||||
<option value="ornithopter">Declare a physical Ornithopter flight after this answer this turn</option>
|
||||
<option value="residualPoison">Complete a printed Residual Poison play after this answer this turn</option>
|
||||
</select>
|
||||
</> : value.kind === 'windowCardPlay' ? <>
|
||||
<label htmlFor={`${id}-window-effect`}>Printed role in the original window</label>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { nativePromiseScopeSupported } from '@/game/native-promise-scope';
|
||||
import { quoteNexusAlliance } from '@/game/nexus-alliance';
|
||||
import { ordinaryCardAvailability } from '@/game/card-availability';
|
||||
import { gholaForcePreparations } from '@/game/ghola-force-preparation';
|
||||
import { richeseCardDefinition } from '@/game/richese-cards';
|
||||
import { nativeBattlePowerOpportunity, type NativeBattlePower } from '@/game/battle-power-question';
|
||||
import { battleCardLabel, VOICE_KINDS } from '@/game/battle-cards';
|
||||
import { WINDOW_CARD_EFFECTS, windowCardNativeEffect, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play';
|
||||
@@ -36,7 +37,7 @@ const namesWindow = (mixed: MixedActionExpression<TurnActionLeaf>, effect: Windo
|
||||
const namesBoxPlay = (mixed: MixedActionExpression<TurnActionLeaf>): boolean => mixed.kind === 'action'
|
||||
? mixed.action.kind === 'cardPlay' && mixed.action.effect === 'nullentropyBox'
|
||||
: mixed.kind !== 'fact' && mixed.terms.some(namesBoxPlay);
|
||||
const namesPrintedPlay = (mixed: MixedActionExpression<TurnActionLeaf>, effect: 'distrans' | 'juiceOfSapho' | 'semutaDrug'): boolean => mixed.kind === 'action'
|
||||
const namesPrintedPlay = (mixed: MixedActionExpression<TurnActionLeaf>, effect: 'distrans' | 'juiceOfSapho' | 'semutaDrug' | 'residualPoison'): boolean => mixed.kind === 'action'
|
||||
? mixed.action.kind === 'cardPlay' && mixed.action.effect === effect
|
||||
: mixed.kind !== 'fact' && mixed.terms.some(term => namesPrintedPlay(term, effect));
|
||||
|
||||
@@ -144,13 +145,32 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const owner = current.players.find(player => player.id === actor);
|
||||
return JSON.stringify({ hand: owner?.hand?.map(card => ({ id: card.id, kind: card.kind, effect: card.effect })), limit: owner?.handLimit });
|
||||
};
|
||||
const printedHandSignature = heldCard?.effect === 'distrans' || heldCard?.effect === 'juiceOfSapho' ? printedHandSource(game) : null;
|
||||
const printedHandSignature = heldCard?.effect === 'distrans' || heldCard?.effect === 'juiceOfSapho' || heldCard?.effect === 'residualPoison' ? printedHandSource(game) : null;
|
||||
const distransCapacitySource = (current: GameView) => {
|
||||
const recipient = current.players.find(player => player.id === action.target);
|
||||
return JSON.stringify(recipient && { id: recipient.id, count: recipient.handCount, limit: recipient.handLimit });
|
||||
};
|
||||
const distransCapacitySignature = heldCard?.effect === 'distrans' ? distransCapacitySource(game) : null;
|
||||
const saphoDecisionSignature = heldCard?.effect === 'juiceOfSapho' ? JSON.stringify(decision) : null;
|
||||
const residual = heldCard?.effect === 'residualPoison' ? game.residualPoison : null;
|
||||
const residualSource = (current: GameView) => JSON.stringify({ quote: current.residualPoison,
|
||||
battle: publicBattleContext(current), preLeader: current.battle?.preLeader });
|
||||
const residualSignature = heldCard?.effect === 'residualPoison' ? residualSource(game) : null;
|
||||
const flightFields = ['movementCard', 'ornithopter', 'ornithopterEvent'];
|
||||
const flightTagged = action.type === 'move' && flightFields.some(key => Object.hasOwn(action, key));
|
||||
const flightNeeded = action.type === 'move' && (flightTagged || !!game.ornithopter?.active);
|
||||
const flight = flightNeeded ? game.ornithopter : null, flightSignature = flightNeeded ? JSON.stringify(flight) : null;
|
||||
const flightOwner = flightNeeded ? game.players.find(player => player.id === actor) : undefined;
|
||||
const flightInitial = action.type === 'move' && Object.hasOwn(action, 'movementCard');
|
||||
const flightCard = flightInitial && typeof action.movementCard === 'string'
|
||||
? flightOwner?.hand?.find(card => card.id === action.movementCard) : undefined;
|
||||
const flightOwnerSource = (current: GameView) => {
|
||||
const owner = current.players.find(player => player.id === actor);
|
||||
return JSON.stringify({ forces: owner?.forces, elites: owner?.elites?.forces, advisors: owner?.advisors,
|
||||
moved: owner?.moved, movesAllowed: owner?.movesAllowed, shipped: owner?.shipped,
|
||||
...(flightInitial ? { hand: owner?.hand?.map(card => ({ id: card.id, kind: card.kind, effect: card.effect })) } : {}) });
|
||||
};
|
||||
const flightOwnerSignature = flightNeeded ? flightOwnerSource(game) : null;
|
||||
const auctionCards = game.karamaAuctionCards, auctionCardsSignature = JSON.stringify(auctionCards);
|
||||
const shipmentCards = game.karamaShipmentCards, shipmentCardsSignature = JSON.stringify(shipmentCards);
|
||||
const thumperCards = game.thumperCards, thumperCardsSignature = JSON.stringify(thumperCards);
|
||||
@@ -178,6 +198,22 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
(semuta.stage === 'offer' ? !semuta.passed && (action.type === 'semutaPass' && !semuta.passBlocked ||
|
||||
action.type === 'semutaCommit' && semuta.canCommit && semuta.blocked === null)
|
||||
: action.type === 'semutaSelect' && typeof action.card === 'string' && semuta.candidates.some(card => card.id === action.card));
|
||||
const flightPacket = Object.keys(action).every(key => ['type', 'from', 'amount', 'elite', 'forces', 'eliteForces', 'territory', 'sector',
|
||||
'fighters', 'noField', 'event', 'planetologist', 'turnActionPromiseEvent', 'turnActionPromisePhase',
|
||||
...(flightInitial ? ['movementCard', 'ornithopter'] : ['ornithopterEvent'])].includes(key));
|
||||
const flightSource = flightNeeded && phase === 5 && current.active === actor && !!me && me === flightOwner && !!flight &&
|
||||
current.ornithopter === flight && JSON.stringify(current.ornithopter) === flightSignature && flightOwnerSource(current) === flightOwnerSignature &&
|
||||
flight.blocked === null && flightPacket && !!windowPromise && current.turnActionPromises.includes(windowPromise) &&
|
||||
JSON.stringify(windowPromise.mixed) === windowQuestion;
|
||||
const nativeFlight = flightSource && !!flight && (flightInitial ? !flight.active && !!flightCard &&
|
||||
me?.hand?.find(card => card.id === action.movementCard) === flightCard && flightCard.kind === 'special' &&
|
||||
richeseCardDefinition(flightCard)?.card.effect === 'ornithopter' && flight.card.id === flightCard.id &&
|
||||
richeseCardDefinition(flight.card)?.card.effect === 'ornithopter' &&
|
||||
(action.ornithopter === 'range3' || action.ornithopter === 'twoGroups') &&
|
||||
flight.modes.some(mode => mode.mode === action.ornithopter && mode.blocked === null)
|
||||
: !!flight.active && Number.isSafeInteger(flight.active.remaining) && flight.active.remaining > 0 &&
|
||||
action.ornithopterEvent === flight.active.event);
|
||||
const movementPacket = action.type !== 'move' || (!flightTagged && !current.ornithopter?.active) || nativeFlight;
|
||||
const ownPhysicalCard = (action.type === 'card' || printedInvoiceAction) && !!heldCard && !!me && me === cardOwner &&
|
||||
me.hand?.find(card => card.id === action.card) === heldCard &&
|
||||
JSON.stringify({ id: heldCard.id, kind: heldCard.kind, effect: heldCard.effect }) === cardSignature;
|
||||
@@ -267,10 +303,17 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
Object.keys(action).every(key => ['type', 'card', 'scope', 'event', 'mode', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key));
|
||||
const saphoDecision = printedSapho && phase === 8 && action.scope === 'extortion' && current.decision === decision &&
|
||||
current.decision?.kind === 'moritaniExtortion' && JSON.stringify(current.decision) === saphoDecisionSignature;
|
||||
const printedResidual = printedRoleSource && heldCard?.effect === 'residualPoison' && namesPrintedPlay(windowPromise!.mixed, 'residualPoison') &&
|
||||
richeseCardDefinition(heldCard)?.card.effect === 'residualPoison' && !!residual && current.residualPoison === residual &&
|
||||
residualSource(current) === residualSignature && residual.blocked === null && residual.card.id === heldCard.id &&
|
||||
richeseCardDefinition(residual.card)?.card.effect === 'residualPoison' && phase === 6 && !!battle && current.battle === battle &&
|
||||
!battle.revealed && [battle.attacker, battle.defender].includes(actor) && action.event === battle.event && action.event === residual.event &&
|
||||
action.target === residual.target && action.target === (battle.attacker === actor ? battle.defender : battle.attacker) &&
|
||||
Object.keys(action).every(key => ['type', 'card', 'event', 'target', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key));
|
||||
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !printedShipmentAction && quotedBoxCard &&
|
||||
(!boxRoleNamed || heldCard?.effect !== 'nullentropyBox' || printedBox) &&
|
||||
(!shipmentPriceAction || (action.target ?? actor) === current.active) &&
|
||||
!!heldCard && heldCard.effect !== 'ghola' && heldCard.effect !== 'hajr' && heldCard.effect !== 'distrans' && heldCard.effect !== 'juiceOfSapho' && heldCard.effect !== 'semutaDrug' && !isWindowEffect(heldCard.effect);
|
||||
!!heldCard && heldCard.effect !== 'ghola' && heldCard.effect !== 'hajr' && heldCard.effect !== 'distrans' && heldCard.effect !== 'juiceOfSapho' && heldCard.effect !== 'semutaDrug' && heldCard.effect !== 'ornithopter' && heldCard.effect !== 'residualPoison' && !isWindowEffect(heldCard.effect);
|
||||
const earlyWindowBox = (phase === 0 || phase === 1 || phase === 3 || phase === 5) && genericCard && heldCard?.effect === 'nullentropyBox' &&
|
||||
action.mode === undefined && Object.keys(action).every(key => ['type', 'card', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key)) &&
|
||||
!!box && current.nullentropy === box &&
|
||||
@@ -353,7 +396,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
!current.ready.includes(actor) && me.charityClaimed === false && current.charity === charity &&
|
||||
charitySource(current) === charitySignature && current.charity.incomePending === false &&
|
||||
Number.isSafeInteger(current.charity.amount) && current.charity.amount > 0;
|
||||
const opportunity = nativeSemuta || printedDistrans || printedSapho || printedBox || openingReady || earlyWindowReady || earlyWindowBox || printedWindow || ownedPower || payment || paidBox || printedHajr || printedGhola || plan || battlePreparation || declaration || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
const opportunity = nativeSemuta || printedResidual || printedDistrans || printedSapho || printedBox || openingReady || earlyWindowReady || earlyWindowBox || printedWindow || ownedPower || payment || paidBox || printedHajr || printedGhola || plan || battlePreparation || declaration || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
? charityClaim || !current.ready.includes(actor) && (action.type === 'ready' || genericCard)
|
||||
: phase === 3
|
||||
? invoice || nativePass || requiredBid || action.type === 'passBid' && !current.richeseAuction && currentAuction && current.auction?.active === actor
|
||||
@@ -367,7 +410,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
return !props.busy && props.act === act && current === game && nativePromiseScopeSupported(current) && current.status === 'playing' &&
|
||||
Number.isSafeInteger(phase) && phase >= 0 && phase <= 8 && current.phase === phase && current.turn === turn && current.me === actor &&
|
||||
current.turnActionGuide === guide && guide.event === event && guide.action === action &&
|
||||
action.turnActionPromiseEvent === event && action.turnActionPromisePhase === phase && JSON.stringify(action) === signature && opportunity &&
|
||||
action.turnActionPromiseEvent === event && action.turnActionPromisePhase === phase && JSON.stringify(action) === signature && opportunity && movementPacket &&
|
||||
current.turnActionPromises.some(promise => promise.event === event && promise.player === actor && promise.turn === turn &&
|
||||
!promise.fulfilled && !promise.released) &&
|
||||
(!amalReadyAction || openingReady) &&
|
||||
@@ -401,6 +444,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
: action.type === 'card' && heldCard?.effect === 'distrans' ? `Give ${distrans?.choices.find(choice => choice.recipient === action.target)?.cards.find(card => card.id === action.give)?.name ?? 'the quoted card'} to ${recipient} with Distrans while honoring your whole answer`
|
||||
: action.type === 'card' && heldCard?.effect === 'juiceOfSapho' ? `Play Juice of Sapho: ${action.scope === 'battleAggressor'
|
||||
? 'become this battle’s aggressor' : `${saphoScope}, ${String(action.mode)}`} while honoring your whole answer`
|
||||
: action.type === 'card' && heldCard?.effect === 'residualPoison' ? `Play Residual Poison against ${recipient} while honoring your whole answer; the native leader death is random`
|
||||
: action.type === 'move' && flightInitial ? `Declare Ornithopter ${action.ornithopter === 'range3' ? 'three-territory' : 'two-group'} flight and move ${amount} physical forces to ${destination}${sector} while honoring your whole answer`
|
||||
: action.type === 'card' && windowEffect ? `Play ${windowRoleNames[windowEffect]} in its original native window${windowEffect === 'weather' ? ` with storm distance ${amount}` : ''} while honoring your whole answer`
|
||||
: action.type === 'battlePlan' ? 'Fill the quoted battle plan for review before sealing'
|
||||
: action.type === 'voice' ? `Declare native Voice: opponent ${action.must ? 'must' : 'cannot'} play ${battleCardLabel(String(action.kind))}`
|
||||
@@ -493,7 +538,7 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
Native Voice and Prescience conditions describe your original use or explicit decline, exact Voice command,
|
||||
or requested Prescience field, before later Karama prevention. The requested field is not the inspected answer or 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 count actual future Hajr, Tleilaxu Ghola, Nullentropy Box, Distrans, Juice of Sapho or Semuta Drug plays in their printed role after the answer this turn.
|
||||
Printed-card conditions count actual future Hajr, Tleilaxu Ghola, Nullentropy Box, Distrans, Juice of Sapho, Semuta Drug, Ornithopter or Residual Poison plays in their printed role after the answer this turn.
|
||||
Earlier plays, discards, sales, Karama roles and recovery alone do not count; an actual later Box replay does.
|
||||
Hajr counts even if its extra movement is unused and closes at your own Movement turn end.
|
||||
Tleilaxu Ghola is separate from ordinary Revival and remains open until the actual turn ends.
|
||||
@@ -505,6 +550,10 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
Semuta Drug counts only when its native transfer and disposal actually complete after the 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. Its deadline, including for a whole No answer, is the actual turn end.
|
||||
Ornithopter counts its actual physical flight declaration after the answer, when the card enters its played zone.
|
||||
Later groups and disposal do not count again. Its deadline is your own Movement turn end; no flight mode or destination is promised.
|
||||
Residual Poison counts the actual printed play, random leader death and disposal after the answer; no victim is promised.
|
||||
Its whole No deadline 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 the 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 promise a selected storm distance or resulting spice amount. Each condition closes only when its original native window closes.
|
||||
@@ -564,12 +613,15 @@ export function TurnActionAnswers(props: TurnActionProps) {
|
||||
A remaining alliance condition stays open through temporary membership until that close. Neither answer promises another player’s consent.
|
||||
Current battle-plan conditions use the actual native plan; an open condition settles when both plans are revealed.
|
||||
Traitor conditions use the original Reveal or decline declaration, before later Karama or betrayal prevention.
|
||||
Hajr, Tleilaxu Ghola, Nullentropy Box, Distrans, Juice of Sapho and Semuta Drug conditions require actual later printed-role plays, not earlier play, disposal or recovery alone.
|
||||
Printed-card conditions require the actual later named printed use, not earlier play, disposal or recovery alone.
|
||||
Box counts at accepted activation and payment of 2 spice; the recovered card is private and is not promised by this condition.
|
||||
Hajr closes at your own Movement turn end even if its extra move is unused; Ghola and Box close at the actual turn end.
|
||||
Distrans and Juice of Sapho, including No, close at the actual turn end; a recipient, transferred card or order outcome is not promised.
|
||||
Semuta Drug counts the completed native transfer and disposal, not a multi-choice inspection commitment or recovery alone.
|
||||
Its selected card is not promised; a committed private choice cannot be canceled. Its whole No deadline is the actual turn end.
|
||||
Ornithopter counts its physical flight declaration once, before later groups or disposal, and closes at your own Movement turn end.
|
||||
Residual Poison counts its actual printed play, random leader death and disposal. No mode, destination or victim is promised;
|
||||
its whole No deadline is the original turn end.
|
||||
Bids close with their lot; Revival closes with your native Revival opportunity.
|
||||
Shipment and movement close at your own Movement turn end; payment conditions last until the actual turn ends.
|
||||
A known Inflation Double opening can close payments earlier without closing Ghola.
|
||||
|
||||
Reference in new issue
Block a user