Allow Basic Emperor gifts before auction payment
Verify and publish checkpoint container / container (push) Successful in 10m39s

This commit is contained in:
Quintine committed 2026-10-30 18:58:02 +11:00
1 parent a1b09c5b02
commit b258c6867f
8 files changed
+342 -13

No files matched your search

+37 -13
View File
@@ -6715,6 +6715,14 @@ function semutaEmperorShareKaramaResponse(g: Game, response: ResponseWindow): bo
response.amount! > 0 && (response.elite === undefined || response.elite === 0 || response.elite === 1) &&
!g.battle && !g.pendingShipment && !g.pendingGuildTransport;
}
/** The auction gift may park only its exact original buyer/payment decision. */
function emperorAuctionGiftResumeMatches(g: Game, response: ResponseWindow, decision: Game['decision']): boolean {
const pending = g.pendingEmperorAuctionGift;
return !!pending && response.kind === 'emperorGift' && response.emperorAuctionGiftEvent === pending.event &&
response.owner === pending.owner && response.recipient === pending.recipient &&
decision?.kind === 'auctionPayment' && decision.player === pending.recipient &&
g.phase === 3 && !!g.auction && g.auction.bidder === pending.recipient && !g.currentAuctionSale && !g.richeseAuction;
}
/** Preserve physical identity: a Shrine authorizes the role, not a manufactured card. */
function semutaKaramaRoleCost(g: Game, owner: string, card: Card): boolean {
return card.effect === 'karama' || (card.effect === 'truthtrance' &&
@@ -8723,7 +8731,8 @@ function treacheryDiscardIntegrity(g: Game) {
c.resume.response?.kind === c.use.response.kind &&
JSON.stringify(c.resume.response) === JSON.stringify(c.use.response) &&
(c.kind === 'karamaBgConversionCancelDiscard' ||
((c.kind === 'karamaTerminalDiscard' || c.kind === 'karamaCombatDiscard' || !c.resume.decision) &&
((c.kind === 'karamaTerminalDiscard' || c.kind === 'karamaCombatDiscard' || !c.resume.decision ||
c.kind === 'karamaEmperorShareDiscard' && emperorAuctionGiftResumeMatches(g, c.use.response, c.resume.decision)) &&
!c.resume.pendingKarama && !c.resume.phaseOpening)) &&
batch.cause === `karama:${c.use.response.kind}` &&
batch.entries.length === 1 &&
@@ -10171,7 +10180,9 @@ function semutaOfferSupported(
: continuation.kind === 'karamaShipmentDiscard'
? semutaGuildShipmentKaramaResponse(g, continuation.use.response)
: ['choamCharity', 'choamInflation', 'bgCharity'].includes(continuation.resume.response?.kind ?? '')) &&
(terminalDiscard || combatDiscard || !continuation.resume.decision) && !continuation.resume.pendingKarama &&
(terminalDiscard || combatDiscard || !continuation.resume.decision ||
continuation.kind === 'karamaEmperorShareDiscard' &&
emperorAuctionGiftResumeMatches(g, continuation.use.response, continuation.resume.decision)) && !continuation.resume.pendingKarama &&
!continuation.resume.phaseOpening && !g.pendingKarama &&
(!g.response || JSON.stringify(g.response) === JSON.stringify(continuation.resume.response))
: true) &&
@@ -10243,7 +10254,9 @@ function semutaOfferSupported(
continuation.use.response.kind === 'emperorIncome'
? !!g.currentAuctionSale && (g.currentAuctionSale.origin === 'normal'
? !!g.auction && !g.richeseAuction : !!g.richeseAuction && !g.auction)
: ordinaryAuctionDiscard || (continuation.kind === 'saphoAuctionDiscard' && continuation.cyclic === 'normal') || !g.auction)) &&
: ordinaryAuctionDiscard || (continuation.kind === 'saphoAuctionDiscard' && continuation.cyclic === 'normal') ||
continuation.kind === 'karamaEmperorShareDiscard' &&
emperorAuctionGiftResumeMatches(g, continuation.use.response, continuation.resume.decision) || !g.auction)) &&
(advisorDiscard || auctionCostDiscard || bonusDiscard || auctionAdvantageDiscard || richeseGiftDiscard || bgDiscard || continuation.kind === 'specialKaramaDiscard' || continuation.kind === 'saphoAuctionDiscard' ||
continuation.kind === 'ixAllyCard' ||
(continuation.kind === 'karamaIncomeDiscard' &&
@@ -18911,14 +18924,21 @@ function auctionNext(g: Game) {
a.active = next.player;
g.active = a.active;
}
function emperorAuctionGiftParked(g: Game) {
const c = g.pendingTreacheryDiscard?.continuation;
if (c?.kind === 'kullCounterDiscard') return c.frame.resume;
if (c?.kind === 'kullSuccessDiscard' || c?.kind === 'karamaEmperorShareDiscard') return c.resume;
return g.pendingKull?.resume ?? null;
}
function emperorAuctionGiftSignature(g: Game): string {
return JSON.stringify([g.turn, g.phase, g.active, g.decision, g.order, g.auction, g.aid]);
return JSON.stringify([g.turn, g.phase, g.active, g.decision ?? emperorAuctionGiftParked(g)?.decision, g.order, g.auction, g.aid]);
}
/** The public alliance and winning seat alone determine this first refusal.
* Only auctionNext creates it: a restored payment never reopens the offer. */
function beginEmperorAuctionGift(g: Game) {
if (!g.advanced || g.status !== 'playing' || g.phase !== 3 ||
g.expansions.length !== 0 || g.players.some(seat => !['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) ||
if (g.status !== 'playing' || g.phase !== 3 ||
(g.advanced ? g.expansions.length !== 0 || g.players.some(seat => !['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction))
: g.expansions.length > 0 && !selectedPhaseModuleRoster(g)) ||
g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || g.nexusCards ||
g.discoveryEnabled || g.sandtrout || g.coalitionState || g.pendingEmperorAuctionGift ||
g.response || g.phaseOpening || g.truthtrance || g.pendingKarama ||
@@ -18943,14 +18963,16 @@ function emperorAuctionGiftIntegrity(g: Game) {
}
const owner = g.players.find(player => player.id === pending.owner);
const recipient = g.players.find(player => player.id === pending.recipient);
requireRule(g.status === 'playing' && g.advanced && g.phase === 3 && g.turn === pending.turn &&
const parked = emperorAuctionGiftParked(g);
const decision = g.decision ?? parked?.decision;
requireRule(g.status === 'playing' && g.phase === 3 && g.turn === pending.turn &&
typeof pending.event === 'string' && !!pending.event && owner?.faction === 'emperor' && recipient &&
owner.ally === recipient.id && recipient.ally === owner.id &&
g.decision?.kind === 'auctionPayment' && g.decision.player === recipient.id &&
decision?.kind === 'auctionPayment' && decision.player === recipient.id &&
g.auction?.bidder === recipient.id && pending.signature === emperorAuctionGiftSignature(g),
'The Emperor auction gift lost its original payment opportunity.');
const response = g.pendingKarama?.use.kind === 'cancel'
? g.pendingKarama.use.response : g.response;
const response = parked?.response ?? (g.pendingKarama?.use.kind === 'cancel'
? g.pendingKarama.use.response : g.response);
if (response)
requireRule(response.kind === 'emperorGift' && response.owner === owner.id &&
response.recipient === recipient.id && response.emperorAuctionGiftEvent === pending.event &&
@@ -20801,7 +20823,9 @@ function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered
g.response = null;
if (bgConversionCancelDiscard) {
g.decision = null; g.pendingKarama = null; g.phaseOpening = null;
} else if (continuation.kind === 'karamaTerminalDiscard' || continuation.kind === 'karamaCombatDiscard') g.decision = null;
} else if (continuation.kind === 'karamaTerminalDiscard' || continuation.kind === 'karamaCombatDiscard' ||
continuation.kind === 'karamaEmperorShareDiscard' &&
emperorAuctionGiftResumeMatches(g, continuation.use.response, continuation.resume.decision)) g.decision = null;
continuation.stateSignature = karamaResponseDiscardSignature(g, continuation);
stageTreacheryDiscard(g, `karama:${use.response.kind}`, entries, continuation);
return;
@@ -44604,7 +44628,7 @@ function applyActionInner(
revealPlayerNoField(g, p, 'voluntary');
return g;
}
if (g.pendingEmperorAuctionGift && !g.response && !g.pendingKarama) {
if (g.pendingEmperorAuctionGift && !g.response && !g.pendingKarama && !g.pendingKull) {
const pending = g.pendingEmperorAuctionGift;
if (t === 'advanceBots') return g;
requireRule(id === pending.owner, 'Wait for the Emperor to finish the allied gift opportunity.');
@@ -44623,7 +44647,7 @@ function applyActionInner(
return g;
}
if (g.pendingEmperorAuctionGift && (g.response || g.pendingKarama) && t !== 'advanceBots')
requireRule(t === 'passResponse' || t === 'card' && action.mode === 'cancel',
requireRule(t === 'passResponse' || t === 'card' && action.mode === 'cancel' || t === 'kullDecision' && !!g.pendingKull,
'Resolve the Emperor gift response before another action.');
if (g.nexusFremenRideBetrayal?.stage === 'choose') {
if (t === 'advanceBots') return g;
+1
View File
@@ -86,6 +86,7 @@ export const FACTION_RULES: Record<
'Start with all twenty forces in reserve and ten spice. One force revival is free each turn.',
'Receive payments for other players’ auction purchases. Your own purchases pay the bank; a Karama purchase pays no spice. A cancellation sends one auction payment to the bank.',
'Give spice directly to your ally. After the response window it is immediately spendable, unlike a deferred bribe between non-allies.',
'In a supported Basic ordinary auction, you may give your mutual ally spice after they win and before their original payment. Continue preserves the bid and pledged split; a canceled gift transfers nothing.',
'In supported Moritani games, you may also give spice while your ally is choosing whether to pay Extortion. The original choice resumes after the gift’s Karama response.',
'In supported Basic Ix-deck games with classic factions, Ixians or Tleilaxu, you may give an ally spice during the Amal phase opening, even after you have passed. The original opening resumes after the gift response.',
'During Revival, pay two spice per extra allied force, up to three extra forces per turn. These are separate from the ally’s normal three-force allowance and may be purchased before or after ordinary revivals.',