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)!;
+1 -1
View File
@@ -2863,7 +2863,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'Build a legal combination and decide what to keep after winning.',
steps: [
'Choose one weapon, one defense, both, or neither. A worthless card can occupy a weapon or defense slot.',
'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played. In supported fresh classic Basic battles, an inability is announced before plans. When dead leaders may be revived, a preparation opportunity lets you play Ghola from your hand before continuing; a later revival updates the public notice. Harkonnen can use a captive leader; its announcement stays at plan submission while it holds a concealed captive. Other Basic and Advanced leaderless plans announce on sealing; its dial, spice and other choices remain sealed. Refresh retains the same announcement without submitting another plan.',
'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played. In supported fresh Basic battles with classic or selected expansion factions, an inability is announced before plans. When dead leaders may be revived, a preparation opportunity lets you play Ghola from your hand before continuing; a later revival or received Cheap Hero updates the public notice. An Ecaz coalition uses its selected lead’s leaders and hand. Harkonnen can use a captive leader; its announcement stays at plan submission while it holds a concealed captive. Other Basic and Advanced leaderless plans announce on sealing; its dial, spice and other choices remain sealed. Refresh retains the same announcement without submitting another plan.',
'On Arrakis, a surviving leader remains in its battle territory for the rest of the Battle phase. A later Lasgun–Shield explosion there also kills previously surviving leader discs still present, even if a different leader fights that later battle. The Basic application is provisional; a successful Traitor call prevents the explosion.',
'A loser normally discards all played cards. If allied with Moritani and the battle has a winner, the defeated ally may retain one played card that a winner could keep. A winner may discard selected played cards; Cheap Heroes are discarded after use.',
],