Wire the Tleilaxu Ambassador force return
The last unimplemented Ambassador effect is now reachable. A placed Tleilaxu token gives its beneficiary — the Ecaz owner or its ally — a private ecazAmbassador decision at the new revival stage: one to four physical counters return from the Tanks to reserves at zero cost, quoted by the existing game/tleilaxu-ambassador-forces.ts, which also enforces the shared Fedaykin/Sardaukar one-per-turn counter and keeps Ixian cyborgs uncapped. The player view projects the maximum, Tanks and elite inventory and current elite usage to the beneficiary only; the entry control offers a per-count button and an explicit "Return no forces" decline; all four AI profiles choose a legal return from the decision owner's own view or decline when the Tanks are empty. Player-facing coverage text now lists Tleilaxu. Kept gated exactly as recorded: the printed leader alternative, ordinary force/free/leader allowance accounting around this independent grant, dead-twice eligibility, and Tleilaxu free-income repeat scope. No ordinary revival allowance, price, phase or income path changes. Evidence: tests/tleilaxu-ambassador-engine.test.ts drives a genuine arrival into a placed token and asserts the beneficiary-only projection, the typed return with unchanged spice, the shared elite limit, malformed/over-maximum rejection with immutability, the ordinary-only return, the decline path and the rendered control. The four-profile ecaz-entry-bots loop now covers tleilaxu, and the former unsupported-effect and copy-list pins were updated to the supported behaviour rather than re-pinned. Offline suite 7,797/7,797 with typecheck and lint clean. No game, seat, saved state or deployment changes.
This commit is contained in:
1 parent
07c20ae806
commit
0e1a442dca
11 files changed
+503
-27
No files matched your search
@@ -32,7 +32,7 @@ export function EcazEntry({
|
||||
!entry ||
|
||||
game.decision?.kind !== 'ecazAmbassador' ||
|
||||
game.decision.player !== game.me ||
|
||||
!['offer', 'allianceReply', 'copy', 'cards', 'move', 'ship'].includes(
|
||||
!['offer', 'allianceReply', 'copy', 'cards', 'move', 'ship', 'revival'].includes(
|
||||
entry.stage,
|
||||
)
|
||||
)
|
||||
@@ -254,6 +254,59 @@ export function EcazEntry({
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : entry.stage === 'revival' ? (
|
||||
<>
|
||||
<p className="m-0 text-sm leading-6">
|
||||
Return up to {entry.revival?.maximum ?? 0} physical{' '}
|
||||
{(entry.revival?.maximum ?? 0) === 1 ? 'force' : 'forces'} from the
|
||||
Tanks to your reserves for free. Ordinary revival allowances,
|
||||
prices and income are unchanged. The printed leader alternative
|
||||
stays unavailable pending its recorded interpretation.
|
||||
</p>
|
||||
{entry.revival?.blocked && (
|
||||
<p className="m-0 text-sm leading-6">{entry.revival.blocked}</p>
|
||||
)}
|
||||
{!entry.revival?.blocked &&
|
||||
(entry.revival?.maximum ?? 0) > 0 && (
|
||||
<fieldset
|
||||
className="flex min-w-0 flex-col gap-2 border-0 p-0"
|
||||
disabled={busy}
|
||||
>
|
||||
<legend className="text-sm">Forces to return</legend>
|
||||
{Array.from(
|
||||
{ length: entry.revival!.maximum },
|
||||
(_, index) => index + 1,
|
||||
).map((count) => (
|
||||
<Button
|
||||
key={count}
|
||||
variant="outline"
|
||||
className={buttonClass}
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
send({
|
||||
forces: count,
|
||||
elite: Math.min(
|
||||
entry.revival!.eliteTanks,
|
||||
Math.max(0, 1 - entry.revival!.eliteRevived),
|
||||
count,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Return {count} {count === 1 ? 'force' : 'forces'}
|
||||
</Button>
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className={buttonClass}
|
||||
disabled={busy}
|
||||
onClick={() => send({ decline: true })}
|
||||
>
|
||||
Return no forces
|
||||
</Button>
|
||||
</>
|
||||
) : entry.stage === 'ship' ? (
|
||||
<AmbassadorShipment
|
||||
key={entry.event}
|
||||
|
||||
Reference in new issue
Block a user