Support Sapho ordering at Testing Station
Verify and publish checkpoint container / container (push) Successful in 10m32s

This commit is contained in:
Quintine committed 2026-10-29 09:24:16 +11:00
1 parent 220d78c779
commit 1972622c11
11 files changed
+445 -9

No files matched your search

+28 -4
View File
@@ -144,7 +144,7 @@ import { ECAZ_START_FORCES, quoteEcazStartingForces } from './ecaz-setup';
import { battleCardSlotEligible, battleCategoryInspectionValue, fixedBattleInspectionMatches, validBattleSlotPair, usesNativeBattleSpecial } from './battle-card-slots';
import { HARASS_WITHDRAW_CARD, HarassWithdrawError, isHarassWithdraw, quoteHarassWithdraw, defaultHarassWithdrawAllocation, harassWithdrawNeedsAllocation, type HarassWithdrawSelection, type HarassWithdrawContext, type HarassWithdrawPreview } from './harass-withdraw';
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, discoveryStormDistance, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, discoveryStormDistance, discoveryStormSaphoModes, reorderDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
import { advanceDiscoveryEntry, beginDiscoveryEntryArrival, createDiscoveryEntryRound, finishDiscoveryEntryArrival, quoteDiscoveryEntry, validateDiscoveryEntryArrivalChild, validateDiscoveryEntryRound, type DiscoveryEntryArrivalChild, type DiscoveryEntryRound, type DiscoveryEntrySelection } from './discovery-entry';
import { createDiscoveryState, validateDiscoveryState, placeDiscovery, rememberDiscoveryFace, revealDiscoveryToken, isDiscoverySpiceCardId, isDiscoveryTokenId, DISCOVERY_SPICE_CARDS, DISCOVERY_CARD_PLACEMENTS, DISCOVERY_TOKEN_BY_ID, JACURUTU_SIETCH, type DiscoveryState } from './discoveries';
@@ -29071,7 +29071,7 @@ function offerMoritaniDuke(g: Game) {
return true;
}
type SaphoOption = {
scope: 'onceAround' | 'cyclicAuction' | 'movement' | 'battleOrder' | 'extortion' | 'revival' | 'charity';
scope: 'onceAround' | 'cyclicAuction' | 'movement' | 'battleOrder' | 'extortion' | 'revival' | 'charity' | 'testingStation';
event: string;
mode: 'first' | 'last';
/** Display only; action submission keeps scope/event/mode. */
@@ -29158,6 +29158,13 @@ function saphoExtortionDecision(g: Game) {
decision?.kind === 'moritaniExtortion' && decision.player === state.queue[state.cursor] &&
decision.event === extortionEvent(state);
}
function saphoTestingStationDecision(g: Game) {
const station = g.ecologicalStorm, decision = g.decision;
return g.phase === 0 && !g.pendingTreacheryDiscard && !g.semutaPreview &&
!g.semutaAdvancedOrdinaryPreview && !g.semutaRecruitsPreview &&
station?.stage === 'choose' && decision?.kind === 'ecologicalStorm' &&
decision.player === station.owner && decision.event === station.event;
}
function saphoBattleCardReserved(g: Game, player: string, card: string): boolean {
const battle = g.battle, plan = battle?.plans[player];
return !!battle && (!!plan && [plan.weapon, plan.defense, plan.leader].includes(card) ||
@@ -29312,7 +29319,7 @@ function saphoCleanWindow(g: Game) {
!g.truthtrance &&
!g.phaseOpening &&
!g.response &&
(!g.decision || saphoExtortionDecision(g)) &&
(!g.decision || saphoExtortionDecision(g) || saphoTestingStationDecision(g)) &&
!g.pendingNullentropy &&
!g.pendingRicheseGift &&
!g.pendingExchange &&
@@ -29408,6 +29415,12 @@ function saphoOptions(g: Game, p: Player): SaphoOption[] {
)
return [];
const options: SaphoOption[] = [];
if (saphoTestingStationDecision(g)) {
const frame = g.ecologicalStorm!;
for (const mode of nexusRule(() => discoveryStormSaphoModes(g, frame, p.id)))
options.push({scope: 'testingStation', event: frame.event, mode});
return options;
}
if (g.phase===2 && g.advanced && g.charityOrder) {
const context=charityOrderContext(g);
const event=charityOrderRule(()=>saphoCharityOrderEvent(context,g.charityOrder!));
@@ -29514,7 +29527,18 @@ function playSapho(g: Game, p: Player, action: Action) {
'This Juice of Sapho opportunity is unavailable, already completed or expired.',
);
recordNativePrintedCardPlay(g, p.id, 'juiceOfSapho');
if (option.scope === 'battleAggressor') {
if (option.scope === 'testingStation') {
requireRule(saphoTestingStationDecision(g) && g.ecologicalStorm,
'The original Testing Station choice is no longer open.');
const frame = nexusRule(() => reorderDiscoveryStorm(g, g.ecologicalStorm!, {
holder: p.id, event: option.event, mode: option.mode, nextEvent: crypto.randomUUID(),
}));
discard(g, p, 'richese-juice-of-sapho');
g.ecologicalStorm = frame;
g.decision = {kind:'ecologicalStorm', player:frame.owner, event:frame.event};
log(g, `${p.name} discarded Juice of Sapho to take the Testing Station choice ${option.mode}. Both occupants retain one adjustment and the original storm movement remains.`,
{faction:p.faction,name:'Juice of Sapho'});
} else if (option.scope === 'battleAggressor') {
const b = g.battle!;
const next = saphoAggressorRule(() => changeSaphoAggressor(b.saphoAggressor,
b.saphoAggressorEvents, b, g.turn, p.id, option.event));