Support Truthtrance charity commitments in Basic play
This commit is contained in:
1 parent
6a31315c95
commit
72ff9db5e8
18 files changed
+815
-5
No files matched your search
@@ -21,7 +21,7 @@ export function CharityClaim(props: Props) {
|
||||
const blocked = busy || !!game.truthtrance || !!game.nexusTraitors?.pending || game.charity.incomePending ||
|
||||
!!game.response || !!game.decision || !!game.phaseOpening || !!game.automaticContinuationPending ||
|
||||
!ownTurn || !!game.charity.orderBlocked;
|
||||
const canClaim = !blocked && !me.charityClaimed && game.charity.amount > 0;
|
||||
const canClaim = !blocked && !game.charity.truthBlocked && !me.charityClaimed && game.charity.amount > 0;
|
||||
const canFinish = !!order && !blocked;
|
||||
return (
|
||||
<>
|
||||
@@ -51,6 +51,7 @@ export function CharityClaim(props: Props) {
|
||||
{game.charity.homeworld > 0 &&
|
||||
` Low Homeworld population adds ${game.charity.homeworld} spice directly from the bank${game.charity.multiplier === 2 ? ' after Inflation doubles the bonus' : ''}.`}
|
||||
</p>
|
||||
{game.charity.truthBlocked && <p className="notice" data-testid="charity-truth-blocked">{game.charity.truthBlocked}</p>}
|
||||
<Button
|
||||
data-testid="charity-claim-submit"
|
||||
className="game-action"
|
||||
|
||||
@@ -181,6 +181,7 @@ import { ChoamCashIn } from './choam-cash-in';
|
||||
import { ChoamMarket } from './choam-market';
|
||||
import { BattlePromises } from './battle-promises';
|
||||
import { Truthtrance, TruthHistory } from './truthtrance';
|
||||
import { TruthCharityPromise } from './truth-charity-promise';
|
||||
import {
|
||||
forceRevivalPrice,
|
||||
eliteRevivalRemaining,
|
||||
@@ -2077,6 +2078,7 @@ export function GameTable({
|
||||
{g.status === 'playing' && g.phase < 5 && (
|
||||
<ShipmentPromises game={g} act={act} busy={busy} />
|
||||
)}
|
||||
{g.status === 'playing' && <TruthCharityPromise game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <BribePromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <BribeExpressionPromises game={g} send={act} busy={busy} />}
|
||||
{g.status === 'playing' && <MixedBribePromises game={g} send={act} busy={busy} />}
|
||||
@@ -5839,6 +5841,7 @@ export function GameTable({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{g.phase === 2 && g.truthCharityGuide && <p className="notice">Claim Charity before finishing your Truthtrance Yes.</p>}
|
||||
{[1, 2, 4, 7, 8].includes(g.phase) && !(g.phase === 4 && g.revivalOrder) && !(g.phase === 2 && g.charityOrder) &&
|
||||
actionButton(
|
||||
g.ready.includes(me.id)
|
||||
@@ -5847,7 +5850,7 @@ export function GameTable({
|
||||
? 'Accept spice blow'
|
||||
: 'Ready for next phase',
|
||||
{ type: 'ready' },
|
||||
g.ready.includes(me.id),
|
||||
g.ready.includes(me.id) || (g.phase === 2 && !!g.truthCharityGuide),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { bribeAction } from '@/game/bribe-options';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type Props = { game: GameView; busy: boolean; send: (action: Action) => void };
|
||||
|
||||
/** The server supplies an exact native claim or payment, never a simulated transfer. */
|
||||
export function TruthCharityPromise({ game, busy, send }: Props) {
|
||||
const own = game.truthCharityPromises.filter(promise => promise.turn === game.turn && promise.player === game.me);
|
||||
if (!own.length) return null;
|
||||
const guide = game.truthCharityGuide, actor = game.me, turn = game.turn, action = guide?.action;
|
||||
const signature = JSON.stringify(action), event = action?.charityPromiseEvent;
|
||||
const canAct = () => {
|
||||
const me = game.players.find(player => player.id === actor);
|
||||
const recipient = typeof action?.target === 'string' ? game.players.find(player => player.id === action.target) : undefined;
|
||||
const amount = action?.amount;
|
||||
const payment = action?.type === 'bribe' && !!me && !!recipient && recipient.id !== actor && recipient.id !== me.ally &&
|
||||
typeof amount === 'number' && !!bribeAction(game.bribeOptions, recipient.id, amount);
|
||||
const claim = action?.type === 'charity' && !!me && !me.charityClaimed &&
|
||||
!game.ready.includes(actor) && !game.charity.truthBlocked && !game.charity.incomePending &&
|
||||
!game.charity.orderBlocked && game.charity.amount > 0;
|
||||
return !busy && !!guide && !!action && game.truthCharityGuide === guide && guide.action === action &&
|
||||
JSON.stringify(action) === signature && typeof event === 'string' && action.charityPromiseTurn === turn &&
|
||||
game.truthCharityPromises.some(promise => promise.player === actor && promise.turn === turn &&
|
||||
promise.event === event && promise.answer && !promise.fulfilled && !promise.released) &&
|
||||
game.status === 'playing' && game.phase === 2 && game.me === actor && game.turn === turn &&
|
||||
!!me && !me.autopilot && (payment || claim) && !game.truthtrance && !game.response &&
|
||||
!game.decision && !game.phaseOpening && !game.automaticContinuationPending &&
|
||||
!game.roomControl?.paused && !game.roomControl?.closed;
|
||||
};
|
||||
const recipient = game.players.find(player => player.id === action?.target)?.name ?? 'the quoted recipient';
|
||||
return <section className="notice space-y-3" aria-label="Your Truthtrance Charity promises" id="charity-promise-guidance">
|
||||
<h3>Your Charity promises</h3>
|
||||
<ul>{own.map(promise => <li key={promise.event}>
|
||||
You answered {promise.answer ? 'Yes' : 'No'} to claiming Charity in this phase.
|
||||
{' '}{promise.released ? 'Released: this commitment is no longer possible.'
|
||||
: promise.fulfilled ? 'Fulfilled: this commitment is complete.' : 'This answer remains binding while possible.'}
|
||||
</li>)}</ul>
|
||||
<p>Yes requires an actual eligible claim after the answer; No forbids claiming during the rest of this Charity phase. Earlier claims do not count.</p>
|
||||
{guide && action?.type === 'bribe' && <p className="fine">The quoted payment can make you eligible for Charity. You pay immediately; the other player receives the bribe at the start of Mentat Pause.</p>}
|
||||
{guide && action && <Button className="game-action min-h-11 whitespace-normal" data-testid="charity-promise-guide"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) send(action); }}>
|
||||
{action.type === 'bribe' ? `Pay ${typeof action.amount === 'number' ? action.amount : 'the quoted'} spice to ${recipient} to prepare for Charity`
|
||||
: `Claim ${game.charity.amount} Charity spice to honor your answer`}
|
||||
</Button>}
|
||||
</section>;
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export function Truthtrance({
|
||||
g.players.find((p) => p.id !== me.id)!.id,
|
||||
);
|
||||
const [kind, setKind] = useState<
|
||||
'fact' | 'freeform' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
|
||||
'fact' | 'freeform' | 'charity' | 'battlePlan' | 'shipment' | 'mixedShipment' | 'movement' | 'traitorCall' | 'bid' | 'phaseBid' | 'richeseBid' | 'revival' | 'bribe' | 'alliance' | 'leaderRevival' | 'revivalExpression' | 'bribeExpression' | 'movementExpression' | 'mixedMovement' | 'mixedBribe' | 'mixedRevival' | 'mixedBid' | 'allianceExpression' | 'mixedAlliance' | 'mixedBattlePlan' | 'mixedTraitorCall' | 'travel' | 'turnActions'
|
||||
>('fact');
|
||||
const [revivalLeader, setRevivalLeader] = useState(g.allLeaders.find(leader =>
|
||||
leader.faction === g.players.find(player => player.id === target)?.faction)?.id ?? '');
|
||||
@@ -245,6 +245,7 @@ export function Truthtrance({
|
||||
: kind === 'bid' ? { kind, target, ...bidClaim }
|
||||
: kind === 'phaseBid' ? { kind, target, ...phaseBidClaim }
|
||||
: kind === 'richeseBid' ? { kind, target, turn:g.turn, event:g.richeseAuction?.event ?? '', minimum:bidMinimum }
|
||||
: kind === 'charity' ? { kind, target, turn:g.turn }
|
||||
: kind === 'shipment'
|
||||
? {
|
||||
kind,
|
||||
@@ -431,6 +432,13 @@ export function Truthtrance({
|
||||
{button(`Answer ${answer === 'yes' ? 'Yes' : answer === 'no' ? 'No' : 'I don’t know'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'charity' ? (
|
||||
<>
|
||||
<p className="notice">This asks whether you will claim Charity after your answer in this Charity phase. Yes requires a legal claim while it remains possible; No means you cannot claim during this phase. Native eligibility and payment rules still apply.</p>
|
||||
{(g.truthCharityAnswers ?? []).map(answer => <div key={answer}>
|
||||
{button(answer === 'unknown' ? 'I don’t know' : `Answer ${answer === 'yes' ? 'Yes' : 'No'}`, { type: 'truthAnswer', answer })}
|
||||
</div>)}
|
||||
</>
|
||||
) : w.question!.kind === 'richeseBid' ? (
|
||||
<>
|
||||
<p className="notice">Yes requires a qualifying bid in your one remaining opportunity while possible. No forbids a qualifying bid. Silent amounts remain sealed until all submit; winning still requires payment.</p>
|
||||
@@ -600,6 +608,7 @@ export function Truthtrance({
|
||||
<option value="leaderRevival">Bind a named leader revival this turn</option>
|
||||
<option value="revival">Bind ordinary force revivals this turn</option>
|
||||
<option value="bid">Bind a bid in this ordinary lot</option>
|
||||
<option value="charity">Bind a Charity claim this phase</option>
|
||||
<option value="phaseBid">Bind a bid this Bidding phase</option>
|
||||
<option value="richeseBid">Bind a bid in this Richese lot</option>
|
||||
<option value="mixedBid">Combine current facts and ordinary bids</option>
|
||||
@@ -745,6 +754,12 @@ export function Truthtrance({
|
||||
{!g.truthRevivalTargets.includes(target) && <p className="notice">Choose a respondent whose native Revival opportunity remains this turn and finish pending decisions. Registered native Basic and Advanced tables without optional modules are supported first.</p>}
|
||||
<p className="notice" aria-label="Public revival question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'charity' ? (
|
||||
<>
|
||||
<p className="fine">Ask whether the selected player will claim Charity after answering during this phase. Earlier claims do not count; native eligibility and payment still apply.</p>
|
||||
{!g.truthCharityTargets.includes(target) && <p className="notice">Choose a player with a supported remaining Charity opportunity.</p>}
|
||||
<p className="notice" aria-label="Public Charity question preview">{truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
) : kind === 'richeseBid' ? (
|
||||
<>
|
||||
<RicheseBidPromiseFields minimum={bidMinimum} onMinimum={setBidMinimum} disabled={busy || !g.richeseAuction} method={g.richeseAuction?.method === 'silent' ? 'silent' : 'onceAround'} />
|
||||
@@ -897,6 +912,7 @@ export function Truthtrance({
|
||||
(kind === 'mixedBid' && (!!mixedBidError || !g.truthMixedBidTargets.includes(target))) ||
|
||||
(kind === 'bid' && (!!bidClaimInputError(bidClaim) || !g.truthBidTargets.includes(target))) ||
|
||||
(kind === 'richeseBid' && (!!richeseBidMinimumError(bidMinimum) || !g.richeseAuction || !g.truthRicheseBidTargets.includes(target))) ||
|
||||
(kind === 'charity' && !g.truthCharityTargets.includes(target)) ||
|
||||
(kind === 'phaseBid' && (!!phaseBidClaimInputError(phaseBidClaim) || !g.truthPhaseBidTargets.includes(target))) ||
|
||||
(kind === 'turnActions' && (!!turnActionsError || !g.truthTurnActionTargets.includes(target))) ||
|
||||
(kind === 'travel' && (!!travelError || !g.truthTravelTargets.includes(target))) ||
|
||||
@@ -921,7 +937,7 @@ export function TruthHistory({ game: g }: { game: GameView }) {
|
||||
<details className="notice truthtrance-history">
|
||||
<summary>Truthtrance answers · Turn {g.turn}</summary>
|
||||
<p className="fine">
|
||||
Supported battle-plan, Nexus-alliance, bid, force/leader and grouped revival, bribe, movement and reserve-shipment
|
||||
Supported Charity, battle-plan, Nexus-alliance, bid, force/leader and grouped revival, bribe, movement and reserve-shipment
|
||||
answers are enforced. Current-turn freeform promises remain binding when possible,
|
||||
but their automatic enforcement is unfinished.
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user