Preserve Homeworld and Junction transfers through advisor conversion
Capture surviving alone BG groups after committed native board-to-world and Junction board transfers. Keep typed payment/custody and shipment use, private original declaration snapshots and one original Junction arrival. Preserve Junction immunity independently of the BG conversion counter. Validation: 18 affected files218/218, types/lint/build, nine native transport and GameTable SSR journeys, sixteen existing-profile response paths. Genuine 37/38/42/36-action sources earn original routes and costs3/5/10/1. Rename two fixture variables rejected by lint without changing rule validation. Broader combinations/full modes remain unfinished. Not deployed; no saved-room/D1, reset or restart operation. Runtime fingerprint 5365a5d6fba829c6bcee1cb1e00977ec41769912a32180062e52af0ca59645da; checkpoint prose/reference overlays are reviewed separately.
This commit is contained in:
1 parent
d83db397c0
commit
0b3413fcd8
12 files changed
+849
-15
No files matched your search
@@ -18,6 +18,17 @@ export type AutomaticAdvisorTransport = {
|
||||
group: Record<string, number>;
|
||||
};
|
||||
|
||||
export type AutomaticAdvisorHomeworldTransport = {
|
||||
event: string;
|
||||
kind: 'native' | 'junction';
|
||||
player: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
amount: number;
|
||||
elite: number;
|
||||
group: Record<string, number>;
|
||||
};
|
||||
|
||||
export type AutomaticAdvisorGamont = {
|
||||
event: string;
|
||||
player: string;
|
||||
@@ -47,6 +58,7 @@ export type AutomaticAdvisorFlipSource = {
|
||||
} & (
|
||||
| { producer: 'groundMovement'; mover: string; move: number }
|
||||
| { producer: 'guildTransport'; transport: AutomaticAdvisorTransport }
|
||||
| { producer: 'homeworldTransport'; transport: AutomaticAdvisorHomeworldTransport }
|
||||
| { producer: 'gamont'; gamont: AutomaticAdvisorGamont }
|
||||
| { producer: 'battleLosses'; battle: AutomaticAdvisorBattle }
|
||||
);
|
||||
@@ -79,6 +91,7 @@ export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & MobileBoard
|
||||
result: string;
|
||||
} | null;
|
||||
battle?: unknown;
|
||||
homeworlds?: unknown;
|
||||
};
|
||||
|
||||
export class AutomaticAdvisorFlipError extends Error {
|
||||
@@ -218,6 +231,50 @@ function validateGamontSource(
|
||||
'Automatic advisor conversion lost its final CHOAM Mentat opportunity.');
|
||||
}
|
||||
|
||||
function validateHomeworldTransportSource(
|
||||
ctx: AutomaticAdvisorFlipContext,
|
||||
source: Extract<AutomaticAdvisorFlipSource, { producer: 'homeworldTransport' }>,
|
||||
) {
|
||||
const transport = source.transport;
|
||||
requireFlip(record(ctx.homeworlds) && record(ctx.homeworlds.custody) && record(transport) &&
|
||||
Object.keys(transport).sort().join(',') === 'amount,destination,elite,event,group,kind,origin,player' &&
|
||||
identifier(transport.event) && (transport.kind === 'native' || transport.kind === 'junction') &&
|
||||
identifier(transport.player) && identifier(transport.origin) && identifier(transport.destination) &&
|
||||
positive(transport.amount) && nonnegative(transport.elite) && transport.elite <= transport.amount &&
|
||||
record(transport.group) && Object.values(transport.group).every(positive) &&
|
||||
Object.values(transport.group).reduce((total, count) => total + count, 0) === transport.amount,
|
||||
'Automatic advisor conversion lost its public Homeworld transport receipt or module.');
|
||||
const actors = ctx.players.filter((player) => player.id === transport.player);
|
||||
const actor = actors[0];
|
||||
requireFlip(actors.length === 1 && actor.shipped === true,
|
||||
'Automatic advisor conversion lost its completed Homeworld transport slot.');
|
||||
const places = gameTerritories(ctx);
|
||||
const origin = places.find((place) => place.id === transport.origin);
|
||||
const point = splitLocation(transport.destination);
|
||||
const boardDestination = places.find((place) =>
|
||||
transport.destination === `${point.territory}:${point.sector}` &&
|
||||
place.id === point.territory && place.sectors.includes(point.sector));
|
||||
const worldDestination = ctx.players.some((player) =>
|
||||
transport.destination === `homeworld:${player.faction}` ||
|
||||
(player.faction === 'emperor' && transport.destination === 'homeworld:emperor:salusa'));
|
||||
requireFlip(origin && Object.keys(transport.group).every((key) => {
|
||||
const selected = splitLocation(key);
|
||||
return key === `${selected.territory}:${selected.sector}` && selected.territory === transport.origin &&
|
||||
origin.sectors.includes(selected.sector);
|
||||
}) && (source.territory === transport.origin ||
|
||||
(boardDestination && source.territory === boardDestination.id)) &&
|
||||
(transport.kind === 'native'
|
||||
? actor.faction === 'guild' && worldDestination
|
||||
: ctx.players.filter((player) => player.faction === 'guild').length === 1 &&
|
||||
(worldDestination || !!boardDestination)),
|
||||
'Automatic advisor conversion needs the original native or Junction Homeworld transport locations.');
|
||||
if (boardDestination) {
|
||||
const deposited = actor.forces[transport.destination];
|
||||
requireFlip(nonnegative(deposited) && deposited >= transport.amount,
|
||||
'Automatic advisor conversion lost the Homeworld transport board deposit.');
|
||||
}
|
||||
}
|
||||
|
||||
function validateBattleSource(
|
||||
ctx: AutomaticAdvisorFlipContext,
|
||||
source: Extract<AutomaticAdvisorFlipSource, { producer: 'battleLosses' }>,
|
||||
@@ -256,6 +313,8 @@ export function validateAutomaticAdvisorFlip(
|
||||
Object.keys(source).sort().join(',') === 'event,forces,move,mover,owner,phase,producer,territory,turn') ||
|
||||
(source.producer === 'guildTransport' &&
|
||||
Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') ||
|
||||
(source.producer === 'homeworldTransport' &&
|
||||
Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') ||
|
||||
(source.producer === 'gamont' &&
|
||||
Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn') ||
|
||||
(source.producer === 'battleLosses' &&
|
||||
@@ -277,6 +336,7 @@ export function validateAutomaticAdvisorFlip(
|
||||
requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move,
|
||||
'Automatic advisor conversion lost its original completed movement.');
|
||||
} else if (source.producer === 'guildTransport') validateTransportSource(ctx, source);
|
||||
else if (source.producer === 'homeworldTransport') validateHomeworldTransportSource(ctx, source);
|
||||
else if (source.producer === 'gamont') validateGamontSource(ctx, source);
|
||||
else validateBattleSource(ctx, source);
|
||||
const forces = physicalGroup(owner, source.territory);
|
||||
|
||||
+96
-5
@@ -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 } from './advisor-automatic-flip';
|
||||
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport } 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';
|
||||
@@ -1270,6 +1270,12 @@ export type Game = {
|
||||
resume: { kind: 'winner'; player: string; territory: string; cards: string[] } | { kind: 'aftermath' } | null;
|
||||
signature?: string;
|
||||
};
|
||||
pendingAutomaticAdvisorHomeworldTransport?: {
|
||||
receipt: AutomaticAdvisorHomeworldTransport;
|
||||
sourceEvent: string;
|
||||
arrival?: { territory: string; sector: number; amount: number; elite: number };
|
||||
signature: string;
|
||||
};
|
||||
advisorPreparation?: AdvisorPreparationWindow;
|
||||
ecazOccupyPreview?: boolean;
|
||||
/** Fresh native Guild/Nexus payment-replacement profile, never inferred from a save. */
|
||||
@@ -20310,6 +20316,12 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
|
||||
'The mandatory conversion has lost its original completed battle.');
|
||||
validateBattleAdvisorResume(g, parent);
|
||||
}
|
||||
if (source.producer === 'homeworldTransport') {
|
||||
const parent = g.pendingAutomaticAdvisorHomeworldTransport;
|
||||
requireRule(parent && JSON.stringify(source.transport) === JSON.stringify(parent.receipt),
|
||||
'The mandatory conversion has lost its original committed Homeworld transport.');
|
||||
validateHomeworldAdvisorTransport(g, parent);
|
||||
}
|
||||
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
|
||||
return source;
|
||||
}
|
||||
@@ -20461,6 +20473,66 @@ function finishAutomaticAdvisorBattle(g: Game) {
|
||||
finishWinner(g, getPlayer(g, resume.player), resume.territory, resume.cards);
|
||||
else finishBattle(g);
|
||||
}
|
||||
function homeworldAdvisorTransportSignature(parent: NonNullable<Game['pendingAutomaticAdvisorHomeworldTransport']>) {
|
||||
return JSON.stringify([parent.receipt, parent.sourceEvent, parent.arrival ?? null]);
|
||||
}
|
||||
function prepareHomeworldAdvisorFlips(g: Game, transport: Omit<AutomaticAdvisorHomeworldTransport, 'event'>,
|
||||
sourceEvent: string, arrival?: NonNullable<Game['pendingAutomaticAdvisorHomeworldTransport']>['arrival']) {
|
||||
const destination = transport.destination.startsWith('homeworld:') ? null : splitLocation(transport.destination).territory;
|
||||
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g))
|
||||
.filter(group => group.territory === transport.origin || group.territory === destination);
|
||||
if (!groups.length) return false;
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorHomeworldTransport &&
|
||||
!g.pendingAutomaticAdvisorTransport && !g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle,
|
||||
'Finish the original advisor conversion before another Homeworld transport.');
|
||||
// The original declaration event includes a private snapshot. Only this new
|
||||
// public UUID and physical transfer metadata enter the conversion response.
|
||||
const receipt: AutomaticAdvisorHomeworldTransport = { event: crypto.randomUUID(), ...transport };
|
||||
const parent: NonNullable<Game['pendingAutomaticAdvisorHomeworldTransport']> = {
|
||||
receipt, sourceEvent, ...(arrival ? { arrival: { ...arrival } } : {}), signature: '',
|
||||
};
|
||||
parent.signature = homeworldAdvisorTransportSignature(parent);
|
||||
g.pendingAutomaticAdvisorHomeworldTransport = 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: 'homeworldTransport', transport: receipt };
|
||||
getPlayer(g, group.player).advisors![group.territory].aloneFlipHold = source.event;
|
||||
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function validateHomeworldAdvisorTransport(g: Game, parent: NonNullable<Game['pendingAutomaticAdvisorHomeworldTransport']>) {
|
||||
const { receipt, arrival } = parent;
|
||||
const actor = getPlayer(g, receipt.player);
|
||||
requireRule(g.status === 'playing' && g.phase === 5 && g.homeworlds?.custody && actor.shipped &&
|
||||
typeof parent.sourceEvent === 'string' && parent.sourceEvent.length > 0 &&
|
||||
parent.signature === homeworldAdvisorTransportSignature(parent),
|
||||
'The committed Homeworld transport lost its original private continuation.');
|
||||
if (receipt.destination.startsWith('homeworld:')) {
|
||||
const deposit = homeworldRule(() => homeworldForceGroups(homeworldContext(g), g.homeworlds!.custody!))
|
||||
.find(world => world.id === receipt.destination)?.forces[actor.id];
|
||||
requireRule(!arrival && deposit && deposit.normal + deposit.elite >= receipt.amount && deposit.elite >= receipt.elite,
|
||||
'The original Homeworld transport lost its deposited physical counters.');
|
||||
} else requireRule(receipt.kind === 'junction' && arrival &&
|
||||
location(arrival.territory, arrival.sector) === receipt.destination &&
|
||||
arrival.amount === receipt.amount && arrival.elite === receipt.elite,
|
||||
'The committed Junction transport lost its original board arrival.');
|
||||
}
|
||||
function finishAutomaticAdvisorHomeworldTransport(g: Game) {
|
||||
const parent = g.pendingAutomaticAdvisorHomeworldTransport;
|
||||
if (!parent) return;
|
||||
requireRule(!g.pendingAutomaticAdvisorFlips?.length,
|
||||
'Finish the original advisor conversions before resuming Homeworld transport.');
|
||||
validateHomeworldAdvisorTransport(g, parent);
|
||||
delete g.pendingAutomaticAdvisorHomeworldTransport;
|
||||
if (parent.arrival) {
|
||||
const { territory: to, sector, amount, elite } = parent.arrival;
|
||||
const actor = getPlayer(g, parent.receipt.player);
|
||||
intrusion(g, actor, to);
|
||||
openTerritoryEntry(g, actor, to, sector, amount, elite, 'shipment');
|
||||
}
|
||||
}
|
||||
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
|
||||
for (const entry of released)
|
||||
delete getPlayer(g, entry.player).advisors?.[entry.territory];
|
||||
@@ -22480,7 +22552,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.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle && !g.pendingAutomaticAdvisorHomeworldTransport,
|
||||
'Resolve mandatory advisor conversions before checking victory.');
|
||||
quote ??= currentVictoryQuote(g);
|
||||
applyAdvisorReleases(g, quote.released);
|
||||
@@ -28302,7 +28374,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' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs 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' : '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;
|
||||
@@ -28310,6 +28382,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
finishAutomaticAdvisorTransport(g);
|
||||
finishAutomaticAdvisorGamont(g);
|
||||
finishAutomaticAdvisorBattle(g);
|
||||
finishAutomaticAdvisorHomeworldTransport(g);
|
||||
} else openAutomaticAdvisorFlip(g);
|
||||
} else finishAdvisorReaction(g, response);
|
||||
}
|
||||
@@ -28758,6 +28831,9 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp
|
||||
const {option, rate, quote, arrival, nativeDeparture, promise, sourceLock, advisors, allyPayment, destination} =
|
||||
quoteJunctionDeclaration(g, p, declaration);
|
||||
if (!settlement && beginGuildBetrayal(g, {source: 'junction', transport: declaration})) return;
|
||||
const advisorArrival = advisors || (quote.originKind === 'arrakis' && !!arrival &&
|
||||
g.advanced && p.faction === 'beneGesserit' && isAdvisor(p, quote.origin) &&
|
||||
!at(p, arrival.territory) && !g.players.some(other => other.id !== p.id && at(other, arrival.territory)));
|
||||
// Junction permission and tariff are Homeworld effects and resist Karama.
|
||||
// All validation precedes this atomic settlement; there is no special-stop frame.
|
||||
payWithAlly(g, p, quote.cost, allyPayment);
|
||||
@@ -28770,14 +28846,19 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp
|
||||
owner.reserves = seat.reserves;
|
||||
if (owner.elites) owner.elites.reserves = seat.eliteReserves;
|
||||
}
|
||||
observeOccupation(g);
|
||||
if (arrival) {
|
||||
place(p, arrival.territory, arrival.sector, quote.amount, quote.elite);
|
||||
if (advisors) {
|
||||
if (advisorArrival) {
|
||||
const lockedTurn = Math.max(sourceLock ?? 0, p.advisors?.[arrival.territory]?.lockedTurn ?? 0);
|
||||
(p.advisors ??= {})[arrival.territory] = { ...p.advisors?.[arrival.territory], ...(lockedTurn ? { lockedTurn } : {}) };
|
||||
} else if (p.advisors) delete p.advisors[arrival.territory];
|
||||
}
|
||||
const heldConversion = quote.originKind === 'arrakis' && prepareHomeworldAdvisorFlips(g,
|
||||
{ kind: 'junction', player: p.id, origin: quote.origin, destination, amount: quote.amount, elite: quote.elite,
|
||||
group: Object.fromEntries(quote.sources.map(source => [source.key,
|
||||
source.before.normal + source.before.elite - source.after.normal - source.after.elite])) },
|
||||
declaration.event, arrival ? { ...arrival, amount: quote.amount, elite: quote.elite } : undefined);
|
||||
observeOccupation(g);
|
||||
p.shipped = true;
|
||||
closeAdvisorPreparationOnShipment(g, p.id, 'junction');
|
||||
if (declaration.forfeitPurchasedRate) g.karamaShipping = null;
|
||||
@@ -28800,6 +28881,10 @@ function commitJunctionTransport(g: Game, p: Player, declaration: JunctionTransp
|
||||
quote.sources.map((s) => combatLocationName(g, s.key)).join(' and ');
|
||||
log(g, `${p.name} accepted ${getPlayer(g, option.owner).name}’s Junction ${rate}-price offer and transported ${quote.amount} physical forces (${quote.elite} special) from ${sourceName} to ${arrival ? `${territory(arrival.territory).name}, sector ${arrival.sector}` : combatLocationName(g, destination)} for ${quote.cost} spice (${quote.cost - allyPayment} own, ${allyPayment} pledged). The shipment is used; movement remains available.${declaration.forfeitPurchasedRate ? ' The separate purchased Karama price ended unused; no card is refunded.' : ''}`,
|
||||
{faction: 'guild', name: 'Junction transport'});
|
||||
if (heldConversion) {
|
||||
openAutomaticAdvisorFlip(g);
|
||||
return;
|
||||
}
|
||||
if (arrival) {
|
||||
const bg = byFaction(g, 'beneGesserit');
|
||||
const followup = offPlanet && bg && bg.id !== p.id && spiritualAdvisorMaximum(g, bg.id) > 0
|
||||
@@ -28926,6 +29011,11 @@ function commitHomeworldShipment(g: Game, shipment: PendingHomeworldShipment, se
|
||||
owner.reserves = seat.reserves;
|
||||
if (owner.elites) owner.elites.reserves = seat.eliteReserves;
|
||||
}
|
||||
const heldConversion = shipment.route === 'arrakis' && prepareHomeworldAdvisorFlips(g,
|
||||
{ kind: 'native', player: p.id, origin: splitLocation(Object.keys(shipment.sources)[0]).territory,
|
||||
destination: shipment.destination, amount: quote.amount, elite: quote.elite,
|
||||
group: Object.fromEntries(Object.entries(shipment.sources).map(([key, selected]) =>
|
||||
[key, selected.normal + selected.elite])) }, shipment.event);
|
||||
observeOccupation(g);
|
||||
p.shipped = true;
|
||||
closeAdvisorPreparationOnShipment(g, p.id, 'homeworld');
|
||||
@@ -28945,6 +29035,7 @@ function commitHomeworldShipment(g: Game, shipment: PendingHomeworldShipment, se
|
||||
if (purchasedRate) g.karamaShipping = null;
|
||||
log(g, `${p.name} shipped ${quote.amount} physical forces (${quote.elite} special) from ${quote.sourceNames.join(' and ')} to ${combatLocationName(g, shipment.destination)} for ${quote.cost} spice (${quote.cost - shipment.allyPayment} own, ${shipment.allyPayment} pledged). This uses their shipment; movement remains available.${shipment.guildSecretEvent ? ' Guild Secret Ally is spent for this half-price shipment; the payment goes to the bank.' : ''}`,
|
||||
{faction: shipment.guildSecretEvent ? 'guild' : p.faction, name: shipment.guildSecretEvent ? 'Secret Ally Homeworld shipment' : 'Homeworld shipment'});
|
||||
if (heldConversion) openAutomaticAdvisorFlip(g);
|
||||
}
|
||||
function declareHomeworldShipment(g: Game, p: Player, intent: HomeworldShipmentIntent & {route?: 'arrakis';guildSecretEvent?:string}, allyPayment?: unknown) {
|
||||
const quote = homeworldShipmentQuote(g, intent);
|
||||
|
||||
@@ -184,6 +184,7 @@ const cancellationSources = {
|
||||
transport: response.automaticAdvisorFlip.producer === 'guildTransport' ? g.pendingAutomaticAdvisorTransport ?? null : null,
|
||||
gamont: response.automaticAdvisorFlip.producer === 'gamont' ? g.pendingAutomaticAdvisorGamont?.receipt ?? null : null,
|
||||
battle: response.automaticAdvisorFlip.producer === 'battleLosses' ? g.pendingAutomaticAdvisorBattle?.receipt ?? null : null,
|
||||
homeworld: response.automaticAdvisorFlip.producer === 'homeworldTransport' ? g.pendingAutomaticAdvisorHomeworldTransport?.receipt ?? null : null,
|
||||
group: g.players.find(p => p.id === response.owner)?.advisors?.[response.location ?? ''] ?? null }
|
||||
: response.advisorPreparationSource
|
||||
? { preparation: response.advisorPreparationSource, window: g.advisorPreparation ?? null }
|
||||
|
||||
+10
-5
@@ -4316,6 +4316,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'After Trip to Gamont completes during Mentat, the last outsider’s return or empty No-Field reveal can leave advisors alone. Karama can then prevent their conversion without undoing the completed effect or card use. The final CHOAM opportunity resumes afterward; victory waits for its original confirmation. Ordinary Mentat does not gain an extra final chooser.',
|
||||
'A committed Guild cross-shipment or return to reserves can also leave advisors alone. Original transport permission, rate and any Guild income resolve first; payment, force transfer and shipment use stay committed if the later conversion is prevented. The ordinary ground move remains unused. Cross-shipment arrival follows the conversion once; a reserve return has no arrival.',
|
||||
'After actual battle losses leave surviving bystanding advisors alone, Karama may prevent their conversion without undoing losses or costs. Mandatory card disposal, original skills and substitution finish first. Provisionally resolve this before optional winner-card cleanup and later aftermath, then resume the original cleanup once. An explosion that destroys the advisors creates no conversion.',
|
||||
'Native Guild board-to-Homeworld and Junction board transfers can leave advisors alone after payment and physical transfer. Karama prevents only the resulting BG conversion; Junction permission and tariff stay immune. Original income resolves first, then any Junction board arrival resumes once. Original declaration snapshots stay private.',
|
||||
'Provisional cancellation duration: a prevented advisor group stays advisors while continuously alone, including across phase and turn changes. Another faction returning or the group becoming empty ends that held occurrence. Later arrivals joining that same group match its stance. Other non-ground automatic conversions and matching-group flip counters remain unfinished.',
|
||||
'When another faction ships, moves or worm rides into your fighters, immediately choose whether to become advisors. The choice opens a Karama response. Repeated entry can offer a new choice.',
|
||||
'After another faction’s off-planet shipment, choose a free force in the Polar Sink or an accompanying force at the shipment destination. An accompanying advisor group cannot flip to fighters in that turn while other factions remain.',
|
||||
@@ -4327,7 +4328,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Implementation",
|
||||
"status": "Partial",
|
||||
"detail": "Ground movement, committed Guild transport, Gamont and actual battle losses now have distinct event-bound mandatory-conversion sources. Shared settlement preserves prevented continuously-alone groups. Battle receipts select public identity/outcome only; a private cursor resumes original winner/aftermath after mandatory disposal, skills and substitution without replaying casualties. Transport retains paid transfer and one arrival; Gamont defers the original final chooser. Phases2–5 preparation retains its own window and per-territory prevention. Other automatic producers and matching-group counters remain unfinished.",
|
||||
"detail": "Homeworld/Junction board transfers now capture alone groups before occupation/settlement, retain completed typed transfer/payment and shipment use, keep original snapshot events private and resume only original Junction board arrival after the consumed parent. Existing ground/Guild transport/Gamont/battle/preparation sources retain separate exact receipts and timing. Other automatic producers and matching-group counters remain unfinished.",
|
||||
"evidence": [
|
||||
"game/advisor-automatic-flip.ts",
|
||||
"game/advisors.ts",
|
||||
@@ -4342,7 +4343,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Player controls",
|
||||
"status": "Partial",
|
||||
"detail": "Standard Karama/Allow controls explain completed movement, paid transport, Gamont card use or battle losses and the provisional continuously-alone hold. Battle guidance distinguishes committed casualties from remaining cleanup. Existing preparation controls remain available at quiet phases2–5 boundaries, including after initial decline; first actual shipment closes that window. No new confirmation or timing delay.",
|
||||
"detail": "Standard Karama/Allow controls distinguish completed Homeworld transport/payment from the pending BG conversion and remaining arrival. Existing ground, ordinary transport, Gamont, battle and phase2–5 preparation guidance retain their contracts. No new confirmation or timer.",
|
||||
"evidence": [
|
||||
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
|
||||
"components/advisor-preparation.tsx",
|
||||
@@ -4353,8 +4354,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "AI",
|
||||
"status": "Partial",
|
||||
"detail": "Eight existing-profile native winner-dial-all/mutual-Traitor response paths resolve legally to actual Collection without replay. Existing ground, transport, Gamont and preparation paths retain their separate evidence. No strategy refinement or difficulty calibration.",
|
||||
"detail": "Sixteen existing-profile native Homeworld/Junction response paths accept legal actions without replaying transfer/payment or consuming ordinary movement. Prior ground/Guild transport/Gamont/battle/preparation paths retain separate evidence. No strategy refinement or difficulty calibration.",
|
||||
"evidence": [
|
||||
"tests/advisor-homeworld-flip-engine.test.ts",
|
||||
"tests/fixture-advisor-homeworld-flip.ts",
|
||||
"tests/advisor-battle-flip-engine.test.ts",
|
||||
"tests/fixture-advisor-battle-flip.ts",
|
||||
"tests/advisor-gamont-flip-engine.test.ts",
|
||||
@@ -4367,7 +4370,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Documentation",
|
||||
"status": "Partial",
|
||||
"detail": "Source physical17/28 E composes the group counter with mandatory conversion. Continuously-alone duration and battle cleanup priority are explicitly provisional. Public receipts, private continuation boundaries, native scenario provenance and remaining producers/combinations are distinguished. Preparation and supported Ecaz coexistence keep their separate contracts.",
|
||||
"detail": "Physical17/28 E composes the group counter with mandatory conversion. Homeworld/Junction guidance distinguishes immune transport from BG conversion, private original snapshots, public physical evidence and one original arrival. Continuously-alone duration and earlier battle priority remain provisional. Remaining producers and combinations stay explicit.",
|
||||
"evidence": [
|
||||
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
|
||||
"docs/ADVISOR_PREPARATION.md",
|
||||
@@ -4378,8 +4381,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{
|
||||
"area": "Verification",
|
||||
"status": "Partial",
|
||||
"detail": "Battle conversion passes frozen16-file264/264, types/lint/build, five native battle/response/GameTable SSR journeys and eight existing-profile paths. Genuine44-action public sources earn original forces/cards and actual two-versus-zero city income; Lasgun/Shield destroys the advisor without a counter. Stale Semuta fixtures now submit existing final acknowledgements; production guards stay unchanged. Earlier ground/transport/Gamont/preparation evidence is in linked contracts. Full skill/Caladan/Face Dance/Semuta combinations, remaining producers, complete modes, browser/mobile/deployed and comprehensive assurance stay open.",
|
||||
"detail": "Homeworld/Junction passes frozen18-file218/218, types/lint/build, nine native transport/response/GameTable SSR journeys and sixteen profile paths. Genuine37/38/42/36-action sources earn original native/Junction returns, destination Intrusion and BG empty arrival. Costs3/5/10/1 remain spent through both outcomes; earlier native permission prevention stays uncommitted. A fixture-variable lint repair preserves strict source validation. Earlier producer proofs remain linked; wider contributor/occupation/Nexus/Semuta, remote Face Dance/storm/other producers, full modes and browser/mobile/deployed/comprehensive acceptance remain open.",
|
||||
"evidence": [
|
||||
"tests/advisor-homeworld-flip-engine.test.ts",
|
||||
"tests/fixture-advisor-homeworld-flip.ts",
|
||||
"tests/advisor-battle-flip-engine.test.ts",
|
||||
"tests/fixture-advisor-battle-flip.ts",
|
||||
"tests/advisor-gamont-flip-engine.test.ts",
|
||||
|
||||
Reference in new issue
Block a user