Bind Truthtrance Karama promises to shipment beneficiaries
This commit is contained in:
1 parent
fd0b2f0fa5
commit
fd38c51ffd
19 files changed
+1148
-41
No files matched your search
@@ -605,6 +605,7 @@ export function Truthtrance({
|
||||
{!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.
|
||||
Weather Control, Family Atomics and Harvester refer only to their original current Storm or fresh spice-blow window.
|
||||
Printed Karama auction use refers only to the current ordinary lot, through immediate purchase or its winning invoice.
|
||||
Printed Karama shipment use grants the named active recipient’s current native shipment price; it does not require an actual force shipment.
|
||||
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>
|
||||
</>
|
||||
|
||||
@@ -48,7 +48,8 @@ const newPowerClaim = (game: GameView, target: string): BattlePowerClaim =>
|
||||
: nativeBattlePowerOpportunity(game, target, 'prescience') ? { kind: 'prescience', use: true }
|
||||
: { kind: 'voice', use: true };
|
||||
const windowRoleNames: Record<WindowCardEffect, string> = { weather: 'Weather Control', atomics: 'Family Atomics', harvester: 'Harvester',
|
||||
karamaAuction: 'Spend a printed Karama for this current ordinary lot' };
|
||||
karamaAuction: 'Spend a printed Karama for this current ordinary lot',
|
||||
karamaShipment: 'Spend a printed Karama for the current shipment-price opportunity' };
|
||||
const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
prefix: 'turn-action',
|
||||
legend: 'Current battle and this-turn action conditions, with optional current facts',
|
||||
@@ -83,6 +84,10 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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.
|
||||
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.
|
||||
@@ -118,7 +123,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
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 currently open Storm, fresh spice-blow or ordinary-lot window for this printed role.');
|
||||
throw new Error('Choose the original current Storm, fresh spice-blow, ordinary-lot or active shipment-price opportunity for this printed role.');
|
||||
}
|
||||
return action;
|
||||
},
|
||||
@@ -153,7 +158,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="windowCardPlay">Use a printed card in this original Storm, spice-blow or ordinary-lot window</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>
|
||||
<option value="traitorCall">Make the current Traitor declaration</option>
|
||||
@@ -180,7 +185,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
<select id={`${id}-window-effect`} value={WINDOW_CARD_EFFECTS.includes(value.effect) ? value.effect : ''}
|
||||
disabled={disabled} aria-invalid={!WINDOW_CARD_EFFECTS.includes(value.effect)} onChange={event => {
|
||||
const effect = event.currentTarget.value;
|
||||
if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction')
|
||||
if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction' || effect === 'karamaShipment')
|
||||
onChange({ kind: 'windowCardPlay', effect, source: windowCardOpportunity(game, effect)?.source ?? '' });
|
||||
}}>
|
||||
<option value="">Choose a printed role</option>
|
||||
@@ -197,6 +202,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
{currentWindow && <option value={currentWindow.source}>{value.effect === 'harvester'
|
||||
? `Turn ${currentWindow.turn} · Fresh spice blow in ${game.combatLocations.find(location => location.id === game.spiceWindow?.territory)?.name ?? game.spiceWindow?.territory}`
|
||||
: value.effect === 'karamaAuction' ? `Turn ${currentWindow.turn} · Ordinary lot ${game.bidPromiseLot?.lot}`
|
||||
: currentWindow.effect === 'karamaShipment' ? `Turn ${currentWindow.turn} · ${game.players.find(player => player.id === currentWindow.recipient)?.name ?? currentWindow.recipient}’s current native shipment price`
|
||||
: `Turn ${currentWindow.turn} · Before this Storm movement`}</option>}
|
||||
</select>
|
||||
{!currentWindow && <p className="fine">This printed role has no current original open native window.</p>}
|
||||
|
||||
@@ -26,9 +26,9 @@ const publicBattleContext = (game: GameView) => game.battle && ({
|
||||
preparation: game.battle.preparation,
|
||||
traitorVoters: game.battle.traitorVoters, traitorSubmitted: game.battle.traitorSubmitted,
|
||||
});
|
||||
const isWindowEffect = (effect: unknown): effect is Exclude<WindowCardEffect, 'karamaAuction'> => effect === 'weather' || effect === 'atomics' || effect === 'harvester';
|
||||
const isWindowEffect = (effect: unknown): effect is Exclude<WindowCardEffect, 'karamaAuction' | 'karamaShipment'> => effect === 'weather' || effect === 'atomics' || effect === 'harvester';
|
||||
const windowRoleNames: Record<WindowCardEffect, string> = { weather: 'Weather Control', atomics: 'Family Atomics', harvester: 'Harvester',
|
||||
karamaAuction: 'printed Karama for this ordinary lot' };
|
||||
karamaAuction: 'printed Karama for this ordinary lot', karamaShipment: 'printed Karama for this shipment price' };
|
||||
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));
|
||||
@@ -67,12 +67,17 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const currentAuctionWindow = windowCardOpportunity(game, 'karamaAuction');
|
||||
const auctionRoleNamed = !!windowPromise && !!currentAuctionWindow && namesWindow(windowPromise.mixed, 'karamaAuction', currentAuctionWindow.source);
|
||||
const auctionPurchaseAction = action.type === 'card' && action.mode === 'purchase';
|
||||
const currentShipmentWindow = windowCardOpportunity(game, 'karamaShipment');
|
||||
const shipmentRoleNamed = !!windowPromise && !!currentShipmentWindow && namesWindow(windowPromise.mixed, 'karamaShipment', currentShipmentWindow.source);
|
||||
const shipmentPriceAction = action.type === 'card' && action.mode === 'shipment';
|
||||
const printedShipmentAction = shipmentPriceAction && shipmentRoleNamed;
|
||||
const printedInvoiceAction = action.type === 'decision' && action.karama === true && auctionRoleNamed;
|
||||
const cardOwner = action.type === 'card' || printedInvoiceAction ? game.players.find(player => player.id === actor) : undefined;
|
||||
const heldCard = cardOwner?.hand?.find(card => card.id === action.card);
|
||||
const cardSignature = heldCard && JSON.stringify({ id: heldCard.id, kind: heldCard.kind, effect: heldCard.effect });
|
||||
const windowEffect = (auctionPurchaseAction || printedInvoiceAction) && heldCard?.effect === windowCardNativeEffect('karamaAuction')
|
||||
? 'karamaAuction' : isWindowEffect(heldCard?.effect) ? heldCard.effect : null;
|
||||
? 'karamaAuction' : printedShipmentAction && heldCard?.effect === windowCardNativeEffect('karamaShipment')
|
||||
? 'karamaShipment' : isWindowEffect(heldCard?.effect) ? heldCard.effect : null;
|
||||
const cardWindow = windowEffect ? windowCardOpportunity(game, windowEffect) : null;
|
||||
const earlyWindowReadyAction = action.type === 'ready' && (phase === 0 || phase === 1);
|
||||
const publicWindowState = (current: GameView) => JSON.stringify({
|
||||
@@ -82,6 +87,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
stormPending: current.stormPending, wallDestroyed: current.shieldWallDestroyed,
|
||||
blow: current.spiceWindow,
|
||||
...(current.phase === 3 ? { auction: auctionSource(current) } : {}),
|
||||
...(current.phase === 5 ? { shipment: { active: current.active, remaining: current.movementRemaining,
|
||||
windows: current.karamaShipmentWindows, grant: current.karamaShipping } } : {}),
|
||||
...(windowEffect === 'atomics' ? { forces: current.players.find(player => player.id === actor)?.forces,
|
||||
advisors: current.players.find(player => player.id === actor)?.advisors } : {}),
|
||||
});
|
||||
@@ -89,6 +96,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const originalBlow = windowEffect === 'harvester' || earlyWindowReadyAction && phase === 1 ? game.spiceWindow : null;
|
||||
const earlyBoxSignature = box && JSON.stringify({ card: box.card.id, blocked: box.blocked, search: box.search?.event });
|
||||
const auctionCards = game.karamaAuctionCards, auctionCardsSignature = JSON.stringify(auctionCards);
|
||||
const shipmentCards = game.karamaShipmentCards, shipmentCardsSignature = JSON.stringify(shipmentCards);
|
||||
const auctionHandSource = (current: GameView) => {
|
||||
const player = current.players.find(player => player.id === actor);
|
||||
return JSON.stringify({ hand: player?.hand?.map(card => ({ id: card.id, kind: card.kind, effect: card.effect })), limit: player?.handLimit });
|
||||
@@ -126,7 +134,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
? typeof action.leader === 'string' && (action.leader === 'kwisatz' ? ghola.kwisatz : ghola.leaders.some(leader => leader.id === action.leader))
|
||||
: action.elite === undefined ? forceChoices.length === 1 : forceChoices.some(choice => choice.elite === action.elite));
|
||||
const windowPacket = Object.keys(action).every(key => ['type', 'card', 'turnActionPromiseEvent', 'turnActionPromisePhase',
|
||||
...(windowEffect === 'weather' ? ['amount'] : windowEffect === 'karamaAuction' ? ['mode'] : [])].includes(key)) &&
|
||||
...(windowEffect === 'weather' ? ['amount'] : windowEffect === 'karamaAuction' ? ['mode'] : windowEffect === 'karamaShipment' ? ['mode', 'target'] : [])].includes(key)) &&
|
||||
(windowEffect !== 'weather' || typeof action.amount === 'number' && Number.isSafeInteger(action.amount) && action.amount >= 0 && action.amount <= 10);
|
||||
const printedWindowSource = ownPhysicalCard && !!heldCard && heldCard.kind === 'special' && !!windowEffect && !!cardWindow &&
|
||||
heldCard.effect === windowCardNativeEffect(windowEffect) && !!windowPromise &&
|
||||
@@ -137,14 +145,24 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
Array.isArray(current.karamaAuctionCards) &&
|
||||
current.karamaAuctionCards === auctionCards && JSON.stringify(current.karamaAuctionCards) === auctionCardsSignature &&
|
||||
current.karamaAuctionCards.includes(heldCard.id) && auctionHandSource(current) === auctionHandSignature;
|
||||
const nativeShipmentCard = windowEffect === 'karamaShipment' && !!heldCard && cardWindow?.effect === 'karamaShipment' &&
|
||||
phase === 5 && current.active === cardWindow.recipient && action.target === cardWindow.recipient &&
|
||||
Array.isArray(current.karamaShipmentCards) && current.karamaShipmentCards === shipmentCards &&
|
||||
JSON.stringify(current.karamaShipmentCards) === shipmentCardsSignature && current.karamaShipmentCards.includes(heldCard.id);
|
||||
const printedWindow = action.type === 'card' && printedWindowSource && !!heldCard && windowPacket &&
|
||||
(windowEffect === 'karamaAuction' ? action.mode === 'purchase' && nativeAuctionCard && !!me && !current.decision &&
|
||||
Number.isSafeInteger(me.handLimit) && Array.isArray(me.hand) && me.hand.length < me.handLimit
|
||||
: action.mode === undefined && ordinaryCardAvailability(current, heldCard.id, windowEffect === 'weather' ? { amount: action.amount } : undefined)?.available === true);
|
||||
: windowEffect === 'karamaShipment' ? action.mode === 'shipment' && nativeShipmentCard
|
||||
: action.mode === undefined && ordinaryCardAvailability(current, heldCard.id, windowEffect === 'weather' ? { amount: action.amount } : undefined)?.available === true);
|
||||
const printedAuctionInvoice = printedInvoiceAction && printedWindowSource && nativeAuctionCard &&
|
||||
Object.keys(action).every(key => ['type', 'card', 'karama', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key));
|
||||
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !!heldCard && heldCard.effect !== 'ghola' && heldCard.effect !== 'hajr' && !isWindowEffect(heldCard.effect);
|
||||
const earlyWindowBox = (phase === 0 || phase === 1 || phase === 3) && genericCard && heldCard?.effect === 'nullentropyBox' &&
|
||||
const quotedBoxCard = heldCard?.effect !== 'nullentropyBox' || !!box && current.nullentropy === box &&
|
||||
JSON.stringify({ card: box.card.id, blocked: box.blocked, search: box.search?.event }) === earlyBoxSignature &&
|
||||
!box.blocked && !box.search && box.card.id === heldCard.id;
|
||||
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !printedShipmentAction && quotedBoxCard &&
|
||||
(!shipmentPriceAction || (action.target ?? actor) === current.active) &&
|
||||
!!heldCard && heldCard.effect !== 'ghola' && heldCard.effect !== 'hajr' && !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 &&
|
||||
JSON.stringify({ card: box.card.id, blocked: box.blocked, search: box.search?.event }) === earlyBoxSignature &&
|
||||
@@ -253,6 +271,7 @@ 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 === 'card' && windowEffect === 'karamaAuction' ? 'Spend a printed Karama to purchase this ordinary lot while honoring your whole answer'
|
||||
: action.type === 'card' && windowEffect === 'karamaShipment' ? `Spend a printed Karama to grant ${recipient} this native shipment price 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))}`
|
||||
@@ -342,6 +361,10 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
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.
|
||||
Shipment conditions concern one future original reserve shipment. Ground-movement conditions count the maximum physical forces in one actual future move,
|
||||
never the sum of smaller moves. Earlier shipment and movement do not count.
|
||||
Bid conditions close with their named ordinary lot; Revival conditions close with your native Revival opportunity.
|
||||
|
||||
Reference in new issue
Block a user