Plan ordinary promises across one native Charity claim

This commit is contained in:
Quintine committed 2026-10-27 05:52:48 +11:00
1 parent b81823bbec
commit 38738eabac
15 files changed
+831 -96

No files matched your search

+25 -9
View File
@@ -15,31 +15,45 @@ export function BribePromiseGuideButton(props: BribePromiseProps) {
if (!guide) return null;
const actor = game.me, turn = game.turn, phase = game.phase, action = guide.action;
const event = action.bribePromiseEvent, signature = JSON.stringify(action);
const send = props.send, charity = game.charity;
const charityOwner = action.type === 'charity' ? game.players.find(player => player.id === actor) : undefined;
const charitySource = (current: GameView) => {
const owner = current.players.find(player => player.id === actor);
return JSON.stringify({ quote: current.charity, claimed: owner?.charityClaimed, spice: owner?.spice });
};
const charitySignature = action.type === 'charity' ? charitySource(game) : null;
const canAct = () => {
const current = props.game, me = current.players.find(player => player.id === current.me), options = current.bribeOptions;
const target = typeof action.target === 'string' ? current.players.find(player => player.id === action.target) : undefined;
const limit = options.targets.find(recipient => recipient.id === action.target);
return !props.busy && current === game && bribePromiseModeSupported(current) && current.status === 'playing' &&
const payment = action.type === 'bribe' && !!me && !!target && target.id !== actor && target.id !== me.ally &&
options.blocked === null && Number.isSafeInteger(options.available) &&
typeof action.amount === 'number' && action.amount <= options.available &&
!!limit && Number.isSafeInteger(limit.maximum) && action.amount <= limit.maximum &&
!!bribeAction(options, target.id, action.amount);
const claim = action.type === 'charity' && phase === 2 && !!me && me === charityOwner &&
Object.keys(action).every(key => ['type', 'bribePromiseEvent'].includes(key)) &&
!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;
return !props.busy && props.send === send && current === game && bribePromiseModeSupported(current) && current.status === 'playing' &&
Number.isSafeInteger(current.phase) && current.phase >= 0 && current.phase <= 8 && current.phase === phase &&
current.turn === turn && current.me === actor &&
current.bribePromiseGuide === guide && current.bribePromiseGuide.action === action && JSON.stringify(action) === signature &&
action.type === 'bribe' && typeof event === 'string' &&
typeof event === 'string' &&
(liveBribePromises(current.bribePromises, actor, turn).some(promise => promise.event === event) ||
liveBribeExpressionPromises(current.bribeExpressionPromises, actor, turn).some(promise => promise.event === event) ||
current.mixedBribePromises.some(promise => promise.event === event && promise.player === actor && promise.turn === turn &&
!promise.fulfilled && !promise.released)) &&
!!me && !me.autopilot && !!target && target.id !== actor && target.id !== me.ally &&
options.blocked === null && Number.isSafeInteger(options.available) &&
typeof action.amount === 'number' && action.amount <= options.available &&
!!limit && Number.isSafeInteger(limit.maximum) && action.amount <= limit.maximum &&
!!bribeAction(options, target.id, action.amount) &&
!!me && !me.autopilot && (payment || claim) &&
!current.truthtrance && !current.response && !current.decision && !current.phaseOpening &&
!current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed;
};
const recipient = game.players.find(player => player.id === action.target)?.name ?? 'the quoted recipient';
return <Button className="game-action min-h-11 whitespace-normal" data-testid="bribe-promise-guide"
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
Pay {typeof action.amount === 'number' ? action.amount : 'the quoted'} spice to {recipient} while honoring your answers
disabled={!canAct()} onClick={() => { if (canAct()) send(action); }}>
{action.type === 'charity' ? `Claim ${game.charity.amount} Charity spice to continue your payments`
: `Pay ${typeof action.amount === 'number' ? action.amount : 'the quoted'} spice to ${recipient} while honoring your answers`}
</Button>;
}
@@ -64,6 +78,8 @@ export function BribePromises(props: BribePromiseProps) {
The sender pays immediately. The recipient collects the payment at the start of Mentat Pause.
Payments made during Mentat Pause wait until the start of next turn&apos;s Mentat Pause.
Ordinary bribes can be paid out of turn when the current bribe controls permit them; original timing and payment rules still apply.
A legal timeline may use payments now, one remaining Charity claim and later installments.
The future claim is not current spendable spice; only actual payments add to the recorded total.
{' '}<a href="/rules#truthtrance-bribe">Bribe promise rules</a>.
</p>
{liveBribePromises(own, props.game.me, props.game.turn).length > 0 && <BribePromiseGuideButton {...props} />}
+16 -2
View File
@@ -40,6 +40,13 @@ export function TurnActionGuideButton(props: TurnActionProps) {
if (!guide) return null;
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 charity = game.charity;
const charityOwner = action.type === 'charity' ? game.players.find(player => player.id === actor) : undefined;
const charitySource = (current: GameView) => {
const owner = current.players.find(player => player.id === actor);
return JSON.stringify({ quote: current.charity, claimed: owner?.charityClaimed, spice: owner?.spice });
};
const charitySignature = action.type === 'charity' ? charitySource(game) : null;
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')
@@ -263,8 +270,13 @@ export function TurnActionGuideButton(props: TurnActionProps) {
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 charityClaim = action.type === 'charity' && phase === 2 && !!me && me === charityOwner &&
Object.keys(action).every(key => ['type', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key)) &&
!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 = openingReady || earlyWindowReady || earlyWindowBox || printedWindow || 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)
? charityClaim || !current.ready.includes(actor) && (action.type === 'ready' || genericCard)
: phase === 3
? invoice || nativePass || requiredBid || action.type === 'passBid' && !current.richeseAuction && currentAuction && current.auction?.active === actor
: phase === 4
@@ -309,7 +321,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
? game.players.find(player => player.id === actor)?.ally ? 'Break your current alliance while preserving your whole answer'
: 'Withdraw your alliance offer while preserving your whole answer'
: `Accept ${recipient}’s alliance invitation while preserving your whole answer`
: action.type === 'charity' ? 'Claim Charity while preserving your whole answer'
: action.type === 'charity' ? `Claim ${game.charity.amount} Charity spice while preserving your whole answer`
: action.type === 'ready' ? amalReadyAction ? 'Pass this original phase opening while preserving your whole answer'
: phase === 8 ? 'Ready to end this turn while honoring your whole answer'
: earlyWindowReadyAction && (phase === 0 || !!game.spiceWindow) ? `Accept this ${phase === 0 ? 'Storm' : 'fresh spice-blow'} window while preserving your whole answer`
@@ -367,6 +379,8 @@ export function TurnActionPromises(props: TurnActionProps) {
<p className="fine">Count the sum of future real ordinary payments to each named recipient after the answer.
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&apos;s Mentat opening; collection never counts twice.
A legal timeline may use payments now, one remaining Charity claim and later installments.
The future claim is not current spendable spice; only actual payments add to the recorded total.
Bid conditions count the highest accepted future declaration in the named current ordinary lot, never the sum of smaller bids.
A qualifying bid counts even when another player wins; winning still requires the original payment.
Force Revival conditions count cumulative future ordinary returns; normal and elite physical counters each count once.