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.
This commit is contained in:
1 parent
16f654071a
commit
152039be5b
12 files changed
+719
-51
No files matched your search
+9
-2
@@ -15,7 +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 { tleilaxuAmbassadorLeaderChoices, tleilaxuAmbassadorLeaderSelection, tleilaxuAmbassadorLeaderOpening } from './tleilaxu-ambassador-leaders';
|
||||
import { createAmbassadorRevivalReceipt, validateAmbassadorRevivalReceipt, AmbassadorRevivalIncomeError,
|
||||
type AmbassadorRevivalReceipt, type AmbassadorRevivalReturn } from './tleilaxu-ambassador-income';
|
||||
import { leaderSkillStrongholdCount, sandmasterVictorySpice, sandmasterVictoryPiles, type SandmasterVictorySpiceOffer } from './leader-skill-battle-board';
|
||||
@@ -18353,7 +18353,7 @@ function resolveAmbassadorEffect(g: Game) {
|
||||
g.decision = { kind: 'ecazAmbassador', player: p.id };
|
||||
log(
|
||||
g,
|
||||
`${p.name} may revive one first-death own leader or return up to four physical forces for free through the Tleilaxu Ambassador. Ordinary revival allowances and income are unchanged.`,
|
||||
`${p.name} may revive one eligible own leader or return up to four physical forces for free through the Tleilaxu Ambassador. Repeated native leaders retain their ordinary death-cohort condition provisionally. Ordinary revival allowances remain independent; a positive return can earn the existing free-revival income.`,
|
||||
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' },
|
||||
);
|
||||
return;
|
||||
@@ -18610,6 +18610,8 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
|
||||
let name: string, returned: AmbassadorRevivalReturn;
|
||||
if (selected === 'kwisatz') {
|
||||
const kwisatz = p.kwisatz!;
|
||||
const opening = tleilaxuAmbassadorLeaderOpening(g, p);
|
||||
if (opening !== null) p.revivalCycle = Math.max(p.revivalCycle, opening);
|
||||
kwisatz.dead = false;
|
||||
delete kwisatz.usedAt;
|
||||
kwisatz.revivalCycle = Math.max(p.revivalCycle, kwisatz.revivalCycle ?? 1) + 1;
|
||||
@@ -18625,7 +18627,12 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
|
||||
returned = { kind: 'leader', leader: duke.id, deaths: duke.deaths };
|
||||
g.dukeVidal = consumeDuke(g.dukeVidal!);
|
||||
} else {
|
||||
// A living leader would hide a newly opened ordinary cohort: preserve
|
||||
// its quote before resurrection. Ecaz records identity against that same
|
||||
// original dead state before generic cycle persistence.
|
||||
const opening = tleilaxuAmbassadorLeaderOpening(g, p);
|
||||
recordEcazLeaderReturn(g, selected);
|
||||
if (opening !== null) p.revivalCycle = Math.max(p.revivalCycle, opening);
|
||||
selected.dead = false;
|
||||
delete selected.usedAt;
|
||||
name = selected.name;
|
||||
|
||||
+11
-7
@@ -1500,8 +1500,9 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
title: 'Ecaz Ambassadors: placement and remaining effects',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary:
|
||||
'End-of-Revival placement, eight random entry effects, direct Ecaz Duke acquisition and consensual alliance formation are supported in development fixtures, with public token guides and private inspection history.',
|
||||
'End-of-Revival placement, entry effects, cohort-eligible free revival, Ecaz Duke acquisition and consensual alliances have connected preview controls and public token guides.',
|
||||
steps: [
|
||||
'Ecaz has its reusable Ambassador plus five randomly selected Ambassadors from the other ten identities. Inspect the public supply, placed tokens, triggered group and unused pool to read each effect.',
|
||||
'Placed Ambassadors are face up by designer clarification. This implementation also shows the unplaced supply openly by default; that supply policy is an interpretation, not an explicit official FAQ ruling.',
|
||||
@@ -1522,7 +1523,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 child arrivals retain their own contracts. The native-module extension now composes ordinary paid arrival order with Atomics, Homeworlds and Nexus; other combinations and complete rules remain unfinished.',
|
||||
'The Tleilaxu Ambassador offers one eligible leader or up to four physical forces free. Ordinary native leaders retain their first-death limit; Atreides may choose its separate first-killed Kwisatz Haderach without adding a roster disc or resetting losses. An Ecaz beneficiary may instead return the original dead shared Duke through his independent permission, preserving death history and any native cohort opening. He becomes alive and set aside, unclaimed; a separate legal acquisition is needed before battle use. Ecaz’s ally cannot inherit that revival permission. Native and BG copied grants share private eligibility. Force/leader choices remain exclusive. Under the provisional income interpretation, a positive return earns seated Tleilaxu one bank spice per beneficiary per turn, sharing ordinary free revival; actual Ghola stays separate. Canceling income keeps the return and spent entitlement. Ordinary allowance independence and generic repeat/custody combinations remain provisional.',
|
||||
'The Tleilaxu Ambassador returns one eligible own leader or up to four physical forces free. First-death leaders and the first-killed Atreides KH need not wait for the ordinary cohort. Under the provisional repeat interpretation, later native leaders must satisfy their ordinary death condition, and later KH returns must meet their saved next-cohort marker. Preserve the actual opening before resurrection; KH advances its marker beyond both owner and old KH cycles without resetting activation losses or adding a native disc. Ecaz retains native identity bookkeeping. Ecaz may instead return the shared Duke alive and unclaimed under his independent permission; its ally cannot inherit that permission. Native and BG copied grants share eligibility and exclusive alternatives. A positive return provisionally earns seated Tleilaxu one bank spice per beneficiary/turn, sharing ordinary free revival; Ghola stays separate. Canceling income keeps the return and spent entitlement. Ordinary allowance independence and wider custody/module combinations remain provisional.',
|
||||
'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. Normal public Ecaz starts remain development previews; complete rules and unsupported combinations stay gated.',
|
||||
],
|
||||
checklist: [
|
||||
@@ -1530,21 +1531,21 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Original effects, copies, Duke acquisition, consent and token returns remain integrated. Ecaz’s Tleilaxu grant now returns the canonical shared dead Duke alive and set aside, preserving his history and satisfied cohort opening without a paid request, ordinary quota or extra disc. Positive Tleilaxu Ambassador returns now use the shared free-income ledger and a bound committed-return response; allow/cancel finishes the original arrival once. Bare Basic Ecaz+Ix with native Tleilaxu admits existing coalition/Face Dance mechanics; other combinations stay partial.',
|
||||
evidence: ['game/engine.ts', 'game/tleilaxu-ambassador-leaders.ts', 'game/tleilaxu-ambassador-income.ts', 'game/karama-context.ts', 'game/ecaz-duke-revival.ts', 'game/bot-arrival.ts'],
|
||||
'A shared ordinary death-cohort quote admits eligible native/KH repeats without importing normal phase/slot/price or native early-service gates into the independent grant. Preserve actual openings before native resurrection, retain Ecaz identity tracking and advance KH above its actual cycle while keeping five native discs and loss activation. Existing Duke, source-bound income and original entrant continuation remain; wider custody/modules stay partial.',
|
||||
evidence: ['game/engine.ts', 'game/revival.ts', 'game/tleilaxu-ambassador-leaders.ts', 'game/tleilaxu-ambassador-income.ts', 'game/karama-context.ts', 'game/ecaz-duke-revival.ts', 'game/bot-arrival.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Existing placement, effect, consent and movement controls remain. An entitled Duke choice explains the alive/unclaimed destination; free-return and income controls explain the provisional shared reward and cancellation retaining the completed return. Earlier four actual390px native transport/income/BG journeys remain separate evidence; original entrant movement and once-only payment are retained.',
|
||||
'Persistent free-return guidance explains first-return and later-cohort eligibility, KH’s separate marker, ordinary-slot independence and the provisional shared income. Original exact native/KH/Duke buttons, busy controls, force alternatives and committed-return/income continuation remain. Earlier mobile arrival evidence stays separate.',
|
||||
evidence: ['components/ecaz-entry.tsx', 'components/game-table.tsx'],
|
||||
},
|
||||
{
|
||||
area: 'AI',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'All four existing profiles legally select the original free Duke return and an eligible revival-income response, then finish the entrant turn: eight native/BG-copy policy continuations with Tleilaxu seated. Earlier placement, consent, income/acquisition and BG arrival evidence remains separate. No strategy refinement or calibration.',
|
||||
'Twelve existing legal profiles select repeated native/copy/KH choices and finish the original entrant, using server-private eligibility. Earlier original Duke/income and placement/consent evidence remains separate. No strategy refinement or strength calibration.',
|
||||
evidence: ['game/bots.ts', 'tests/ambassador-revival-income-engine.test.ts', 'tests/duke-ambassador-engine.test.ts', 'docs/TLEILAXU_AMBASSADOR_RULES.md'],
|
||||
},
|
||||
{
|
||||
@@ -1558,8 +1559,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Verification',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Current income8-file81/81, types/lint/build, four original Basic native/copy allow/cancel gameplay/rendering programmes and eight legal policies pass. Original47/22 components, actual acquisition/death/paid Guild entry and dealt Karama precede the return and bound bank income. Six labelled Basic Tleilaxu coalition consumers retain actual setup/alliance/funded battles. Earlier Duke-only42 and arrival/browser evidence remain separate; full modes, wider combinations and deployed/final assurance stay open.',
|
||||
'Current repeat13-file114/114, types/lint/build and three controlled gameplay/rendering programmes pass: original Basic paid-entry sources plus assigned native histories reach the next actual ordinary Revival; separate Advanced actual entry returns KH with old2/cohort3 → marker4. Twelve legal policies finish original entry. First-opening, income allow/cancel and ordinary controls pass. Controlled histories are not earned repeat-battle/full-game proof. Earlier original Duke/income evidence stays separate; full modes, wider combinations, assurance and deployed acceptance remain open.',
|
||||
evidence: [
|
||||
'tests/ambassador-repeat-leader-engine.test.ts',
|
||||
'tests/fixture-ambassador-repeat-leaders.ts',
|
||||
'tests/tleilaxu-ambassador-leaders.test.ts',
|
||||
'tests/fixture-ambassador-revival-income.ts',
|
||||
'tests/ambassador-revival-income-engine.test.ts',
|
||||
'tests/tleilaxu-ambassador-income.test.ts',
|
||||
|
||||
+44
-20
@@ -5,7 +5,7 @@ import { isAuditorLeader } from './choam-auditor';
|
||||
import { homeworldLowBonus } from './homeworld-benefits';
|
||||
import { recruitsRevivalAllowance } from './recruits';
|
||||
import { DUKE_VIDAL_ID } from './duke-vidal';
|
||||
import { ecazDukeRevivalBlock, quoteEcazDukeRevival } from './ecaz-duke-revival';
|
||||
import { ecazDukeRevivalBlock, quoteEcazDukeRevival, type EcazDukeRevivalContext } from './ecaz-duke-revival';
|
||||
import {
|
||||
quoteEcazRevivalCohort, completeEcazRevivalCohortUse,
|
||||
recordEcazRevivalOpening, recordEcazFirstExecution,
|
||||
@@ -164,8 +164,45 @@ export function normalRevivalCycle(
|
||||
return cycle;
|
||||
}
|
||||
|
||||
/** Ordinary Ecaz cohorts keep native identity separate from independent Duke deaths.
|
||||
* Only the absent-Tleilaxu card quote may waive the unopened first cohort. */
|
||||
export type LeaderRevivalCohortContext = EcazDukeRevivalContext;
|
||||
export type LeaderRevivalCohortPlayer = Pick<
|
||||
Player, 'id' | 'faction' | 'leaders' | 'revivalCycle' | 'ecazRevivalCohort'
|
||||
>;
|
||||
export type LeaderRevivalCohortQuote = {
|
||||
cycle: number;
|
||||
leaders: readonly string[];
|
||||
dukeAvailable: boolean;
|
||||
dukeBlocked: string | null;
|
||||
};
|
||||
|
||||
/** Ordinary death/cohort permission only, independent of phase, allowance,
|
||||
* payment and prevention. The native Tleilaxu early service is separate.
|
||||
* Ecaz keeps native identity separate from independent Duke deaths; only the
|
||||
* absent-Tleilaxu card quote may waive the unopened first cohort. */
|
||||
export function quoteLeaderRevivalCohort(
|
||||
g: LeaderRevivalCohortContext,
|
||||
p: LeaderRevivalCohortPlayer,
|
||||
waiveOpening = false,
|
||||
): LeaderRevivalCohortQuote {
|
||||
const shared = p.faction === 'ecaz' && g.dukeVidal;
|
||||
const dukeContext = shared && g.status === 'playing' ? g : null;
|
||||
const dukeBlocked = dukeContext ? ecazDukeRevivalBlock(dukeContext, p.id) : null;
|
||||
const dukeAvailable = !!dukeContext && !dukeBlocked;
|
||||
const cohort = shared ? quoteEcazRevivalCohort(p, dukeAvailable, waiveOpening) : null;
|
||||
let cycle = cohort?.cycle ?? normalRevivalCycle(p);
|
||||
if (waiveOpening && !cohort) cycle = Math.max(1, cycle);
|
||||
const leaders = p.leaders.flatMap((leader) => {
|
||||
if (!leader.dead || leader.capturedBy || leader.gholaBy) return [];
|
||||
// Supplied Advanced p29 E permits Auditor revival each turn regardless
|
||||
// of the ordinary five-disc cohort, including its repeated deaths.
|
||||
const auditorReturn = g.advanced && p.faction === 'choam' && isAuditorLeader(leader);
|
||||
return auditorReturn || (cycle > 0 && (cohort
|
||||
? !cohort.returned.includes(leader.id) : leader.deaths === cycle))
|
||||
? [leader.id] : [];
|
||||
});
|
||||
return { cycle, leaders, dukeAvailable, dukeBlocked };
|
||||
}
|
||||
|
||||
export function leaderRevivalOptions(
|
||||
g: Pick<
|
||||
Game,
|
||||
@@ -192,27 +229,14 @@ export function leaderRevivalOptions(
|
||||
waiveOpening = false,
|
||||
) {
|
||||
const shared = p.faction === 'ecaz' && g.dukeVidal;
|
||||
const dukeContext = shared && g.status === 'playing' ? g : null;
|
||||
const dukeBlocked = dukeContext ? ecazDukeRevivalBlock(dukeContext, p.id) : null;
|
||||
const dukeAvailable = !!dukeContext && !dukeBlocked;
|
||||
const cohort = shared ? quoteEcazRevivalCohort(p, dukeAvailable, waiveOpening) : null;
|
||||
let cycle = cohort?.cycle ?? normalRevivalCycle(p);
|
||||
if (waiveOpening && !cohort) cycle = Math.max(1, cycle);
|
||||
const { cycle, leaders: cohortLeaders, dukeAvailable, dukeBlocked } =
|
||||
quoteLeaderRevivalCohort(g, p, waiveOpening);
|
||||
const enabled = g.phase === 4 && !revivalPrevented(g, p.id);
|
||||
const discount = revivalDiscount(g, p);
|
||||
const leaders = p.leaders.flatMap((leader) => {
|
||||
if (!enabled || !leader.dead || leader.capturedBy || leader.gholaBy)
|
||||
return [];
|
||||
// Supplied Advanced p29 E permits Auditor revival each turn regardless
|
||||
// of the other leaders in Tanks; retain the ordinary one-leader allowance.
|
||||
const auditorReturn =
|
||||
g.advanced &&
|
||||
p.faction === 'choam' &&
|
||||
isAuditorLeader(leader);
|
||||
const normal =
|
||||
!p.leaderRevived &&
|
||||
(auditorReturn || (cycle > 0 && (cohort
|
||||
? !cohort.returned.includes(leader.id) : leader.deaths === cycle)));
|
||||
const normal = !p.leaderRevived && cohortLeaders.includes(leader.id);
|
||||
if (
|
||||
!normal &&
|
||||
(p.faction !== 'tleilaxu' ||
|
||||
@@ -232,7 +256,7 @@ export function leaderRevivalOptions(
|
||||
];
|
||||
});
|
||||
if (dukeAvailable && enabled && !p.leaderRevived) {
|
||||
const quote = quoteEcazDukeRevival(dukeContext!, p.id, !!discount);
|
||||
const quote = quoteEcazDukeRevival(g, p.id, !!discount);
|
||||
leaders.push({
|
||||
id: DUKE_VIDAL_ID,
|
||||
name: quote.duke.leader.name,
|
||||
|
||||
@@ -2,17 +2,47 @@ 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'>>;
|
||||
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;
|
||||
|
||||
const ownFirstDeath = (player: Beneficiary, leader: Leader) =>
|
||||
leader.id !== DUKE_VIDAL_ID && leader.faction === player.faction && leader.dead && leader.deaths === 1 &&
|
||||
!leader.capturedBy && !leader.gholaBy;
|
||||
/** 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
|
||||
@@ -27,12 +57,16 @@ function sharedDuke(context: Context, player: Beneficiary): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
/** 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. */
|
||||
@@ -40,19 +74,21 @@ 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 firstKwisatz(context, player) ? 'kwisatz' : null;
|
||||
return player.leaders.find(leader => leader.id === id && ownFirstDeath(player, leader)) ?? 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 (ownFirstDeath(player, leader)) choices.push({ id: leader.id, name: leader.name, strength: leader.strength });
|
||||
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 (firstKwisatz(context, player)) choices.push({ id: 'kwisatz', name: 'Kwisatz Haderach', strength: 2 });
|
||||
if (eligibleKwisatz(context, player)) choices.push({ id: 'kwisatz', name: 'Kwisatz Haderach', strength: 2 });
|
||||
return choices;
|
||||
}
|
||||
Reference in new issue
Block a user