Support Moritani assassination of captured and foreign-ghola targets

Retain printed native identity and valid original custody while projecting only
uniform potential bounty before commitment. Resolve living/dead targets once,
clear selected capture/concealment and retain ordinary Ghola return custody.
Selected concealed-outcome disclosure is explicitly provisional.

Verified: 14 affected offline files100/100, types/lint and production build;
six actual choice/gameplay/GameTable SSR journeys and twelve profile paths.
Native capture source47 actions; actual paid Ghola prefix140 actions followed
by explicit conserved seven-action battle. No saved-room/D1/reset/restart or
deployment operation. Runtime-only driver fixes reused unchanged passed checks.
This commit is contained in:
Quintine committed 2026-10-24 19:03:03 +11:00
1 parent 61828150b9
commit eff668b202
25 files changed
+1041 -57

No files matched your search

+29 -4
View File
@@ -45,6 +45,8 @@ export type MoritaniAssassinateContext = {
held: readonly string[];
normallyRevealed: readonly string[];
leaders: readonly Leader[];
/** Native producer identities; never included in the pending player choices. */
custodians?: readonly { id: string; faction: FactionId }[];
};
/** The engine owns the full traitor universe and the actual Mentat draw.
* These lists verify the still-reserved and permanently removed card zones. */
@@ -59,6 +61,12 @@ export type MoritaniAssassinateChoice = {
bounty: number;
dead: boolean;
};
export type MoritaniAssassinateProjectedChoice = {
card: string;
name: string;
/** Printed potential only: actual life/capture/Ghola status is not disclosed. */
potentialBounty: number;
};
const opponents: readonly FactionId[] = [
'atreides',
@@ -335,6 +343,9 @@ export function moritaniAssassinateChoices(
'That Traitor Card is already reserved or removed by Assassinate.';
const matches = leaders.filter((l) => l.id === card),
leader = matches[0];
const nativeCustodian = (id: string | undefined, faction: FactionId) =>
!!id && id !== receipt.opponent &&
context.custodians?.some(player => player.id === id && player.faction === faction);
if (
!reason &&
(matches.length !== 1 ||
@@ -343,14 +354,15 @@ export function moritaniAssassinateChoices(
leader.strength !== native.strength ||
leader.faction !== receipt.faction ||
typeof leader.dead !== 'boolean' ||
leader.capturedBy ||
leader.gholaBy ||
leader.concealed ||
(leader.capturedBy !== undefined && !nativeCustodian(leader.capturedBy, 'harkonnen')) ||
(leader.gholaBy !== undefined && !nativeCustodian(leader.gholaBy, 'tleilaxu')) ||
(leader.concealed && !nativeCustodian(leader.concealed.captor, 'harkonnen')) ||
(card === CHOAM_AUDITOR_ID && (leader.capturedBy || leader.gholaBy || leader.concealed)) ||
(leader.controller !== undefined &&
leader.controller !== receipt.opponent))
)
reason =
'Assassinate with missing, altered, captured or foreign-ghola target custody remains pending.';
'Assassinate needs one canonical native target with valid original custody.';
if (reason) {
unavailable.push({ card, reason });
continue;
@@ -371,6 +383,19 @@ export function moritaniAssassinateChoices(
unavailable,
};
}
/** Pending options do not reveal whether a secret captive has been executed.
* Normal, captured, already-dead and foreign-ghola identities use the same shape.
* Only a committed reveal resolves actual death and awarded bounty.
*/
export function projectedMoritaniAssassinateChoices(context: MoritaniAssassinateContext) {
const choices = moritaniAssassinateChoices(context);
const cards: MoritaniAssassinateProjectedChoice[] = choices.cards.map(choice => ({
card: choice.card,
name: choice.name,
potentialBounty: printed(context.receipt.faction, choice.card)!.strength,
}));
return { blocked: choices.blocked, cards };
}
export function quoteMoritaniAssassinate(
context: MoritaniAssassinateContext,
card: string,