Bind Truthtrance answers to native battle power declarations
This commit is contained in:
1 parent
c93b693137
commit
09a81a9b65
17 files changed
+1598
-28
No files matched your search
@@ -581,7 +581,7 @@ export function Truthtrance({
|
||||
<option value="mixedBid">Combine current facts and ordinary bids</option>
|
||||
<option value="traitorCall">Bind this battle’s Traitor declaration</option>
|
||||
<option value="mixedTraitorCall">Combine current facts and this Traitor declaration</option>
|
||||
<option value="turnActions">Combine Hajr/Ghola plays, the current battle and this turn’s other actions</option>
|
||||
<option value="turnActions">Combine native battle powers, Hajr/Ghola plays and this turn’s other actions</option>
|
||||
<option value="travel">Combine this turn’s shipment and ground movements</option>
|
||||
<option value="shipment">
|
||||
Bind a shipment from reserves
|
||||
@@ -602,7 +602,7 @@ export function Truthtrance({
|
||||
<>
|
||||
<TurnActionFields value={turnActions} game={g} target={target}
|
||||
disabled={busy || !g.truthTurnActionTargets.includes(target)} onChange={setTurnActions} />
|
||||
{!g.truthTurnActionTargets.includes(target) && <p className="notice">Choose a player with supported printed card plays, current battle, Traitor declaration, alliance, ordinary bid, Revival, payment, shipment or ground-movement opportunities this turn.
|
||||
{!g.truthTurnActionTargets.includes(target) && <p className="notice">Choose a player with supported native Voice or Prescience, printed card plays, current battle, Traitor declaration, alliance, ordinary bid, Revival, payment, shipment or ground-movement opportunities this turn.
|
||||
Ordinary payments may remain available after their Shipment and Movement opportunity ends.</p>}
|
||||
<p className="notice" aria-label="Public turn-action question preview">{turnActionsError ?? truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { TERRITORIES } from '@/game/board';
|
||||
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 { 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';
|
||||
@@ -36,10 +38,18 @@ const currentBattle = (game: GameView, target: string, kind: 'battlePlan' | 'tra
|
||||
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 config: MixedActionConfig<TurnActionLeaf> = {
|
||||
prefix: 'turn-action',
|
||||
legend: 'Current battle and this-turn action conditions, with optional current facts',
|
||||
actionLabel: 'printed card play, battle, alliance, bid, Revival, payment, shipment or ground-movement condition',
|
||||
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.
|
||||
@@ -56,6 +66,10 @@ 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.
|
||||
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 or Tleilaxu Ghola 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.
|
||||
@@ -87,10 +101,16 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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.');
|
||||
}
|
||||
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;
|
||||
return <>
|
||||
<label htmlFor={`${id}-action`}>Future action</label>
|
||||
<select id={`${id}-action`} value={value.kind} disabled={disabled} onChange={event => {
|
||||
@@ -98,6 +118,10 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
if (kind === value.kind) return;
|
||||
const minimum = 'minimum' in value ? value.minimum : 1;
|
||||
if (kind === 'cardPlay') onChange({ kind, effect: 'hajr' });
|
||||
if (kind === 'battlePower') {
|
||||
const claim = newPowerClaim(game, target), opportunity = powerOpportunity(game, target, claim);
|
||||
onChange({ kind, battle: opportunity?.event ?? '', territory: opportunity?.territory ?? '', claim });
|
||||
}
|
||||
if (kind === 'battlePlan') onChange({ kind, battle: game.battle?.event ?? '', territory: game.battle?.territory ?? '',
|
||||
claim: { kind: 'dial', compare: 'eq', value: 0 } });
|
||||
if (kind === 'traitorCall') onChange({ kind, battle: game.battle?.event ?? '', territory: game.battle?.territory ?? '', call: true });
|
||||
@@ -110,6 +134,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
territory: value.kind === 'ship' || value.kind === 'move' ? value.territory : 'carthag', minimum });
|
||||
}}>
|
||||
<option value="cardPlay">Play Hajr or Tleilaxu Ghola in its printed role</option>
|
||||
<option value="battlePower">Declare native Voice or Prescience in this battle</option>
|
||||
<option value="battlePlan">Use an actual current battle-plan choice</option>
|
||||
<option value="traitorCall">Make the current Traitor declaration</option>
|
||||
<option value="alliance">End this Nexus allied with a named partner</option>
|
||||
@@ -130,6 +155,75 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
<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>
|
||||
</select>
|
||||
</> : value.kind === 'battlePower' ? <>
|
||||
<label htmlFor={`${id}-power-claim`}>Original native declaration</label>
|
||||
<select id={`${id}-power-claim`} value={value.claim.kind} disabled={disabled} onChange={event => {
|
||||
const kind = event.currentTarget.value;
|
||||
const claim: BattlePowerClaim | null = kind === 'voice' || kind === 'prescience' ? { kind, use: true }
|
||||
: kind === 'voiceCommand' ? { kind, cardType: 'poison', must: false }
|
||||
: kind === 'prescienceField' ? { kind, field: 'leader' } : null;
|
||||
if (claim) {
|
||||
const opportunity = powerOpportunity(game, target, claim);
|
||||
onChange({ ...value, claim, battle: opportunity?.event ?? '', territory: opportunity?.territory ?? '' });
|
||||
}
|
||||
}}>
|
||||
<option value="voice">Use or explicitly decline native Voice</option>
|
||||
<option value="voiceCommand">Declare an exact native Voice command</option>
|
||||
<option value="prescience">Use or explicitly decline native Prescience</option>
|
||||
<option value="prescienceField">Request an exact native Prescience field</option>
|
||||
</select>
|
||||
<label htmlFor={`${id}-power-battle`}>Original current battle opportunity</label>
|
||||
<select id={`${id}-power-battle`} value={currentPower?.event === value.battle && currentPower.territory === value.territory ? value.battle : ''}
|
||||
disabled={disabled} aria-invalid={!currentPower || currentPower.event !== value.battle || currentPower.territory !== value.territory} onChange={event => {
|
||||
const opportunity = powerOpportunity(game, target, value.claim);
|
||||
if (event.currentTarget.value === '') onChange({ ...value, battle: '', territory: '' });
|
||||
else if (opportunity?.event === event.currentTarget.value)
|
||||
onChange({ ...value, battle: opportunity.event, territory: opportunity.territory });
|
||||
}}>
|
||||
<option value="">Choose an owned native opportunity</option>
|
||||
{currentPower && <option value={currentPower.event}>
|
||||
{game.combatLocations.find(location => location.id === currentPower.territory)?.name ?? currentPower.territory}
|
||||
</option>}
|
||||
</select>
|
||||
{value.claim.kind === 'voice' || value.claim.kind === 'prescience' ? <>
|
||||
<label htmlFor={`${id}-power-use`}>Original choice</label>
|
||||
<select id={`${id}-power-use`} value={value.claim.use ? 'use' : 'decline'} disabled={disabled}
|
||||
aria-invalid={typeof value.claim.use !== 'boolean'} onChange={event => {
|
||||
if (event.currentTarget.value === 'use' || event.currentTarget.value === 'decline')
|
||||
onChange({ ...value, claim: { kind: value.claim.kind as 'voice' | 'prescience', use: event.currentTarget.value === 'use' } });
|
||||
}}>
|
||||
<option value="use">Use this native power</option><option value="decline">Explicitly decline this native power</option>
|
||||
</select>
|
||||
</> : value.claim.kind === 'voiceCommand' ? <>
|
||||
<label htmlFor={`${id}-power-card-type`}>Voice card type</label>
|
||||
<select id={`${id}-power-card-type`} value={VOICE_KINDS.includes(value.claim.cardType) ? value.claim.cardType : ''}
|
||||
disabled={disabled} aria-invalid={!VOICE_KINDS.includes(value.claim.cardType)} onChange={event => {
|
||||
if (value.claim.kind === 'voiceCommand' && (event.currentTarget.value === '' || VOICE_KINDS.includes(event.currentTarget.value)))
|
||||
onChange({ ...value, claim: { ...value.claim, cardType: event.currentTarget.value } });
|
||||
}}>
|
||||
<option value="">Choose a native Voice card type</option>
|
||||
{VOICE_KINDS.map(kind => <option key={kind} value={kind}>{battleCardLabel(kind)}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-power-must`}>Exact Voice command</label>
|
||||
<select id={`${id}-power-must`} value={value.claim.must ? 'must' : 'cannot'} disabled={disabled}
|
||||
aria-invalid={typeof value.claim.must !== 'boolean'} onChange={event => {
|
||||
if (value.claim.kind === 'voiceCommand' && (event.currentTarget.value === 'must' || event.currentTarget.value === 'cannot'))
|
||||
onChange({ ...value, claim: { ...value.claim, must: event.currentTarget.value === 'must' } });
|
||||
}}>
|
||||
<option value="cannot">Opponent cannot play this type</option><option value="must">Opponent must play this type</option>
|
||||
</select>
|
||||
</> : value.claim.kind === 'prescienceField' ? <>
|
||||
<label htmlFor={`${id}-power-field`}>Requested Prescience field</label>
|
||||
<select id={`${id}-power-field`} value={value.claim.field} disabled={disabled} onChange={event => {
|
||||
const field = event.currentTarget.value;
|
||||
if (field === 'leader' || field === 'weapon' || field === 'defense' || field === 'dial')
|
||||
onChange({ ...value, claim: { kind: 'prescienceField', field } });
|
||||
}}>
|
||||
<option value="leader">Leader</option><option value="weapon">Weapon</option>
|
||||
<option value="defense">Defense</option><option value="dial">Number dialed</option>
|
||||
</select>
|
||||
</> : null}
|
||||
{!currentPower && <p className="fine">This respondent has no original native opportunity for this power in the current battle.</p>}
|
||||
</> : value.kind === 'battlePlan' || value.kind === 'traitorCall' ? <>
|
||||
<label htmlFor={`${id}-battle`}>Current battle</label>
|
||||
<select id={`${id}-battle`} value={game.battle?.event === value.battle && game.battle.territory === value.territory ? value.battle ?? '' : ''}
|
||||
|
||||
@@ -7,6 +7,8 @@ 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 { nativeBattlePowerOpportunity, type NativeBattlePower } from '@/game/battle-power-question';
|
||||
import { battleCardLabel, VOICE_KINDS } from '@/game/battle-cards';
|
||||
import { truthQuestionText, type TruthAnswer } from '@/game/truthtrance';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import { Button } from './ui/button';
|
||||
@@ -18,6 +20,7 @@ const publicAllianceContext = (game: GameView) => ({
|
||||
const publicBattleContext = (game: GameView) => game.battle && ({
|
||||
event: game.battle.event, territory: game.battle.territory, attacker: game.battle.attacker, defender: game.battle.defender,
|
||||
revealed: game.battle.revealed, submitted: game.battle.submitted,
|
||||
preparation: game.battle.preparation,
|
||||
traitorVoters: game.battle.traitorVoters, traitorSubmitted: game.battle.traitorSubmitted,
|
||||
});
|
||||
|
||||
@@ -28,6 +31,11 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const actor = game.me, turn = game.turn, phase = game.phase, event = guide.event, action = guide.action;
|
||||
const act = props.act, fillPlan = props.fillPlan, battle = game.battle;
|
||||
const battleSignature = JSON.stringify(publicBattleContext(game));
|
||||
const power: NativeBattlePower | null = action.type === 'voice' ? 'voice' : action.type === 'prescience' ? 'prescience'
|
||||
: action.type === 'declineBattlePower' && (battle?.preparation?.kind === 'voice' || battle?.preparation?.kind === 'prescience')
|
||||
? battle.preparation.kind : null;
|
||||
const powerSource = power ? nativeBattlePowerOpportunity(game, actor, power) : null;
|
||||
const powerSignature = JSON.stringify(powerSource), powerPreparation = battle?.preparation;
|
||||
const signature = JSON.stringify(action), auction = game.auction, decision = game.decision;
|
||||
const native = game.richeseAuction, order = game.revivalOrder, revival = game.revival;
|
||||
const auctionSource = (current: GameView) => current.auction && JSON.stringify({
|
||||
@@ -117,12 +125,22 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
}
|
||||
const currentBattle = phase === 6 && !!battle && current.battle === battle &&
|
||||
JSON.stringify(publicBattleContext(current)) === battleSignature;
|
||||
const powerPacket = Object.keys(action).every(key => ['type', 'turnActionPromiseEvent', 'turnActionPromisePhase',
|
||||
...(action.type === 'voice' ? ['kind', 'must'] : action.type === 'prescience' ? ['field'] : [])].includes(key)) &&
|
||||
(action.type === 'voice' ? typeof action.kind === 'string' && VOICE_KINDS.includes(action.kind) && typeof action.must === 'boolean'
|
||||
: action.type === 'prescience' ? ['leader', 'weapon', 'defense', 'dial'].includes(String(action.field))
|
||||
: action.type === 'declineBattlePower');
|
||||
const ownedPower = currentBattle && !!power && !!powerSource && powerSource.ready && powerSource.owner === actor &&
|
||||
powerSource.event === battle?.event && powerSource.territory === battle?.territory &&
|
||||
current.battle?.preparation === powerPreparation && powerPreparation?.kind === power && powerPacket &&
|
||||
(action.type === 'declineBattlePower' || !powerPreparation.blocked) &&
|
||||
JSON.stringify(nativeBattlePowerOpportunity(current, actor, power)) === powerSignature;
|
||||
const planWindow = !!battle && currentBattle && !battle.revealed && [battle.attacker, battle.defender].includes(actor) && !battle.submitted.includes(actor);
|
||||
const plan = action.type === 'battlePlan' && planWindow && !!fillPlan && props.fillPlan === fillPlan;
|
||||
const battlePreparation = genericCard && planWindow;
|
||||
const declaration = action.type === 'traitorCall' && typeof action.call === 'boolean' && !!battle && currentBattle && battle.revealed &&
|
||||
battle.traitorVoters.includes(actor) && !battle.traitorSubmitted.includes(actor);
|
||||
const opportunity = payment || paidBox || printedHajr || printedGhola || plan || battlePreparation || declaration || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
const opportunity = ownedPower || payment || paidBox || printedHajr || printedGhola || plan || battlePreparation || declaration || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
? !current.ready.includes(actor) && (['charity', 'ready'].includes(action.type) || genericCard)
|
||||
: phase === 3
|
||||
? invoice || nativePass || requiredBid || action.type === 'passBid' && !current.richeseAuction && currentAuction && current.auction?.active === actor
|
||||
@@ -156,6 +174,9 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
? `Play Tleilaxu Ghola to return ${gholaLeader} while honoring your whole answer`
|
||||
: `Play Tleilaxu Ghola to return ${amount} physical forces 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))}`
|
||||
: action.type === 'prescience' ? `Request the ${String(action.field)} field with native Prescience while honoring your whole answer`
|
||||
: action.type === 'declineBattlePower' ? `Explicitly decline native ${power === 'voice' ? 'Voice' : 'Prescience'} while honoring your whole answer`
|
||||
: action.type === 'traitorCall' ? action.call ? 'Reveal traitor while honoring your whole answer' : 'Decline traitor while honoring your whole answer'
|
||||
: action.type === 'alliance' ? action.target === null
|
||||
? game.players.find(player => player.id === actor)?.ally ? 'Break your current alliance while preserving your whole answer'
|
||||
@@ -225,6 +246,9 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
Battle conditions describe your actual plan in the named current battle. An open plan condition settles when both plans are revealed;
|
||||
sealing your own plan does not finish it. A plan already fixed can decide its condition at the answer.
|
||||
Traitor conditions describe your original Reveal or decline declaration, before any later Karama or betrayal prevention.
|
||||
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 or Tleilaxu Ghola 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.
|
||||
|
||||
Reference in new issue
Block a user