Support sector-specific Truthtrance movement
This commit is contained in:
1 parent
536d334543
commit
1059cc1682
24 files changed
+1016
-100
No files matched your search
@@ -15,7 +15,7 @@ const config: MixedActionConfig<MovementClaim> = {
|
||||
actionLabel: 'ground-movement condition',
|
||||
actionTiming: 'Future ground move · this turn',
|
||||
actionHelp: <>Each movement condition needs one actual future ground move after the answer, before your Movement turn ends this round.
|
||||
Smaller moves never add together. Prior moves and shipments do not count. The claim grants no extra movement opportunity.</>,
|
||||
You may name an exact sector. Smaller moves never add together. Prior moves and shipments do not count. The claim grants no extra movement opportunity.</>,
|
||||
fallbackError: 'Choose a current fact and a future ground-movement condition.',
|
||||
maxLeaves: MOVEMENT_EXPRESSION_MAX_LEAVES,
|
||||
maxDepth: MOVEMENT_EXPRESSION_MAX_DEPTH,
|
||||
@@ -25,10 +25,21 @@ const config: MixedActionConfig<MovementClaim> = {
|
||||
<label htmlFor={`${id}-territory`}>Ground-movement destination</label>
|
||||
<select id={`${id}-territory`} value={TERRITORIES.some(destination => destination.id === value.territory) ? value.territory : ''}
|
||||
disabled={disabled} aria-invalid={!TERRITORIES.some(destination => destination.id === value.territory)}
|
||||
onChange={event => onChange({ ...value, territory: event.currentTarget.value })}>
|
||||
onChange={event => onChange({ territory: event.currentTarget.value, minimum: value.minimum })}>
|
||||
<option value="">Choose a printed Arrakis territory</option>
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-sector`}>Ground-movement sector</label>
|
||||
<select id={`${id}-sector`} disabled={disabled} value={value.sector === undefined ? '' : String(value.sector)}
|
||||
aria-invalid={value.sector !== undefined && !TERRITORIES.find(row => row.id === value.territory)?.sectors.includes(value.sector)}
|
||||
onChange={event => {
|
||||
const selected = event.currentTarget.value, printed = TERRITORIES.find(row => row.id === value.territory);
|
||||
if (selected === '') onChange({ territory: value.territory, minimum: value.minimum });
|
||||
else { const sector = Number(selected); if (printed?.sectors.includes(sector)) onChange({ ...value, sector }); }
|
||||
}}>
|
||||
<option value="">Any sector in this territory</option>
|
||||
{TERRITORIES.find(row => row.id === value.territory)?.sectors.map(sector => <option key={sector} value={sector}>Sector {sector}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-minimum`}>Minimum physical forces in one ground move</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={20} step={1} required disabled={disabled}
|
||||
value={Number.isNaN(value.minimum) ? '' : value.minimum}
|
||||
|
||||
@@ -10,9 +10,20 @@ export function MovementClaimFields({ id, value, onChange }: {
|
||||
<legend>Ground-movement condition</legend>
|
||||
<label htmlFor={`${id}-territory`}>Ground-movement destination</label>
|
||||
<select id={`${id}-territory`} value={value.territory}
|
||||
onChange={event => onChange({ ...value, territory: event.target.value })}>
|
||||
onChange={event => onChange({ territory: event.currentTarget.value, minimum: value.minimum })}>
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-sector`}>Ground-movement sector</label>
|
||||
<select id={`${id}-sector`} value={value.sector === undefined ? '' : String(value.sector)}
|
||||
aria-invalid={value.sector !== undefined && !TERRITORIES.find(row => row.id === value.territory)?.sectors.includes(value.sector)}
|
||||
onChange={event => {
|
||||
const selected = event.currentTarget.value, printed = TERRITORIES.find(row => row.id === value.territory);
|
||||
if (selected === '') onChange({ territory: value.territory, minimum: value.minimum });
|
||||
else { const sector = Number(selected); if (printed?.sectors.includes(sector)) onChange({ ...value, sector }); }
|
||||
}}>
|
||||
<option value="">Any sector in this territory</option>
|
||||
{TERRITORIES.find(row => row.id === value.territory)?.sectors.map(sector => <option key={sector} value={sector}>Sector {sector}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-minimum`}>Minimum physical forces in one ground move</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={20} step={1} required
|
||||
value={Number.isNaN(value.minimum) ? '' : value.minimum} aria-invalid={!validMovementClaim(value)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import { TERRITORIES } from '@/game/board';
|
||||
import {
|
||||
parseMovementExpression, MOVEMENT_EXPRESSION_MAX_DEPTH, MOVEMENT_EXPRESSION_MAX_LEAVES,
|
||||
type MovementExpression,
|
||||
@@ -17,7 +18,7 @@ export function movementExpressionInputError(value: unknown): string | null {
|
||||
// Structural edits preserve repairable destination and numeric drafts. The
|
||||
// detached valid placeholders are used only to check tree shape and limits.
|
||||
const structuralDraft = (node: MovementExpression): MovementExpression => node.kind === 'move'
|
||||
? { ...node, territory: 'carthag', minimum: 1 }
|
||||
? { kind: 'move', territory: 'carthag', minimum: 1 }
|
||||
: { ...node, terms: node.terms.map(structuralDraft) };
|
||||
const inputSize = (node: MovementExpression): { leaves: number; depth: number } => {
|
||||
if (node.kind === 'move') return { leaves: 1, depth: 0 };
|
||||
@@ -49,10 +50,21 @@ export function MovementExpressionFields({ value, territories, disabled, onChang
|
||||
<label htmlFor={`${id}-territory`}>Ground-movement destination</label>
|
||||
<select id={`${id}-territory`} value={territories.some(destination => destination.id === node.territory) ? node.territory : ''}
|
||||
disabled={disabled} aria-invalid={!territories.some(destination => destination.id === node.territory)}
|
||||
onChange={event => change({ ...node, territory: event.currentTarget.value })}>
|
||||
onChange={event => change({ kind: 'move', territory: event.currentTarget.value, minimum: node.minimum })}>
|
||||
<option value="">Choose a printed Arrakis territory</option>
|
||||
{territories.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-sector`}>Ground-movement sector</label>
|
||||
<select id={`${id}-sector`} disabled={disabled} value={node.sector === undefined ? '' : String(node.sector)}
|
||||
aria-invalid={node.sector !== undefined && !TERRITORIES.find(row => row.id === node.territory)?.sectors.includes(node.sector)}
|
||||
onChange={event => {
|
||||
const selected = event.currentTarget.value, printed = TERRITORIES.find(row => row.id === node.territory);
|
||||
if (selected === '') change({ kind: 'move', territory: node.territory, minimum: node.minimum });
|
||||
else { const sector = Number(selected); if (printed?.sectors.includes(sector)) change({ ...node, sector }); }
|
||||
}}>
|
||||
<option value="">Any sector in this territory</option>
|
||||
{TERRITORIES.find(row => row.id === node.territory)?.sectors.map(sector => <option key={sector} value={sector}>Sector {sector}</option>)}
|
||||
</select>
|
||||
<label htmlFor={`${id}-minimum`}>Minimum physical forces in one ground move</label>
|
||||
<Input id={`${id}-minimum`} type="number" min={1} max={20} step={1} required disabled={disabled}
|
||||
value={Number.isNaN(node.minimum) ? '' : node.minimum} aria-invalid={!!movementExpressionInputError(node)}
|
||||
@@ -98,7 +110,7 @@ export function MovementExpressionFields({ value, territories, disabled, onChang
|
||||
AND requires every condition; OR requires at least one. No makes the whole expression false,
|
||||
not necessarily every condition. Legal alternatives remain available; the question does not pin a fixed branch.
|
||||
Each condition needs one future actual ground move after the answer, before your Movement turn ends this round.
|
||||
Smaller moves never add together; a shipment can prepare a move but does not fulfill a condition.
|
||||
Name an exact sector when needed. Smaller moves never add together; a shipment can prepare a move but does not fulfill a condition.
|
||||
</p>
|
||||
<p className="fine">App input limits: {MOVEMENT_EXPRESSION_MAX_LEAVES} conditions and {MOVEMENT_EXPRESSION_MAX_DEPTH} levels of grouping.
|
||||
Groups do not count as conditions. These are input limits, not additional printed rules.</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ const config: MixedActionConfig<TravelActionClaim> = {
|
||||
actionTiming: 'Future shipment or ground move · this turn',
|
||||
actionHelp: <>Shipment conditions concern your single future original reserve shipment after the answer.
|
||||
Choose any sector or one exact printed sector in that territory; only that sector satisfies an exact condition.
|
||||
Ground-movement conditions concern one actual future ground move before your Movement turn ends this round.
|
||||
Ground-movement conditions concern one actual future ground move before your Movement turn ends this round; its sector may also be named.
|
||||
Count physical forces in one action; smaller moves never add together. Earlier actions do not count.
|
||||
OR keeps legal shipment and movement alternatives available. No makes the whole expression false.
|
||||
Original costs, movement rights, cards and preparation still apply; this claim grants no extra shipment or movement.</>,
|
||||
@@ -29,8 +29,8 @@ const config: MixedActionConfig<TravelActionClaim> = {
|
||||
<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 === 'ship') onChange({ ...value, kind });
|
||||
else if (kind === 'move') onChange({ kind, territory: value.territory, minimum: value.minimum });
|
||||
if ((kind === 'ship' || kind === 'move') && kind !== value.kind)
|
||||
onChange({ kind, territory: value.territory, minimum: value.minimum });
|
||||
}}>
|
||||
<option value="ship">Ship from reserves</option>
|
||||
<option value="move">Make ground movement</option>
|
||||
@@ -40,20 +40,19 @@ const config: MixedActionConfig<TravelActionClaim> = {
|
||||
disabled={disabled} aria-invalid={!TERRITORIES.some(destination => destination.id === value.territory)}
|
||||
onChange={event => {
|
||||
const destination = event.currentTarget.value;
|
||||
if (value.kind === 'ship') onChange({ kind: 'ship', territory: destination, minimum: value.minimum });
|
||||
else onChange({ ...value, territory: destination });
|
||||
onChange({ kind: value.kind, territory: destination, minimum: value.minimum });
|
||||
}}>
|
||||
<option value="">Choose a printed Arrakis territory</option>
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
{value.kind === 'ship' && (() => {
|
||||
{(value.kind === 'ship' || value.kind === 'move') && (() => {
|
||||
const printed = TERRITORIES.find(destination => destination.id === value.territory);
|
||||
return <>
|
||||
<label htmlFor={`${id}-sector`}>Shipment sector</label>
|
||||
<label htmlFor={`${id}-sector`}>{value.kind === 'ship' ? 'Shipment sector' : 'Ground-movement sector'}</label>
|
||||
<select id={`${id}-sector`} disabled={disabled} value={value.sector === undefined ? '' : String(value.sector)}
|
||||
aria-invalid={value.sector !== undefined && !printed?.sectors.includes(value.sector)} onChange={event => {
|
||||
const selected = event.currentTarget.value;
|
||||
if (selected === '') onChange({ kind: 'ship', territory: value.territory, minimum: value.minimum });
|
||||
if (selected === '') onChange({ kind: value.kind, territory: value.territory, minimum: value.minimum });
|
||||
else {
|
||||
const sector = Number(selected);
|
||||
if (printed?.sectors.includes(sector)) onChange({ ...value, sector });
|
||||
|
||||
@@ -119,7 +119,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
Amal halves available cash before ordinary payments become legal; preserve enough remaining cash for any payment condition.
|
||||
Shipment conditions concern the single future original reserve shipment after the answer. Choose any sector or one exact printed sector;
|
||||
a different sector in the same territory does not satisfy an exact-sector condition.
|
||||
Ground-movement conditions use the maximum physical forces in one actual future move to that destination; smaller moves never add together.
|
||||
Ground-movement conditions use the maximum physical forces in one actual future move to that destination and optional exact sector; 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.
|
||||
@@ -434,20 +434,19 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
const destination = event.currentTarget.value;
|
||||
// An exact sector belongs to its printed territory. Selecting a
|
||||
// different territory returns to the explicit any-sector choice.
|
||||
if (value.kind === 'ship') onChange({ kind: 'ship', territory: destination, minimum: value.minimum });
|
||||
else onChange({ ...value, territory: destination });
|
||||
onChange({ kind: value.kind, territory: destination, minimum: value.minimum });
|
||||
}}>
|
||||
<option value="">Choose a printed Arrakis territory</option>
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
{value.kind === 'ship' && (() => {
|
||||
{(value.kind === 'ship' || value.kind === 'move') && (() => {
|
||||
const printed = TERRITORIES.find(destination => destination.id === value.territory);
|
||||
return <>
|
||||
<label htmlFor={`${id}-sector`}>Shipment sector</label>
|
||||
<label htmlFor={`${id}-sector`}>{value.kind === 'ship' ? 'Shipment sector' : 'Ground-movement sector'}</label>
|
||||
<select id={`${id}-sector`} disabled={disabled} value={value.sector === undefined ? '' : String(value.sector)}
|
||||
aria-invalid={value.sector !== undefined && !printed?.sectors.includes(value.sector)} onChange={event => {
|
||||
const chosen = event.currentTarget.value;
|
||||
if (chosen === '') onChange({ kind: 'ship', territory: value.territory, minimum: value.minimum });
|
||||
if (chosen === '') onChange({ kind: value.kind, territory: value.territory, minimum: value.minimum });
|
||||
else {
|
||||
const sector = Number(chosen);
|
||||
if (printed?.sectors.includes(sector)) onChange({ ...value, sector });
|
||||
|
||||
Reference in new issue
Block a user