Support Semuta after market Ghola
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
6ecd26c8d1
commit
79fc12c1c1
11 files changed
+557
-5
No files matched your search
+6
-2
@@ -551,6 +551,7 @@ import {
|
||||
import { transferDistrans } from './distrans';
|
||||
import { distransAuctionBlock } from './distrans-timing';
|
||||
import { SEMUTA_DRUG_ID, committedSemutaCandidates, resolveSemutaDrug, type FreshDiscardBatch, type SemutaContext } from './semuta-drug';
|
||||
import { semutaMarketGholaSupported } from './semuta-market-ghola';
|
||||
import {
|
||||
prepareRicheseGift,
|
||||
transferRicheseGift,
|
||||
@@ -9614,6 +9615,9 @@ function semutaOfferSupported(
|
||||
['alliance', 'allianceExpression', 'mixedAlliance'].includes(continuation.record.question.kind) &&
|
||||
(!continuation.allianceBinding || !bareBasicSemutaProfile(g) || !alliancePromiseModeSupported(g))) return false;
|
||||
const richeseGiftDiscard = continuation.kind === 'karamaRicheseGiftDiscard';
|
||||
const marketGholaDiscard = continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'ghola' &&
|
||||
g.semutaPreview === true && bareBasicSemutaProfile(g) && ordinaryDiscardIncomeMatches(g, continuation) &&
|
||||
semutaMarketGholaSupported(g, continuation, entries);
|
||||
const richeseDiscard = richeseGiftDiscard || continuation.kind === 'karamaRicheseAuctionDiscard';
|
||||
const auctionAdvantageDiscard = continuation.kind === 'karamaAuctionAdvantageDiscard';
|
||||
const terminalDiscard = continuation.kind === 'karamaTerminalDiscard';
|
||||
@@ -10046,13 +10050,13 @@ function semutaOfferSupported(
|
||||
(richeseGiftDiscard || !g.pendingRicheseGift) &&
|
||||
(richeseGiftDiscard || (continuation.kind === 'karamaIncomeDiscard' &&
|
||||
continuation.use.response.kind === 'richesePurchaseIncome') || !g.pendingRichesePurchaseIncome) &&
|
||||
(richeseGiftDiscard || saleCostDiscard || (continuation.kind === 'choamSaleDiscard'
|
||||
(richeseGiftDiscard || saleCostDiscard || marketGholaDiscard || (continuation.kind === 'choamSaleDiscard'
|
||||
? !!g.choamMarket && !g.choamMarket.sale &&
|
||||
!g.choamMarket.trade && !g.biddingEnd && !g.currentAuctionSale
|
||||
: continuation.kind === 'kaitainDiscard'
|
||||
? g.biddingEnd?.event === continuation.event && biddingEndError(g) === null
|
||||
: !g.choamMarket)) &&
|
||||
(richeseGiftDiscard || !g.pendingChoamMarketGhola);
|
||||
(richeseGiftDiscard || marketGholaDiscard || !g.pendingChoamMarketGhola);
|
||||
}
|
||||
function semutaContext(g: Game, owner: Player): SemutaContext {
|
||||
const pending = g.pendingTreacheryDiscard!;
|
||||
|
||||
+2
-2
@@ -2945,7 +2945,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A Basic ground-movement promise can also require Ghola force preparation. The guide returns physical Tanks to reserves, then preserves an unused affordable shipment and one later matching ground move. Ghola itself does not fulfill movement or consume normal revival allowances. A known discarded Ghola may be recovered with the paid Box and played again.',
|
||||
'Returned forces go to reserves. A leader revived with this card can fight again this turn, including in another territory. Atreides can also select a dead Kwisatz Haderach; revive it separately from its accompanying leader. Returning a controlled foreign ghola does not transfer it back to its original faction. Additional advanced and expansion interactions remain under audit.',
|
||||
'An actual Ecaz native Ghola return uses that disc in its current ordinary cohort without consuming the ordinary phase slot. A repeat waits for the remaining native cohort and another opening. Independent Duke Ghola preserves a satisfied five-Tanks opening and returns him set aside; only Ecaz may revive him.',
|
||||
'Semuta may recover the used Ghola before its native Tleilaxu one-spice income resolves. Keep the returned piece and normal revival allowance; claim or Continue then restores the original income response. Taking the card never repeats its revival.',
|
||||
'Semuta may recover the used Ghola before its native Tleilaxu one-spice income resolves. Keep the returned piece and normal revival allowance; claim or Continue then restores the original income response. This also works during a supported Basic CHOAM sale: the original sale waits for the Semuta choice and any income response, then continues once. Taking the card never repeats its revival.',
|
||||
'A saved play preserves the revived pieces, any earned technology income and the discarded card. Recovery restores any pending Tleilaxu income response without reviving or paying twice. Private preparation guidance waits for recovery to finish.',
|
||||
],
|
||||
checklist: [
|
||||
@@ -2954,7 +2954,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Ordinary and typed forces, controlled leaders and Kwisatz retain original use and custody. Actual Ecaz native Ghola/foreign returns now record completed native cohort identity; independent Duke resurrection preserves its five-Tanks opening without aligning death counters. Ecaz-only living set-aside custody remains; broader combinations and assurance stay unfinished.',
|
||||
evidence: ['game/ordinary-discard-income.ts', 'game/ghola-force-preparation.ts', 'game/engine.ts', 'game/ecaz-duke-revival.ts', 'game/ecaz-revival-cohort.ts', 'game/revival.ts'],
|
||||
evidence: ['game/ordinary-discard-income.ts', 'game/semuta-market-ghola.ts', 'game/ghola-force-preparation.ts', 'game/engine.ts', 'game/ecaz-duke-revival.ts', 'game/ecaz-revival-cohort.ts', 'game/revival.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Game } from './engine';
|
||||
import type { FreshDiscardBatch } from './semuta-drug';
|
||||
import { choamGholaEvent, choamMarketGholaError } from './choam-market-ghola';
|
||||
import { quoteChoamSaleCancellation } from './choam-sale-cancellation';
|
||||
|
||||
type OrdinaryDiscard = Extract<NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'ordinaryCardDiscard' }>;
|
||||
|
||||
/** Qualify the original Ghola discard while its CHOAM sale is suspended.
|
||||
* The caller separately owns the Basic Semuta profile and the ordinary
|
||||
* revival-income qualification. This never changes a saved frame. */
|
||||
export function semutaMarketGholaSupported(
|
||||
game: Game, continuation: OrdinaryDiscard, entries: FreshDiscardBatch['entries'],
|
||||
): boolean {
|
||||
const pending = game.pendingChoamMarketGhola, discard = game.pendingTreacheryDiscard;
|
||||
if (game.status !== 'playing' || game.advanced || game.semutaPreview !== true ||
|
||||
!game.expansions.includes('choam') || game.homeworlds || game.nexusCards ||
|
||||
game.leaderSkills || game.techTokens || game.strongholdCards || game.ecazTreachery ||
|
||||
game.kullPreview || game.nexusKullPreview ||
|
||||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
|
||||
game.sandtrout || game.coalitionState || !pending || !game.choamMarket ||
|
||||
continuation.kind !== 'ordinaryCardDiscard' || continuation.effect !== 'ghola' ||
|
||||
pending.stage !== 'discard' || pending.turn !== game.turn || pending.phase !== game.phase ||
|
||||
pending.player !== continuation.player || pending.card !== continuation.card ||
|
||||
pending.discardSequence !== game.treacheryDiscardSequence || pending.event !== choamGholaEvent(pending) ||
|
||||
JSON.stringify(pending.market) !== JSON.stringify(game.choamMarket) ||
|
||||
pending.response?.kind !== 'choamSale' || pending.response.owner !== game.choamMarket.owner ||
|
||||
!Array.isArray(pending.response.passed) || new Set(pending.response.passed).size !== pending.response.passed.length ||
|
||||
pending.response.passed.some(id => !game.players.some(player => player.id === id)) ||
|
||||
continuation.resume.decision || continuation.resume.pendingKarama || continuation.resume.phaseOpening ||
|
||||
game.response || game.decision || game.pendingKarama || game.phaseOpening ||
|
||||
discard && (!discard.reaction || discard.sequence !== pending.discardSequence)) return false;
|
||||
|
||||
const entry = entries[0], cards = game.discard.filter(card => card.id === pending.card);
|
||||
if (entries.length !== 1 || !entry || !entry.publicFace || entry.discardedBy !== pending.player ||
|
||||
entry.card.id !== pending.card || entry.card.kind !== 'special' || entry.card.effect !== 'ghola' ||
|
||||
cards.length !== 1 || cards[0].name !== entry.card.name || cards[0].kind !== entry.card.kind ||
|
||||
cards[0].effect !== entry.card.effect) return false;
|
||||
const income = continuation.resume.response;
|
||||
if (income && (income.kind !== 'revivalIncome' || income.intent !== pending.event ||
|
||||
income.recipient !== pending.player || income.amount !== 1 || !Array.isArray(income.passed) || income.passed.length !== 0 ||
|
||||
!game.players.some(player => player.id === income.owner && player.faction === 'tleilaxu'))) return false;
|
||||
try {
|
||||
if (!quoteChoamSaleCancellation(game, pending.response)) return false;
|
||||
// Before the discard is installed, the sequence is already incremented;
|
||||
// the ordinary frame validator can only run after installation.
|
||||
if (discard && choamMarketGholaError(game)) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user