Prototype Semuta after Recruits in Basic play
Verify and publish checkpoint container / container (push) Successful in 10m20s
Verify and publish checkpoint container / container (push) Successful in 10m20s
This commit is contained in:
1 parent
0e9209531a
commit
1ba942138d
9 files changed
+311
-18
No files matched your search
+40
-3
@@ -1659,7 +1659,7 @@ export type Game = {
|
||||
kind: 'ordinaryCardDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
effect: 'hajr' | 'ghola' | 'harvester' | 'weather' | 'atomics';
|
||||
effect: 'hajr' | 'ghola' | 'harvester' | 'weather' | 'atomics' | 'recruits';
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
@@ -2605,6 +2605,8 @@ export type Game = {
|
||||
moritaniAssassinatePreview?: true;
|
||||
/** Explicit development-only neutral Semuta reaction profile. */
|
||||
semutaPreview?: true;
|
||||
/** Fresh Basic independent-card preview for Semuta after Recruits only. */
|
||||
semutaRecruitsPreview?: true;
|
||||
moritaniAssassinate?: MoritaniAssassinateState;
|
||||
moritaniAssassinateCallEvents?: string[];
|
||||
moritaniAssassinateResume?: {
|
||||
@@ -6230,6 +6232,8 @@ function ordinaryDiscardSignature(g: Game, next: OrdinaryDiscardContinuation) {
|
||||
...(g.grummanCollection === undefined ? {} : { grummanCollection: g.grummanCollection }),
|
||||
dukeVidal: g.dukeVidal,
|
||||
revivalFreeIncome: g.revivalFreeIncome,
|
||||
...(next.effect === 'recruits' ? {recruits: g.recruits, freeRevival: g.freeRevival,
|
||||
revivalRules: g.revivalRules, revivalOrder: g.revivalOrder} : {}),
|
||||
players: g.players.map((p) => ({
|
||||
id: p.id,
|
||||
faction: p.faction,
|
||||
@@ -6243,6 +6247,7 @@ function ordinaryDiscardSignature(g: Game, next: OrdinaryDiscardContinuation) {
|
||||
moved: p.moved,
|
||||
shipped: p.shipped,
|
||||
revived: p.revived,
|
||||
...(next.effect === 'recruits' ? {freeForcesRevived: p.freeForcesRevived} : {}),
|
||||
revivalCycle: p.revivalCycle,
|
||||
...(p.ecazRevivalCohort === undefined ? {} : { ecazRevivalCohort: p.ecazRevivalCohort }),
|
||||
leaderRevived: p.leaderRevived,
|
||||
@@ -7809,6 +7814,9 @@ function tupileDiscardSignature(g: Game, c: TupileDiscardContinuation) {
|
||||
}
|
||||
function treacheryDiscardIntegrity(g: Game) {
|
||||
const discovery = g.discoveryEnabled === true && !!g.discoveries && discoveryModeSupported(g);
|
||||
requireRule(g.semutaRecruitsPreview === undefined || g.semutaRecruitsPreview === true &&
|
||||
basicIndependentSemutaProfile(g) && !g.semutaPreview,
|
||||
'This Recruits Semuta preview no longer matches its original Basic profile.');
|
||||
requireRule(g.semutaPreview === undefined || g.semutaPreview === true &&
|
||||
g.expansions.includes('choam') && !!byFaction(g, 'richese') &&
|
||||
(!g.homeworlds || g.homeworldOccupationPreview === true && semutaHomeworldRoster(g)) &&
|
||||
@@ -7999,7 +8007,7 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
const owner = g.players.find((p) => p.id === c.player);
|
||||
requireRule(
|
||||
owner &&
|
||||
['hajr', 'ghola', 'harvester', 'weather', 'atomics'].includes(
|
||||
['hajr', 'ghola', 'harvester', 'weather', 'atomics', 'recruits'].includes(
|
||||
c.effect,
|
||||
) &&
|
||||
batch.cause === `ordinary:${c.effect}` &&
|
||||
@@ -8022,7 +8030,11 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
'The consumed card no longer matches its pending revival income.',
|
||||
);
|
||||
requireRule(
|
||||
c.effect === 'hajr'
|
||||
c.effect === 'recruits'
|
||||
? g.semutaRecruitsPreview === true && basicIndependentSemutaProfile(g) &&
|
||||
g.phase === 4 && g.recruits?.turn === g.turn && g.recruits.player === c.player &&
|
||||
g.recruits.card === c.card
|
||||
: c.effect === 'hajr'
|
||||
? g.phase === 5 &&
|
||||
g.active === c.player &&
|
||||
g.hajr.filter((id) => id === c.player).length === 1
|
||||
@@ -9282,6 +9294,14 @@ function semutaOfferSupported(
|
||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
entries: FreshDiscardBatch['entries'],
|
||||
) {
|
||||
if (continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'recruits')
|
||||
return g.semutaRecruitsPreview === true && basicIndependentSemutaProfile(g) &&
|
||||
g.status === 'playing' && g.phase === 4 && g.recruits?.turn === g.turn &&
|
||||
g.recruits.player === continuation.player && g.recruits.card === continuation.card &&
|
||||
entries.length === 1 && entries[0].publicFace && entries[0].discardedBy === continuation.player &&
|
||||
entries[0].card.id === RECRUITS_CARD_ID && entries[0].card.effect === 'recruits' &&
|
||||
!continuation.resume.response && !continuation.resume.decision &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening;
|
||||
if (continuation.kind === 'kullSuccessDiscard' || continuation.kind === 'kullCounterDiscard')
|
||||
return g.semutaPreview === true && g.advanced === false && bareBasicSemutaProfile(g) &&
|
||||
!!byFaction(g, 'richese') && kullSemutaParentSupported(kullSource(g, continuation.frame), continuation.frame) &&
|
||||
@@ -12928,6 +12948,17 @@ export function initializeMoritaniAssassinateGameForAudit(state: Game): Game {
|
||||
'Assassination requires Advanced Moritani in a supported selected native roster and module composition.');
|
||||
return initializeFactionExpansionsGameForAudit(state);
|
||||
}
|
||||
/** Independent Ecaz cards with Richese and classic factions, without other
|
||||
* modules, reuse the already supported Basic Semuta discard machinery. */
|
||||
function basicIndependentSemutaProfile(g: Game): boolean {
|
||||
return g.advanced === false && g.ecazTreachery === true && g.expansions.length === 1 &&
|
||||
g.expansions[0] === 'choam' && !!byFaction(g, 'richese') &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
g.players.every(p => p.faction === 'richese' || faction(p.faction).expansion === 'base') &&
|
||||
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens && !g.strongholdCards &&
|
||||
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker &&
|
||||
!g.kullPreview && !g.nexusKullPreview && !g.ecazOccupyPreview && !g.moritaniAssassinatePreview;
|
||||
}
|
||||
/** Bare native Basic starts share the original deck/roster validation. Optional
|
||||
* modules retain their own explicit Semuta envelopes; saved games are not upgraded. */
|
||||
function bareBasicSemutaProfile(g: Game): boolean {
|
||||
@@ -13123,6 +13154,8 @@ function independentCardModulesProfile(g: Game): boolean {
|
||||
/** Shared fresh setup for the independent three-card Ecaz variant. */
|
||||
export function initializeEcazTreacheryGameForAudit(state: Game): Game {
|
||||
requireRule(!state.ecazTreachery, 'Ecaz Treachery Cards cannot redeal an existing inventory.');
|
||||
requireRule(state.semutaRecruitsPreview === undefined,
|
||||
'Fresh independent cards cannot inherit an existing Recruits Semuta marker.');
|
||||
const g = structuredClone(state);
|
||||
g.ecazTreachery = true;
|
||||
const discovery = independentDiscoveryProfile(g) && discoveryModuleProfile(g);
|
||||
@@ -13139,6 +13172,7 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
|
||||
requireFreshFactionInventory(g);
|
||||
}
|
||||
const initialized = initializeSetupGameForAudit(g, homeworlds, independentNexusProfile(g), false, discovery, false, false, true, true);
|
||||
if (basicIndependentSemutaProfile(initialized)) initialized.semutaRecruitsPreview = true;
|
||||
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
|
||||
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
|
||||
return initialized;
|
||||
@@ -41692,6 +41726,8 @@ function applyActionInner(
|
||||
`${p.name} played Recruits. Current free revival rates are doubled and the ordinary force revival limit is seven for this turn.`,
|
||||
{ faction: p.faction, name: 'Recruits' },
|
||||
);
|
||||
if (execution === 'live' && g.semutaRecruitsPreview && basicIndependentSemutaProfile(g))
|
||||
stageOrdinaryCardDiscard(g, p, card);
|
||||
return g;
|
||||
}
|
||||
if (t === 'card' && action.card === 'richese-juice-of-sapho') {
|
||||
@@ -45915,6 +45951,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
botsPending: g.botsPending ?? false,
|
||||
semutaReaction: projectedSemutaReaction(g, me),
|
||||
semutaPreview: g.semutaPreview === true,
|
||||
semutaRecruitsPreview: g.semutaRecruitsPreview === true,
|
||||
automaticContinuationPending: (!!g.pendingTreacheryDiscard && !g.pendingTreacheryDiscard.reaction) || homeworldRevealPending(g) || homeworldShipmentAutomatic(g) || homeworldOccupiedIncomeAutomatic(g) || grummanCollectionAutomatic(g) || tupileAutomatic(g),
|
||||
botNextActionAt: g.botNextActionAt ?? null,
|
||||
code: g.code,
|
||||
|
||||
Reference in new issue
Block a user