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
@@ -23,7 +23,7 @@ import { TERRITORIES } from '@/game/board';
|
||||
import { faction } from '@/game/catalog';
|
||||
|
||||
export const AMBASSADOR_COVERAGE =
|
||||
'Development coverage: Ambassador placement, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are supported, plus Bene Gesserit copies of those eight ordinary effects. Duke loan choices, other effects and some competing arrival reactions remain unfinished; full Ecaz starts remain disabled.';
|
||||
'Development coverage: Ambassador placement, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen, Guild and Tleilaxu entry effects are supported, plus Bene Gesserit copies of those eight ordinary effects. The Tleilaxu effect returns physical forces only; its printed leader alternative and ordinary revival accounting remain unfinished. Duke loan choices, other effects and some competing arrival reactions remain unfinished; full Ecaz starts remain disabled.';
|
||||
function tokenLocation(token: AmbassadorToken) {
|
||||
if (token.zone === 'placed')
|
||||
return `Placed in ${TERRITORIES.find((t) => t.id === token.location)?.name ?? 'a stronghold'}`;
|
||||
|
||||
@@ -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