Files
Dune/components/shipment-promises.tsx
T
Quintine 32791ed5e2 Support typed Advanced Truthtrance shipment commitments
Extend the existing reserve-shipment promise pipeline to base Advanced
games without Guild or optional modules. Share scope between parser, save
validation and UI. Reuse canonical Ghola normal/elite revival eligibility
and retain live BG conversion responses while proving conditional private
feasibility. Hide future-price guidance until pending responses finish.
Show exact Sardaukar/Fedaykin allocation in private executable steps.

Validation: types, lint and all 4,192 offline tests pass; production build
and all 40 HTTP tests pass. 41 focused shipment tests include 15 new cases
covering genuine Advanced setup, all five non-Guild factions and four bot
profiles, typed Ghola quota, BG allowance/cancellation/alternatives,
private views, saved continuations and production SQLite duplicate races.
Independent review found and resolved optional-module scope and pending
price-guidance issues. Browser rooms EUZZ8D4M and GWNCVCAP verified an AI No
route and human Yes with four-force/one-Sardaukar Ghola then six-force
shipment. Phone settlement controls and refresh passed. Both existing
browser room hashes/versions preserved through HTTP tests; 30 rooms added.

Full source-bound report: 20260913-advanced-shipment-final; fingerprint
6c2f29d9b3e61bf6b9544ee3a95c47538abfb05e1c3b2ae9d0a603ef0cef38dd
Broad checks ran once after focused fixes/review. No full-game AI study
or complete Advanced mode acceptance is claimed; all existing release
gates and pending Guild settlement questions remain intact.
2026-09-13 11:08:29 +10:00

107 lines
3.5 KiB
TypeScript

'use client';
import type { Action, GameView } from '@/game/engine';
import { liveShipmentPromises } from '@/game/shipment-promises';
import { territory } from '@/game/board';
import { reserveShipmentCost, guildShipmentCost } from '@/game/shipment-price';
import { Button } from './ui/button';
export function ShipmentPromises({
game: g,
act,
busy,
}: {
game: GameView;
act: (action: Action) => void;
busy: boolean;
}) {
const promises = liveShipmentPromises(g.shipmentPromises, g.me, g.turn);
if (!promises.length) return null;
const me = g.players.find((p) => p.id === g.me)!;
const next = g.shipmentCompletion?.actions[0];
const shipping = next && (next.type === 'ship' || next.type === 'guildShip');
const cost = shipping
? next.type === 'guildShip'
? guildShipmentCost(
territory(String(next.territory)).type,
Number(next.amount),
)
: reserveShipmentCost(
{
faction: me.faction,
halfRate:
me.faction === 'guild' ||
g.players.some(
(p) => p.faction === 'guild' && p.id === me.ally,
) ||
g.karamaShipping?.player === me.id,
},
territory(String(next.territory)).type,
Number(next.amount),
)
: null;
const share = shipping
? Number(next.allyPayment ?? Math.max(0, cost! - (me.spice ?? 0)))
: 0;
const elite = Number(next?.elite ?? 0);
const allocation =
elite > 0
? `, including ${elite} ${me.faction === 'emperor' ? 'Sardaukar' : me.faction === 'fremen' ? 'Fedaykin' : 'elite force' + (elite === 1 ? '' : 's')}`
: '';
const description = next
? shipping
? `Ship ${Number(next.amount)} forces${allocation} to ${territory(String(next.territory)).name}, sector ${Number(next.sector)}`
: next.type === 'pledgeAid'
? 'Reclaim your unused ally pledge'
: `Play ${me.hand?.find((c) => c.id === next.card)?.name ?? 'your preparation card'}${next.amount ? ` to revive ${Number(next.amount)} forces${allocation}` : ' for your shipment discount'}`
: '';
return (
<section
className="notice"
aria-label="Your Truthtrance shipment promises"
id="shipment-promise-guidance"
>
<h3>Your shipment promises</h3>
<ul>
{promises.map((p, index) => (
<li key={index}>
{p.answer ? 'Ship' : 'Do not ship'} at least {p.minimum} physical
forces from reserves to {territory(p.territory).name} this turn.
</li>
))}
</ul>
{promises.some((p) => p.answer) && (
<p>
Complete a qualifying shipment before ground movement or finishing
your turn. You can choose any legal count, sector and funding that
honor your answers.
</p>
)}
{next && (
<details>
<summary>Suggested next step · private</summary>
<p>{description}.</p>
{cost !== null && (
<p>
Cost: {cost} spice · Your payment: {cost - share} · Ally pledge:{' '}
{share}.
</p>
)}
<Button
className="game-action"
disabled={
busy ||
!!g.truthtrance ||
!!g.response ||
!!g.decision ||
!!g.phaseOpening
}
onClick={() => act(next)}
>
{description}
</Button>
</details>
)}
</section>
);
}