Announce leaderless native Basic battles before plans
Verify and publish checkpoint container / container (push) Successful in 10m14s

This commit is contained in:
Quintine committed 2026-10-29 20:33:33 +11:00
1 parent 00e01a7cd2
commit f73a6e0a9c
9 files changed
+481 -21

No files matched your search

+40 -20
View File
@@ -38383,6 +38383,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
!Object.keys(b.plans).length, 'Resolve Occupy before native powers and plans.');
occupy.canceled = canceled;
ecazBattleProfile(g);
prepareEarlyBasicLeaderless(g);
// The chosen plan owner and final Occupy force profile are now fixed.
// A nonleading ally keeps its waiting receipt for its own later battle.
bindFutureBattleLeaderQuestions(g);
@@ -43791,6 +43792,27 @@ function publicNativeBattleLeaders(player: Player, territory: string) {
unavailable: !leaders.some(leader => !leader.dead && !leader.captured &&
(!leader.usedAt || leader.usedAt === territory)) };
}
function publicSharedDukeAvailable(g: Game, player: string, territory: string): boolean {
const duke = projectedSharedDuke(g, '');
return !!duke && duke.controller === player && !duke.leader.dead &&
!duke.leader.capturedBy && !duke.leader.gholaBy &&
(!duke.leader.usedAt || duke.leader.usedAt === territory);
}
/** Open the neutral Ghola opportunity only for the final battle participants.
* Richese already opens the same shared preparation for Sapho. */
function prepareEarlyBasicLeaderless(g: Game) {
const b = g.battle;
if (!b?.event || b.basicEarlyLeaderlessVersion !== 1) return;
const participants = [b.attacker, b.defender];
const candidates = participants.filter(id => {
const publicState = publicNativeBattleLeaders(getPlayer(g, id), b.territory);
return publicState.hasDead && publicState.unavailable && !publicSharedDukeAvailable(g, id, b.territory);
});
if (!candidates.length) return;
b.basicDeadLeaderPreparation = true;
if (!b.preLeader) b.preLeader = {event: b.event,
ready: participants.filter(id => !candidates.includes(id)), closed: false};
}
/** Publish a clean Basic inability once preparation and battle powers finish. */
function announceEarlyBasicLeaderless(g: Game) {
const b = g.battle;
@@ -43807,16 +43829,18 @@ function announceEarlyBasicLeaderless(g: Game) {
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) {
if (availableLeader || availableHero) {
b.basicEarlyLeaderless = b.basicEarlyLeaderless.filter(player => player !== id);
log(g, `${p.name} now has an available leader for this battle.`);
log(g, availableLeader ? `${p.name} now has an available leader for this battle.` :
`${p.name} now has an available Cheap Hero for this battle.`);
}
continue;
}
if (b.basicPublicLeaderlessVersion === 1 &&
(!publicNativeBattleLeaders(p, b.territory).unavailable ||
p.faction === 'harkonnen' && g.players.some(owner => owner.leaders.some(leader =>
leader.concealed?.captor === p.id && leader.capturedBy === p.id)))) continue;
(!publicNativeBattleLeaders(p, b.territory).unavailable || publicSharedDukeAvailable(g, id, b.territory) ||
p.faction === 'harkonnen' && (g.players.some(owner => owner.leaders.some(leader =>
leader.concealed?.captor === p.id && leader.capturedBy === p.id)) ||
g.dukeVidal?.leader.concealed?.captor === p.id))) continue;
if ((!leaders.length && !b.basicDeadLeaderPreparation) || availableLeader || availableHero ||
!b.basicDeadLeaderPreparation && leaders.some(l => l.dead || l.capturedBy)) continue;
(b.basicEarlyLeaderless ??= []).push(id);
@@ -44951,6 +44975,9 @@ function applyActionInner(
// The selected lead supplies normal aggressor priority, not the scheduling ally.
if (g.order.indexOf(b.attacker) > g.order.indexOf(b.defender))
[b.attacker, b.defender] = [b.defender, b.attacker];
// Richese's shared preparation must name the final combatants.
if (b.basicEarlyLeaderlessVersion === 1 && b.preLeader)
b.preLeader.ready = [];
ecazBattleProfile(g);
g.decision = null;
g.response = { kind: 'ecazOccupy', owner: occupy.ecaz, passed: [] };
@@ -48186,27 +48213,17 @@ 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 &&
const earlyBasic = !g.advanced && (g.expansions.length === 0
? g.players.every(seat => CLASSIC_FACTIONS[seat.faction] === true)
: selectedPhaseModuleRoster(g)) && !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));
// Use the same pre-capture snapshot visible to uninvolved players. A
// neutral preparation opportunity must not identify a concealed captive.
const deadLeaderCandidates = earlyBasic
? [choice.attacker, choice.defender].filter(playerId => {
const publicState = publicNativeBattleLeaders(getPlayer(g, playerId), choice.territory);
return publicState.hasDead && publicState.unavailable;
}) : [];
!g.coalitionState && !g.kullPreview && !g.nexusKullPreview;
g.battle = {
...choice,
chooserEvent: battleChooserEvent(g.turn, battleOrderAfterBattle(g), g.battleOrder?.uses.length ?? 0),
event: battleEvent,
...(earlyBasic ? { basicEarlyLeaderlessVersion: 1 as const } : {}),
...(earlyBasic && byFaction(g, 'harkonnen') ? { basicPublicLeaderlessVersion: 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 } : {}),
@@ -48223,7 +48240,10 @@ function applyActionInner(
revealed: false,
traitorCalls: {},
};
if (!combined) bindFutureBattleLeaderQuestions(g);
if (!combined) {
prepareEarlyBasicLeaderless(g);
bindFutureBattleLeaderQuestions(g);
}
if (combined) {
g.battle.ecazOccupy = { event: battleEvent, ecaz: ecaz!.id, ally: ally!.id, lead: null, canceled: false };
const opponent = [choice.attacker, choice.defender].find(id => id !== ecaz!.id && id !== ally!.id)!;