Route Emperor Nexus extra revival through native pending settlement
This commit is contained in:
1 parent
022040972c
commit
22948ff6e4
27 files changed
+1901
-268
No files matched your search
+41
-31
@@ -44,7 +44,7 @@ import { quoteNexusMoritaniRetention, nexusMoritaniRetentionSignature, type Nexu
|
||||
import { quoteNexusChoamBetrayal, validateNexusChoamBetrayal, nexusChoamBetrayalSignature, type NexusChoamBetrayal } from './nexus-choam-betrayal';
|
||||
import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySignature, validateEcazInquiry, type EcazInquiryReceipt } from './nexus-ecaz-inquiry';
|
||||
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
|
||||
import { EMPEROR_NEXUS_REVIVALS, emperorNexusEvent, emperorNexusSignature, emperorNexusModeSupported, emperorNexusPools, emperorNexusRevivalElites, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { EMPEROR_NEXUS_REVIVALS, emperorNexusModeSupported, quoteEmperorNexusRevival, createEmperorNexusRevival, settleEmperorNexusRevival, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
|
||||
import { createEmperorPaymentReceipt, validateEmperorPaymentReceipt, emperorPaymentUseBlock, emperorPaymentSourceQuote, requiredEmperorPaymentResponders, emperorPaymentReceiptSignature, type EmperorPaymentReceipt } from './nexus-emperor-payment';
|
||||
import { FREMEN_NEXUS_FREE_FORCES, fremenNexusRevivalOffer, createFremenNexusRevival, upgradeFremenNexusRevival, validateFremenNexusRevival, type FremenNexusRevival } from './nexus-fremen-revival';
|
||||
@@ -878,6 +878,7 @@ export type Decision =
|
||||
player: string;
|
||||
recipient: string;
|
||||
revival: 'forces' | 'leader';
|
||||
source?: 'nexusEmperor';
|
||||
}
|
||||
| {
|
||||
kind: 'faceDance';
|
||||
@@ -2731,12 +2732,13 @@ function emperorNexusUniformAuctionPayment(g: Game): boolean {
|
||||
function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
|
||||
const p = g.players.find(p => p.id === owner);
|
||||
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor' || byFaction(g,'emperor')) return null;
|
||||
const revival = quoteEmperorNexusRevival(g, owner);
|
||||
const purchasing = g.status === 'playing' && g.phase === 3 &&
|
||||
g.decision?.kind === 'auctionPayment' && g.decision.player === owner &&
|
||||
g.auction?.bidder === owner && !g.currentAuctionSale;
|
||||
const event = purchasing
|
||||
? emperorNexusPurchaseEvent(g.turn,owner,g.nexusEmperorPurchaseHistory?.length ?? 0,g.auction!.index,g.auction!.bid)
|
||||
: emperorNexusEvent(g.turn,g.phase,owner,g.nexusEmperorSecretHistory?.length ?? 0);
|
||||
: revival.event;
|
||||
let purchase: {blocked:string|null;price:number}|null = null;
|
||||
if (purchasing) {
|
||||
let purchaseBlock:string|null = null;
|
||||
@@ -2757,31 +2759,25 @@ function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
|
||||
}
|
||||
purchase = {blocked:purchaseBlock,price:g.auction!.bid};
|
||||
}
|
||||
let blocked:string|null = null;
|
||||
if (p.ally) blocked = 'Emperor Nexus Secret Ally requires an unallied holder.';
|
||||
else if (!emperorNexusModeSupported(g)) blocked = 'Emperor Nexus Secret Ally requires a supported classic Nexus profile; expansions and unrelated modules remain pending.';
|
||||
else if (g.status !== 'playing' || g.phase !== 4) blocked = 'Use the three-force Emperor Nexus return during Revival.';
|
||||
else if (g.response || g.decision || g.truthtrance || g.phaseOpening || g.pendingRevival || g.pendingKarama ||
|
||||
g.pendingTreacheryDiscard || g.pendingNullentropy || g.pendingExchange || g.pendingRicheseGift ||
|
||||
g.pendingRichesePurchaseIncome || g.pendingAmbassador || g.pendingCapture || g.battle ||
|
||||
g.nexusTraitorPending || g.nexusCards.phase?.stage === 'drawing') blocked = 'Finish the current interaction before using Emperor Nexus revival.';
|
||||
else if ((g.revivalPrevention?.turn === g.turn && g.revivalPrevention.player === owner) || g.revivalRules?.freeBlocked?.includes(owner) ||
|
||||
g.revivalRules?.expanded.includes(owner) || g.freeRevival.includes(owner))
|
||||
blocked = 'Emperor Nexus revival with free-revival suppression or expanded revival effects remains pending.';
|
||||
const eliteOptions = emperorNexusRevivalElites(p,g.advanced);
|
||||
if (!blocked && !eliteOptions.length) blocked = 'Emperor Nexus needs exactly three eligible forces in Tanks; a smaller return remains pending.';
|
||||
return {event,revival:{blocked,eliteOptions},purchase};
|
||||
return {event,revival,purchase};
|
||||
}
|
||||
function nexusEmperorSecretIntegrity(g:Game) {
|
||||
const history = g.nexusEmperorSecretHistory, events = g.nexusEmperorSecretEvents;
|
||||
if (history === undefined)
|
||||
requireRule(events === undefined,'Emperor Nexus has lost its saved revival history.');
|
||||
else {
|
||||
requireRule(g.nexusCards?.cards && emperorNexusModeSupported(g) && Array.isArray(history) && history.length > 0 &&
|
||||
requireRule(g.nexusCards?.cards && Array.isArray(history) && history.length > 0 &&
|
||||
Array.isArray(events) && events.length === history.length && new Set(events).size === events.length,
|
||||
'Emperor Nexus has lost its physical module or independent use markers.');
|
||||
for (const [index,record] of history.entries()) {
|
||||
nexusRule(() => validateEmperorNexusRevival(g,record));
|
||||
if (!('version' in record))
|
||||
requireRule(emperorNexusModeSupported(g),
|
||||
'A legacy Emperor return lost its original supported roster.');
|
||||
else if (record.stage === 'requested')
|
||||
requireRule(index === history.length - 1 && g.pendingRevival?.nexusEmperorEvent === record.event &&
|
||||
g.pendingRevival.player === record.owner,
|
||||
'The extra-three request lost its original pending revival.');
|
||||
requireRule(record.sequence === index && events[index] === record.event,
|
||||
'Emperor Nexus has duplicated or reordered a completed revival.');
|
||||
}
|
||||
@@ -2812,19 +2808,27 @@ function playNexusEmperorRevive(g:Game,p:Player,action:Action) {
|
||||
requireRule(offer && !offer.revival.blocked && action.event === offer.event &&
|
||||
Object.keys(action).sort().join(',') === 'elite,event,type' && offer.revival.eliteOptions.includes(action.elite as number),
|
||||
offer?.revival.blocked ?? 'Choose the current Emperor Nexus revival and an eligible exact three-force group.');
|
||||
const elite = action.elite as number, before = emperorNexusPools(p);
|
||||
const record = nexusRule(() => createEmperorNexusRevival(g, p.id, action.elite as number));
|
||||
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!,p.id,g.players));
|
||||
addNativeReserves(g,p,EMPEROR_NEXUS_REVIVALS,elite); p.tanks -= EMPEROR_NEXUS_REVIVALS;
|
||||
if (p.elites) {p.elites.tanks -= elite;p.elites.revived += elite;}
|
||||
techIncome(g, 'axlotl', p);
|
||||
const record:NexusEmperorRevival = {kind:'revival',event:offer.event,owner:p.id,turn:g.turn,phase:4,
|
||||
faction:p.faction,advanced:g.advanced,
|
||||
sequence:g.nexusEmperorSecretHistory?.length ?? 0,elite,before,after:emperorNexusPools(p),signature:''};
|
||||
record.signature = emperorNexusSignature(record);
|
||||
(g.nexusEmperorSecretHistory ??= []).push(record);
|
||||
(g.nexusEmperorSecretEvents ??= []).push(record.event);
|
||||
log(g,`${p.name} spent Emperor Nexus Secret Ally to return three forces${elite ? `, including ${elite} Fedaykin` : ''} from Tanks to reserves for free. Ordinary force and free-revival allowances are unchanged.`,
|
||||
log(g, `${p.name} spent Emperor Nexus Secret Ally to request exactly three additional forces for free through the original revival continuation. Ordinary force/free allowances are unchanged; native Tleilaxu prevention applies under the provisional independent-extra-return policy.`,
|
||||
{faction:p.faction,name:'Emperor Nexus revival'});
|
||||
beginRevival(g, { player: p.id, kind: 'forces', amount: EMPEROR_NEXUS_REVIVALS,
|
||||
elite: record.elite, free: EMPEROR_NEXUS_REVIVALS, normalCost: 0, cost: 0,
|
||||
checks: [], nexusEmperorEvent: record.event });
|
||||
}
|
||||
function finishNexusEmperorRevival(g: Game, revival: PendingRevival, outcome: 'returned' | 'denied') {
|
||||
if (!revival.nexusEmperorEvent) return;
|
||||
const history = g.nexusEmperorSecretHistory;
|
||||
const index = history?.findIndex(record => record.event === revival.nexusEmperorEvent) ?? -1;
|
||||
const record = history?.[index];
|
||||
requireRule(record && 'version' in record && record.version === 2 &&
|
||||
record.stage === 'requested' && record.owner === revival.player,
|
||||
'The extra-three revival lost its original consumed Nexus source.');
|
||||
history![index] = nexusRule(() => settleEmperorNexusRevival(g, record, outcome));
|
||||
if (outcome === 'denied')
|
||||
log(g, `${getPlayer(g, revival.player).name}'s Emperor Nexus extra return was prevented. The Nexus remains spent; no counters, payment, revival income or Tech activity was granted.`);
|
||||
}
|
||||
function playNexusEmperorPurchase(g:Game,p:Player,action:Action) {
|
||||
const offer = nexusEmperorSecretAllyOffer(g,p.id), purchase = offer?.purchase;
|
||||
@@ -24280,7 +24284,8 @@ function finishRevival(
|
||||
return;
|
||||
}
|
||||
const deploymentGrant = revival.kind === 'forces'
|
||||
? requireHomeworldRevivalGrant(g, p, revival.emperorExtra ? 'emperorExtra' : 'normal', {
|
||||
? requireHomeworldRevivalGrant(g, p, revival.nexusEmperorEvent ? 'nexusEmperor' :
|
||||
revival.emperorExtra ? 'emperorExtra' : 'normal', {
|
||||
amount: revival.amount!, elite: revival.elite ?? 0, free: revival.free,
|
||||
})
|
||||
: null;
|
||||
@@ -24294,13 +24299,14 @@ function finishRevival(
|
||||
if (revival.kind === 'forces') {
|
||||
const n = revival.amount!,
|
||||
elite = revival.elite ?? 0;
|
||||
const source = revival.emperorExtra ? 'emperorExtra' : 'normal';
|
||||
const source = revival.nexusEmperorEvent ? 'nexusEmperor' :
|
||||
revival.emperorExtra ? 'emperorExtra' : 'normal';
|
||||
const group = { amount: n, elite, free: revival.free };
|
||||
addNativeReserves(g, p, n, elite);
|
||||
p.tanks -= n;
|
||||
if (revival.emperorExtra)
|
||||
g.emperorExtra[p.id] = (g.emperorExtra[p.id] ?? 0) + n;
|
||||
else {
|
||||
else if (!revival.nexusEmperorEvent) {
|
||||
// Preserve unknown legacy usage instead of guessing how earlier paid returns were priced.
|
||||
if (p.freeForcesRevived !== undefined || p.revived === 0)
|
||||
p.freeForcesRevived = (p.freeForcesRevived ?? 0) + revival.free;
|
||||
@@ -24310,6 +24316,7 @@ function finishRevival(
|
||||
p.elites.tanks -= elite;
|
||||
p.elites.revived += elite;
|
||||
}
|
||||
finishNexusEmperorRevival(g, revival, 'returned');
|
||||
if (quote.techIncome) {
|
||||
const income = quote.techIncome;
|
||||
const token = g.techTokens![income.id];
|
||||
@@ -24375,7 +24382,8 @@ function beginRevival(g: Game, revival: PendingRevival) {
|
||||
'Tleilaxu prevented this faction’s normal revivals for this turn.',
|
||||
);
|
||||
if (revival.kind === 'forces')
|
||||
requireHomeworldRevivalGrant(g, getPlayer(g, revival.player), revival.emperorExtra ? 'emperorExtra' : 'normal',
|
||||
requireHomeworldRevivalGrant(g, getPlayer(g, revival.player),
|
||||
revival.nexusEmperorEvent ? 'nexusEmperor' : revival.emperorExtra ? 'emperorExtra' : 'normal',
|
||||
{ amount: revival.amount!, elite: revival.elite ?? 0, free: revival.free });
|
||||
g.pendingRevival = revival;
|
||||
const choam = byFaction(g, 'choam');
|
||||
@@ -24383,7 +24391,7 @@ function beginRevival(g: Game, revival: PendingRevival) {
|
||||
choam &&
|
||||
choam.id !== revival.player &&
|
||||
revival.free > 0 &&
|
||||
!revival.emperorExtra
|
||||
!revival.emperorExtra && !revival.nexusEmperorEvent
|
||||
) {
|
||||
g.decision = {
|
||||
kind: 'choamFreeRevival',
|
||||
@@ -28322,6 +28330,8 @@ export function executeSpecialKaramaIntent(
|
||||
discard(g, p, card.id);
|
||||
p.specialKaramaUsed = true;
|
||||
g.revivalPrevention = { player: target, turn: g.turn };
|
||||
if (g.pendingRevival?.nexusEmperorEvent)
|
||||
finishNexusEmperorRevival(g, g.pendingRevival, 'denied');
|
||||
g.pendingRevival = null;
|
||||
g.decision = null;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export type HomeworldRevivalDeploymentSource =
|
||||
| 'normal'
|
||||
| 'emperorExtra'
|
||||
| 'ghola'
|
||||
| 'nexusEmperor'
|
||||
| 'nexusTleilaxu';
|
||||
export type HomeworldRevivalDeploymentGroup = {
|
||||
amount: number;
|
||||
@@ -62,7 +63,7 @@ export function quoteHomeworldRevivalDeployment(
|
||||
group: HomeworldRevivalDeploymentGroup,
|
||||
): HomeworldRevivalDeploymentQuote | null {
|
||||
if (
|
||||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu'].includes(source) ||
|
||||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu', 'nexusEmperor'].includes(source) ||
|
||||
!group ||
|
||||
typeof group !== 'object' ||
|
||||
Array.isArray(group) ||
|
||||
@@ -74,7 +75,9 @@ export function quoteHomeworldRevivalDeployment(
|
||||
group.elite > group.amount ||
|
||||
!Number.isSafeInteger(group.free) ||
|
||||
group.free < 0 ||
|
||||
group.free > group.amount
|
||||
group.free > group.amount ||
|
||||
(source === 'nexusEmperor' &&
|
||||
(group.amount !== 3 || group.free !== 3))
|
||||
)
|
||||
throw new HomeworldCustodyError(
|
||||
'Revival deployment needs a valid source and exact newly returned physical group.',
|
||||
|
||||
@@ -136,13 +136,15 @@ function validateOriginal(frame: Original): void {
|
||||
!text(frame.player) ||
|
||||
!count(frame.turn, 1, Number.MAX_SAFE_INTEGER) ||
|
||||
!count(frame.phase, 0, 8) ||
|
||||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu'].includes(frame.source) ||
|
||||
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu', 'nexusEmperor'].includes(frame.source) ||
|
||||
(frame.source === 'ghola' ? !text(frame.card) : frame.card !== undefined) ||
|
||||
!keys(frame.group, ['amount', 'elite', 'free']) ||
|
||||
!count(frame.group.amount, 1) ||
|
||||
!count(frame.group.elite, 0, frame.group.amount) ||
|
||||
!count(frame.group.free, 0, frame.group.amount) ||
|
||||
(frame.source !== 'normal' && frame.group.free !== 0) ||
|
||||
(frame.source === 'nexusEmperor'
|
||||
? frame.group.amount !== 3 || frame.group.free !== 3
|
||||
: frame.source !== 'normal' && frame.group.free !== 0) ||
|
||||
!keys(frame.quote, [
|
||||
'kind',
|
||||
'normal',
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import type { Game, Player } from './engine';
|
||||
import { eliteRevivalRemaining } from './revival';
|
||||
import { classicNexusModulesProfile } from './nexus-module-profile';
|
||||
import { classicNexusLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { eliteRevivalRemaining, revivalPrevented } from './revival';
|
||||
import { FACTIONS } from './catalog';
|
||||
import {
|
||||
classicNexusModulesProfile,
|
||||
pairedNexusModulesProfile,
|
||||
singleE1E2NexusProfile,
|
||||
mixedE1E2NexusProfile,
|
||||
standaloneE3NexusProfile,
|
||||
pairedE3NexusProfile,
|
||||
mixedE3NexusProfile,
|
||||
} from './nexus-module-profile';
|
||||
import {
|
||||
classicNexusLeaderSkillsProfile,
|
||||
pairedNexusLeaderSkillsProfile,
|
||||
singleE1E2NexusLeaderSkillsProfile,
|
||||
mixedE1E2NexusLeaderSkillsProfile,
|
||||
standaloneE3NexusLeaderSkillsProfile,
|
||||
pairedE3NexusLeaderSkillsProfile,
|
||||
mixedE3NexusLeaderSkillsProfile,
|
||||
} from './leader-skill-profile';
|
||||
|
||||
export const EMPEROR_NEXUS_REVIVALS = 3;
|
||||
export type EmperorNexusPools = {
|
||||
@@ -14,7 +31,7 @@ export type EmperorNexusPools = {
|
||||
freeForcesRevived: number | null;
|
||||
spice: number;
|
||||
};
|
||||
export type NexusEmperorRevival = {
|
||||
type EmperorNexusRevivalIdentity = {
|
||||
kind: 'revival';
|
||||
event: string;
|
||||
owner: string;
|
||||
@@ -25,9 +42,23 @@ export type NexusEmperorRevival = {
|
||||
advanced: boolean;
|
||||
elite: number;
|
||||
before: EmperorNexusPools;
|
||||
after: EmperorNexusPools;
|
||||
signature: string;
|
||||
};
|
||||
/** Old receipts are completed physical evidence, not permission to return again. */
|
||||
export type NexusEmperorRevival =
|
||||
| (EmperorNexusRevivalIdentity & {
|
||||
version?: never;
|
||||
stage?: never;
|
||||
after: EmperorNexusPools;
|
||||
})
|
||||
| (EmperorNexusRevivalIdentity & { version: 2 } & (
|
||||
| { stage: 'requested'; after: null }
|
||||
| { stage: 'returned' | 'denied'; after: EmperorNexusPools }
|
||||
));
|
||||
export type NexusEmperorRevivalRequest = Extract<
|
||||
NexusEmperorRevival,
|
||||
{ version: 2; stage: 'requested' }
|
||||
>;
|
||||
export function emperorNexusPools(p: Player): EmperorNexusPools {
|
||||
return {
|
||||
reserves: p.reserves,
|
||||
@@ -91,60 +122,198 @@ export function emperorNexusRevivalElites(
|
||||
choices.push(elite);
|
||||
return choices;
|
||||
}
|
||||
const poolKeys =
|
||||
'eliteReserves,eliteRevived,eliteTanks,freeForcesRevived,reserves,revived,spice,tanks';
|
||||
const receiptKeys =
|
||||
'advanced,after,before,elite,event,faction,kind,owner,phase,sequence,signature,turn';
|
||||
const receiptV2Keys =
|
||||
'advanced,after,before,elite,event,faction,kind,owner,phase,sequence,signature,stage,turn,version';
|
||||
const whole = (n: unknown): n is number =>
|
||||
Number.isSafeInteger(n) && (n as number) >= 0;
|
||||
function shape(value: unknown, keys: string): boolean {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const prototype: unknown = Object.getPrototypeOf(value);
|
||||
return (prototype === Object.prototype || prototype === null) &&
|
||||
Object.keys(value).sort().join(',') === keys;
|
||||
}
|
||||
function validPools(pool: EmperorNexusPools): boolean {
|
||||
return shape(pool, poolKeys) &&
|
||||
Object.entries(pool).every(([key, n]) =>
|
||||
(key === 'freeForcesRevived' && n === null) || whole(n)) &&
|
||||
pool.eliteReserves <= pool.reserves && pool.eliteTanks <= pool.tanks &&
|
||||
pool.reserves + pool.tanks <= 20;
|
||||
}
|
||||
function samePools(a: EmperorNexusPools, b: EmperorNexusPools): boolean {
|
||||
return Object.keys(b).every(key =>
|
||||
a[key as keyof EmperorNexusPools] === b[key as keyof EmperorNexusPools]);
|
||||
}
|
||||
function returnedPools(r: NexusEmperorRevival): EmperorNexusPools {
|
||||
return {
|
||||
...r.before,
|
||||
reserves: r.before.reserves + EMPEROR_NEXUS_REVIVALS,
|
||||
tanks: r.before.tanks - EMPEROR_NEXUS_REVIVALS,
|
||||
eliteReserves: r.before.eliteReserves + r.elite,
|
||||
eliteTanks: r.before.eliteTanks - r.elite,
|
||||
eliteRevived: r.before.eliteRevived + r.elite,
|
||||
};
|
||||
}
|
||||
type ReceiptContext = Pick<Game, 'players' | 'turn' | 'advanced' | 'expansions'>;
|
||||
|
||||
/** Validate historical evidence only; later ordinary returns and income are not replayed. */
|
||||
export function validateEmperorNexusRevival(
|
||||
g: Game,
|
||||
g: ReceiptContext,
|
||||
r: NexusEmperorRevival,
|
||||
): void {
|
||||
const p = r && g.players.find((p) => p.id === r.owner);
|
||||
const validPools = (pool: EmperorNexusPools) =>
|
||||
pool &&
|
||||
Object.keys(pool).sort().join(',') ===
|
||||
'eliteReserves,eliteRevived,eliteTanks,freeForcesRevived,reserves,revived,spice,tanks' &&
|
||||
Object.entries(pool).every(
|
||||
([key, n]) =>
|
||||
(key === 'freeForcesRevived' && n === null) ||
|
||||
(Number.isSafeInteger(n) && n! >= 0),
|
||||
) &&
|
||||
pool.eliteReserves <= pool.reserves &&
|
||||
pool.eliteTanks <= pool.tanks &&
|
||||
pool.reserves + pool.tanks <= 20;
|
||||
if (
|
||||
!p ||
|
||||
Object.keys(r).sort().join(',') !==
|
||||
'advanced,after,before,elite,event,faction,kind,owner,phase,sequence,signature,turn' ||
|
||||
r.kind !== 'revival' ||
|
||||
r.phase !== 4 ||
|
||||
!Number.isSafeInteger(r.turn) ||
|
||||
r.turn < 1 ||
|
||||
r.turn > g.turn ||
|
||||
r.faction !== p.faction ||
|
||||
r.advanced !== g.advanced ||
|
||||
!Number.isSafeInteger(r.sequence) ||
|
||||
r.sequence < 0 ||
|
||||
!Number.isSafeInteger(r.elite) ||
|
||||
r.elite < 0 ||
|
||||
r.elite > 3 ||
|
||||
const p = r && g.players.find(p => p.id === r.owner);
|
||||
const legacy = shape(r, receiptKeys);
|
||||
if (!p || !(legacy || shape(r, receiptV2Keys)) ||
|
||||
r.kind !== 'revival' || r.phase !== 4 ||
|
||||
typeof r.owner !== 'string' || !r.owner.trim() ||
|
||||
!whole(r.turn) || r.turn < 1 || r.turn > g.turn ||
|
||||
r.faction !== p.faction || r.advanced !== g.advanced ||
|
||||
!whole(r.sequence) || !whole(r.elite) || r.elite > EMPEROR_NEXUS_REVIVALS ||
|
||||
r.event !== emperorNexusEvent(r.turn, 4, r.owner, r.sequence) ||
|
||||
r.signature !== emperorNexusSignature(r) ||
|
||||
!validPools(r.before) ||
|
||||
!validPools(r.after) ||
|
||||
r.before.tanks < 3 ||
|
||||
r.signature !== emperorNexusSignature(r) || !validPools(r.before) ||
|
||||
r.before.tanks < EMPEROR_NEXUS_REVIVALS ||
|
||||
r.elite > r.before.eliteTanks ||
|
||||
3 - r.elite > r.before.tanks - r.before.eliteTanks ||
|
||||
(g.advanced && r.before.eliteRevived + r.elite > 1) ||
|
||||
(p.faction !== 'fremen' &&
|
||||
(r.elite || r.before.eliteTanks || r.before.eliteReserves)) ||
|
||||
JSON.stringify(r.after) !==
|
||||
JSON.stringify({
|
||||
...r.before,
|
||||
reserves: r.before.reserves + 3,
|
||||
tanks: r.before.tanks - 3,
|
||||
eliteReserves: r.before.eliteReserves + r.elite,
|
||||
eliteTanks: r.before.eliteTanks - r.elite,
|
||||
eliteRevived: r.before.eliteRevived + r.elite,
|
||||
})
|
||||
)
|
||||
throw new Error(
|
||||
'Emperor Nexus revival has lost its exact original physical return or allowance.',
|
||||
);
|
||||
EMPEROR_NEXUS_REVIVALS - r.elite > r.before.tanks - r.before.eliteTanks)
|
||||
throw new Error('Emperor Nexus revival has lost its exact original physical return or allowance.');
|
||||
if (legacy) {
|
||||
if (g.expansions.length ||
|
||||
g.players.some(seat => !['atreides', 'harkonnen', 'fremen', 'guild', 'beneGesserit'].includes(seat.faction)) ||
|
||||
(g.advanced && r.before.eliteRevived + r.elite > 1) ||
|
||||
(p.faction !== 'fremen' &&
|
||||
(r.elite || r.before.eliteTanks || r.before.eliteReserves)) ||
|
||||
!r.after || !validPools(r.after) ||
|
||||
JSON.stringify(r.after) !== JSON.stringify(returnedPools(r)))
|
||||
throw new Error('Emperor Nexus revival has lost its exact original physical return or allowance.');
|
||||
return;
|
||||
}
|
||||
const typed = p.faction === 'ixians' || p.faction === 'fremen';
|
||||
if (r.version !== 2 ||
|
||||
g.players.some(seat => seat.faction === 'emperor') ||
|
||||
(!typed && (r.elite || r.before.eliteTanks || r.before.eliteReserves || r.before.eliteRevived)) ||
|
||||
(typed && r.before.eliteReserves + r.before.eliteTanks > (p.faction === 'ixians' ? 7 : 3)) ||
|
||||
(g.advanced && p.faction !== 'ixians' && r.before.eliteRevived + r.elite > 1) ||
|
||||
(r.before.freeForcesRevived !== null && r.before.freeForcesRevived > r.before.revived) ||
|
||||
(r.stage === 'requested'
|
||||
? r.after !== null
|
||||
: !['returned', 'denied'].includes(r.stage) || !r.after || !validPools(r.after) ||
|
||||
!samePools(r.after, r.stage === 'returned' ? returnedPools(r) : r.before)))
|
||||
throw new Error('Emperor Nexus revival has lost its requested, returned or denied source facts.');
|
||||
}
|
||||
|
||||
/** Reuse original fresh roster/module predicates, independently of purchase admission. */
|
||||
function revivalModeSupported(g: Game): boolean {
|
||||
if (g.semutaPreview || g.advancedPreview || g.kullPreview || g.nexusKullPreview ||
|
||||
g.guildBetrayalPreview || g.richeseBetrayalPreview || g.nexusIxianReplacementPreview ||
|
||||
g.nexusIxianBetrayalPreview || g.nexusHarkonnenBetrayalPreview || g.ecazTreachery)
|
||||
return false;
|
||||
if (g.leaderSkills)
|
||||
return classicNexusLeaderSkillsProfile(g) || pairedNexusLeaderSkillsProfile(g) ||
|
||||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
|
||||
standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g) ||
|
||||
mixedE3NexusLeaderSkillsProfile(g);
|
||||
if (classicNexusModulesProfile(g) || pairedNexusModulesProfile(g) ||
|
||||
singleE1E2NexusProfile(g) || mixedE1E2NexusProfile(g) ||
|
||||
standaloneE3NexusProfile(g) || pairedE3NexusProfile(g) || mixedE3NexusProfile(g))
|
||||
return true;
|
||||
// Bare classic and paired E1/E2 Nexus predate the optional-module profiles.
|
||||
if (!g.nexusCards || g.techTokens || g.strongholdCards || g.discoveryEnabled ||
|
||||
g.discoveries || g.discoveryStash || g.greatMaker ||
|
||||
g.players.length < 2 || g.players.length > 6 ||
|
||||
new Set(g.players.map(p => p.faction)).size !== g.players.length) return false;
|
||||
if (!g.expansions.length)
|
||||
return g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base'));
|
||||
if (g.expansions.length !== 1 || !['ix', 'choam'].includes(g.expansions[0])) return false;
|
||||
const expansion = g.expansions[0];
|
||||
return g.players.every(p => FACTIONS.some(f => f.id === p.faction &&
|
||||
(f.expansion === 'base' || f.expansion === expansion))) &&
|
||||
g.players.filter(p => FACTIONS.some(f => f.id === p.faction && f.expansion === expansion)).length === 2;
|
||||
}
|
||||
|
||||
/** Provisional composition, not a publisher clarification or numerical user ruling. */
|
||||
const revivalPolicy =
|
||||
'Provisional composed policy: exactly three additional free physical forces from Tanks, beyond ordinary force and free-revival allowances. Native typed caps and Tleilaxu prevention still apply; this is not ordinary free-rate revival, so ordinary free blocking (including LaLaLa) and higher or expanded rates do not reprice it. Recruits does not double this fixed grant. Spend the card on declaration; denial returns no forces and grants no revival income or Tech benefit. No leader, missing-native-Emperor payment, extra Karama response or refund is granted. Actual returns continue through native Tleilaxu, Tech and Homeworld handling.';
|
||||
|
||||
export function quoteEmperorNexusRevival(g: Game, owner: string): {
|
||||
event: string;
|
||||
blocked: string | null;
|
||||
eliteOptions: number[];
|
||||
policy: string;
|
||||
} {
|
||||
const p = g.players.find(p => p.id === owner);
|
||||
const event = emperorNexusEvent(g.turn, g.phase, owner, g.nexusEmperorSecretHistory?.length ?? 0);
|
||||
const eliteOptions = p ? emperorNexusRevivalElites(p, g.advanced) : [];
|
||||
let blocked: string | null = null;
|
||||
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor')
|
||||
blocked = 'Only the actual holder of the Emperor Nexus card may use this revival.';
|
||||
else if (g.players.some(p => p.faction === 'emperor'))
|
||||
blocked = 'Emperor Secret Ally requires the native Emperor absent from the roster.';
|
||||
else if (p.ally) blocked = 'Emperor Nexus Secret Ally requires an unallied holder.';
|
||||
else if (!revivalModeSupported(g))
|
||||
blocked = 'Emperor Nexus revival requires an original supported Nexus roster and module profile.';
|
||||
else if (g.status !== 'playing' || g.phase !== 4)
|
||||
blocked = 'Use the three-force Emperor Nexus return during Revival.';
|
||||
else if (g.response || g.decision || g.truthtrance || g.phaseOpening ||
|
||||
g.pendingRevival || g.pendingKarama || g.pendingTreacheryDiscard ||
|
||||
g.pendingNullentropy || g.pendingExchange || g.pendingRicheseGift ||
|
||||
g.pendingRichesePurchaseIncome || g.pendingAmbassador || g.pendingCapture ||
|
||||
g.battle || g.nexusTraitorPending || g.nexusTraitorParent ||
|
||||
g.nexusCards?.phase?.stage === 'drawing' ||
|
||||
(g.homeworldRevivalReturn && g.homeworldRevivalReturn.stage !== 'complete'))
|
||||
blocked = 'Finish the current interaction before using Emperor Nexus revival.';
|
||||
else if (revivalPrevented(g, owner))
|
||||
blocked = 'Native Tleilaxu prevention blocks this revival.';
|
||||
else if (!eliteOptions.length)
|
||||
blocked = 'Emperor Nexus needs exactly three eligible forces in Tanks.';
|
||||
return { event, blocked, eliteOptions, policy: revivalPolicy };
|
||||
}
|
||||
|
||||
/** Pure declaration receipt: the engine commits card custody and the original pending revival. */
|
||||
export function createEmperorNexusRevival(
|
||||
g: Game,
|
||||
owner: string,
|
||||
elite: number,
|
||||
): NexusEmperorRevivalRequest {
|
||||
const quote = quoteEmperorNexusRevival(g, owner);
|
||||
if (quote.blocked || !quote.eliteOptions.includes(elite))
|
||||
throw new Error(quote.blocked ?? 'Choose an eligible exact three-force typed group.');
|
||||
const p = g.players.find(p => p.id === owner)!;
|
||||
const r: NexusEmperorRevivalRequest = {
|
||||
kind: 'revival', version: 2, event: quote.event, owner, turn: g.turn, phase: 4,
|
||||
sequence: g.nexusEmperorSecretHistory?.length ?? 0,
|
||||
faction: p.faction, advanced: g.advanced, elite,
|
||||
before: emperorNexusPools(p), after: null, stage: 'requested', signature: '',
|
||||
};
|
||||
r.signature = emperorNexusSignature(r);
|
||||
validateEmperorNexusRevival(g, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Capture the actual physical finish before native income or Homeworld deployment. */
|
||||
export function settleEmperorNexusRevival(
|
||||
g: Game,
|
||||
receipt: NexusEmperorRevival,
|
||||
outcome: 'returned' | 'denied',
|
||||
): Extract<NexusEmperorRevival, { version: 2; stage: 'returned' | 'denied' }> {
|
||||
validateEmperorNexusRevival(g, receipt);
|
||||
const saved = g.nexusEmperorSecretHistory?.[receipt.sequence];
|
||||
const p = g.players.find(p => p.id === receipt.owner)!;
|
||||
if (receipt.version !== 2 || receipt.stage !== 'requested' ||
|
||||
!['returned', 'denied'].includes(outcome) ||
|
||||
receipt.turn !== g.turn || g.phase !== 4 || g.status !== 'playing' || p.ally ||
|
||||
!saved || saved.signature !== receipt.signature ||
|
||||
emperorNexusSignature(saved) !== receipt.signature ||
|
||||
g.nexusEmperorSecretEvents?.[receipt.sequence] !== receipt.event ||
|
||||
g.nexusEmperorSecretHistory?.length !== receipt.sequence + 1 ||
|
||||
g.nexusCards?.cards?.hands[receipt.owner] !== null ||
|
||||
g.nexusCards.cards.discard.filter(card => card === 'emperor').length !== 1)
|
||||
throw new Error('Emperor Nexus settlement needs the original committed requested source.');
|
||||
const result = {
|
||||
...receipt, stage: outcome, after: emperorNexusPools(p), signature: '',
|
||||
};
|
||||
result.signature = emperorNexusSignature(result);
|
||||
validateEmperorNexusRevival(g, result);
|
||||
return result;
|
||||
}
|
||||
+5
-4
@@ -4569,6 +4569,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Bene Gesserit Betrayal can prevent the native Voice while the ordinary pre-plan response is open. In Nexus games every seat responds even without a canceling card, so the window does not reveal the hidden holder. An unallied opponent spends one physical Bene Gesserit Nexus card to remove only that declared Voice; Prescience and plan commitments continue. Truthtrance has priority. This path does not grant borrowed Voice.',
|
||||
'Emperor Cunning requires an Advanced battle with at least five ordinary Emperor counters and no actual Sardaukar, before submitting your plan. The played Nexus card opens one Karama response. Allowing it makes five ordinary counters count as Sardaukar for this battle; cancellation prevents the entire enhancement and leaves the Nexus card spent.',
|
||||
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. Secret Ally revival and normal bank-auction purchase have connected controls; normal forced allied funding is the separate operation below. Seller-paid Secret Ally purchases remain unfinished.',
|
||||
'Emperor Secret Ally revival selects exactly three real normal/starred counters for free, outside ordinary total/free and native allied-extra usage. Declaration spends one Nexus and enters original pending revival; no leader or absent native payer is granted. Under the visible provisional independent-source policy, native Tleilaxu may prevent it and actual free returns award income once; ordinary free-rate blocking and Recruits do not reprice or double this fixed grant. Denial keeps both spent cards without forces/income/Tech or refund. Native typed caps, reserve deposits, Axlotl and eligible newly returned Fedaykin placement continue. Tleilax ordinary Free Revival deployment is not extended to this extra source. Source receipts distinguish requested, returned and denied; old physical receipts never replay.',
|
||||
'Emperor Betrayal has a battle alternative in Advanced play: an unallied opponent holding the Emperor Nexus card can spend it during the ordinary pre-plan Sardaukar response to suppress actual Sardaukar strength for this battle. Every seat receives the same response opportunity even with no card, keeping hidden custody private. The starred pieces remain physical Sardaukar; no card grants a new force or cancels their separate support benefit.',
|
||||
'During an original positive normal-auction purchase by Emperor’s ally, possible unallied held-Nexus rivals acknowledge in storm order before winner payment, regardless of secret face. Only the current actual Emperor-card holder may Use; Pass keeps cards and original payment. Under the provisional conservative full-price interpretation, native Emperor covers the full original price, reusing prepaid aid and adding only missing own spice. The buyer keeps their own spice and acquires one original card. Native Emperor income remains separately cancelable without refund; original next-lot inspection continues. No excess gift, bank-created subsidy or future-income borrowing is granted. Underfunded/fully prepaid Use is unavailable; special seller lots and full combinations remain open.',
|
||||
'With Fremen absent, an actual unallied Fremen-card holder may enable a fixed ordinary free quota of three at a quiet Revival boundary, then use normal partial/typed revival controls. Activation moves no counters, leader or spice. Actual prior free use counts; paid returns keep their cost and total usage. Native total limits, higher free rates, typed caps, prevention, Homeworld deposits/deployment, Axlotl activity and once-only Tleilaxu income remain original. Recruits doubles its native rate, not this fixed quota. Useless or unknown-ledger grants are unavailable. This is the visibly provisional recommended interpretation; strict old physical receipts adopt metadata without replay. Independent sandworm protection remains the separate operation below.',
|
||||
@@ -4598,11 +4599,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
|
||||
checklist: [
|
||||
{area:'Implementation',status:'Partial',detail:'Existing physical effects and Fremen fixed ordinary quota remain. Emperor Betrayal now binds original positive normal-auction allied obligation, neutral possible-holder acknowledgements, exact original lot/price/alliance/escrow/wallet and one physical card. Provisional full-price funding reuses prepaid aid, adds only missing own spice and calls native acquisition/income/next-lot continuation. Truthtrance discard restoration is retained; no refund, new bid, excessive gift or bank subsidy. Special seller composition, wider combinations and custody remain open.',evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts']},
|
||||
{area:'Player controls',status:'Partial',detail:'Emperor payment uses neutral original-event Continue and a current-holder-only Use with visible full-price interpretation and blocking reason. Actual390px neutral/pass/use followed by original income Allow or printed Karama cancellation preserves native acquisition and next-lot inspection. Existing Fremen normal partial controls, Voice/protection/Cunning and movement guidance remain. Wider controls and complete-mode acceptance stay open.',evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx']},
|
||||
{area:'AI',status:'Partial',detail:'Existing minimal legal profiles retain original responses and continuation. Fremen holders enable ordinary quota before native revival selection. Emperor compulsory-funding responders pass from the public event unless their current private offer permits Use. Genuine Easy neutral/pass/use and original paid acquisition are exercised; strategy and four-tier calibration remain deferred and unfinished.',evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts']},
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Existing effects and compulsory funding remain. Emperor Secret Ally now declares a signed exact-three independent source into original pending revival, with requested/returned/denied physical receipts and no ordinary or native allied-extra usage. Native Tleilaxu prevention/income, typed reserves, once-only Tech and Homeworld handling continue. Cyborg caps and returned Fedaykin placement stay native; zero-cost extras are not Tleilax ordinary Free Revival. Legacy physical evidence never replays. Wider combinations, custody and seller purchases remain open.', evidence: ['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Extra-three retains its actual typed selector and labels the provisional independent-source policy. Original native Tleilaxu clearance shows the committed extra source and no-refund consequence. Actual390px Allow then ordinary3, native Prevent and one returned Fedaykin placement use production controls; receipt lifecycle follows actual settlement. Existing compulsory funding and other Nexus controls remain. Wider controls and complete-mode acceptance stay open.', evidence: ['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing minimal profiles retain shared legal grant actions and native responses. Extra-three now enters the original pending pipeline instead of directly moving forces, with actual current typed quote/caps. Genuine Easy classic activation commits the independent physical return; compulsory funding and Fremen ordinary quota remain legal. No strategic tuning or four-tier calibration is claimed; both remain deferred and unfinished.', evidence: ['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts'] },
|
||||
{area:'Documentation',status:'Partial',detail:'Common rules and twelve faces retain remaining custody, inquiry, nested Cunning and combinations. Contracts distinguish named borrowed Voice cancellation, independent worm protection, fresh/storm BG card exception, Fremen ordinary fixed quota and Emperor original full-price funding. Interpretations are visibly provisional, not publisher answers or separately answered numerical rulings. Special seller composition and full-mode gates remain.',evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md']},
|
||||
{area:'Verification',status:'Partial',detail:'Compulsory-funding affected10-file union155/155 passes. Genuine Basic/Advanced setup, real worm/pact/closing draw and Guild price2 auction exercise prepaid0/1, allpass0/1/2, original income allow/cancel, Truthtrance restore and immutable event/actor/key refusals. Four API profiles and two real390px allow/cancel surfaces retain buyer6, reuse prepaid1/add missing1, acquire one card/spend one Nexus and restore lot1 inspection. Native Emperor10 returns on allowance or stays8 after cancellation; no refund/overflow/browser errors. Types/lint/build pass. Prior quota329/329 and BG Cunning65/65 evidence remains; special seller, full combinations, assurance and deployed acceptance remain open.',evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md']},
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Extra-return affected16-file union266/266 and types/lint/build pass. Genuine classic/native-Tleilaxu/Ix/Fedaykin setup, casualties, worm/pact/deal and Revival exercise original Allow/Prevent/income, later ordinary returns, typed caps and actual Homeworld placement. Fourteen API profiles reach Movement, including native deposits and Basic/Advanced Fedaykin. Three real390px surfaces prove requested-before-return, extra3 then ordinary3 with unchanged separate ledgers, once-only income/Tech, denied no-return/no-refund, and one star to Tabr14. Original legal reinforcement geometry and Fremen zero-support plans are respected; fixture-only wrong assumptions/obsolete pins are removed. No overflow/errors. Prior funding155/155/quota329/329 evidence remains; wider combinations, assurance and deployed acceptance stay open.', evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -64,6 +64,10 @@ function requireRevival(
|
||||
const nonnegative = (n: unknown): n is number =>
|
||||
Number.isSafeInteger(n) && (n as number) >= 0;
|
||||
|
||||
/** The Emperor Nexus return is its own fixed source: it moves real Tanks and
|
||||
* reserves without incrementing the ordinary revived/free revival ledgers or
|
||||
* the Emperor-extra allowance, so those counters are never re-floated for it.
|
||||
* Native physical and typed caps below still apply. */
|
||||
export function continuationCustody(
|
||||
g: RevivalCancellationContext,
|
||||
p: Player,
|
||||
@@ -79,13 +83,14 @@ export function continuationCustody(
|
||||
requireRevival(
|
||||
nonnegative(p.reserves + n) &&
|
||||
nonnegative(p.revived) &&
|
||||
(pending.emperorExtra
|
||||
? nonnegative(g.emperorExtra[p.id] ?? 0) &&
|
||||
nonnegative((g.emperorExtra[p.id] ?? 0) + n)
|
||||
: nonnegative(p.revived + n) &&
|
||||
((p.freeForcesRevived === undefined && p.revived !== 0) ||
|
||||
(nonnegative(p.freeForcesRevived ?? 0) &&
|
||||
nonnegative((p.freeForcesRevived ?? 0) + pending.free)))),
|
||||
(pending.nexusEmperorEvent !== undefined ||
|
||||
(pending.emperorExtra
|
||||
? nonnegative(g.emperorExtra[p.id] ?? 0) &&
|
||||
nonnegative((g.emperorExtra[p.id] ?? 0) + n)
|
||||
: nonnegative(p.revived + n) &&
|
||||
((p.freeForcesRevived === undefined && p.revived !== 0) ||
|
||||
(nonnegative(p.freeForcesRevived ?? 0) &&
|
||||
nonnegative((p.freeForcesRevived ?? 0) + pending.free))))),
|
||||
'The pending revival would overflow its force or revival counters.',
|
||||
);
|
||||
if (p.elites)
|
||||
@@ -207,6 +212,7 @@ export function quoteRevivalCancellation(
|
||||
);
|
||||
requireRevival(
|
||||
['forces', 'leader', 'kwisatz', 'foreignGhola'].includes(original.kind) &&
|
||||
original.nexusEmperorEvent === undefined &&
|
||||
nonnegative(original.cost) &&
|
||||
nonnegative(original.normalCost) &&
|
||||
original.cost <= original.normalCost &&
|
||||
|
||||
+109
-13
@@ -11,6 +11,11 @@ import { TECH_TOKENS, ownedTech } from './tech-tokens';
|
||||
import { tleilaxuHomeworldFreeIncomeBlocked } from './homeworld-benefits';
|
||||
import { DUKE_VIDAL_ID } from './duke-vidal';
|
||||
import { resolveEcazDukeRevival } from './ecaz-duke-revival';
|
||||
import {
|
||||
emperorNexusPools,
|
||||
validateEmperorNexusRevival,
|
||||
} from './nexus-emperor-secret-ally';
|
||||
import { validateNexusCards } from './nexus-cards';
|
||||
|
||||
export type RevivalResumeContext = Pick<
|
||||
Game,
|
||||
@@ -18,6 +23,7 @@ export type RevivalResumeContext = Pick<
|
||||
| 'phase'
|
||||
| 'turn'
|
||||
| 'advanced'
|
||||
| 'expansions'
|
||||
| 'homeworlds'
|
||||
| 'homeworldRevival'
|
||||
| 'players'
|
||||
@@ -29,6 +35,9 @@ export type RevivalResumeContext = Pick<
|
||||
| 'revivalFreeIncome'
|
||||
| 'dukeVidal'
|
||||
| 'revivalRequests'
|
||||
| 'nexusCards'
|
||||
| 'nexusEmperorSecretHistory'
|
||||
| 'nexusEmperorSecretEvents'
|
||||
>;
|
||||
export type RevivalResumeQuote = {
|
||||
/** The frozen request, including the unshifted remaining check queue. */
|
||||
@@ -55,6 +64,84 @@ function requireResume(condition: unknown, message: string): asserts condition {
|
||||
}
|
||||
const count = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
/** The independent source carries its own event marker and nothing else. */
|
||||
const nexusEmperorPendingKeys: Record<string, true> = {
|
||||
player: true,
|
||||
kind: true,
|
||||
amount: true,
|
||||
elite: true,
|
||||
free: true,
|
||||
cost: true,
|
||||
normalCost: true,
|
||||
checks: true,
|
||||
nexusEmperorEvent: true,
|
||||
};
|
||||
|
||||
/** The consumed card authorizes only its one still-requested frozen source.
|
||||
* Historical returned/denied receipts never authorize another pending revival. */
|
||||
function validateNexusEmperorSource(
|
||||
g: RevivalResumeContext,
|
||||
pending: PendingRevival,
|
||||
): void {
|
||||
const event = pending.nexusEmperorEvent;
|
||||
requireResume(
|
||||
typeof event === 'string' && event.length > 0 &&
|
||||
Object.keys(pending).every((key) => nexusEmperorPendingKeys[key]) &&
|
||||
pending.kind === 'forces' && pending.amount === 3 &&
|
||||
Number.isSafeInteger(pending.elite) && pending.elite! >= 0 &&
|
||||
pending.elite! <= 3 && pending.free === 3 &&
|
||||
pending.cost === 0 && pending.normalCost === 0 &&
|
||||
pending.payer === undefined && pending.emperorExtra === undefined &&
|
||||
pending.leader === undefined && Array.isArray(pending.checks) &&
|
||||
pending.checks.length === 0,
|
||||
'The Emperor Nexus source needs its original exact-three free request.',
|
||||
);
|
||||
const owner = g.players.find((p) => p.id === pending.player);
|
||||
const history = g.nexusEmperorSecretHistory;
|
||||
const events = g.nexusEmperorSecretEvents;
|
||||
requireResume(
|
||||
owner && !owner.ally &&
|
||||
!g.players.some((p) => p.faction === 'emperor') &&
|
||||
Array.isArray(history) && Array.isArray(events) &&
|
||||
history.length === events.length &&
|
||||
new Set(events).size === events.length &&
|
||||
history.every((receipt, index) =>
|
||||
receipt && typeof receipt === 'object' &&
|
||||
receipt.event === events[index] && receipt.sequence === index) &&
|
||||
history.filter((receipt) => receipt.event === event).length === 1,
|
||||
'The Emperor Nexus request has lost its original owner or source history.',
|
||||
);
|
||||
const receipt = history.find((entry) => entry.event === event)!;
|
||||
const currentPools = emperorNexusPools(owner);
|
||||
requireResume(
|
||||
receipt.version === 2 && receipt.stage === 'requested' &&
|
||||
receipt.owner === owner.id && receipt.turn === g.turn &&
|
||||
receipt.phase === g.phase && receipt.elite === pending.elite &&
|
||||
receipt.after === null &&
|
||||
receipt.before && Object.entries(currentPools).every(([key, value]) =>
|
||||
receipt.before[key as keyof typeof currentPools] === value) &&
|
||||
history.at(-1) === receipt &&
|
||||
history.filter((entry) =>
|
||||
entry.version === 2 && entry.stage === 'requested').length === 1,
|
||||
'The Emperor Nexus request does not match its still-requested physical source.',
|
||||
);
|
||||
try {
|
||||
validateEmperorNexusRevival(g, receipt);
|
||||
const cards = g.nexusCards?.cards;
|
||||
requireResume(cards,
|
||||
'The Emperor Nexus request has lost its consumed physical card.');
|
||||
validateNexusCards(cards, g.players);
|
||||
requireResume(
|
||||
cards.hands[owner.id] === null && cards.discard.includes('emperor'),
|
||||
'The Emperor Nexus request has lost its consumed physical card.',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof RevivalResumeError) throw error;
|
||||
throw new RevivalResumeError(
|
||||
error instanceof Error ? error.message : 'The Emperor Nexus source is invalid.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Only the next deterministic continuation is quoted. A genuine stop decision
|
||||
* or benefit response is a boundary, even when the outer automatic-response
|
||||
@@ -105,6 +192,14 @@ export function quoteRevivalResume(
|
||||
: original.payer === undefined,
|
||||
'The pending revival has an invalid Emperor funding source.',
|
||||
);
|
||||
requireResume(
|
||||
!g.nexusEmperorSecretHistory?.some((receipt) =>
|
||||
receipt.version === 2 && receipt.stage === 'requested') ||
|
||||
original.nexusEmperorEvent !== undefined,
|
||||
'The requested Emperor Nexus source is missing from its pending revival.',
|
||||
);
|
||||
if (original.nexusEmperorEvent !== undefined)
|
||||
validateNexusEmperorSource(g, original);
|
||||
if (original.kind === 'forces') {
|
||||
requireResume(
|
||||
count(original.amount) &&
|
||||
@@ -121,7 +216,8 @@ export function quoteRevivalResume(
|
||||
// ordinary revival usage. Validate the accepted group's free allocation
|
||||
// before settlement crosses a population threshold. A later independent
|
||||
// request gets the rate at its own declaration.
|
||||
if (g.homeworlds && !original.emperorExtra)
|
||||
if (g.homeworlds && !original.emperorExtra &&
|
||||
original.nexusEmperorEvent === undefined)
|
||||
requireResume(
|
||||
original.free ===
|
||||
Math.min(original.amount, freeRevivalRemaining(g, p)),
|
||||
@@ -131,7 +227,7 @@ export function quoteRevivalResume(
|
||||
const freeCyborgs = paid * 2 + elite - original.normalCost;
|
||||
requireResume(
|
||||
count(paid * 2) &&
|
||||
(p.faction === 'ixians'
|
||||
(p.faction === 'ixians' && original.nexusEmperorEvent === undefined
|
||||
? count(freeCyborgs) &&
|
||||
freeCyborgs >=
|
||||
Math.max(0, original.free - (original.amount - elite)) &&
|
||||
@@ -152,6 +248,7 @@ export function quoteRevivalResume(
|
||||
original.amount === undefined &&
|
||||
original.elite === undefined &&
|
||||
!original.emperorExtra &&
|
||||
original.nexusEmperorEvent === undefined &&
|
||||
(original.kind === 'kwisatz'
|
||||
? g.advanced &&
|
||||
p.faction === 'atreides' &&
|
||||
@@ -189,7 +286,8 @@ export function quoteRevivalResume(
|
||||
allowed.indexOf(check) >
|
||||
allowed.indexOf(original.checks[index - 1])),
|
||||
) &&
|
||||
(!original.emperorExtra || original.checks.length === 0) &&
|
||||
(!(original.emperorExtra || original.nexusEmperorEvent !== undefined) ||
|
||||
original.checks.length === 0) &&
|
||||
original.checks.every((check) => check === 'choamRevival' || !!tleilaxu),
|
||||
'The pending revival has invalid ordered benefit checks or no benefit owner.',
|
||||
);
|
||||
@@ -214,6 +312,7 @@ export function quoteRevivalResume(
|
||||
if (
|
||||
pending.kind === 'forces' &&
|
||||
!pending.emperorExtra &&
|
||||
pending.nexusEmperorEvent === undefined &&
|
||||
p.faction === 'fremen'
|
||||
) {
|
||||
requireResume(
|
||||
@@ -306,17 +405,14 @@ export function quoteRevivalResume(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stop)
|
||||
return {
|
||||
...quote,
|
||||
outcome: 'decision',
|
||||
nextDecision: {
|
||||
kind: 'revivalStop',
|
||||
player: tleilaxu.id,
|
||||
recipient: p.id,
|
||||
revival: pending.kind === 'forces' ? 'forces' : 'leader',
|
||||
},
|
||||
if (stop) {
|
||||
const nextDecision: NonNullable<RevivalResumeQuote['nextDecision']> = {
|
||||
kind: 'revivalStop', player: tleilaxu.id, recipient: p.id,
|
||||
revival: pending.kind === 'forces' ? 'forces' : 'leader',
|
||||
};
|
||||
if (pending.nexusEmperorEvent) nextDecision.source = 'nexusEmperor';
|
||||
return { ...quote, outcome: 'decision', nextDecision };
|
||||
}
|
||||
if (check)
|
||||
return {
|
||||
...quote,
|
||||
|
||||
@@ -265,6 +265,8 @@ export type PendingRevival = {
|
||||
kind: 'forces' | 'leader' | 'kwisatz' | 'foreignGhola';
|
||||
payer?: string;
|
||||
emperorExtra?: boolean;
|
||||
/** Independent fixed-three source; never native Emperor funding or ordinary usage. */
|
||||
nexusEmperorEvent?: string;
|
||||
amount?: number;
|
||||
elite?: number;
|
||||
leader?: string;
|
||||
|
||||
Reference in new issue
Block a user