Support earlier classic Basic movement promises

Allow current-round classic Basic questions before Movement, with owned Charity, ordinary auction payment and Revival preparation. Reopen readiness, retain combined shipment obligations, reset stale allowances and release forced storm impossibility. Current-phase guidance and minimal bots preserve the route; held Karama now prepares its actual Guild shipment rate.

Validation: affected rules/control tests, types/lint, production build and four gameplay/control/table-rendering programmes. Native early invoices, wider preparations/modules, full rules and deployed acceptance remain open. No saved-game, database, restart or deployment operation.
This commit is contained in:
Quintine committed 2026-10-24 15:49:35 +11:00
1 parent 49586efd2f
commit f995b6f4cd
18 files changed
+708 -46

No files matched your search

+16 -3
View File
@@ -6,7 +6,13 @@ import { Button } from './ui/button';
export function MovementPromises({ game, act, busy }: { game: GameView; act: (action: Action) => void; busy: boolean }) {
const own = liveMovementPromises(game.movementPromises ?? [], game.me, game.turn);
if (!own.length) return null;
const next = game.movementPromisePolicy?.[0];
const proposed = game.movementPromisePolicy?.[0];
const paidDecision = proposed?.type === 'decision' && game.decision?.player === game.me &&
(game.decision.kind === 'nullentropy' || game.phase === 3 && game.decision.kind === 'auctionPayment');
const next = game.phase >= 5 || paidDecision ||
(game.phase === 2 && ['charity', 'ready'].includes(proposed?.type ?? '')) ||
(game.phase === 4 && ['revive', 'ready'].includes(proposed?.type ?? '')) ||
(game.phase === 3 && proposed?.type === 'passBid') ? proposed : undefined;
const amount = next?.forces && typeof next.forces === 'object'
? Object.values(next.forces as Record<string, number>).reduce((sum, count) => sum + count, 0) : typeof next?.amount === 'number' ? next.amount : 0;
const destination = next && game.combatLocations.find(location => location.id === next.territory)?.name;
@@ -14,7 +20,14 @@ export function MovementPromises({ game, act, busy }: { game: GameView; act: (ac
const sector = typeof next?.sector === 'number' ? next.sector : 0;
const card = game.players.find(player => player.id === game.me)?.hand?.find(card => card.id === next?.card);
const recovered = game.nullentropy?.search?.cards.find(card => card.id === next?.card);
const label = next?.type === 'decision' ? `Choose ${recovered?.name ?? 'the required card'} in your paid Box search`
const payment = next?.type === 'decision' && game.decision?.kind === 'auctionPayment';
const label = payment ? next.karama === true ? `Use ${card?.name ?? 'your card'} for the current auction payment` : 'Pay the current auction price'
: next?.type === 'decision' ? `Choose ${recovered?.name ?? 'the required card'} in your paid Box search`
: next?.type === 'charity' ? 'Claim Charity for your later movement'
: next?.type === 'revive' ? `Revive ${amount} forces for your later movement`
: next?.type === 'ready' ? 'Finish this preparation phase while preserving your promises'
: next?.type === 'passBid' ? 'Pass this bid and preserve later movement'
: next?.type === 'card' && next.mode === 'shipment' ? `Use ${card?.name ?? 'your card'} for Guild shipment rates`
: next?.type === 'card' && card?.effect === 'nullentropyBox' ? 'Pay 2 spice and search with Nullentropy Box'
: next?.type === 'card' && next.mode === 'choam' ? 'Play Kulon to extend the required movement'
: next?.type === 'card' ? 'Play Hajr to prepare the required movement'
@@ -23,7 +36,7 @@ export function MovementPromises({ game, act, busy }: { game: GameView; act: (ac
: `${next?.movementCard ? 'Use Ornithopter and move' : 'Move'} ${amount} forces to ${destinationLabel}, sector ${sector}`;
return <section className="notice space-y-3" aria-label="Your ground-movement promises">
<h4>Your ground-movement promises</h4>
{game.active !== game.me && <p>Your Movement turn is still ahead. Keep the needed forces, spice and cards available; movement controls appear on your turn.{next?.type === 'decision' && ' Your already-paid Box search can finish now.'}</p>}
{(game.phase < 5 || game.active !== game.me) && <p>Your Movement turn is still ahead. Keep the needed forces, spice and cards available; movement controls appear on your turn.{game.phase < 5 && ' Complete any current preparation below before confirming readiness.'}{next?.type === 'decision' && game.decision?.kind === 'nullentropy' && ' Your already-paid Box search can finish now.'}</p>}
{own.map((promise, index) => <p key={index}>{promise.answer ? 'Yes: you must' : 'No: you must not'} {movementClaimText(promise)} before your Movement turn ends this round.</p>)}
<p className="fine">One qualifying ground move fulfills Yes; a shipment can prepare that move but does not fulfill it. No remains binding through every later Hajr or Ornithopter move. A paid Box search must finish before moving; its fee is paid only once. Opposing effects release an answer only if it becomes impossible.</p>
{next && <Button className="game-action min-h-11 whitespace-normal" disabled={busy} onClick={() => { if (!busy) act(next); }}>{label}</Button>}
+3 -2
View File
@@ -21,9 +21,10 @@ export function ShipmentPromises({
if (!promises.length) return null;
const me = g.players.find((p) => p.id === g.me)!;
const early = g.phase < 5;
const jointMovement = liveMovementPromises(g.movementPromises ?? [], g.me, g.turn).length > 0;
const movement = liveMovementPromises(g.movementPromises ?? [], g.me, g.turn);
const jointMovement = movement.length > 0;
const next = g.phase === 5 && !jointMovement ? g.shipmentCompletion?.actions[0] : undefined;
const preparation = early ? g.shipmentPreparation?.actions[0] : undefined;
const preparation = early && !movement.some(promise => promise.answer) ? g.shipmentPreparation?.actions[0] : undefined;
const preparationCard = preparation?.type === 'card'
? me.hand?.find(card => card.id === preparation.card) : undefined;
const preparationLabel = preparation?.type === 'charity'
+2 -1
View File
@@ -349,7 +349,8 @@ export function Truthtrance({
<>
<MovementClaimFields id={`${factId}-movement`} value={movement} onChange={setMovement} />
<p className="fine">One ground movement of the chosen physical minimum, from now until the respondent’s Movement turn ends. Shipment, HMS relocation and Ambassador relocation do not count. Owned Hajr, Ornithopter, native Kulon and paid Box recovery of a known discard can prepare a later move. No stays binding after earlier nonmatching moves.</p>
{!g.truthMovementTargets.includes(target) && <p className="notice">Choose an active or still-waiting Basic player this round and finish pending decisions. Optional-module and Advanced movement promises remain unfinished.</p>}
{g.phase < 5 && <p className="notice">Movement comes later this round. Keep the forces, spice and cards needed by your answer; available Charity or Revival may prepare the route. Current preparation appears when it is available. Earlier-phase questions currently support classic factions.</p>}
{!g.truthMovementTargets.includes(target) && <p className="notice">Choose a supported Basic respondent this round and finish pending decisions. Earlier phases use classic factions; native questions currently begin during Movement. Optional-module and Advanced movement promises remain unfinished.</p>}
<p className="notice" aria-label="Public movement question preview">{truthQuestionText(question, leaderName, locationName)}</p>
</>
) : kind === 'shipment' || kind === 'mixedShipment' ? (