Preserve committed Guild transport through advisor conversion responses
Offer mandatory advisor conversion prevention after Guild cross-shipment and reserve returns. Keep original permission/rate/income, payment, physical pieces and shipment use; bind the exact group to one transport parent and resume original arrival/Intrusion once. Ordinary movement stays unused and the existing provisional continuously-alone duration remains. Preserve old ground receipts, private projection behavior and all transport/module guards. Validation: 17 files243/243, types/lint/build, eight native transport/response/GameTable SSR journeys and eight existing-profile paths. Genuine36/42-action sources earn original counters/cards and actual paid-shipment Intrusion. Preserve the unaffordable full-price cross outcome and prove the affordable return. No saved-room/D1/restart/deployment operation; broader producers, full modes and browser/deployed acceptance remain open.
This commit is contained in:
1 parent
3aca64412f
commit
d0f52c7bae
13 files changed
+805
-43
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { FactionId } from './catalog';
|
||||
import { splitLocation } from './board';
|
||||
import { gameTerritories, splitLocation, type MobileBoard } from './board';
|
||||
import { presenceAt, type ForcePresence } from './force-presence';
|
||||
|
||||
export type AdvisorStance = {
|
||||
@@ -7,23 +7,36 @@ export type AdvisorStance = {
|
||||
aloneFlipHold?: string;
|
||||
};
|
||||
|
||||
export type AutomaticAdvisorTransport = {
|
||||
event: string;
|
||||
player: string;
|
||||
origin: string;
|
||||
to: string;
|
||||
sector: number;
|
||||
amount: number;
|
||||
elite: number;
|
||||
group: Record<string, number>;
|
||||
};
|
||||
|
||||
export type AutomaticAdvisorFlipSource = {
|
||||
event: string;
|
||||
owner: string;
|
||||
territory: string;
|
||||
turn: number;
|
||||
phase: number;
|
||||
producer: 'groundMovement';
|
||||
mover: string;
|
||||
move: number;
|
||||
forces: Record<string, number>;
|
||||
};
|
||||
} & (
|
||||
| { producer: 'groundMovement'; mover: string; move: number }
|
||||
| { producer: 'guildTransport'; transport: AutomaticAdvisorTransport }
|
||||
);
|
||||
|
||||
export type AutomaticAdvisorFlipPlayer = ForcePresence & {
|
||||
id: string;
|
||||
faction: FactionId;
|
||||
advisors?: Readonly<Record<string, AdvisorStance>>;
|
||||
moved?: number;
|
||||
shipped?: boolean;
|
||||
reserves?: number;
|
||||
};
|
||||
|
||||
export type AdvisorSettlementContext = {
|
||||
@@ -31,7 +44,7 @@ export type AdvisorSettlementContext = {
|
||||
players: readonly AutomaticAdvisorFlipPlayer[];
|
||||
};
|
||||
|
||||
export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & {
|
||||
export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & MobileBoard & {
|
||||
status: string;
|
||||
turn: number;
|
||||
phase: number;
|
||||
@@ -48,6 +61,8 @@ const identifier = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0;
|
||||
const positive = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) > 0;
|
||||
const nonnegative = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||||
function requireFlip(value: unknown, message: string): asserts value {
|
||||
@@ -68,7 +83,7 @@ function currentWindow(ctx: AutomaticAdvisorFlipContext) {
|
||||
return ctx.advanced === true && ctx.status === 'playing' && ctx.phase === 5 && positive(ctx.turn);
|
||||
}
|
||||
|
||||
/** Only the ground-movement producer opens a response for these current groups.
|
||||
/** A supported committed producer opens a response for these current groups.
|
||||
* No cards, resources, leaders or hidden marker values determine eligibility. */
|
||||
export function quoteAutomaticAdvisorFlips(ctx: AutomaticAdvisorFlipContext): {
|
||||
player: string;
|
||||
@@ -108,18 +123,55 @@ export function advisorStanceAfterSettlement(
|
||||
return stance.aloneFlipHold !== undefined ? { ...stance } : undefined;
|
||||
}
|
||||
|
||||
/** Validate the same committed move and exact held group before either response outcome. */
|
||||
function validateTransportSource(
|
||||
ctx: AutomaticAdvisorFlipContext,
|
||||
source: Extract<AutomaticAdvisorFlipSource, { producer: 'guildTransport' }>,
|
||||
) {
|
||||
const transport = source.transport;
|
||||
requireFlip(record(transport) &&
|
||||
Object.keys(transport).sort().join(',') === 'amount,elite,event,group,origin,player,sector,to' &&
|
||||
identifier(transport.event) && identifier(transport.player) && identifier(transport.origin) &&
|
||||
identifier(transport.to) && nonnegative(transport.sector) && transport.sector <= 18 &&
|
||||
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 exact committed Guild transport metadata.');
|
||||
const places = gameTerritories(ctx);
|
||||
const origin = places.find((place) => place.id === transport.origin);
|
||||
const destination = places.find((place) => place.id === transport.to);
|
||||
requireFlip(origin && (transport.to === 'reserves' ||
|
||||
(destination && destination.sectors.includes(transport.sector))) &&
|
||||
Object.keys(transport.group).every((key) => {
|
||||
const point = splitLocation(key);
|
||||
return key === `${point.territory}:${point.sector}` && point.territory === transport.origin &&
|
||||
origin.sectors.includes(point.sector);
|
||||
}) && (source.territory === transport.origin ||
|
||||
(transport.to !== 'reserves' && source.territory === transport.to)),
|
||||
'Automatic advisor conversion must belong to the original transport origin or destination.');
|
||||
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 native transport slot.');
|
||||
const deposited = transport.to === 'reserves'
|
||||
? actor.reserves : actor.forces[`${transport.to}:${transport.sector}`];
|
||||
requireFlip(nonnegative(deposited) && deposited >= transport.amount,
|
||||
'Automatic advisor conversion lost the original transported physical counters.');
|
||||
}
|
||||
|
||||
/** Validate the same committed producer and exact held group before either response outcome. */
|
||||
export function validateAutomaticAdvisorFlip(
|
||||
ctx: AutomaticAdvisorFlipContext,
|
||||
source: AutomaticAdvisorFlipSource,
|
||||
): void {
|
||||
requireFlip(record(source) &&
|
||||
Object.keys(source).sort().join(',') === 'event,forces,move,mover,owner,phase,producer,territory,turn' &&
|
||||
((source.producer === 'groundMovement' && identifier(source.mover) &&
|
||||
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')) &&
|
||||
identifier(source.event) && identifier(source.owner) && identifier(source.territory) &&
|
||||
identifier(source.mover) && source.producer === 'groundMovement' &&
|
||||
positive(source.turn) && source.turn === ctx.turn && source.phase === 5 &&
|
||||
currentWindow(ctx) && source.phase === ctx.phase,
|
||||
'Automatic advisor conversion lost its original current-turn ground movement.');
|
||||
'Automatic advisor conversion lost its original current-turn ground movement or Guild transport.');
|
||||
const owners = ctx.players.filter((player) => player.id === source.owner);
|
||||
const owner = owners[0];
|
||||
requireFlip(owners.length === 1 && owner?.faction === 'beneGesserit' &&
|
||||
@@ -127,9 +179,11 @@ export function validateAutomaticAdvisorFlip(
|
||||
'Automatic advisor conversion needs its original native owner and held occurrence.');
|
||||
requireFlip(!otherPresence(ctx, owner, source.territory),
|
||||
'Automatic advisor conversion requires the original group to remain alone.');
|
||||
const movers = ctx.players.filter((player) => player.id === source.mover);
|
||||
requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move,
|
||||
'Automatic advisor conversion lost its original completed movement.');
|
||||
if (source.producer === 'groundMovement') {
|
||||
const movers = ctx.players.filter((player) => player.id === source.mover);
|
||||
requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move,
|
||||
'Automatic advisor conversion lost its original completed movement.');
|
||||
} else validateTransportSource(ctx, source);
|
||||
const forces = physicalGroup(owner, source.territory);
|
||||
requireFlip(record(source.forces) && Object.keys(forces).length > 0 &&
|
||||
Object.values(forces).every(positive) && Object.values(source.forces).every(positive) &&
|
||||
|
||||
Reference in new issue
Block a user