Resolve Basic Ambassador child arrivals during Terror
This commit is contained in:
1 parent
a45d81aa34
commit
fbdf11c789
15 files changed
+817
-16
No files matched your search
+53
-10
@@ -2156,6 +2156,8 @@ export type Game = {
|
||||
/** A committed revival Sneak group waits for the original overlap before BG reactions. */
|
||||
revivalSneakAdvisor?: true;
|
||||
caladanSneakAdvisor?: true;
|
||||
/** An ordinary Sneak group waits for an existing Ambassador/Terror parent. */
|
||||
nativeSneakAdvisor?: true;
|
||||
}[] | null;
|
||||
/** One committed entry with two independently optional expansion reactions. */
|
||||
pendingArrivalOverlap?: {
|
||||
@@ -8849,6 +8851,10 @@ function semutaOfferSupported(
|
||||
const bgDiscard = continuation.kind === 'bgKaramaCostDiscard' ||
|
||||
continuation.kind === 'karamaBgConversionCancelDiscard';
|
||||
const ambassadorDiscard = continuation.kind === 'ambassador';
|
||||
const childTerrorDiscard = continuation.kind === 'terrorDiscard' &&
|
||||
!!g.pendingArrivalOverlap && bareBasicSemutaProfile(g) &&
|
||||
g.pendingArrivalOverlap.ambassadorEvent === continuation.entry.ambassadorEvent &&
|
||||
ambassadorTerrorEntryMatches(g, continuation.entry);
|
||||
if (ambassadorDiscard &&
|
||||
(!bareBasicSemutaProfile(g) || g.phase !== 5 ||
|
||||
continuation.entry.stage !== 'cards' || continuation.entry.resume !== 'none' ||
|
||||
@@ -8987,10 +8993,10 @@ function semutaOfferSupported(
|
||||
g.moritaniRetention.stage !== 'choose' || continuation.kind === 'battleResolved')) ||
|
||||
g.pendingIxSubstitution)) return false;
|
||||
if (continuation.kind === 'terrorDiscard' &&
|
||||
(continuation.entry.resume !== 'none' ||
|
||||
(!childTerrorDiscard && (continuation.entry.resume !== 'none' ||
|
||||
(continuation.entry.cause !== 'shipment' && continuation.entry.cause !== 'movement') ||
|
||||
(g.pendingArrivalOverlap && (!bareBasicSemutaProfile(g) ||
|
||||
!overlapOwnsTerrorDiscard(g, continuation.entry, continuation.owner))) ||
|
||||
!overlapOwnsTerrorDiscard(g, continuation.entry, continuation.owner)))) ||
|
||||
g.pendingChoamMove || g.pendingIxMove ||
|
||||
g.pendingFremenMove || g.ornithopter || g.summonedWorm ||
|
||||
g.wormRides.length > 0)) return false;
|
||||
@@ -9178,7 +9184,7 @@ function semutaOfferSupported(
|
||||
!g.pendingArrivalOverlap && !g.pendingRevival &&
|
||||
!g.ornithopter && !g.ecologicalStorm)) &&
|
||||
(richeseGiftDiscard || ambassadorBenefitDiscard || (!g.pendingExchange && (replacementCostDiscard || !g.pendingIxAlly) && !g.pendingNullentropy &&
|
||||
(!g.pendingAmbassador || (advisorDiscard || terminalDiscard) &&
|
||||
(!g.pendingAmbassador || childTerrorDiscard || (advisorDiscard || terminalDiscard) &&
|
||||
semutaAdvisorAmbassadorParent(g,continuation.use.response)) &&
|
||||
(terrorAllianceDiscard || !g.pendingTerrorEntry))) &&
|
||||
(richeseGiftDiscard || noFieldDiscard || continuation.kind === 'karamaShipmentDiscard' ||
|
||||
@@ -22462,8 +22468,9 @@ function ambassadorEffectBlock(
|
||||
beneficiary: Player,
|
||||
entrant: Player,
|
||||
): string | null {
|
||||
if (g.pendingArrivalOverlap && (effect === 'fremen' || effect === 'guild'))
|
||||
return 'Finish the overlapping Terror before another Ambassador-created arrival; decline this effect.';
|
||||
if (g.pendingArrivalOverlap && (effect === 'fremen' || effect === 'guild') &&
|
||||
!bareBasicNativeArrivalOverlap(g))
|
||||
return 'This overlapping Ambassador-created arrival is only available in the bare Basic faction preview.';
|
||||
if (!implementedAmbassadorEffects.includes(effect))
|
||||
return 'This Ambassador effect is still being implemented.';
|
||||
if (effect === 'ecaz') {
|
||||
@@ -22489,13 +22496,15 @@ function ambassadorEffectBlock(
|
||||
return 'The entrant has no held Traitor Card to inspect.';
|
||||
return null;
|
||||
}
|
||||
function arrivalOverlapModeSupported(g: Game, revivalArrival?: RevivalEntryParent): boolean {
|
||||
function bareBasicNativeArrivalOverlap(g: Game): boolean {
|
||||
// Native Basic faction decks share the same ordered arrival queue. A deck
|
||||
// selection alone is not an optional phase module or a new arrival effect.
|
||||
if (!g.advanced && selectedPhaseModuleRoster(g) && byFaction(g, 'ecaz') && byFaction(g, 'moritani') &&
|
||||
return !g.advanced && selectedPhaseModuleRoster(g) && !!byFaction(g, 'ecaz') && !!byFaction(g, 'moritani') &&
|
||||
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens && !g.strongholdCards &&
|
||||
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker && !g.ecazTreachery)
|
||||
return true;
|
||||
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker && !g.ecazTreachery;
|
||||
}
|
||||
function arrivalOverlapModeSupported(g: Game, revivalArrival?: RevivalEntryParent): boolean {
|
||||
if (bareBasicNativeArrivalOverlap(g)) return true;
|
||||
if (homeworldPhaseModulesProfile(g) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
|
||||
if (revivalArrival && g.homeworlds && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
|
||||
if (bareBasicSemutaProfile(g) && byFaction(g, 'ecaz') && byFaction(g, 'moritani')) return true;
|
||||
@@ -22555,7 +22564,7 @@ function arrivalOverlapIntegrity(g: Game) {
|
||||
record.terrorTokens.every(id => typeof id === 'string' && id.length > 0) &&
|
||||
new Set(record.terrorTokens).size === record.terrorTokens.length &&
|
||||
(active === 'ambassador'
|
||||
? !!ambassador && !terror &&
|
||||
? !!ambassador && (!terror || ambassadorTerrorEntryMatches(g, terror)) &&
|
||||
ambassador.event === record.ambassadorEvent &&
|
||||
ambassador.owner === record.ecaz &&
|
||||
ambassador.entrant === record.entrant &&
|
||||
@@ -22760,6 +22769,16 @@ function finishAmbassador(g: Game, deferResume = false) {
|
||||
'The Ambassador has lost its original overlapping arrival.');
|
||||
if (overlap.stage === 'first' && overlap.first === 'ambassador') {
|
||||
overlap.stage = 'second';
|
||||
// The Ambassador's child arrival may already have revealed or returned
|
||||
// the same physical Terror token. Do not recreate its original offer.
|
||||
const remaining = g.moritaniTerror?.tokens.some(token =>
|
||||
overlap.terrorTokens.includes(token.id) && token.status === 'placed' &&
|
||||
token.location === overlap.territory);
|
||||
if (!remaining) {
|
||||
log(g, 'The original Terror opportunity ends because its triggered token is no longer in the stronghold.');
|
||||
finishArrivalOverlap(g, overlap);
|
||||
return;
|
||||
}
|
||||
requireRule(openTerrorEntry(g, getPlayer(g, overlap.entrant),
|
||||
overlap.territory, overlap.sector, overlap.amount, overlap.elite,
|
||||
overlap.cause, 'none', undefined, overlap.revivalArrival, overlap.caladanArrival),
|
||||
@@ -25468,6 +25487,15 @@ function decideTerror(g: Game, p: Player, action: Action) {
|
||||
phase:g.phase,revivalArrival,revivalSneakAdvisor:true});
|
||||
return;
|
||||
}
|
||||
if (amount > 0 && !g.advanced && (g.pendingArrivalOverlap || g.pendingAmbassador || g.pendingTerrorEntry ||
|
||||
g.response || g.decision)) {
|
||||
// The child may have opened the original Terror decision. Let it finish
|
||||
// before offering advisor reactions for this already-committed group.
|
||||
(g.pendingArrivalReaction ??= []).push({entrant:p.id,territory:entry.territory,
|
||||
sector:entry.sector,amount,elite:0,cause:'shipment',resume:'none',turn:g.turn,
|
||||
phase:g.phase,nativeSneakAdvisor:true});
|
||||
return;
|
||||
}
|
||||
// The Terror effect is the arrival itself, so the adopted order resolves
|
||||
// the committed entry first and then offers the Bene Gesserit reactions
|
||||
// the same off-planet reserve entry already uses for ordinary shipments.
|
||||
@@ -25481,6 +25509,10 @@ function decideTerror(g: Game, p: Player, action: Action) {
|
||||
: undefined;
|
||||
if (!intrusion(g, p, entry.territory, followup ? { followup } : undefined) && followup)
|
||||
g.decision = { kind: 'advisor', player: bg!.id, ...followup };
|
||||
// The committed Sneak group is a separate arrival. If advisor controls
|
||||
// opened, the existing entry queue waits for them before its reactions.
|
||||
if (!g.advanced)
|
||||
openTerritoryEntry(g, p, entry.territory, entry.sector, amount, 0, 'shipment');
|
||||
}
|
||||
} else if (entry.stage === 'robbery') {
|
||||
requireRule(
|
||||
@@ -39983,6 +40015,17 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (!intrusion(g,entrant!,deferred.territory,{...source,...(followup ? {followup} : {})}) && followup)
|
||||
g.decision = {kind:'advisor',player:bg!.id,...followup};
|
||||
}
|
||||
if (deferred.nativeSneakAdvisor) {
|
||||
requireRule(deferred.turn === g.turn && deferred.phase === g.phase &&
|
||||
deferred.cause === 'shipment' && deferred.resume === 'none' &&
|
||||
entrant!.faction === 'moritani' && deferred.amount > 0 && deferred.elite === 0,
|
||||
'The queued Sneak reactions have lost their committed native entry.');
|
||||
const bg = byFaction(g, 'beneGesserit');
|
||||
const followup = bg && bg.id !== entrant!.id && spiritualAdvisorMaximum(g, bg.id) > 0
|
||||
? {shipment:entrant!.id,destination:location(deferred.territory,deferred.sector)} : undefined;
|
||||
if (!intrusion(g,entrant!,deferred.territory,followup ? {followup} : undefined) && followup)
|
||||
g.decision = {kind:'advisor',player:bg!.id,...followup};
|
||||
}
|
||||
openTerritoryEntry(g, entrant!, deferred.territory, deferred.sector,
|
||||
deferred.amount, deferred.elite, deferred.cause, deferred.resume, deferred.revivalArrival, deferred.caladanArrival);
|
||||
if (deferred.revivalArrival) resumeHomeworldRevivalReturn(g);
|
||||
|
||||
+1
-1
@@ -1697,7 +1697,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When an eligible entrant reaches the reusable Ecaz token, Ecaz may acquire the available shared Duke for itself or its current reciprocal ally. The token returns to supply and its companions are unchanged. Direct acquisition persists unused until battle use or Moritani acquisition; that shared tenure is provisional. It is distinct from the optional Basic or automatic Advanced new-alliance turn loan. A fresh token entry is required, with no standing transfer action.',
|
||||
'Unallied Ecaz may propose an alliance to an unallied entrant. Acceptance activates alliance abilities immediately. Basic then offers an optional Duke loan; Advanced automatically adds an available Duke under supplied PDF p30, without another acknowledgement. The first version releases a surviving loan after one battle, provisionally; an unused living loan is set aside at turn end. Unavailable Duke never prevents consent.',
|
||||
'When another eligible faction enters a marked stronghold, Ecaz may trigger its Ambassador for itself or its current ally, or leave it in place. Ecaz itself, its ally, advisors and the faction matching the physical Ambassador do not trigger it. Ordinary shipment and native Guild transport settle their own income, BG Intrusion and accompaniment controls before queued entry, without replaying payment or forces. Actual busy controls and unsupported paired or module combinations remain guarded.',
|
||||
'In supported bare Basic games with Ecaz and Moritani, a single entry that earns both an Ambassador and Terror opportunity retains both in their existing provisional storm order, including when other selected native factions are seated. Finish the first response before the second; the original arrival and payment occur once. Nested Fremen or Guild Ambassador arrivals and other unsupported module interactions remain guarded.',
|
||||
'In supported bare Basic games with Ecaz and Moritani, a single entry that earns both an Ambassador and Terror opportunity retains both in their existing provisional storm order, including when other selected native factions are seated. A Fremen relocation or Guild shipment from the Ambassador resolves its own child arrival before the original queue continues. If that child reveals the same Terror token, it cannot trigger again for the original entrant; if it is left hidden, the original opportunity remains. Unsupported module interactions remain guarded.',
|
||||
'Accompaniment joining retained BG fighters creates a distinct eligible child entry before the original shipper. Declining the child leaves the live token for the original entry; consuming it does not create a token to offer again. Advisors remain exempt. This existing child-first, live-token composition is explicitly provisional, not a publisher priority ruling.',
|
||||
'Emperor grants five bank spice. Atreides records the entrant’s current Treachery hand privately for the beneficiary; Harkonnen records one randomly selected held Traitor identity, or Face Dancer identity for a Tleilaxu entrant. These snapshots remain in the recipient’s private inspection history, identify when they were taken, and do not become a live view of later changes. Opening, enlarging or closing them needs no game acknowledgement.',
|
||||
'CHOAM lets the beneficiary choose any available hand cards, including none, for three spice each. Ixians require one available card to discard before drawing its replacement. Card controls respect projected reservations. After committing an Ixian exchange, a voluntary interruption must leave a card available for its required discard; a queued Truthtrance counts as already committed. A Bene Gesserit trigger permanently removes its token, then the beneficiary chooses an available effect outside the original supply for this group; unsupported copies remain visibly blocked.',
|
||||
|
||||
Reference in new issue
Block a user