Prioritize Basic faction prototypes and connect three missing rule paths
This commit is contained in:
1 parent
f36632accc
commit
c28e24b5fa
17 files changed
+152
-110
No files matched your search
+54
-23
@@ -1537,6 +1537,8 @@ export type Game = {
|
||||
| 'complete';
|
||||
position: 'first' | 'last' | null;
|
||||
normalCount: number | null;
|
||||
/** Frozen before the cache deduction; cancellation never recounts live hands. */
|
||||
normalCountBeforeCache?: number;
|
||||
blackMarketSold: boolean;
|
||||
cacheCanceled: boolean;
|
||||
cacheAuthority?: string;
|
||||
@@ -9445,7 +9447,7 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
|
||||
!g.discoveryStash && !g.greatMaker &&
|
||||
(g.ecazTreachery === undefined || g.ecazTreachery === true);
|
||||
}
|
||||
/** Fresh native Occupy profile; Basic's disputed odd-force arithmetic stays guarded. */
|
||||
/** Fresh native Occupy profile; Basic odd counts use the main printed rule provisionally. */
|
||||
export function initializeEcazOccupyGameForAudit(state: Game): Game {
|
||||
requireRule(ecazOccupyCompositionSupported(state),
|
||||
'Occupy requires supported native factions and their distinct selected decks; Basic uses Ecaz/classic/optional Moritani without optional modules.');
|
||||
@@ -10755,14 +10757,32 @@ function richeseDeclaration(g: Game) {
|
||||
richeseDeclarationCache(g);
|
||||
const round = g.richeseBidding!;
|
||||
round.stage = 'declaration';
|
||||
round.normalCountBeforeCache = Math.max(
|
||||
0, auctionEligible(g).length - Number(round.blackMarketSold),
|
||||
);
|
||||
round.normalCount = Math.max(
|
||||
0,
|
||||
auctionEligible(g).length -
|
||||
Number(richeseCacheRuns(g)) -
|
||||
Number(round.blackMarketSold),
|
||||
0, round.normalCountBeforeCache - Number(richeseCacheRuns(g)),
|
||||
);
|
||||
richeseDecision(g, 'richeseDeclaration');
|
||||
}
|
||||
function validateRicheseCacheCancellation(g: Game, response: ResponseWindow) {
|
||||
const round = g.richeseBidding;
|
||||
requireRule(g.status === 'playing' && g.phase === 3 && round &&
|
||||
round.turn === g.turn && round.stage === 'declaration' &&
|
||||
round.owner === response.owner && getPlayer(g, round.owner).faction === 'richese' &&
|
||||
(round.position === 'first' || round.position === 'last') &&
|
||||
Number.isSafeInteger(round.normalCount) && round.normalCount! >= 0 &&
|
||||
round.normalCount! <= g.players.length && !round.cacheCanceled &&
|
||||
!g.auction && !g.richeseAuction && !g.ixAuction &&
|
||||
(round.normalCountBeforeCache === undefined ||
|
||||
Number.isSafeInteger(round.normalCountBeforeCache) &&
|
||||
round.normalCountBeforeCache >= round.normalCount! &&
|
||||
round.normalCountBeforeCache <= g.players.length &&
|
||||
round.normalCountBeforeCache - round.normalCount! <= 1),
|
||||
'Cache cancellation needs the original undealt Richese declaration.');
|
||||
richeseDeclarationCache(g);
|
||||
return round;
|
||||
}
|
||||
function prepareRicheseNormal(g: Game) {
|
||||
g.richeseBidding!.stage = 'normal';
|
||||
setAuction(g);
|
||||
@@ -12775,10 +12795,8 @@ function validateKaramaUse(
|
||||
response: { kind: use.response.kind, owner: use.response.owner },
|
||||
canceled: true,
|
||||
});
|
||||
requireRule(
|
||||
use.response.kind !== 'richeseAuction',
|
||||
'The canceled Richese auction count is awaiting an official ruling or an explicit table interpretation.',
|
||||
);
|
||||
if (use.response.kind === 'richeseAuction')
|
||||
validateRicheseCacheCancellation(g, use.response);
|
||||
if (
|
||||
use.response.kind === 'richeseBlackMarket' ||
|
||||
(use.response.kind === 'atreidesAuction' && g.richeseAuction)
|
||||
@@ -16319,15 +16337,13 @@ function resolveAmbassadorEffect(g: Game) {
|
||||
return;
|
||||
}
|
||||
if (effect === 'tleilaxu') {
|
||||
// Printed grant: "Revive one of your leaders or up to 4 of your forces for
|
||||
// free." Only the physical force alternative is source-supported here; the
|
||||
// leader alternative, ordinary allowance accounting and Tleilaxu income for
|
||||
// this distinct event stay gated pending the recorded interpretations.
|
||||
// Initial leader path: first-death own discs, independent of ordinary quota.
|
||||
// Repeat-death and shared-disc interactions remain explicit refinement work.
|
||||
entry.stage = 'revival';
|
||||
g.decision = { kind: 'ecazAmbassador', player: p.id };
|
||||
log(
|
||||
g,
|
||||
`${p.name} may return up to four physical forces from the Tanks for free through the Tleilaxu Ambassador. Ordinary revival allowances and income are unchanged.`,
|
||||
`${p.name} may revive one first-death own leader or return up to four physical forces for free through the Tleilaxu Ambassador. Ordinary revival allowances and income are unchanged.`,
|
||||
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' },
|
||||
);
|
||||
return;
|
||||
@@ -16468,9 +16484,9 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
|
||||
if (entry.stage === 'revival') {
|
||||
requireRule(
|
||||
Object.keys(action).every((key) =>
|
||||
['type', 'event', 'decline', 'forces', 'elite'].includes(key),
|
||||
['type', 'event', 'decline', 'forces', 'elite', 'leader'].includes(key),
|
||||
),
|
||||
'Choose a physical force return or decline it.',
|
||||
'Choose one leader, a physical force return, or decline.',
|
||||
);
|
||||
if (action.decline === true) {
|
||||
log(
|
||||
@@ -16481,6 +16497,20 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
|
||||
finishAmbassador(g);
|
||||
return;
|
||||
}
|
||||
if (action.leader !== undefined) {
|
||||
requireRule(action.forces === undefined && action.elite === undefined,
|
||||
'Choose a leader or forces, not both.');
|
||||
const leader = p.leaders.find(l => l.id === action.leader &&
|
||||
l.faction === p.faction && l.dead && l.deaths === 1 &&
|
||||
!l.capturedBy && !l.gholaBy);
|
||||
requireRule(leader, 'Choose a first-death own leader from the Tanks.');
|
||||
leader.dead = false;
|
||||
delete leader.usedAt;
|
||||
log(g, `${p.name} revived ${leader.name} for free through the Tleilaxu Ambassador. The ordinary leader allowance is unchanged.`,
|
||||
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' });
|
||||
finishAmbassador(g);
|
||||
return;
|
||||
}
|
||||
const forces = integer(action.forces, 1, 4, 'Forces to return');
|
||||
const elite =
|
||||
action.elite === undefined
|
||||
@@ -23946,12 +23976,13 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
|
||||
if (pending.movement && g.pendingChoamMove) resumeChoamMovement(g);
|
||||
if (pending.mentat) g.decision = { kind: 'choamMentat', player: choam.id };
|
||||
} else if (response.kind === 'richeseAuction') {
|
||||
const round = g.richeseBidding!;
|
||||
const round = validateRicheseCacheCancellation(g, response);
|
||||
if (canceled) {
|
||||
// User ruling 7 October 2026: a canceled or prevented compulsory cache
|
||||
// auction restores one ordinary lot, since no cache lot replaces it.
|
||||
// Restore the frozen count, retaining the independent Black Market deduction.
|
||||
// Older saved declarations lack the additive before-cache field.
|
||||
round.normalCount = round.normalCountBeforeCache ??
|
||||
Math.max(0, round.normalCount! + Number(richeseCacheRuns(g)));
|
||||
round.cacheCanceled = true;
|
||||
round.normalCount = Math.max(0, (round.normalCount ?? 0) + 1);
|
||||
log(
|
||||
g,
|
||||
'The compulsory Richese cache auction was canceled; one ordinary lot is restored.',
|
||||
@@ -31279,8 +31310,6 @@ function applyActionInner(
|
||||
if (combined) {
|
||||
requireRule(ecazOccupyCompositionSupported(g),
|
||||
'Combined Occupy requires source-selected native factions; optional overlays remain gated.');
|
||||
requireRule(g.advanced || fighterCount(ecaz!, choice.territory) % 2 === 0,
|
||||
'Odd-force Basic Occupy awaits the preserved publisher casualty-rounding ruling.');
|
||||
}
|
||||
const battleEvent = crypto.randomUUID();
|
||||
g.battle = {
|
||||
@@ -32073,7 +32102,9 @@ export function viewGame(state: Game, id: string) {
|
||||
tanks: me.tanks,
|
||||
eliteTanks: me.elites?.tanks ?? 0,
|
||||
eliteRevived: me.elites?.revived ?? 0,
|
||||
leaderAlternative: false,
|
||||
leaders: me.leaders.filter(l => l.faction === me.faction &&
|
||||
l.dead && l.deaths === 1 && !l.capturedBy && !l.gholaBy)
|
||||
.map(l => ({ id: l.id, name: l.name, strength: l.strength })),
|
||||
blocked:
|
||||
ambassadorEffectBlock(
|
||||
g,
|
||||
|
||||
Reference in new issue
Block a user