Offer first-death Kwisatz Haderach through the Tleilaxu Ambassador

This commit is contained in:
Quintine committed 2026-10-12 23:20:37 +11:00
1 parent bbf27c4f3d
commit d14f3d1717
11 files changed
+397 -19

No files matched your search

+19 -12
View File
@@ -15,6 +15,7 @@ import {
} from './leader-skill-combat';
import { quoteSukPhysicalRescue, quoteSukRescue, SukPhysicalRescueError, sukReceiptSignature, sukRescueOptions, type SukForceGroup, type SukPhysicalRescueQuote, type SukPhysicalRescueReceipt, type SukReserveDestinations, type SukRescueOption, type SukRescueReceipt } from './suk-graduate';
import { quoteTleilaxuAmbassadorForces, TleilaxuAmbassadorForceError } from './tleilaxu-ambassador-forces';
import { tleilaxuAmbassadorLeaderChoices, tleilaxuAmbassadorLeaderSelection } from './tleilaxu-ambassador-leaders';
import { leaderSkillStrongholdCount, sandmasterVictorySpice, sandmasterVictoryPiles, type SandmasterVictorySpiceOffer } from './leader-skill-battle-board';
import { beginRihani, chooseRihaniDraw, finishRihani, validateRihani, type RihaniReceipt, type RihaniSkill } from './rihani-decipherer';
import { planetologistMovementModeSupported, planetologistLeader, planetologistRange, selectedOriginElites, groundMovementRange, type PlanetologistMovement } from './planetologist-movement';
@@ -16675,8 +16676,8 @@ function resolveAmbassadorEffect(g: Game) {
return;
}
if (effect === 'tleilaxu') {
// Initial leader path: first-death own discs, independent of ordinary quota.
// Repeat-death and shared-disc interactions remain explicit refinement work.
// First-death own identities include KH's separate token under base p17.
// Repeat returns, shared discs and ordinary-quota policy stay separate.
entry.stage = 'revival';
g.decision = { kind: 'ecazAmbassador', player: p.id };
log(
@@ -16879,13 +16880,21 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
if (action.leader !== undefined) {
requireRule(action.forces === undefined && action.elite === undefined,
'Choose a leader or forces, not both.');
const leader = p.leaders.find(l => l.id === action.leader &&
l.faction === p.faction && l.dead && l.deaths === 1 &&
!l.capturedBy && !l.gholaBy);
requireRule(leader, 'Choose a first-death own leader from the Tanks.');
leader.dead = false;
delete leader.usedAt;
log(g, `${p.name} revived ${leader.name} for free through the Tleilaxu Ambassador. The ordinary leader allowance is unchanged.`,
const selected = tleilaxuAmbassadorLeaderSelection(g, p, action.leader);
requireRule(selected, 'Choose an eligible first-death own leader or Kwisatz Haderach from the Tanks.');
let name: string;
if (selected === 'kwisatz') {
const kwisatz = p.kwisatz!;
kwisatz.dead = false;
delete kwisatz.usedAt;
kwisatz.revivalCycle = Math.max(p.revivalCycle, kwisatz.revivalCycle ?? 1) + 1;
name = 'Kwisatz Haderach';
} else {
selected.dead = false;
delete selected.usedAt;
name = selected.name;
}
log(g, `${p.name} revived ${name} for free through the Tleilaxu Ambassador. The ordinary leader allowance is unchanged.`,
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' });
finishAmbassador(g);
return;
@@ -32730,9 +32739,7 @@ export function viewGame(state: Game, id: string) {
tanks: me.tanks,
eliteTanks: me.elites?.tanks ?? 0,
eliteRevived: me.elites?.revived ?? 0,
leaders: me.leaders.filter(l => l.faction === me.faction &&
l.dead && l.deaths === 1 && !l.capturedBy && !l.gholaBy)
.map(l => ({ id: l.id, name: l.name, strength: l.strength })),
leaders: tleilaxuAmbassadorLeaderChoices(g, me),
blocked:
ambassadorEffectBlock(
g,
+3 -1
View File
@@ -1368,7 +1368,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The Guild Ambassador grants an immediate free shipment of zero through four physical reserve forces for Ecaz or its ally, including typed elites. Choose a clear territory and sector; this does not spend ordinary shipment or movement, ally aid or a retained rate card. Fremen may use destinations outside their ordinary reinforcement radius, but their southern reserves stay on-planet and this effect never permits storm entry. Only Ixians may ship directly into their placed mobile stronghold.',
'During Shipment and Movement, Guild can use its special Karama to stop another eligible off-planet Ambassador shipment before forces arrive; it does not stop this grant during a phase-one worm sequence. A successful off-planet shipment may invite Bene Gesserit accompaniment and accrues applicable phase-five Heighliner income once. BG can accompany Ixians into the mobile stronghold. Projected accompaniment choices show blocked sectors and keep Polar Sink and decline available when legal. Intrusion, separate Terror and accompanying fighter-triggered Ambassadors finish before the original entrant or worm rider resumes.',
'No-Field substitution into the free four-force grant remains unavailable. In bounded classic plus Ecaz/Moritani games, a shared Ambassador/Terror entry gives both owners saved optional prompts in current storm order without repeating the committed entry; this is a provisional table order, not a printed publisher priority. A Terror entry now waits for the arrival’s own interaction — Guild income, a Bene Gesserit fighter Intrusion or advisor choice, or Fremen storm protection — and then opens without replaying payment or force transfer. Ambassador-created Fremen/Guild child arrivals remain gated. Atomics Aftermath, Homeworlds, Nexus and Discovery combinations do not gain coverage from this overlap path.',
'The Tleilaxu Ambassador offers one first-death own-leader revival or up to four physical forces for free. These are exclusive choices; the beneficiary sees its own eligible leaders privately. The ordinary allowance remains unchanged provisionally. Repeat-death, shared-leader and revival-income interactions remain for refinement.',
'The Tleilaxu Ambassador offers one first-death own-leader revival or up to four physical forces for free. Atreides may choose its separate first-killed Kwisatz Haderach under the base leader-revival rule; this does not add a disc to the ordinary roster, reset battle losses or spend the ordinary leader allowance. Native and Bene Gesserit copied grants share the same private eligibility. Every existing KH return advances its original cycle marker; a prior return remains outside this first-death preview. The force/leader choices are exclusive and ordinary allowance independence stays provisional. Repeat-return, shared-leader and revival-income interpretations remain separate.',
'All eleven effect guides are readable. Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are integrated, together with Bene Gesserit copies of those eight effects and direct Duke acquisition or consensual alliance through the reusable Ecaz token. Advanced combined Occupy has a separate bounded Development Verified guide, and Basic even-force Occupy now composes the same shared controls. Remaining effects, exceptional Duke custody and complete Ecaz rules remain unfinished. Mobile stronghold placement is gated. Full Ecaz starts remain disabled.',
],
checklist: [
@@ -1412,6 +1412,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/ecaz-alliance-engine.test.ts',
'tests/ecaz-alliance-recovery.test.ts',
'tests/ecaz-entry-bots.test.ts',
'game/tleilaxu-ambassador-leaders.ts',
'tests/kwisatz-ambassador.test.ts',
'tests/ecaz-entry-continuations.test.ts',
'tests/ecaz-entry-recovery.test.ts',
'tests/richese-ambassador.test.ts',
+36
View File
@@ -0,0 +1,36 @@
import type { Game, Player } from './engine';
import type { Leader } from './cards';
type Context = Pick<Game, 'advanced'>;
type Beneficiary = Pick<Player, 'faction' | 'leaders' | 'kwisatz' | 'battleLosses'>;
export type TleilaxuAmbassadorLeaderChoice = { id: string; name: string; strength: number };
const ownFirstDeath = (player: Beneficiary, leader: Leader) =>
leader.faction === player.faction && leader.dead && leader.deaths === 1 &&
!leader.capturedBy && !leader.gholaBy;
/** Base p17 revives KH like a leader without adding it to the ordinary roster.
* Native KH death records start at cycle1; every existing return advances that
* marker. Repeat-return/ordinary-quota interpretation remains outside this grant. */
function firstKwisatz(context: Context, player: Beneficiary): boolean {
return context.advanced && player.faction === 'atreides' && player.battleLosses >= 7 &&
player.kwisatz?.dead === true && (player.kwisatz.revivalCycle ?? 1) === 1;
}
/** Return the actual own identity, never a foreign disc or a cloned physical leader. */
export function tleilaxuAmbassadorLeaderSelection(
context: Context, player: Beneficiary, id: unknown,
): Leader | 'kwisatz' | null {
if (id === 'kwisatz') return firstKwisatz(context, player) ? 'kwisatz' : null;
return player.leaders.find(leader => leader.id === id && ownFirstDeath(player, leader)) ?? null;
}
export function tleilaxuAmbassadorLeaderChoices(
context: Context, player: Beneficiary,
): TleilaxuAmbassadorLeaderChoice[] {
const choices: TleilaxuAmbassadorLeaderChoice[] = [];
for (const leader of player.leaders)
if (ownFirstDeath(player, leader)) choices.push({ id: leader.id, name: leader.name, strength: leader.strength });
if (firstKwisatz(context, player)) choices.push({ id: 'kwisatz', name: 'Kwisatz Haderach', strength: 2 });
return choices;
}