Add dead leader battle preparation
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 08:42:28 +11:00
1 parent bcca08cac5
commit e439f3f7b6
8 files changed
+386 -21

No files matched your search

+33 -12
View File
@@ -991,6 +991,8 @@ export type Battle = {
/** Fresh clean Basic battles announce a bounded no-revival case before plans. */
basicEarlyLeaderlessVersion?: 1;
basicEarlyLeaderless?: string[];
/** Fresh classic battles with a neutral dead-leader preparation opportunity. */
basicDeadLeaderPreparation?: true;
revealed: boolean;
traitorCalls: Record<string, boolean>;
traitorDecisionPromises?: TraitorDecisionPromise[];
@@ -43024,9 +43026,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
charityOrderIntegrity(g);
return g;
}
/** The published inability is safe before a plan only when no leader can be
* revived and this clean Basic roster cannot privately transfer a Cheap Hero.
* Other cases retain the original seal-time announcement. */
/** Publish a clean Basic inability once preparation and battle powers finish. */
function announceEarlyBasicLeaderless(g: Game) {
const b = g.battle;
if (!b || b.basicEarlyLeaderlessVersion !== 1 || g.status !== 'playing' || g.advanced ||
@@ -43034,11 +43034,20 @@ function announceEarlyBasicLeaderless(g: Game) {
g.response || g.decision || g.truthtrance || g.phaseOpening || g.pendingKarama ||
g.pendingKull || g.pendingTreacheryDiscard || g.pendingNullentropy) return;
for (const id of [b.attacker, b.defender]) {
if (b.plans[id] || b.basicEarlyLeaderless?.includes(id)) continue;
if (b.plans[id]) continue;
const p = getPlayer(g, id), leaders = controlledLeaders(g, p);
if (!leaders.length || leaders.some(l => l.dead || l.capturedBy || !l.usedAt || l.usedAt === b.territory) ||
p.hand.some(c => c.kind === 'hero' && !(b.voice?.target === id && !b.voice.must &&
playedVoiceMatch(c, 'leader', b.voice.kind)))) continue;
const availableLeader = leaders.some(l => !l.dead && !l.capturedBy && (!l.usedAt || l.usedAt === b.territory));
const availableHero = p.hand.some(c => c.kind === 'hero' && !(b.voice?.target === id && !b.voice.must &&
playedVoiceMatch(c, 'leader', b.voice.kind)));
if (b.basicEarlyLeaderless?.includes(id)) {
if (b.basicDeadLeaderPreparation && availableLeader) {
b.basicEarlyLeaderless = b.basicEarlyLeaderless.filter(player => player !== id);
log(g, `${p.name} now has an available leader for this battle.`);
}
continue;
}
if (!leaders.length || availableLeader || availableHero ||
!b.basicDeadLeaderPreparation && leaders.some(l => l.dead || l.capturedBy)) continue;
(b.basicEarlyLeaderless ??= []).push(id);
log(g, `${p.name} has no available leader or Cheap Hero for this battle.`);
}
@@ -47358,15 +47367,27 @@ function applyActionInner(
'Combined Occupy requires source-selected native factions; optional overlays remain gated.');
}
const battleEvent = crypto.randomUUID();
const earlyBasic = !g.advanced && g.expansions.length === 0 && !g.ecazTreachery && !g.techTokens &&
!g.strongholdCards && !g.leaderSkills && !g.homeworlds && !g.nexusCards && !g.discoveryEnabled &&
!g.sandtrout && !g.coalitionState && g.players.every(seat =>
['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction));
// The candidate depends only on public native leaders, never either hand.
// Harkonnen's concealed captures need a separate public-projection rule.
const deadLeaderCandidates = earlyBasic && !byFaction(g, 'harkonnen')
? [choice.attacker, choice.defender].filter(playerId => {
const leaders = getPlayer(g, playerId).leaders;
return leaders.some(leader => leader.dead) && !leaders.some(leader =>
!leader.dead && !leader.capturedBy && (!leader.usedAt || leader.usedAt === choice.territory));
}) : [];
g.battle = {
...choice,
chooserEvent: battleChooserEvent(g.turn, battleOrderAfterBattle(g), g.battleOrder?.uses.length ?? 0),
event: battleEvent,
...(!g.advanced && g.expansions.length === 0 && !g.ecazTreachery && !g.techTokens &&
!g.strongholdCards && !g.leaderSkills && !g.homeworlds && !g.nexusCards && !g.discoveryEnabled &&
!g.sandtrout && !g.coalitionState && g.players.every(seat =>
['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction))
? { basicEarlyLeaderlessVersion: 1 as const } : {}),
...(earlyBasic ? { basicEarlyLeaderlessVersion: 1 as const } : {}),
...(deadLeaderCandidates.length ? { basicDeadLeaderPreparation: true as const,
preLeader: { event: battleEvent,
ready: [choice.attacker, choice.defender].filter(playerId => !deadLeaderCandidates.includes(playerId)),
closed: false } } : {}),
stoneAllocationVersion: 1,
...(g.leaderSkills ? { smugglerCollectionVersion: 1 as const } : {}),
...(g.leaderSkills && mentatQuestionModeSupported(g) ? { mentatQuestionVersion: 1 as const } : {}),