Support Truthtrance questions about storm dials
This commit is contained in:
1 parent
56b3aeab68
commit
8006e53580
17 files changed
+823
-14
No files matched your search
@@ -7,6 +7,7 @@ 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 { nativeStormDialOpportunity } from '@/game/storm-dial-question';
|
||||
import { karamaBattleCancelOpportunity, 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';
|
||||
@@ -74,6 +75,8 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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.
|
||||
A Storm dial condition names one exact value the respondent will submit in their original current Storm dial.
|
||||
The first Storm allows 0–20; later Basic storms allow 1–3. The other player's value stays sealed until both dials reveal.
|
||||
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.
|
||||
@@ -129,6 +132,8 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
const action = parseTurnActionLeaf(input);
|
||||
if (windowCardOpportunity(game, 'karamaBattleCancel') && (action.kind !== 'windowCardPlay' || action.effect !== 'karamaBattleCancel'))
|
||||
throw new Error('During this response, choose only a printed Karama cancellation of the current Voice or Prescience; current facts may still be combined.');
|
||||
if (action.kind === 'stormDial' && !nativeStormDialOpportunity(game, target, action.source))
|
||||
throw new Error('Choose the respondent’s still-unsubmitted current Storm dial.');
|
||||
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)
|
||||
@@ -168,6 +173,10 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
const kind = event.currentTarget.value;
|
||||
if (kind === value.kind) return;
|
||||
const minimum = 'minimum' in value ? value.minimum : 1;
|
||||
if (kind === 'stormDial') {
|
||||
const opportunity = nativeStormDialOpportunity(game, target);
|
||||
onChange({ kind, source: opportunity?.source ?? '', value: opportunity?.min ?? 0 });
|
||||
}
|
||||
if (kind === 'cardPlay') onChange({ kind, effect: 'hajr' });
|
||||
if (kind === 'windowCardPlay') {
|
||||
const effect = battleCancelOpportunity(game, target) ? 'karamaBattleCancel'
|
||||
@@ -190,6 +199,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="stormDial">Submit an exact value in this current Storm dial</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>
|
||||
@@ -203,7 +213,19 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
<option value="ship">Ship from reserves</option>
|
||||
<option value="move">Make ground movement</option>
|
||||
</select>
|
||||
{value.kind === 'cardPlay' ? <>
|
||||
{value.kind === 'stormDial' ? (() => {
|
||||
const opportunity = nativeStormDialOpportunity(game, target, value.source);
|
||||
return <>
|
||||
<p className="fine">The respondent’s original current Storm dial counts once. Other dials remain sealed until reveal.</p>
|
||||
<label htmlFor={`${id}-storm-dial-value`}>Exact Storm dial value</label>
|
||||
<Input id={`${id}-storm-dial-value`} type="number" min={opportunity?.min ?? (game.turn === 1 ? 0 : 1)}
|
||||
max={opportunity?.max ?? (game.turn === 1 ? 20 : 3)} step={1} required disabled={disabled}
|
||||
value={Number.isFinite(value.value) ? value.value : ''}
|
||||
aria-invalid={!opportunity || !Number.isSafeInteger(value.value) || value.value < opportunity.min || value.value > opportunity.max}
|
||||
onChange={event => onChange({ ...value, value: event.currentTarget.valueAsNumber })} />
|
||||
{!opportunity && <p className="fine">Choose a still-unsubmitted dialer in this current Storm.</p>}
|
||||
</>;
|
||||
})() : 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', 'ornithopter', 'residualPoison'].includes(value.effect)} onChange={event => {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 { nativeStormDialOpportunity } from '@/game/storm-dial-question';
|
||||
import { karamaBattleCancelOpportunity, WINDOW_CARD_EFFECTS, windowCardNativeEffect, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play';
|
||||
import type { TurnActionLeaf } from '@/game/turn-action-promises';
|
||||
import type { MixedActionExpression } from '@/game/mixed-action-question';
|
||||
@@ -35,6 +36,9 @@ const windowRoleNames: Record<WindowCardEffect, string> = { weather: 'Weather Co
|
||||
const namesWindow = (mixed: MixedActionExpression<TurnActionLeaf>, effect: WindowCardEffect, source?: string): boolean =>
|
||||
mixed.kind === 'action' ? mixed.action.kind === 'windowCardPlay' && mixed.action.effect === effect && (source === undefined || mixed.action.source === source)
|
||||
: mixed.kind !== 'fact' && mixed.terms.some(term => namesWindow(term, effect, source));
|
||||
const namesStormDial = (mixed: MixedActionExpression<TurnActionLeaf>, source: string): boolean =>
|
||||
mixed.kind === 'action' ? mixed.action.kind === 'stormDial' && mixed.action.source === source
|
||||
: mixed.kind !== 'fact' && mixed.terms.some(term => namesStormDial(term, source));
|
||||
const namesBoxPlay = (mixed: MixedActionExpression<TurnActionLeaf>): boolean => mixed.kind === 'action'
|
||||
? mixed.action.kind === 'cardPlay' && mixed.action.effect === 'nullentropyBox'
|
||||
: mixed.kind !== 'fact' && mixed.terms.some(namesBoxPlay);
|
||||
@@ -80,6 +84,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const box = game.nullentropy, search = box?.search;
|
||||
const windowPromise = game.turnActionPromises.find(promise => promise.event === event && promise.player === actor && promise.turn === turn);
|
||||
const windowQuestion = windowPromise && JSON.stringify(windowPromise.mixed);
|
||||
const stormDialSource = action.type === 'stormDial' ? nativeStormDialOpportunity(game, actor) : null;
|
||||
const semutaAction = action.type === 'semutaCommit' || action.type === 'semutaSelect' || action.type === 'semutaPass';
|
||||
const semuta = semutaAction ? game.semutaReaction : null;
|
||||
const semutaSource = (reaction: GameView['semutaReaction']) => reaction && JSON.stringify({
|
||||
@@ -410,7 +415,14 @@ 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 || printedResidual || printedDistrans || printedSapho || printedBox || openingReady || earlyWindowReady || earlyWindowBox || printedWindow || ownedPower || payment || paidBox || printedHajr || printedGhola || plan || battlePreparation || declaration || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
const currentStormDial = action.type === 'stormDial' && !!stormDialSource && !!windowPromise &&
|
||||
current.turnActionPromises.includes(windowPromise) && JSON.stringify(windowPromise.mixed) === windowQuestion &&
|
||||
namesStormDial(windowPromise.mixed, stormDialSource.source) &&
|
||||
nativeStormDialOpportunity(current, actor, stormDialSource.source)?.source === stormDialSource.source &&
|
||||
Object.keys(action).every(key => ['type', 'amount', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key)) &&
|
||||
typeof action.amount === 'number' && Number.isSafeInteger(action.amount) &&
|
||||
action.amount >= stormDialSource.min && action.amount <= stormDialSource.max;
|
||||
const opportunity = currentStormDial || 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
|
||||
@@ -445,7 +457,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
: action.scope === 'movement' ? 'remaining Movement order' : action.scope === 'battleOrder' ? 'remaining battle order'
|
||||
: action.scope === 'extortion' ? 'Extortion order' : action.scope === 'revival' ? 'remaining Revival order' : 'the quoted native order';
|
||||
const sector = typeof action.sector === 'number' ? `, sector ${action.sector}` : '';
|
||||
const label = action.type === 'bribe' ? `Pay ${amount} spice to ${recipient} while honoring your whole answer`
|
||||
const label = action.type === 'stormDial' ? `Submit Storm dial ${amount} while honoring your whole answer`
|
||||
: action.type === 'bribe' ? `Pay ${amount} spice to ${recipient} while honoring your whole answer`
|
||||
: action.type === 'semutaCommit' ? 'Commit Semuta Drug to this fresh discard while honoring your whole answer'
|
||||
: action.type === 'semutaSelect' ? `Complete Semuta Drug by claiming ${semuta?.stage === 'select'
|
||||
? semuta.candidates.find(card => card.id === action.card)?.name ?? 'the quoted card' : 'the quoted card'} while honoring your whole answer`
|
||||
|
||||
Reference in new issue
Block a user