Enable Semuta in public Discovery games
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
b2b9a024bd
commit
98ead3a4b5
11 files changed
+436
-8
No files matched your search
+72
-3
@@ -1802,6 +1802,15 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoStationDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
event: string;
|
||||
mode: 'first' | 'last';
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoAuctionDiscard';
|
||||
cyclic?: 'normal' | 'blackMarket';
|
||||
@@ -6458,6 +6467,17 @@ type SaphoExtortionDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoExtortionDiscard' }
|
||||
>;
|
||||
type SaphoStationDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoStationDiscard' }
|
||||
>;
|
||||
function saphoStationDiscardSignature(g: Game, c: SaphoStationDiscardContinuation) {
|
||||
return JSON.stringify({player:c.player, card:c.card, event:c.event, mode:c.mode,
|
||||
parent:nullentropyParentSignature(g,c.resume,c.event), turn:g.turn, phase:g.phase,
|
||||
order:g.order, ready:g.ready, storm:g.storm, stormPending:g.stormPending,
|
||||
source:g.stormMovementSource, station:g.ecologicalStorm, discoveries:g.discoveries,
|
||||
players:g.players.map(p=>({id:p.id, faction:p.faction, forces:p.forces, advisors:p.advisors}))});
|
||||
}
|
||||
type SaphoCharityDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoCharityDiscard' }
|
||||
@@ -8360,6 +8380,12 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === saphoExtortionDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its current Extortion payer order.');
|
||||
moritaniExtortionIntegrity(g);
|
||||
} else if (continuation?.kind === 'saphoStationDiscard') {
|
||||
const c = continuation;
|
||||
requireRule(batch.cause === 'sapho:station' &&
|
||||
semutaOfferSupported(g, c, batch.entries) &&
|
||||
c.stateSignature === saphoStationDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its Testing Station choice.');
|
||||
} else if (continuation?.kind === 'saphoMovementDiscard') {
|
||||
const c = continuation, entry = batch.entries[0];
|
||||
requireRule(
|
||||
@@ -9594,6 +9620,19 @@ function semutaOfferSupported(
|
||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
entries: FreshDiscardBatch['entries'],
|
||||
) {
|
||||
if (continuation.kind === 'saphoStationDiscard') {
|
||||
const frame = g.ecologicalStorm, decision = continuation.resume.decision, receipt = frame?.sapho;
|
||||
return g.semutaPreview === true && g.discoveryEnabled === true && !!g.discoveries &&
|
||||
discoveryModeSupported(g) && g.status === 'playing' && g.phase === 0 &&
|
||||
frame?.stage === 'choose' && frame.queue?.choices.length === 0 &&
|
||||
receipt?.event === continuation.event && receipt.holder === continuation.player &&
|
||||
receipt.mode === continuation.mode && decision?.kind === 'ecologicalStorm' &&
|
||||
decision.player === frame.owner && decision.event === frame.event && !g.decision &&
|
||||
!g.response && !g.pendingKarama && !g.phaseOpening && !continuation.resume.response &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening &&
|
||||
entries.length === 1 && entries[0].publicFace && entries[0].discardedBy === continuation.player &&
|
||||
entries[0].card.id === continuation.card && entries[0].card.effect === 'juiceOfSapho';
|
||||
}
|
||||
if (continuation.kind === 'saphoArrivalOrderDiscard') {
|
||||
const order = g.pendingArrivalOverlap?.saphoOrder;
|
||||
return g.semutaPreview === true && bareBasicSemutaProfile(g) && g.phase === 5 &&
|
||||
@@ -10364,6 +10403,11 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'saphoStationDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'saphoMovementDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
@@ -11025,6 +11069,17 @@ function publicSemutaSkillsProfile(g: Game): boolean {
|
||||
!g.homeworlds && !g.nexusCards && !g.discoveryEnabled && !g.ecazTreachery &&
|
||||
!g.leaderSkills && !g.kullPreview && !g.nexusKullPreview;
|
||||
}
|
||||
/** Fresh public Discovery entry for the existing native Richese Semuta profile. */
|
||||
function publicSemutaDiscoveryProfile(g: Game): boolean {
|
||||
return g.status === 'lobby' && typeof g.advanced === 'boolean' &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
g.expansions.length === 1 && g.expansions[0] === 'choam' && !!byFaction(g, 'richese') &&
|
||||
g.players.every(p => p.faction === 'richese' || p.faction === 'choam' || CLASSIC_FACTIONS[p.faction] === true) &&
|
||||
(!g.techTokens || g.players.length >= 3) && (!g.strongholdCards || g.advanced) &&
|
||||
!g.twoPlayerControllers && !g.shortAllianceVictory && !g.coalitionPreview &&
|
||||
!g.homeworlds && !g.nexusCards && g.discoveryEnabled === true && !g.ecazTreachery &&
|
||||
!g.leaderSkills && !g.kullPreview && !g.nexusKullPreview && discoveryModuleProfile(g);
|
||||
}
|
||||
/** Route a normal start through the existing genuine setup for the selected modules. */
|
||||
function startModules(state: Game): Game {
|
||||
const options = state.lobbyOptions ?? {};
|
||||
@@ -11061,7 +11116,8 @@ function startModules(state: Game): Game {
|
||||
return pair && pair.every(id => g.players.some(p => p.faction === id))
|
||||
? initializePairedNexusGameForAudit(g) : initializeNexusGameForAudit(g);
|
||||
}
|
||||
if (g.discoveryEnabled) return initializeDiscoveryGameForAudit(g);
|
||||
if (g.discoveryEnabled) return publicSemutaDiscoveryProfile(g)
|
||||
? initializeSemutaGameForAudit(g) : initializeDiscoveryGameForAudit(g);
|
||||
if (g.homeworlds)
|
||||
return g.expansions.length ? initializeHomeworldOccupationGameForAudit(g) : initializeHomeworldGameForAudit(g);
|
||||
if (g.strongholdCards) return initializeStrongholdFactionsGameForAudit(g);
|
||||
@@ -29173,7 +29229,7 @@ function saphoExtortionDecision(g: Game) {
|
||||
}
|
||||
function saphoTestingStationDecision(g: Game) {
|
||||
const station = g.ecologicalStorm, decision = g.decision;
|
||||
return g.phase === 0 && !g.pendingTreacheryDiscard && !g.semutaPreview &&
|
||||
return g.phase === 0 && !g.pendingTreacheryDiscard &&
|
||||
!g.semutaAdvancedOrdinaryPreview && !g.semutaRecruitsPreview &&
|
||||
station?.stage === 'choose' && decision?.kind === 'ecologicalStorm' &&
|
||||
decision.player === station.owner && decision.event === station.event;
|
||||
@@ -29546,11 +29602,24 @@ function playSapho(g: Game, p: Player, action: Action) {
|
||||
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');
|
||||
const used = 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'});
|
||||
if (g.semutaPreview) {
|
||||
const continuation: SaphoStationDiscardContinuation = {
|
||||
kind:'saphoStationDiscard', player:p.id, card:used.id, event:option.event, mode:option.mode,
|
||||
resume:{response:g.response, decision:g.decision, pendingKarama:g.pendingKarama, phaseOpening:g.phaseOpening},
|
||||
stateSignature:'',
|
||||
};
|
||||
const entries = [{card:used, discardedBy:p.id, publicFace:true}];
|
||||
g.decision = null;
|
||||
requireRule(semutaOfferSupported(g, continuation, entries),
|
||||
'This Testing Station discard cannot safely resume its Semuta reaction.');
|
||||
continuation.stateSignature = saphoStationDiscardSignature(g, continuation);
|
||||
stageTreacheryDiscard(g, 'sapho:station', entries, continuation);
|
||||
}
|
||||
} else if (option.scope === 'battleAggressor') {
|
||||
const b = g.battle!;
|
||||
const next = saphoAggressorRule(() => changeSaphoAggressor(b.saphoAggressor,
|
||||
|
||||
Reference in new issue
Block a user