Resume storm traversal after surviving advisor conversion
Preserve current-sector losses and original Fremen casualty priority before the mandatory BG counter. Bind public sector facts to a private continuation and resume only the remaining storm once.
This commit is contained in:
1 parent
9959f1b347
commit
d66b594770
13 files changed
+869
-17
No files matched your search
+76
-3
@@ -12,7 +12,7 @@ import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, h
|
||||
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
|
||||
import { quoteAtreidesFullPlan } from './atreides-full-plan';
|
||||
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
|
||||
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance } from './advisor-automatic-flip';
|
||||
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance, type AutomaticAdvisorStorm } from './advisor-automatic-flip';
|
||||
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
|
||||
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
|
||||
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
|
||||
@@ -1277,6 +1277,7 @@ export type Game = {
|
||||
signature: string;
|
||||
};
|
||||
pendingAutomaticAdvisorFaceDance?: AutomaticAdvisorFaceDance;
|
||||
pendingAutomaticAdvisorStorm?: { receipt: AutomaticAdvisorStorm; ready: boolean; cursor?: string };
|
||||
advisorPreparation?: AdvisorPreparationWindow;
|
||||
ecazOccupyPreview?: boolean;
|
||||
/** Fresh native Guild/Nexus payment-replacement profile, never inferred from a save. */
|
||||
@@ -11539,6 +11540,8 @@ function continueStorm(g: Game) {
|
||||
if (g.decision) return;
|
||||
}
|
||||
}
|
||||
// All current-sector casualties finish before conversion or the next step.
|
||||
if (openStormAdvisorFlips(g)) return;
|
||||
if (resolution.traversed === resolution.distance) break;
|
||||
resolution.traversed++;
|
||||
const s = stormSectorAfter(resolution.from, resolution.traversed);
|
||||
@@ -11578,6 +11581,8 @@ function continueStorm(g: Game) {
|
||||
}
|
||||
for (const key of Object.keys(g.spice))
|
||||
if (splitLocation(key).sector === s) delete g.spice[key];
|
||||
// Retain surviving advisor stances through any pending Fremen choices.
|
||||
prepareStormAdvisorFlips(g);
|
||||
}
|
||||
if (g.ecologicalStorm?.stage === 'traversal')
|
||||
g.ecologicalStorm = nexusRule(() => finishDiscoveryStorm(g.ecologicalStorm!));
|
||||
@@ -20329,6 +20334,12 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
|
||||
'The mandatory conversion has lost its original completed Face Dance.');
|
||||
validateCompletedFaceDance(g, parent);
|
||||
}
|
||||
if (source.producer === 'storm') {
|
||||
const parent = g.pendingAutomaticAdvisorStorm;
|
||||
requireRule(parent && JSON.stringify(source.storm) === JSON.stringify(parent.receipt),
|
||||
'The mandatory conversion has lost its original storm sector.');
|
||||
validateStormAdvisorCursor(g, parent);
|
||||
}
|
||||
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
|
||||
return source;
|
||||
}
|
||||
@@ -20338,6 +20349,7 @@ function openAutomaticAdvisorFlip(g: Game) {
|
||||
if (!source) return;
|
||||
// Hold immediate losses through the original mandatory discard/casualty work.
|
||||
if (source.producer === 'battleLosses' && !g.pendingAutomaticAdvisorBattle?.resume) return;
|
||||
if (source.producer === 'storm' && !g.pendingAutomaticAdvisorStorm?.ready) return;
|
||||
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
|
||||
g.response = { kind: 'advisorFlip', owner: source.owner, location: source.territory,
|
||||
advisors: false, passed: [], automaticAdvisorFlip: source,
|
||||
@@ -20593,6 +20605,66 @@ function finishAutomaticAdvisorFaceDance(g: Game) {
|
||||
delete g.pendingAutomaticAdvisorFaceDance;
|
||||
finishBattle(g);
|
||||
}
|
||||
function stormAdvisorCursor(g: Game) {
|
||||
return JSON.stringify([g.stormResolution, g.stormMovementSource ?? null]);
|
||||
}
|
||||
function prepareStormAdvisorFlips(g: Game) {
|
||||
const resolution = g.stormResolution;
|
||||
if (!g.advanced || g.status !== 'playing' || g.phase !== 0 || !resolution || resolution.traversed <= 0) return;
|
||||
const sector = stormSectorAfter(resolution.from, resolution.traversed);
|
||||
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g)).filter(group =>
|
||||
gameTerritories(g).some(place => place.id === group.territory && place.sectors.includes(sector) &&
|
||||
stormExposesTerritory(place.id, g.shieldWallDestroyed)));
|
||||
if (!groups.length) return;
|
||||
let parent = g.pendingAutomaticAdvisorStorm;
|
||||
if (parent) requireRule(!parent.ready && parent.receipt.from === resolution.from &&
|
||||
parent.receipt.distance === resolution.distance && parent.receipt.traversed === resolution.traversed,
|
||||
'The storm conversion must retain its original current sector.');
|
||||
else {
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
|
||||
!g.pendingAutomaticAdvisorHomeworldTransport && !g.pendingAutomaticAdvisorGamont &&
|
||||
!g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorFaceDance,
|
||||
'Finish the original advisor conversion before another storm sector.');
|
||||
parent = { receipt: { event: crypto.randomUUID(), from: resolution.from,
|
||||
distance: resolution.distance, traversed: resolution.traversed, sector }, ready: false };
|
||||
g.pendingAutomaticAdvisorStorm = parent;
|
||||
}
|
||||
for (const group of groups) {
|
||||
const source: AutomaticAdvisorFlipSource = { event: crypto.randomUUID(), owner: group.player,
|
||||
territory: group.territory, turn: g.turn, phase: g.phase, forces: group.forces,
|
||||
producer: 'storm', storm: parent.receipt };
|
||||
getPlayer(g, group.player).advisors![group.territory].aloneFlipHold = source.event;
|
||||
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
|
||||
}
|
||||
}
|
||||
function openStormAdvisorFlips(g: Game) {
|
||||
prepareStormAdvisorFlips(g);
|
||||
const parent = g.pendingAutomaticAdvisorStorm;
|
||||
if (!parent) return false;
|
||||
requireRule(!parent.ready && !g.response && !g.decision && g.stormResolution?.pending.length === 0 &&
|
||||
!!g.pendingAutomaticAdvisorFlips?.length,
|
||||
'Finish all original current-sector casualties before advisor conversion.');
|
||||
parent.ready = true;
|
||||
parent.cursor = stormAdvisorCursor(g);
|
||||
openAutomaticAdvisorFlip(g);
|
||||
return true;
|
||||
}
|
||||
function validateStormAdvisorCursor(g: Game, parent: NonNullable<Game['pendingAutomaticAdvisorStorm']>) {
|
||||
const resolution = g.stormResolution, receipt = parent.receipt;
|
||||
requireRule(g.status === 'playing' && g.phase === 0 && parent.ready && resolution &&
|
||||
resolution.pending.length === 0 && resolution.from === receipt.from &&
|
||||
resolution.distance === receipt.distance && resolution.traversed === receipt.traversed &&
|
||||
parent.cursor === stormAdvisorCursor(g),
|
||||
'The mandatory conversion lost its original completed-sector storm cursor.');
|
||||
}
|
||||
function finishAutomaticAdvisorStorm(g: Game) {
|
||||
const parent = g.pendingAutomaticAdvisorStorm;
|
||||
if (!parent) return;
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length, 'Finish all original sector conversions before storm traversal.');
|
||||
validateStormAdvisorCursor(g, parent);
|
||||
delete g.pendingAutomaticAdvisorStorm;
|
||||
continueStorm(g);
|
||||
}
|
||||
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
|
||||
for (const entry of released)
|
||||
delete getPlayer(g, entry.player).advisors?.[entry.territory];
|
||||
@@ -22613,7 +22685,7 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
|
||||
function victory(g: Game, quote?: VictoryQuote) {
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
|
||||
!g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorHomeworldTransport &&
|
||||
!g.pendingAutomaticAdvisorFaceDance,
|
||||
!g.pendingAutomaticAdvisorFaceDance && !g.pendingAutomaticAdvisorStorm,
|
||||
'Resolve mandatory advisor conversions before checking victory.');
|
||||
quote ??= currentVictoryQuote(g);
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
@@ -28435,7 +28507,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
}
|
||||
if (automaticAdvisorFlip) {
|
||||
if (canceled)
|
||||
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' || automaticAdvisorFlip.producer === 'homeworldTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs stay' : automaticAdvisorFlip.producer === 'faceDance' ? 'Face Dance replacement and reveal stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
|
||||
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' || automaticAdvisorFlip.producer === 'homeworldTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs stay' : automaticAdvisorFlip.producer === 'faceDance' ? 'Face Dance replacement and reveal stay' : automaticAdvisorFlip.producer === 'storm' ? 'current-sector storm casualties stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
|
||||
{ faction: bg.faction, name: 'Mandatory advisor conversion prevented' });
|
||||
g.pendingAutomaticAdvisorFlips!.shift();
|
||||
if (!g.pendingAutomaticAdvisorFlips!.length) delete g.pendingAutomaticAdvisorFlips;
|
||||
@@ -28445,6 +28517,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
finishAutomaticAdvisorBattle(g);
|
||||
finishAutomaticAdvisorHomeworldTransport(g);
|
||||
finishAutomaticAdvisorFaceDance(g);
|
||||
finishAutomaticAdvisorStorm(g);
|
||||
} else openAutomaticAdvisorFlip(g);
|
||||
} else finishAdvisorReaction(g, response);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user