Connect Kull and Semuta with independent cards
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 03:28:30 +11:00
1 parent 87d1887139
commit bc0db31134
11 files changed
+412 -23

No files matched your search

+24 -12
View File
@@ -6941,7 +6941,8 @@ type KullSemutaDiscardContinuation = Extract<
function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKull']>): boolean {
const battle = g.battle;
const use = frame?.intent.kind === 'ordinary' ? frame.intent.use : undefined;
const nativeResponse = !!frame && !g.advanced && bareBasicSemutaProfile(g) &&
const nativeResponse = !!frame && !g.advanced &&
(bareBasicSemutaProfile(g) || g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g)) &&
use?.kind === 'cancel' && JSON.stringify(use.response) === JSON.stringify(frame.resume.response) &&
!frame.resume.decision && !frame.resume.pendingKarama && !frame.resume.phaseOpening;
let revivalSupported = false;
@@ -9477,7 +9478,9 @@ function semutaOfferSupported(
!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) &&
return (g.semutaPreview === true && bareBasicSemutaProfile(g) ||
g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g) &&
(!g.pendingTreacheryDiscard || !!g.pendingTreacheryDiscard.reaction)) && g.advanced === false &&
!!byFaction(g, 'richese') && kullSemutaParentSupported(kullSource(g, continuation.frame), continuation.frame) &&
(!!g.battle === !!continuation.battleBinding) && entries.length === 1 &&
entries[0].publicFace && entries[0].discardedBy === continuation.player &&
@@ -9709,7 +9712,8 @@ function semutaOfferSupported(
const independentNativeDiscard = ambassadorDiscard || continuation.kind === 'terrorDiscard' ||
continuation.kind === 'choamSaleDiscard' || continuation.kind === 'choamKulonDiscard' ||
continuation.kind === 'choamLaLaLaDiscard' || continuation.kind === 'choamBalisetDiscard' ||
continuation.kind === 'choamGamontDiscard' || continuation.kind === 'choamJubbaDiscard';
continuation.kind === 'choamGamontDiscard' || continuation.kind === 'choamJubbaDiscard' ||
continuation.kind === 'karamaShipmentPriceDiscard';
return (g.semutaPreview === true || g.semutaRecruitsPreview === true &&
(!g.pendingTreacheryDiscard || !!g.pendingTreacheryDiscard.reaction) &&
(independentNativeDiscard || continuation.kind === 'ordinaryCardDiscard' &&
@@ -9717,6 +9721,7 @@ function semutaOfferSupported(
continuation.kind === 'battleResolved' || continuation.kind === 'winnerMandatoryDiscard' ||
continuation.kind === 'battleCleanup' && (continuation.source === 'winner' ||
continuation.source === 'moritani' && !g.advanced && continuation.retention?.source === undefined) ||
continuation.kind === 'residualPoisonDiscard' ||
continuation.kind === 'nullentropyDiscard' || continuation.kind === 'distransDiscard' ||
continuation.kind === 'truthtranceDiscard' && continuation.promise === null &&
(continuation.record.question.kind === 'fact' || continuation.record.question.kind === 'freeform') ||
@@ -10845,10 +10850,16 @@ function startModules(state: Game): Game {
g.players.every(player => player.faction === 'choam' || player.faction === 'richese' || player.faction === 'ixians' ||
player.faction === 'tleilaxu' || g.expansions.includes('ecaz') &&
(player.faction === 'ecaz' || player.faction === 'moritani') || CLASSIC_FACTIONS[player.faction] === true) &&
!options.leaderSkills && !options.ecazTreachery && !g.nexusCards && !g.discoveryEnabled &&
!options.leaderSkills && !g.nexusCards && !g.discoveryEnabled &&
!g.homeworlds && !g.techTokens && !g.strongholdCards;
requireRule(supportedBasic || advancedKullLobbyEligible(state),
'Kull preview needs CHOAM and Ix decks with no optional modules. Advanced currently supports CHOAM and classic factions only; Basic also supports the selected native expansion factions.');
'Kull preview needs CHOAM and Ix decks. Advanced supports CHOAM and classic factions without optional modules; Basic also supports selected native expansion factions and independent Ecaz Treachery Cards.');
if (options.ecazTreachery) {
requireRule(supportedBasic, 'Kull with independent Ecaz cards currently needs the supported Basic native roster.');
const initialized = initializeEcazTreacheryGameForAudit(g);
initialized.kullPreview = true;
return initialized;
}
return initializeKullGameForAudit(g);
}
if (options.ecazTreachery && options.leaderSkills) g.ecazTreachery = true;
@@ -13081,7 +13092,8 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
// Family/roster checks below remain authoritative; optional configurations
// retain their separate registered profiles rather than inheriting admission.
const basicNatives = g.advanced === false && !g.strongholdCards &&
((!g.ecazTreachery && (!g.techTokens || nativeFactionTechProfile(g))) || selectedIndependentCardsProfile(g));
((!g.ecazTreachery && (!g.techTokens || nativeFactionTechProfile(g))) || selectedIndependentCardsProfile(g) ||
g.kullPreview === true && selectedIndependentCardsProfile({...g,kullPreview:undefined}));
if ((independentNexusProfile(g) || independentDiscoveryProfile(g)) && byFaction(g, 'ecaz')) return true;
if (g.leaderSkills && nativeEcazLeaderSkillsProfile(g)) return true;
if (g.ecazOccupyPreview === true && (mixedE3NexusProfile(g) || standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
@@ -13211,7 +13223,7 @@ function basicIndependentRecruitsSemutaProfile(g: Game): boolean {
g.expansions.includes(faction(p.faction).expansion)) &&
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens && !g.strongholdCards &&
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker &&
!g.kullPreview && !g.nexusKullPreview && !g.moritaniAssassinatePreview;
!g.nexusKullPreview && !g.moritaniAssassinatePreview;
}
function semutaCardProducerEnabled(g: Game): boolean {
return g.semutaPreview === true || g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g);
@@ -19469,7 +19481,7 @@ function kullCostPromisesAllow(g: Game, p: Player, card: Card, fuelSource: 'prin
}
function kullCostCards(g: Game, p: Player, source: 'printed' | 'nexus' = 'printed'): Card[] {
const original = g.pendingKull ? kullSource(g, g.pendingKull) : g;
if (g.semutaPreview && (!kullSemutaParentSupported(original, g.pendingKull ?? undefined) || source !== 'printed')) return [];
if (semutaCardProducerEnabled(g) && (!kullSemutaParentSupported(original, g.pendingKull ?? undefined) || source !== 'printed')) return [];
if (source === 'nexus' && (!g.nexusKullPreview || p.faction !== 'choam' || p.ally ||
(g.pendingKull?.selection?.source !== 'nexus' && g.nexusCards?.cards?.hands[p.id] !== 'choam')))
return [];
@@ -19884,7 +19896,7 @@ function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered
return stage && !stage.closed && windowCardOpportunity(g, effect, stage.source)?.source === stage.source;
});
})) guardTurnActionPrintedCardCost(g, p, used);
if (g.semutaPreview && !g.advanced && g.pendingKull?.stage === 'counter' &&
if (semutaCardProducerEnabled(g) && !g.advanced && g.pendingKull?.stage === 'counter' &&
use.kind === 'cancel' && use.response.kind === 'choamWorthless' && use.response.intent === 'Kull Wahad') {
const declaration = g.pendingChoamWorthless;
requireRule(declaration && kullSemutaParentSupported(kullSource(g, g.pendingKull), g.pendingKull),
@@ -19903,7 +19915,7 @@ function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered
stageTreacheryDiscard(g, 'karama:kullCounter', [{card:used, discardedBy:p.id, publicFace:true}], continuation);
return;
}
const semutaRoleCost = g.semutaPreview && semutaKaramaRoleCost(g, p.id, card);
const semutaRoleCost = semutaCardProducerEnabled(g) && semutaKaramaRoleCost(g, p.id, card);
const guildShipmentDiscard = semutaRoleCost &&
use.kind === 'cancel' && semutaGuildShipmentKaramaResponse(g, use.response);
const bgConversionCancelDiscard = semutaRoleCost &&
@@ -30821,7 +30833,7 @@ function playResidualPoison(
`${p.name} played Residual Poison against ${target.name}. ${victim.name} was randomly selected from the available leaders and sent to the Tanks. No spice is awarded, no forces are lost, and battle preparation continues.`,
{ faction: p.faction, name: 'Residual Poison' },
);
if (g.semutaPreview || g.semutaRecruitsPreview && basicIndependentSemutaProfile(g)) {
if (g.semutaPreview || g.semutaRecruitsPreview && basicIndependentRecruitsSemutaProfile(g)) {
const continuation: ResidualPoisonDiscardContinuation = {
kind: 'residualPoisonDiscard', player: p.id, target: target.id,
victim: victim.id, battleEvent: b.event!, card: used.id,
@@ -37121,7 +37133,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
log(g, `${getPlayer(g, g.pendingKull!.owner).name} cannot activate ${restriction.purpose
? kullPurposeLabel(restriction.purpose) : 'Karama'} for this phase. The attempted card stays held.`);
resumeKullAttempt(g, true);
if (g.semutaPreview && !g.advanced) {
if (semutaCardProducerEnabled(g) && !g.advanced) {
const continuation: KullSemutaDiscardContinuation = {
kind: 'kullSuccessDiscard', player: choam.id, card: used.id, frame,
resume: { response: g.response, decision: g.decision,