Support Truthtrance No-Field shipment promises
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
9ab9b9a613
commit
7fccd62814
16 files changed
+658
-32
No files matched your search
@@ -8,15 +8,18 @@ import {
|
||||
} from '@/game/shipment-promises';
|
||||
import { shipmentExpressionInputError, shipmentExpressionInputSize } from '@/game/shipment-expression-input';
|
||||
import { ShipmentClauseFields } from './shipment-claim-fields';
|
||||
import { TruthNoFieldShipmentFields } from './truth-no-field-shipment-fields';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const newLeaf = (): ShipmentExpression => ({ territory: 'carthag', minimum: 1 });
|
||||
|
||||
/** Hook-free controlled tree. Stateful leaf descendants are rendered by React. */
|
||||
export function ShipmentExpressionFields({ value, id = 'truth-shipment', disabled, onChange }: {
|
||||
export function ShipmentExpressionFields({ value, id = 'truth-shipment', disabled, allowNoField = false, onChange }: {
|
||||
value: ShipmentExpression;
|
||||
id?: string;
|
||||
disabled: boolean;
|
||||
/** Expose the public marker question only for an eligible Richese target. */
|
||||
allowNoField?: boolean;
|
||||
onChange: (expression: ShipmentExpression) => void;
|
||||
}) {
|
||||
const size = shipmentExpressionInputSize(value);
|
||||
@@ -30,6 +33,7 @@ export function ShipmentExpressionFields({ value, id = 'truth-shipment', disable
|
||||
remove?: () => ShipmentExpression,
|
||||
): ReactElement => {
|
||||
const group = 'op' in node;
|
||||
const marker = !group && 'kind' in node && node.kind === 'noField';
|
||||
const allowed = (next: ShipmentExpression) => !disabled && !shipmentExpressionInputError(replace(next));
|
||||
const change = (next: ShipmentExpression) => commit(replace(next));
|
||||
const wrap = (op: 'and' | 'or'): ShipmentExpression => ({ op, terms: [node, newLeaf()] });
|
||||
@@ -63,7 +67,24 @@ export function ShipmentExpressionFields({ value, id = 'truth-shipment', disable
|
||||
</div>
|
||||
</> : 'constant' in node ? <p className="notice" role="alert">
|
||||
Frozen current facts are unavailable in a pure shipment question. Remove this condition.
|
||||
</p> : <ShipmentClauseFields id={`${id}-${path}`} value={node} onChange={change} />}
|
||||
</p> : <>
|
||||
{(allowNoField || marker) && <>
|
||||
<label htmlFor={`${id}-${path}-condition-type`}>Shipment condition type</label>
|
||||
<select id={`${id}-${path}-condition-type`} value={marker ? 'noField' : 'physical'}
|
||||
disabled={disabled} onChange={event => {
|
||||
const kind = event.currentTarget.value;
|
||||
if (kind === 'physical') change(newLeaf());
|
||||
else if (kind === 'noField' && allowNoField) change({ kind: 'noField' });
|
||||
}}>
|
||||
<option value="physical">Physical forces from reserves</option>
|
||||
<option value="noField" disabled={!allowNoField}>Owned No-Field from reserves</option>
|
||||
</select>
|
||||
</>}
|
||||
{marker ? <>
|
||||
{!allowNoField && <p className="notice">Choose Richese for a No-Field shipment condition, or change this condition to physical forces.</p>}
|
||||
<TruthNoFieldShipmentFields id={`${id}-${path}`} value={node} disabled={disabled || !allowNoField} onChange={change} />
|
||||
</> : 'minimum' in node ? <ShipmentClauseFields id={`${id}-${path}`} value={node} onChange={change} /> : null}
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" data-shipment-expression-control={`and-${path}`} disabled={!allowed(wrap('and'))}
|
||||
onClick={() => change(wrap('and'))}>Group with AND</Button>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { TERRITORIES } from '@/game/board';
|
||||
|
||||
/** This public question never includes a concealed token identity or value. */
|
||||
export type NoFieldShipmentInput = {
|
||||
kind: 'noField';
|
||||
territory?: string;
|
||||
sector?: number;
|
||||
};
|
||||
|
||||
/** Author one No-Field leaf inside the ordinary shipment expression. */
|
||||
export function TruthNoFieldShipmentFields({ id, value, disabled, onChange }: {
|
||||
id: string;
|
||||
value: NoFieldShipmentInput;
|
||||
disabled: boolean;
|
||||
onChange: (value: NoFieldShipmentInput) => void;
|
||||
}) {
|
||||
const destination = TERRITORIES.find(territory => territory.id === value.territory);
|
||||
return <>
|
||||
<label htmlFor={`${id}-no-field-territory`}>No-Field shipment destination</label>
|
||||
<select id={`${id}-no-field-territory`} value={value.territory ?? ''} disabled={disabled}
|
||||
aria-invalid={value.territory !== undefined && !destination} onChange={event => {
|
||||
if (disabled) return;
|
||||
const territory = event.currentTarget.value;
|
||||
if (territory === '') onChange({ kind: 'noField' });
|
||||
else if (TERRITORIES.some(candidate => candidate.id === territory))
|
||||
onChange({ kind: 'noField', territory });
|
||||
}}>
|
||||
<option value="">Any destination</option>
|
||||
{TERRITORIES.map(territory => <option key={territory.id} value={territory.id}>{territory.name}</option>)}
|
||||
</select>
|
||||
{destination && <>
|
||||
<label htmlFor={`${id}-no-field-sector`}>No-Field shipment sector</label>
|
||||
<select id={`${id}-no-field-sector`} value={value.sector === undefined ? '' : String(value.sector)}
|
||||
disabled={disabled} aria-invalid={value.sector !== undefined && !destination.sectors.includes(value.sector)}
|
||||
onChange={event => {
|
||||
if (disabled) return;
|
||||
const selected = event.currentTarget.value;
|
||||
if (selected === '') onChange({ kind: 'noField', territory: destination.id });
|
||||
else {
|
||||
const sector = Number(selected);
|
||||
if (destination.sectors.includes(sector)) onChange({ kind: 'noField', territory: destination.id, sector });
|
||||
}
|
||||
}}>
|
||||
<option value="">Any sector in this territory</option>
|
||||
{destination.sectors.map(sector => <option key={sector} value={sector}>Sector {sector}</option>)}
|
||||
</select>
|
||||
</>}
|
||||
<p className="fine">Ask whether the player ships an owned No-Field from reserves this turn.
|
||||
The question does not identify its token or value. A physical-force minimum is a separate condition.</p>
|
||||
</>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseShipmentExpression,
|
||||
ShipmentClaimError,
|
||||
shipmentExpressionOf,
|
||||
shipmentClaimHasNoField,
|
||||
type ShipmentExpression,
|
||||
} from '@/game/shipment-promises';
|
||||
import { shipmentExpressionInputError } from '@/game/shipment-expression-input';
|
||||
@@ -23,7 +24,8 @@ export function TruthShipmentQuestion({ game, question, busy, act, onChange }: {
|
||||
const ownsQuestion = () => !!game.me && game.players.some(player => player.id === game.me) &&
|
||||
game.truthtrance?.stage === 'ask' && game.truthtrance.queue[0]?.player === game.me;
|
||||
const hasTarget = () => question.target !== game.me && game.players.some(player => player.id === question.target);
|
||||
const supported = () => hasTarget() && game.truthShipmentTargets.includes(question.target);
|
||||
const supported = () => hasTarget() && game.truthShipmentTargets.includes(question.target) &&
|
||||
(!shipmentClaimHasNoField(question) || game.players.some(player => player.id === question.target && player.faction === 'richese'));
|
||||
const inputError = () => {
|
||||
try {
|
||||
parseShipmentExpression(shipmentExpressionOf(question), { allowConstants: false });
|
||||
@@ -37,8 +39,9 @@ export function TruthShipmentQuestion({ game, question, busy, act, onChange }: {
|
||||
const error = inputError();
|
||||
return <>
|
||||
{hasTarget() ? <ShipmentExpressionFields value={shipmentExpressionOf(question)}
|
||||
disabled={busy || !ownsQuestion() || !supported()} onChange={next => {
|
||||
if (!busy && ownsQuestion() && supported() && !shipmentExpressionInputError(next)) onChange(next);
|
||||
allowNoField={game.players.some(player => player.id === question.target && player.faction === 'richese')}
|
||||
disabled={busy || !ownsQuestion() || !game.truthShipmentTargets.includes(question.target)} onChange={next => {
|
||||
if (!busy && ownsQuestion() && hasTarget() && game.truthShipmentTargets.includes(question.target) && !shipmentExpressionInputError(next)) onChange(next);
|
||||
}} /> : <p className="notice">Choose another seated player for this question.</p>}
|
||||
{!supported() && <p className="notice">Choose an eligible respondent with an unused shipment this turn and finish pending decisions.</p>}
|
||||
<blockquote data-testid="truth-shipment-preview">
|
||||
|
||||
Reference in new issue
Block a user