Add bids and ordinary Revival to combined turn commitments
This commit is contained in:
1 parent
0d72a33ee5
commit
921b4fe1e3
15 files changed
+1255
-65
No files matched your search
@@ -200,7 +200,7 @@ export function Truthtrance({
|
||||
const [fact, setFact] = useState<TruthFact>({ kind: 'hand', name: 'Shield' });
|
||||
const factId = useId();
|
||||
const leaderName = (id: string) =>
|
||||
id === CHEAP_HERO_TRAITOR
|
||||
id === 'kwisatz' ? 'Kwisatz Haderach' : id === CHEAP_HERO_TRAITOR
|
||||
? 'Cheap Hero / Heroine'
|
||||
: (g.allLeaders.find((l) => l.id === id)?.name ?? id);
|
||||
const name = (id: string) => g.players.find((p) => p.id === id)!.name;
|
||||
@@ -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 this turn’s payments, shipment and ground movements</option>
|
||||
<option value="turnActions">Combine this turn’s bids, Revival, payments, shipment and ground movements</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 a supported payment, shipment or ground-movement opportunity this turn.
|
||||
{!g.truthTurnActionTargets.includes(target) && <p className="notice">Choose a player with a supported ordinary bid, Revival, payment, shipment or ground-movement opportunity 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>
|
||||
</>
|
||||
@@ -887,7 +887,7 @@ export function TruthHistory({ game: g }: { game: GameView }) {
|
||||
<p>
|
||||
{name(r.asker)} → {name(r.question.target)}:{' '}
|
||||
{truthQuestionText(r.question, (id) =>
|
||||
id === CHEAP_HERO_TRAITOR
|
||||
id === 'kwisatz' ? 'Kwisatz Haderach' : id === CHEAP_HERO_TRAITOR
|
||||
? 'Cheap Hero / Heroine'
|
||||
: (g.allLeaders.find((l) => l.id === id)?.name ?? id),
|
||||
id => g.combatLocations.find(place => place.id === id)!.name,
|
||||
|
||||
@@ -12,20 +12,34 @@ const recipients = (game: GameView, target: string) => {
|
||||
const player = game.players.find(player => player.id === target);
|
||||
return player ? game.players.filter(recipient => recipient.id !== target && recipient.id !== player.ally) : [];
|
||||
};
|
||||
/** Public faction names only; casualties, custody and native eligibility stay
|
||||
* on the server. Advanced Atreides may name their Kwisatz Haderach. */
|
||||
const leaders = (game: GameView, target: string) => {
|
||||
const faction = game.players.find(player => player.id === target)?.faction;
|
||||
return [...game.allLeaders.filter(leader => !!faction && leader.faction === faction),
|
||||
...(game.advanced && faction === 'atreides' ? [{ id: 'kwisatz', name: 'Kwisatz Haderach' }] : [])];
|
||||
};
|
||||
const currentLot = (game: GameView) => game.bidPromiseLot?.turn === game.turn ? game.bidPromiseLot : null;
|
||||
const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
prefix: 'turn-action',
|
||||
legend: 'Future payments, shipment and ground movements, with optional current facts',
|
||||
actionLabel: 'payment, shipment or ground-movement condition',
|
||||
actionTiming: 'Future ordinary payment, reserve shipment or ground move · this turn',
|
||||
legend: 'Future bids, Revival, payments, shipment and ground movements, with optional current facts',
|
||||
actionLabel: 'bid, Revival, payment, shipment or ground-movement condition',
|
||||
actionTiming: 'Future ordinary bid, Revival, payment, reserve shipment or ground move · 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.
|
||||
Payments made during Mentat Pause wait until next turn's Mentat opening; collection never counts twice.
|
||||
Ordinary bid conditions use the highest accepted future bid declaration in the named current lot, never the sum of smaller bids.
|
||||
A qualifying bid counts even if another player wins; it does not promise a purchase. Winning still requires the original payment.
|
||||
Ordinary Revival conditions count cumulative future normal and elite physical force returns, with each counter counted once,
|
||||
or an actual future return of a named native leader. Smaller ordinary force returns add together.
|
||||
Earlier returns, requests alone, Ghola, foreign leaders and special card returns do not count.
|
||||
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.
|
||||
Shipment and movement close at your own Movement turn end, while payment conditions last until the actual turn ends.
|
||||
AND requires every condition; OR keeps legal alternatives. No makes the whole expression false, not every condition separately.
|
||||
Original costs, timing, cards, preparation and action rights still apply; this claim grants no extra action.</>,
|
||||
fallbackError: 'Choose a future ordinary payment, reserve shipment or ground-movement condition, with optional current facts.',
|
||||
fallbackError: 'Choose a future ordinary bid, Revival, payment, reserve shipment or ground-movement condition, with optional current facts.',
|
||||
maxLeaves: TURN_ACTION_MAX_LEAVES,
|
||||
maxDepth: TURN_ACTION_MAX_DEPTH,
|
||||
requireFact: false,
|
||||
@@ -34,24 +48,67 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
const action = parseTurnActionLeaf(input);
|
||||
if (action.kind === 'payment' && !recipients(game, target).some(recipient => recipient.id === action.recipient))
|
||||
throw new Error('Choose a public non-allied recipient for the future ordinary payment.');
|
||||
if (action.kind === 'bid' && currentLot(game)?.lot !== action.lot)
|
||||
throw new Error('Choose the current public ordinary lot for the future bid.');
|
||||
if (action.kind === 'leader' && !leaders(game, target).some(leader => leader.id === action.leader))
|
||||
throw new Error('Choose a public native leader for the respondent’s faction.');
|
||||
return action;
|
||||
},
|
||||
renderAction: ({ value, id, game, target, disabled, onChange }) => {
|
||||
const choices = recipients(game, target);
|
||||
const choices = recipients(game, target), leaderChoices = leaders(game, target), lot = currentLot(game);
|
||||
return <>
|
||||
<label htmlFor={`${id}-action`}>Future action</label>
|
||||
<select id={`${id}-action`} value={value.kind} disabled={disabled} onChange={event => {
|
||||
const kind = event.currentTarget.value;
|
||||
if (kind === value.kind) return;
|
||||
if (kind === 'payment') onChange({ kind, recipient: '', minimum: value.minimum });
|
||||
const minimum = value.kind === 'leader' ? 1 : value.minimum;
|
||||
if (kind === 'payment') onChange({ kind, recipient: '', minimum });
|
||||
if (kind === 'bid') onChange({ kind, lot: lot?.lot ?? NaN, minimum });
|
||||
if (kind === 'forces') onChange({ kind, counter: 'total', minimum });
|
||||
if (kind === 'leader') onChange({ kind, leader: leaderChoices[0]?.id ?? '' });
|
||||
if (kind === 'ship' || kind === 'move') onChange({ kind,
|
||||
territory: value.kind === 'payment' ? 'carthag' : value.territory, minimum: value.minimum });
|
||||
territory: value.kind === 'ship' || value.kind === 'move' ? value.territory : 'carthag', minimum });
|
||||
}}>
|
||||
<option value="bid">Place an ordinary-lot bid</option>
|
||||
<option value="forces">Revive ordinary physical forces</option>
|
||||
<option value="leader">Revive a named ordinary leader</option>
|
||||
<option value="payment">Make an ordinary payment</option>
|
||||
<option value="ship">Ship from reserves</option>
|
||||
<option value="move">Make ground movement</option>
|
||||
</select>
|
||||
{value.kind === 'payment' ? <>
|
||||
{value.kind === 'bid' ? <>
|
||||
<label htmlFor={`${id}-lot`}>Current ordinary lot</label>
|
||||
<select id={`${id}-lot`} value={lot?.lot === value.lot ? String(value.lot) : ''} disabled={disabled}
|
||||
aria-invalid={lot?.lot !== value.lot} onChange={event => {
|
||||
const selected = event.currentTarget.value;
|
||||
if (selected === '' || lot && selected === String(lot.lot))
|
||||
onChange({ ...value, lot: selected === '' ? NaN : Number(selected) });
|
||||
}}>
|
||||
<option value="">Choose the current ordinary lot</option>
|
||||
{lot && <option value={lot.lot}>Turn {lot.turn} · Ordinary lot {lot.lot}</option>}
|
||||
</select>
|
||||
{!lot && <p className="fine">No public ordinary lot is currently available. Special auctions do not count.</p>}
|
||||
</> : value.kind === 'forces' ? <>
|
||||
<label htmlFor={`${id}-counter`}>Physical force counters</label>
|
||||
<select id={`${id}-counter`} value={value.counter} disabled={disabled} onChange={event => {
|
||||
const counter = event.currentTarget.value;
|
||||
if (counter === 'total' || counter === 'normal' || counter === 'elite') onChange({ ...value, counter });
|
||||
}}>
|
||||
<option value="total">All physical forces</option>
|
||||
<option value="normal">Normal forces</option>
|
||||
<option value="elite">Elite forces</option>
|
||||
</select>
|
||||
</> : value.kind === 'leader' ? <>
|
||||
<label htmlFor={`${id}-leader`}>Named ordinary leader</label>
|
||||
<select id={`${id}-leader`} value={leaderChoices.some(leader => leader.id === value.leader) ? value.leader : ''}
|
||||
disabled={disabled} aria-invalid={!leaderChoices.some(leader => leader.id === value.leader)} onChange={event => {
|
||||
const leader = event.currentTarget.value;
|
||||
if (leader === '' || leaderChoices.some(choice => choice.id === leader)) onChange({ ...value, leader });
|
||||
}}>
|
||||
<option value="">Choose a named leader</option>
|
||||
{leaderChoices.map(leader => <option key={leader.id} value={leader.id}>{leader.name}</option>)}
|
||||
</select>
|
||||
</> : value.kind === 'payment' ? <>
|
||||
<label htmlFor={`${id}-recipient`}>Ordinary payment recipient</label>
|
||||
<select id={`${id}-recipient`} value={choices.some(recipient => recipient.id === value.recipient) ? value.recipient : ''}
|
||||
disabled={disabled} aria-invalid={!choices.some(recipient => recipient.id === value.recipient)}
|
||||
@@ -71,12 +128,17 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
</>}
|
||||
<label htmlFor={`${id}-minimum`}>{value.kind === 'payment' ? 'Minimum future ordinary payments in spice'
|
||||
: `Minimum physical forces in one ${value.kind === 'ship' ? 'reserve shipment' : 'ground move'}`}</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={value.kind === 'payment' ? Number.MAX_SAFE_INTEGER : 20}
|
||||
step={1} required disabled={disabled} value={Number.isFinite(value.minimum) ? value.minimum : ''}
|
||||
aria-invalid={!Number.isSafeInteger(value.minimum) || value.minimum < 1 || value.kind !== 'payment' && value.minimum > 20}
|
||||
onChange={event => onChange({ ...value, minimum: event.currentTarget.valueAsNumber })} />
|
||||
{value.kind !== 'leader' && <>
|
||||
<label htmlFor={`${id}-minimum`}>{value.kind === 'payment' ? 'Minimum future ordinary payments in spice'
|
||||
: value.kind === 'bid' ? 'Minimum future accepted bid in spice'
|
||||
: value.kind === 'forces' ? 'Minimum future ordinary force returns'
|
||||
: `Minimum physical forces in one ${value.kind === 'ship' ? 'reserve shipment' : 'ground move'}`}</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={value.kind === 'payment' || value.kind === 'bid' ? Number.MAX_SAFE_INTEGER : 20}
|
||||
step={1} required disabled={disabled} value={Number.isFinite(value.minimum) ? value.minimum : ''}
|
||||
aria-invalid={!Number.isSafeInteger(value.minimum) || value.minimum < 1 ||
|
||||
value.kind !== 'payment' && value.kind !== 'bid' && value.minimum > 20}
|
||||
onChange={event => onChange({ ...value, minimum: event.currentTarget.valueAsNumber })} />
|
||||
</>}
|
||||
</>;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const native = game.richeseAuction, order = game.revivalOrder, revival = game.revival;
|
||||
const auctionSource = (current: GameView) => current.auction && JSON.stringify({
|
||||
bid: current.auction.bid, bidder: current.auction.bidder, active: current.auction.active,
|
||||
remaining: current.auction.remaining, allyPayment: current.auction.allyPayment,
|
||||
remaining: current.auction.remaining, allyPayment: current.auction.allyPayment, lot: current.bidPromiseLot,
|
||||
});
|
||||
const nativeSource = (current: GameView) => current.richeseAuction && JSON.stringify({
|
||||
event: current.richeseAuction.event, method: current.richeseAuction.method,
|
||||
@@ -119,10 +119,10 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
if (!own.length) return null;
|
||||
const live = own.some(promise => promise.turn === game.turn && !promise.fulfilled && !promise.released);
|
||||
return <section className="notice space-y-3" aria-label="Your Truthtrance turn-action promises" id="turn-action-promise-guidance">
|
||||
<h3>Your payment, shipment and movement promises</h3>
|
||||
<h3>Your bid, Revival, payment, shipment and movement promises</h3>
|
||||
<ul>{own.map(promise => <li key={promise.event}>
|
||||
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {truthQuestionText({ kind: 'turnActions', target: promise.player,
|
||||
turn: promise.turn, mixed: promise.mixed }, id => id === CHEAP_HERO_TRAITOR ? 'Cheap Hero / Heroine'
|
||||
turn: promise.turn, mixed: promise.mixed }, id => id === 'kwisatz' ? 'Kwisatz Haderach' : id === CHEAP_HERO_TRAITOR ? 'Cheap Hero / Heroine'
|
||||
: game.allLeaders.find(leader => leader.id === id)?.name ?? id,
|
||||
id => game.combatLocations.find(location => location.id === id)?.name ?? id,
|
||||
id => game.players.find(player => player.id === id)?.name ?? id)}.
|
||||
@@ -136,8 +136,14 @@ 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's Mentat opening; collection never counts twice.
|
||||
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.
|
||||
Named leader conditions require an actual future ordinary native return. Earlier returns, requests alone, Ghola,
|
||||
foreign leaders and special card returns do not count.
|
||||
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.
|
||||
Shipment and movement close at your own Movement turn end; ordinary payments remain possible until the actual turn ends.
|
||||
Original costs, timing, cards, preparation and action rights still apply. This claim grants no extra action.</p>
|
||||
{live && <p>{game.phase === 8 ? 'Complete any remaining ordinary payment before the actual turn ends. Late recipients collect next turn.'
|
||||
@@ -164,9 +170,12 @@ export function TurnActionAnswers(props: TurnActionProps) {
|
||||
!!me && !me.autopilot && !current.response && !current.decision && !current.phaseOpening &&
|
||||
!current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed;
|
||||
};
|
||||
return <section aria-label="Answer the whole payment, shipment and movement question" className="space-y-3">
|
||||
return <section aria-label="Answer the whole bid, Revival, payment, shipment and movement question" className="space-y-3">
|
||||
<p className="notice">Answer the whole expression. Current facts freeze now; OR keeps legal alternatives and No keeps the whole expression false.
|
||||
Future real ordinary payments add together per recipient. Only one future original reserve shipment and actual future ground-movement groups count.
|
||||
Ordinary bids use the maximum future accepted declaration in the named current lot; ordinary force returns add together,
|
||||
while named leaders must actually return through ordinary native Revival.
|
||||
Bids close with their lot; Revival closes with your native Revival opportunity.
|
||||
Shipment and movement close at your own Movement turn end; payment conditions last until the actual turn ends.
|
||||
Original rights, preparation, timing and costs still apply. Only your whole answer is shared.</p>
|
||||
{answers.map(answer => {
|
||||
|
||||
Reference in new issue
Block a user