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);
|
||||
|
||||
Reference in new issue
Block a user