Resolve advisor conversion after Gamont before final victory
Commit original Gamont use/return or empty revelation before its separate mandatory-conversion counter. Preserve private fuel provenance beside a public phase8 receipt, retain legacy phase5 sources and defer the genuine final chooser through both direct and Semuta cost completion. Consume the parent once; ordinary Mentat gains no chooser and victory waits for original confirmation. Existing provisional continuously-alone duration remains. Validation: 18 files277/277, types/lint/build, five native Gamont/response/GameTable SSR journeys and eight existing-profile paths. Genuine52/56-action public Advanced sources earn a real third-stronghold win versus prevented next-turn outcome. Empty revelation remains pure-source proof; full combinations/remaining producers, browser/deployed and final assurance stay open. No saved-room/D1/restart/deployment operation or AI refinement.
This commit is contained in:
1 parent
d0f52c7bae
commit
035be51915
12 files changed
+748
-26
No files matched your search
+59
-10
@@ -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 } from './advisor-automatic-flip';
|
||||
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont } 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';
|
||||
@@ -1264,6 +1264,7 @@ type RicheseAllyOffer = {
|
||||
export type Game = {
|
||||
pendingAutomaticAdvisorFlips?: AutomaticAdvisorFlipSource[];
|
||||
pendingAutomaticAdvisorTransport?: AutomaticAdvisorTransport;
|
||||
pendingAutomaticAdvisorGamont?: { receipt: AutomaticAdvisorGamont; card: string; nexusEvent?: string };
|
||||
advisorPreparation?: AdvisorPreparationWindow;
|
||||
ecazOccupyPreview?: boolean;
|
||||
/** Fresh native Guild/Nexus payment-replacement profile, never inferred from a save. */
|
||||
@@ -8026,8 +8027,10 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
log(g, 'The declared move was prevented without moving forces or spending a movement.');
|
||||
} else if (next.kind === 'choamGamontDiscard') {
|
||||
completeChoamGamont(g, getPlayer(g, next.player), next.target,
|
||||
next.location, next.elite ?? 0, next.noFieldEvent);
|
||||
if (next.mentat) g.decision = { kind: 'choamMentat', player: next.player };
|
||||
next.location, next.elite ?? 0, next.noFieldEvent,
|
||||
{ card: next.card, mentat: next.mentat, nexusEvent: next.nexusEvent });
|
||||
if (next.mentat && !g.pendingAutomaticAdvisorGamont)
|
||||
g.decision = { kind: 'choamMentat', player: next.player };
|
||||
} else if (next.kind === 'choamJubbaDiscard') {
|
||||
completeChoamJubba(g, getPlayer(g, next.player), next.territory);
|
||||
offerChoamStorm(g);
|
||||
@@ -20293,6 +20296,9 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
|
||||
if (source.producer === 'guildTransport')
|
||||
requireRule(JSON.stringify(source.transport) === JSON.stringify(g.pendingAutomaticAdvisorTransport),
|
||||
'The mandatory conversion has lost its original committed Guild transport.');
|
||||
if (source.producer === 'gamont')
|
||||
requireRule(JSON.stringify(source.gamont) === JSON.stringify(g.pendingAutomaticAdvisorGamont?.receipt),
|
||||
'The mandatory conversion has lost its original completed Trip to Gamont.');
|
||||
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
|
||||
return source;
|
||||
}
|
||||
@@ -20341,6 +20347,40 @@ function finishAutomaticAdvisorTransport(g: Game) {
|
||||
openTerritoryEntry(g, p, transport.to, transport.sector, transport.amount, transport.elite, 'guildTransport');
|
||||
}
|
||||
}
|
||||
function prepareGamontAdvisorFlips(g: Game, owner: Player, target: Player,
|
||||
from: string, elite: number, returned: boolean,
|
||||
completed: { card: string; mentat?: boolean; nexusEvent?: string }) {
|
||||
const site = splitLocation(from).territory;
|
||||
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g))
|
||||
.filter(group => group.territory === site);
|
||||
if (!groups.length) return false;
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorGamont &&
|
||||
!g.pendingAutomaticAdvisorTransport, 'Finish the original advisor conversion first.');
|
||||
// Public effect evidence never includes CHOAM Cunning's private fuel card.
|
||||
const receipt: AutomaticAdvisorGamont = { event: crypto.randomUUID(), player: owner.id,
|
||||
target: target.id, location: from, elite, returned, mentat: completed.mentat === true };
|
||||
g.pendingAutomaticAdvisorGamont = { receipt, card: completed.card,
|
||||
...(completed.nexusEvent ? { nexusEvent: completed.nexusEvent } : {}) };
|
||||
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: 'gamont', gamont: receipt };
|
||||
getPlayer(g, group.player).advisors![group.territory].aloneFlipHold = source.event;
|
||||
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
|
||||
}
|
||||
openAutomaticAdvisorFlip(g);
|
||||
return true;
|
||||
}
|
||||
function finishAutomaticAdvisorGamont(g: Game) {
|
||||
const parent = g.pendingAutomaticAdvisorGamont;
|
||||
if (!parent) return;
|
||||
requireRule(g.status === 'playing' && g.phase === 8 && !g.pendingAutomaticAdvisorFlips?.length &&
|
||||
(!parent.receipt.mentat || g.choamMentatPending === true),
|
||||
'Finish the original advisor conversion before resuming Mentat.');
|
||||
delete g.pendingAutomaticAdvisorGamont;
|
||||
if (parent.receipt.mentat)
|
||||
g.decision = { kind: 'choamMentat', player: parent.receipt.player };
|
||||
}
|
||||
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
|
||||
for (const entry of released)
|
||||
delete getPlayer(g, entry.player).advisors?.[entry.territory];
|
||||
@@ -22358,7 +22398,10 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
|
||||
}
|
||||
}
|
||||
|
||||
function victory(g: Game, quote: VictoryQuote = currentVictoryQuote(g)) {
|
||||
function victory(g: Game, quote?: VictoryQuote) {
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
|
||||
!g.pendingAutomaticAdvisorGamont, 'Resolve mandatory advisor conversions before checking victory.');
|
||||
quote ??= currentVictoryQuote(g);
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
g.winner = quote.winner;
|
||||
g.status = quote.status;
|
||||
@@ -27250,7 +27293,8 @@ function completeChoamJubba(g: Game, owner: Player, location: string) {
|
||||
}
|
||||
function completeChoamGamont(
|
||||
g: Game, owner: Player, targetId: string, from: string,
|
||||
elite: number, noFieldEvent?: string,
|
||||
elite: number, noFieldEvent: string | undefined,
|
||||
completed: { card: string; mentat?: boolean; nexusEvent?: string },
|
||||
) {
|
||||
const target = getPlayer(g, targetId);
|
||||
if (noFieldEvent) revealPlayerNoField(g, target, 'gamont');
|
||||
@@ -27260,6 +27304,7 @@ function completeChoamGamont(
|
||||
target.reserves++;
|
||||
if (target.elites) target.elites.reserves += elite;
|
||||
}
|
||||
prepareGamontAdvisorFlips(g, owner, target, from, elite, returned, completed);
|
||||
observeOccupation(g);
|
||||
settleAdvisors(g);
|
||||
log(g, returned
|
||||
@@ -27789,7 +27834,8 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
}
|
||||
}
|
||||
completeChoamGamont(g, choam, pending.target!, pending.location!,
|
||||
pending.elite!, pending.noFieldEvent);
|
||||
pending.elite!, pending.noFieldEvent,
|
||||
{ card: used.id, mentat: pending.mentat, nexusEvent: pending.nexusEvent });
|
||||
} else {
|
||||
if (g.semutaPreview && (used.name === 'La La La' || pending.nexusEvent) &&
|
||||
!pending.storm && !pending.movement && !pending.mentat &&
|
||||
@@ -27830,7 +27876,8 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
if (pending.storm && g.stormResolution) offerChoamStorm(g);
|
||||
if (pending.revival && g.pendingRevival) offerRevivalStop(g);
|
||||
if (pending.movement && g.pendingChoamMove) resumeChoamMovement(g);
|
||||
if (pending.mentat) g.decision = { kind: 'choamMentat', player: choam.id };
|
||||
if (pending.mentat && !g.pendingAutomaticAdvisorGamont)
|
||||
g.decision = { kind: 'choamMentat', player: choam.id };
|
||||
} else if (response.kind === 'richeseAuction') {
|
||||
const round = validateRicheseCacheCancellation(g, response);
|
||||
if (canceled) {
|
||||
@@ -28170,12 +28217,14 @@ 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' ? 'transport and payment 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' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use 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;
|
||||
if (!g.pendingAutomaticAdvisorFlips?.length) finishAutomaticAdvisorTransport(g);
|
||||
else openAutomaticAdvisorFlip(g);
|
||||
if (!g.pendingAutomaticAdvisorFlips?.length) {
|
||||
finishAutomaticAdvisorTransport(g);
|
||||
finishAutomaticAdvisorGamont(g);
|
||||
} else openAutomaticAdvisorFlip(g);
|
||||
} else finishAdvisorReaction(g, response);
|
||||
}
|
||||
} else if (response.kind === 'capture') {
|
||||
|
||||
Reference in new issue
Block a user