Handle advisor conversion after worm rides
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 09:10:53 +11:00
1 parent ad0abfef98
commit c608d6fea6
15 files changed
+507 -14

No files matched your search

+62 -2
View File
@@ -112,7 +112,7 @@ import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, h
import { bribeReceiptTiming, bribeTimingBlock, bribesCloseBeforeNextMentat, 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, type AutomaticAdvisorStorm, type AutomaticAdvisorRelocation } from './advisor-automatic-flip';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance, type AutomaticAdvisorStorm, type AutomaticAdvisorRelocation, type AutomaticAdvisorWormRide } 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';
@@ -1521,6 +1521,7 @@ export type Game = {
signature: string;
resume: { kind: 'ambassador'; event: string } | { kind: 'discovery'; child: DiscoveryEntryArrivalChild };
};
pendingAutomaticAdvisorWormRide?: { receipt: AutomaticAdvisorWormRide; signature: string };
pendingAdvisorMatching?: {
receipt: AdvisorMatchingSource;
signature: string;
@@ -28508,6 +28509,12 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
'The mandatory conversion has lost its original completed relocation.');
validateRelocationAdvisorParent(g, parent);
}
if (source.producer === 'wormRide') {
const parent = g.pendingAutomaticAdvisorWormRide;
requireRule(parent && JSON.stringify(source.wormRide) === JSON.stringify(parent.receipt),
'The mandatory conversion has lost its original completed worm ride.');
validateWormRideAdvisorParent(g, parent);
}
if(source.producer==='spiritualArrival'){
const frame=spiritualFrameBody(g);
requireRule(frame.stage==='arrival'&&JSON.stringify(frame.source)===JSON.stringify(source.spiritual),
@@ -28922,6 +28929,54 @@ function finishAutomaticAdvisorRelocation(g: Game) {
completeDiscoveryEntryArrival(g, child);
}
}
/** The ride and any destination collection have already happened. Bind the
* untouched worm cursor and the rider's physical pools until arrival resumes. */
function wormRideAdvisorSignature(g: Game, receipt: AutomaticAdvisorWormRide) {
const rider = getPlayer(g, receipt.player);
return JSON.stringify({ receipt, turn:g.turn, phase:g.phase, wormRides:g.wormRides,
spiceSequence:g.spiceSequence, spiceResolution:g.spiceResolution,
summonedNexusBeforeRides:g.summonedNexusBeforeRides, greatMaker:g.greatMaker,
forces:rider.forces, elites:rider.elites, spice:g.spice, riderSpice:rider.spice });
}
function validateWormRideAdvisorParent(g: Game, parent: NonNullable<Game['pendingAutomaticAdvisorWormRide']>) {
requireRule(g.status === 'playing' && g.advanced && g.phase === 1 &&
parent.signature === wormRideAdvisorSignature(g, parent.receipt),
'The mandatory conversion lost its committed worm ride or original continuation.');
}
function prepareWormRideAdvisorFlips(g: Game, ride: Omit<AutomaticAdvisorWormRide, 'event'>) {
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g))
.filter(group => group.territory === ride.origin);
if (!groups.length) return false;
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorWormRide &&
!g.pendingAutomaticAdvisorRelocation && !g.pendingAutomaticAdvisorTransport &&
!g.pendingAutomaticAdvisorHomeworldTransport && !g.pendingAutomaticAdvisorGamont &&
!g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorFaceDance && !g.pendingAutomaticAdvisorStorm,
'Finish the original advisor conversion before a worm ride.');
const receipt = { event:crypto.randomUUID(), ...ride };
g.pendingAutomaticAdvisorWormRide = { receipt, signature:wormRideAdvisorSignature(g,receipt) };
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:'wormRide', wormRide:receipt};
getPlayer(g,group.player).advisors![group.territory].aloneFlipHold = source.event;
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
}
openAutomaticAdvisorFlip(g);
return true;
}
function finishAutomaticAdvisorWormRide(g: Game) {
const parent = g.pendingAutomaticAdvisorWormRide;
if (!parent) return;
requireRule(!g.pendingAutomaticAdvisorFlips?.length,
'Finish the original worm-ride conversions before arrival.');
validateWormRideAdvisorParent(g,parent);
delete g.pendingAutomaticAdvisorWormRide;
const ride = parent.receipt, rider = getPlayer(g,ride.player);
const intruded = intrusion(g,rider,ride.destination,{wormRide:true});
if (openTerritoryEntry(g,rider,ride.destination,ride.sector,ride.amount,ride.elite,'wormRide','wormRide') || intruded)
return;
nextWormRide(g);
}
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
for (const entry of released)
delete getPlayer(g, entry.player).advisors?.[entry.territory];
@@ -38560,7 +38615,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' : automaticAdvisorFlip.producer === 'storm' ? 'current-sector storm casualties stay' : automaticAdvisorFlip.producer === 'specialRelocation' ? 'relocation and any collection stay' : automaticAdvisorFlip.producer === 'spiritualArrival' ? 'free Spiritual force placement and any income 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' : automaticAdvisorFlip.producer === 'specialRelocation' ? 'relocation and any collection stay' : automaticAdvisorFlip.producer === 'wormRide' ? 'worm ride and any collection stay' : automaticAdvisorFlip.producer === 'spiritualArrival' ? 'free Spiritual force placement and any income 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;
@@ -38572,6 +38627,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
finishAutomaticAdvisorFaceDance(g);
finishAutomaticAdvisorStorm(g);
finishAutomaticAdvisorRelocation(g);
finishAutomaticAdvisorWormRide(g);
if(g.pendingSpiritualAdvisor?.stage==='arrival')completeSpiritualAdvisorArrival(g);
} else openAutomaticAdvisorFlip(g);
} else finishAdvisorReaction(g, response);
@@ -41877,6 +41933,8 @@ export function prepareSpecialKaramaIntent(
g.phase === 1 && !g.summonedWorm,
'Summon Shai-Hulud during Spice Blow and Nexus.',
);
requireRule(!g.pendingAutomaticAdvisorWormRide,
'Resolve the completed worm ride’s advisor conversion before summoning another worm.');
const t = stringField(action.territory);
requireRule(
TERRITORIES.some((x) => x.id === t && x.type === 'sand'),
@@ -45700,6 +45758,8 @@ function applyActionInner(
log(g, `${p.name}'s Sandmaster collected 1 spice on arriving in ${territory(to).name} by worm. ${sandmaster.before - 1} spice remains in that pile. This ride does not consume normal movement.`,
{ faction: p.faction, name: 'Sandmaster collection' });
}
if (prepareWormRideAdvisorFlips(g,{player:p.id,origin:decision.territory,
destination:to,sector,amount:total,elite:totalElite})) return g;
const intruded = intrusion(g, p, to, { wormRide: true });
if (
openTerritoryEntry(