Files
Dune/game/tleilaxu-ambassador-leaders.ts
Quintine 152039be5b Support cohort-eligible Ambassador leader and KH repeats
Replace lifetime first-death caps with the selected provisional composition: first returns remain immediate, while later native leaders and KH must satisfy their existing ordinary death/cohort conditions. Extract one phase/slot/price-independent cohort quote for ordinary Revival and the independent Ambassador. Preserve ordinary timing, payment, prevention, native early service and one-leader controls.

Snapshot an opened cohort before a free return makes a leader alive, preserving remaining ordinary returns. Keep Ecaz identity tracking and the shared Duke permission separate. KH retains activation/loss history and five native discs; update its marker above the actual opened owner cycle and old marker. Independent grants retain ordinary allowance and income policies. Explain eligibility in persistent controls and source-labelled reference.

Verify related rules/controls, types/lint/build, controlled native/copied free-return-to-next-ordinary-Revival programmes layered on original public Basic paid-entry sources, controlled Advanced KH actual entry/return, income allow/cancel and twelve minimal legal policy continuations. Assigned repeat/cohort facts are explicitly controlled, not claimed original battle histories. Full modes, wider custody/module combinations, comprehensive assurance and deployed acceptance remain unfinished. No strategic calibration, saved-room operation, reset, restart or deployment.
2026-10-24 13:43:11 +11:00

95 lines
4.7 KiB
TypeScript

import type { Game, Player } from './engine';
import type { Leader } from './cards';
import { createDukeVidal, DUKE_VIDAL_ID } from './duke-vidal';
import { resolveEcazDukeRevival, type EcazDukeRevivalContext } from './ecaz-duke-revival';
import {
quoteLeaderRevivalCohort, type LeaderRevivalCohortPlayer,
type LeaderRevivalCohortQuote,
} from './revival';
type Context = Pick<Game, 'advanced'> & Partial<Omit<EcazDukeRevivalContext, 'advanced'>>;
type Beneficiary = Pick<Player, 'faction' | 'leaders' | 'kwisatz' | 'battleLosses'> &
Partial<Pick<Player, 'id' | 'revivalCycle' | 'ecazRevivalCohort'>>;
export type TleilaxuAmbassadorLeaderChoice = {
id: string; name: string; strength: number; destination?: 'setAside';
};
const dukeIdentity = createDukeVidal().leader;
/** Legacy identity-only callers retain their first-death grant. Repeated
* returns need the actual seated owner's ordinary cohort, never invented facts
* or the native Tleilaxu negotiated early-revival service. */
function ordinaryCohort(context: Context, player: Beneficiary): LeaderRevivalCohortQuote | null {
if (typeof player.id !== 'string' || !Number.isSafeInteger(player.revivalCycle) ||
player.revivalCycle! < 0 || context.status !== 'playing' ||
!Number.isSafeInteger(context.turn) || context.turn! <= 0 ||
!Array.isArray(context.players) ||
!context.players.some(seated => seated.id === player.id && seated.faction === player.faction)) return null;
try {
return quoteLeaderRevivalCohort(context as EcazDukeRevivalContext, player as LeaderRevivalCohortPlayer);
} catch {
return null;
}
}
const ownLeader = (player: Beneficiary, leader: Leader, cohort: LeaderRevivalCohortQuote | null) =>
leader.id !== DUKE_VIDAL_ID && leader.faction === player.faction && leader.dead &&
!leader.capturedBy && !leader.gholaBy &&
Number.isSafeInteger(leader.deaths) &&
(leader.deaths === 1 || (leader.deaths > 1 && !!cohort?.leaders.includes(leader.id)));
/** Snapshot an actually open ordinary cycle before an independent return
* clears death. This does not waive a closed first cohort or mutate its owner. */
export function tleilaxuAmbassadorLeaderOpening(context: Context, player: Beneficiary): number | null {
const cycle = ordinaryCohort(context, player)?.cycle;
return cycle !== undefined && Number.isSafeInteger(cycle) && cycle > 0 ? cycle : null;
}
/** Duke's independent Ecaz-only permission retains its original death history.
* Reuse native identity/custody validation, projecting every unavailable source
* uniformly rather than exposing capture details or a profile-specific reason. */
function sharedDuke(context: Context, player: Beneficiary): boolean {
if (player.faction !== 'ecaz' || typeof player.id !== 'string') return false;
try {
resolveEcazDukeRevival(context as EcazDukeRevivalContext, player.id);
return true;
} catch {
return false;
}
}
/** KH keeps its own next-cohort marker, never a sixth native disc. The first
* return remains immediate; later returns need the actual ordinary cohort. */
function eligibleKwisatz(context: Context, player: Beneficiary): boolean {
if (!context.advanced || player.faction !== 'atreides' || !(player.battleLosses >= 7) ||
player.kwisatz?.dead !== true) return false;
const marker = player.kwisatz.revivalCycle ?? 1;
if (!Number.isSafeInteger(marker) || marker < 1) return false;
if (marker === 1) return true;
const cycle = tleilaxuAmbassadorLeaderOpening(context, player);
return cycle !== null && cycle >= marker;
}
/** 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' | typeof DUKE_VIDAL_ID | null {
if (id === DUKE_VIDAL_ID) return sharedDuke(context, player) ? DUKE_VIDAL_ID : null;
if (id === 'kwisatz') return eligibleKwisatz(context, player) ? 'kwisatz' : null;
const cohort = ordinaryCohort(context, player);
return player.leaders.find(leader => leader.id === id && ownLeader(player, leader, cohort)) ?? null;
}
export function tleilaxuAmbassadorLeaderChoices(
context: Context, player: Beneficiary,
): TleilaxuAmbassadorLeaderChoice[] {
const choices: TleilaxuAmbassadorLeaderChoice[] = [];
const cohort = ordinaryCohort(context, player);
for (const leader of player.leaders)
if (ownLeader(player, leader, cohort)) choices.push({ id: leader.id, name: leader.name, strength: leader.strength });
if (sharedDuke(context, player)) choices.push({
id: DUKE_VIDAL_ID, name: dukeIdentity.name, strength: dukeIdentity.strength, destination: 'setAside',
});
if (eligibleKwisatz(context, player)) choices.push({ id: 'kwisatz', name: 'Kwisatz Haderach', strength: 2 });
return choices;
}