Connect physical Jacurutu allocation and cumulative station choices
This commit is contained in:
1 parent
373badd560
commit
554f2bf9e9
22 files changed
+1943
-129
No files matched your search
@@ -24,6 +24,7 @@ const nullableCount = (value: unknown) => value === null || count(value);
|
||||
|
||||
const decisions = {
|
||||
ecazBattleLead: true,
|
||||
jacurutuDial: true,
|
||||
harassWithdraw: true,
|
||||
smugglerCollection: true,
|
||||
sandmasterVictorySpice: true,
|
||||
|
||||
@@ -2682,6 +2682,8 @@ function policyActions(g: GameView): Action[] {
|
||||
if (d.kind === 'discoveryDiscard') return discoveryBotActions(g);
|
||||
if (d.kind === 'discoveryEntry') return discoveryEntryBotActions(g);
|
||||
if (d.kind === 'ecologicalStorm') return discoveryStormActions(g);
|
||||
if (d.kind === 'jacurutuDial')
|
||||
return g.jacurutuDial?.choices.map((_,allocation) => ({type:'decision',event:d.event,allocation})) ?? [];
|
||||
if (d.kind === 'greatMakerVote' || d.kind === 'greatMakerRide') return greatMakerBotActions(g);
|
||||
if (d.kind === 'nexusFremenCunningOffer')
|
||||
return [{ type: 'decision', event: d.event,
|
||||
|
||||
@@ -15,6 +15,25 @@ export type JacurutuTankLosses = Readonly<{
|
||||
elite: number;
|
||||
}>;
|
||||
|
||||
/** One legal physical reading of the revealed opposing dial. */
|
||||
export type JacurutuDialChoice = Readonly<{
|
||||
/** The physical ordinary/elite counters the defeated player committed to the
|
||||
* revealed dial, with the plan's paid support split. */
|
||||
dialed: Casualties;
|
||||
/** Bank spice the winner collects for the counters left undialed. */
|
||||
amount: number;
|
||||
}>;
|
||||
|
||||
/** The defeated player's pre-settlement choice between legal physical readings
|
||||
* of a single revealed dial. */
|
||||
export type JacurutuDialOffer = Readonly<{
|
||||
event: string;
|
||||
/** The defeated player, who owns the physical allocation of their own dial. */
|
||||
owner: string;
|
||||
winner: string;
|
||||
choices: readonly JacurutuDialChoice[];
|
||||
}>;
|
||||
|
||||
export type JacurutuBattleIncomeQuote =
|
||||
| Readonly<{
|
||||
kind: 'income';
|
||||
@@ -29,7 +48,7 @@ export type JacurutuBattleIncomeQuote =
|
||||
winner: string;
|
||||
opponent: string;
|
||||
sentToTanks: JacurutuTankLosses;
|
||||
choices: readonly Readonly<{ dialed: Casualties; amount: number }>[];
|
||||
choices: readonly JacurutuDialChoice[];
|
||||
}>;
|
||||
|
||||
export type JacurutuBattleIncomeInput = Readonly<{
|
||||
@@ -43,6 +62,10 @@ export type JacurutuBattleIncomeInput = Readonly<{
|
||||
/** Actual opposing counters destroyed by this battle, after any future
|
||||
* withdrawal or other non-Tanks movement has been removed from the pool. */
|
||||
sentToTanks: JacurutuTankLosses | null;
|
||||
/** The defeated player's own physical allocation for the revealed dial, once
|
||||
* they have selected one. Absent while the automatic single reading, or a
|
||||
* saved choice, remains pending. */
|
||||
selectedDial?: Casualties | null;
|
||||
}>;
|
||||
|
||||
export class JacurutuBattleIncomeError extends Error {
|
||||
@@ -61,7 +84,10 @@ function whole(value: number): boolean {
|
||||
*
|
||||
* A numeric Battle Wheel dial is not a physical counter count in Advanced
|
||||
* combat. When different legal ordinary/elite dial allocations would change
|
||||
* the income, the quote preserves those choices instead of inventing one.
|
||||
* the income, the quote preserves those choices instead of inventing one. A
|
||||
* supplied `selectedDial` must equal one offered reading exactly, including its
|
||||
* paid support split: the same counters with a different paid distribution
|
||||
* describe a different committed strength.
|
||||
*/
|
||||
export function quoteJacurutuBattleIncome(
|
||||
input: JacurutuBattleIncomeInput,
|
||||
@@ -90,11 +116,45 @@ export function quoteJacurutuBattleIncome(
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'Jacurutu income needs the opposing plan and exact forces sent to the Tanks.',
|
||||
);
|
||||
const dialed = casualtyOptions(
|
||||
const eligible = casualtyOptions(
|
||||
forces,
|
||||
input.opponentDial,
|
||||
input.opponentSupport,
|
||||
).filter(
|
||||
);
|
||||
if (!eligible.length)
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'Jacurutu income has no physical dial allocation matching the opposing plan.',
|
||||
);
|
||||
const selected = input.selectedDial;
|
||||
if (selected != null) {
|
||||
// All four canonical fields must match one offered reading: the same
|
||||
// ordinary/elite counters with the same paid support describe a different
|
||||
// committed strength than a different paid distribution of those counters.
|
||||
const match = eligible.find(
|
||||
(choice) =>
|
||||
choice.normal === selected.normal &&
|
||||
choice.elite === selected.elite &&
|
||||
choice.paidNormal === selected.paidNormal &&
|
||||
choice.paidElite === selected.paidElite,
|
||||
);
|
||||
if (!match)
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'The chosen Jacurutu dial allocation is not a legal physical allocation of the opposing revealed forces, dial and support.',
|
||||
);
|
||||
if (match.normal > sent.normal || match.elite > sent.elite)
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'The chosen Jacurutu dial allocation exceeds the opposing counters actually sent to the Tanks.',
|
||||
);
|
||||
return {
|
||||
kind: 'income',
|
||||
winner: input.winner,
|
||||
opponent: input.opponent,
|
||||
amount: sent.normal + sent.elite - match.normal - match.elite,
|
||||
sentToTanks: { ...sent },
|
||||
dialed: [{ ...match }],
|
||||
};
|
||||
}
|
||||
const dialed = eligible.filter(
|
||||
(choice) => choice.normal <= sent.normal && choice.elite <= sent.elite,
|
||||
);
|
||||
if (!dialed.length)
|
||||
@@ -123,3 +183,13 @@ export function quoteJacurutuBattleIncome(
|
||||
dialed: choices.map((choice) => choice.dialed),
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolves the canonical saved choice index to its exact physical allocation. */
|
||||
export function jacurutuDialSelection(
|
||||
offer: JacurutuDialOffer,
|
||||
allocation: number,
|
||||
): Casualties | null {
|
||||
if (!Number.isSafeInteger(allocation) || allocation < 0) return null;
|
||||
const choice = offer.choices[allocation];
|
||||
return choice ? { ...choice.dialed } : null;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function discoveryStormActions(game: GameView): Action[] {
|
||||
offer.blocked
|
||||
)
|
||||
return [];
|
||||
// Keeping the ordinary movement is the conservative first prototype policy.
|
||||
// Keeping the current cumulative distance is the conservative policy.
|
||||
return [...offer.options]
|
||||
.sort((a, b) => Math.abs(a.delta) - Math.abs(b.delta))
|
||||
.map((option) => ({
|
||||
|
||||
+83
-28
@@ -26,6 +26,10 @@ export type DiscoveryStorm = {
|
||||
delta: -1 | 0 | 1 | null;
|
||||
stage: 'choose' | 'traversal' | 'complete';
|
||||
signature: string;
|
||||
queue?: {
|
||||
order: string[];
|
||||
choices: { owner: string; delta: -1 | 0 | 1 }[];
|
||||
};
|
||||
};
|
||||
export type DiscoveryStormContext = Pick<
|
||||
Game,
|
||||
@@ -37,6 +41,7 @@ export type DiscoveryStormContext = Pick<
|
||||
| 'discoveryEnabled'
|
||||
| 'discoveries'
|
||||
| 'players'
|
||||
| 'order'
|
||||
> & { stormMovementSource?: StormMovementSource };
|
||||
export function stormSourceSignature(
|
||||
source: Omit<StormMovementSource, 'signature'>,
|
||||
@@ -69,7 +74,7 @@ export function validateStormSource(source: StormMovementSource) {
|
||||
export function discoveryStormSignature(
|
||||
frame: Omit<DiscoveryStorm, 'signature'>,
|
||||
) {
|
||||
return JSON.stringify([
|
||||
const values: unknown[] = [
|
||||
frame.event,
|
||||
frame.turn,
|
||||
frame.owner,
|
||||
@@ -78,9 +83,22 @@ export function discoveryStormSignature(
|
||||
frame.base,
|
||||
frame.delta,
|
||||
frame.stage,
|
||||
]);
|
||||
];
|
||||
if (frame.queue) values.push(frame.queue.order, frame.queue.choices);
|
||||
return JSON.stringify(values);
|
||||
}
|
||||
export function discoveryStormOffer(g: DiscoveryStormContext) {
|
||||
export function discoveryStormDistance(frame: DiscoveryStorm): number {
|
||||
return frame.base + (frame.queue
|
||||
? frame.queue.choices.reduce((sum, choice) => sum + choice.delta, 0)
|
||||
: frame.delta ?? 0);
|
||||
}
|
||||
function stationOccupants(g: DiscoveryStormContext): string[] {
|
||||
return g.order.filter((id) => {
|
||||
const player = g.players.find((p) => p.id === id);
|
||||
return player && fighterCount(player, 'ecological-testing-station') > 0;
|
||||
});
|
||||
}
|
||||
export function discoveryStormOffer(g: DiscoveryStormContext, frame?: DiscoveryStorm) {
|
||||
const token =
|
||||
g.discoveryEnabled &&
|
||||
g.discoveries?.tokens.find(
|
||||
@@ -90,11 +108,10 @@ export function discoveryStormOffer(g: DiscoveryStormContext) {
|
||||
t.revealedTurn !== null,
|
||||
);
|
||||
if (!token) return null;
|
||||
const occupants = g.players.filter(
|
||||
(p) => fighterCount(p, 'ecological-testing-station') > 0,
|
||||
);
|
||||
if (occupants.length !== 1) return null;
|
||||
const owner = occupants[0].id,
|
||||
const occupants = stationOccupants(g);
|
||||
if (!occupants.length || occupants.length > 2) return null;
|
||||
if (frame) validateDiscoveryStorm(g, frame);
|
||||
const owner = frame?.owner ?? occupants[0],
|
||||
source = g.stormMovementSource;
|
||||
if (source) validateStormSource(source);
|
||||
let blocked: string | null = null;
|
||||
@@ -105,10 +122,10 @@ export function discoveryStormOffer(g: DiscoveryStormContext) {
|
||||
'This storm was already pending before the station became available. Its original movement is preserved.';
|
||||
else if (source.kind === 'weather')
|
||||
blocked = 'Ecological Testing Station cannot change Weather Control.';
|
||||
else if (source.distance !== g.stormPending)
|
||||
else if (source.distance !== (frame?.base ?? g.stormPending))
|
||||
blocked =
|
||||
'The current storm movement no longer matches this station opportunity.';
|
||||
const base = g.stormPending ?? 0;
|
||||
const base = frame ? discoveryStormDistance(frame) : g.stormPending ?? 0;
|
||||
const options = ([-1, 0, 1] as const)
|
||||
.filter((delta) => base + delta >= 0 && base + delta <= 40)
|
||||
.map((delta) => ({ delta, distance: base + delta }));
|
||||
@@ -121,7 +138,7 @@ export function createDiscoveryStorm(
|
||||
const offer = discoveryStormOffer(g);
|
||||
need(
|
||||
offer && !offer.blocked && typeof event === 'string' && event.length > 0,
|
||||
offer?.blocked ?? 'No sole occupant may adjust this storm.',
|
||||
offer?.blocked ?? 'No station occupant may adjust this storm.',
|
||||
);
|
||||
const frame: DiscoveryStorm = {
|
||||
event,
|
||||
@@ -134,6 +151,8 @@ export function createDiscoveryStorm(
|
||||
stage: 'choose',
|
||||
signature: '',
|
||||
};
|
||||
const occupants = stationOccupants(g);
|
||||
if (occupants.length === 2) frame.queue = { order: occupants, choices: [] };
|
||||
frame.signature = discoveryStormSignature(frame);
|
||||
validateDiscoveryStorm(g, frame);
|
||||
return frame;
|
||||
@@ -144,8 +163,9 @@ export function validateDiscoveryStorm(
|
||||
) {
|
||||
need(
|
||||
record(frame) &&
|
||||
Object.keys(frame).sort().join(',') ===
|
||||
'base,delta,event,from,owner,signature,stage,token,turn' &&
|
||||
Object.keys(frame).sort().join(',') === (frame.queue
|
||||
? 'base,delta,event,from,owner,queue,signature,stage,token,turn'
|
||||
: 'base,delta,event,from,owner,signature,stage,token,turn') &&
|
||||
typeof frame.event === 'string' &&
|
||||
frame.event.length > 0 &&
|
||||
typeof frame.owner === 'string' &&
|
||||
@@ -161,9 +181,40 @@ export function validateDiscoveryStorm(
|
||||
['choose', 'traversal', 'complete'].includes(frame.stage) &&
|
||||
(frame.stage === 'choose'
|
||||
? frame.delta === null
|
||||
: [-1, 0, 1].includes(frame.delta as number) &&
|
||||
frame.base + frame.delta! >= 0 &&
|
||||
frame.base + frame.delta! <= 40) &&
|
||||
: [-1, 0, 1].includes(frame.delta as number)),
|
||||
'The station has lost its signed storm choice.',
|
||||
);
|
||||
if (frame.queue) {
|
||||
const queue = frame.queue;
|
||||
need(
|
||||
record(queue) &&
|
||||
Object.keys(queue).sort().join(',') === 'choices,order' &&
|
||||
Array.isArray(queue.order) &&
|
||||
queue.order.length === 2 &&
|
||||
new Set(queue.order).size === 2 &&
|
||||
queue.order.every((id) => typeof id === 'string' && g.players.some((p) => p.id === id)) &&
|
||||
Array.isArray(queue.choices) &&
|
||||
queue.choices.length <= 2 &&
|
||||
queue.choices.every((choice, index) =>
|
||||
record(choice) &&
|
||||
Object.keys(choice).sort().join(',') === 'delta,owner' &&
|
||||
choice.owner === queue.order[index] &&
|
||||
[-1, 0, 1].includes(choice.delta)) &&
|
||||
frame.owner === queue.order[Math.min(queue.choices.length, 1)] &&
|
||||
(frame.stage === 'choose'
|
||||
? queue.choices.length < 2
|
||||
: queue.choices.length === 2 && frame.delta === queue.choices[1].delta),
|
||||
'The station must retain exactly one ordered choice per original occupant.',
|
||||
);
|
||||
let distance = frame.base;
|
||||
for (const choice of queue.choices) {
|
||||
distance += choice.delta;
|
||||
need(distance >= 0 && distance <= 40, 'Every station choice must retain a legal current distance.');
|
||||
}
|
||||
}
|
||||
need(
|
||||
discoveryStormDistance(frame) >= 0 &&
|
||||
discoveryStormDistance(frame) <= 40 &&
|
||||
frame.signature === discoveryStormSignature(frame),
|
||||
'The station has lost its signed storm choice.',
|
||||
);
|
||||
@@ -197,18 +248,15 @@ export function validateDiscoveryStorm(
|
||||
'The station cannot replace its original storm source or affect Weather Control.',
|
||||
);
|
||||
if (frame.stage === 'choose') {
|
||||
const offer = discoveryStormOffer(g);
|
||||
const occupants = stationOccupants(g);
|
||||
need(
|
||||
offer &&
|
||||
!offer.blocked &&
|
||||
offer.owner === frame.owner &&
|
||||
offer.token === frame.token &&
|
||||
offer.base === frame.base,
|
||||
JSON.stringify(occupants) === JSON.stringify(frame.queue?.order ?? [frame.owner]) &&
|
||||
g.stormPending === discoveryStormDistance(frame),
|
||||
'The station choice no longer belongs to its original occupant and storm.',
|
||||
);
|
||||
} else
|
||||
need(
|
||||
g.stormPending === frame.base + frame.delta!,
|
||||
g.stormPending === discoveryStormDistance(frame),
|
||||
'The adjusted storm must retain the chosen distance.',
|
||||
);
|
||||
}
|
||||
@@ -226,14 +274,21 @@ export function chooseDiscoveryStorm(
|
||||
frame.event === event &&
|
||||
typeof delta === 'number' &&
|
||||
[-1, 0, 1].includes(delta) &&
|
||||
frame.base + delta >= 0 &&
|
||||
frame.base + delta <= 40,
|
||||
discoveryStormDistance(frame) + delta >= 0 &&
|
||||
discoveryStormDistance(frame) + delta <= 40,
|
||||
'Choose the current station opportunity: decrease one, keep the distance, or increase one.',
|
||||
);
|
||||
const next = {
|
||||
const choices = frame.queue
|
||||
? [...frame.queue.choices, { owner, delta: delta as -1 | 0 | 1 }]
|
||||
: null;
|
||||
const more = choices !== null && choices.length < frame.queue!.order.length;
|
||||
const next: DiscoveryStorm = {
|
||||
...frame,
|
||||
delta: delta as -1 | 0 | 1,
|
||||
stage: 'traversal' as const,
|
||||
owner: more ? frame.queue!.order[choices!.length] : frame.owner,
|
||||
event: more ? `${frame.event}:1` : frame.event,
|
||||
delta: more ? null : delta as -1 | 0 | 1,
|
||||
stage: more ? 'choose' : 'traversal',
|
||||
...(choices ? { queue: { order: [...frame.queue!.order], choices } } : {}),
|
||||
signature: '',
|
||||
};
|
||||
next.signature = discoveryStormSignature(next);
|
||||
|
||||
+88
-41
@@ -32,11 +32,11 @@ import { ECAZ_START_FORCES, quoteEcazStartingForces } from './ecaz-setup';
|
||||
import { battleCardSlotEligible, battleCategoryInspectionValue, fixedBattleInspectionMatches, validBattleSlotPair } from './battle-card-slots';
|
||||
import { HARASS_WITHDRAW_CARD, HarassWithdrawError, isHarassWithdraw, quoteHarassWithdraw, defaultHarassWithdrawAllocation, harassWithdrawNeedsAllocation, type HarassWithdrawSelection, type HarassWithdrawContext, type HarassWithdrawPreview } from './harass-withdraw';
|
||||
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
|
||||
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
|
||||
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, discoveryStormDistance, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
|
||||
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
|
||||
import { advanceDiscoveryEntry, beginDiscoveryEntryArrival, createDiscoveryEntryRound, finishDiscoveryEntryArrival, quoteDiscoveryEntry, validateDiscoveryEntryArrivalChild, validateDiscoveryEntryRound, type DiscoveryEntryArrivalChild, type DiscoveryEntryRound, type DiscoveryEntrySelection } from './discovery-entry';
|
||||
import { createDiscoveryState, validateDiscoveryState, placeDiscovery, rememberDiscoveryFace, revealDiscoveryToken, isDiscoverySpiceCardId, isDiscoveryTokenId, DISCOVERY_SPICE_CARDS, DISCOVERY_CARD_PLACEMENTS, DISCOVERY_TOKEN_BY_ID, type DiscoveryState } from './discoveries';
|
||||
import { quoteJacurutuBattleIncome } from './discovery-battle';
|
||||
import { createDiscoveryState, validateDiscoveryState, placeDiscovery, rememberDiscoveryFace, revealDiscoveryToken, isDiscoverySpiceCardId, isDiscoveryTokenId, DISCOVERY_SPICE_CARDS, DISCOVERY_CARD_PLACEMENTS, DISCOVERY_TOKEN_BY_ID, JACURUTU_SIETCH, type DiscoveryState } from './discoveries';
|
||||
import { quoteJacurutuBattleIncome, jacurutuDialSelection, type JacurutuDialOffer } from './discovery-battle';
|
||||
import { greatMakerSignature, greatMakerMajority, validateGreatMaker, type GreatMaker } from './great-maker';
|
||||
import { quoteNexusChoamTrade, validateNexusChoamTrade, nexusChoamTradeSignature, type NexusChoamTrade } from './nexus-choam-trade';
|
||||
import { quoteNexusChoamInspection, sampleNexusChoamInspection, nexusChoamInspectionSignature, type NexusChoamInspectionReceipt } from './nexus-choam-inspection';
|
||||
@@ -695,6 +695,8 @@ export type Battle = {
|
||||
smugglerCollectionVersion?: 1;
|
||||
smugglerCollection?: SmugglerBattleReceipt | null;
|
||||
sandmasterPile?: string;
|
||||
/** The defeated player identifies its revealed physical dial before settlement. */
|
||||
jacurutuDial?: JacurutuDialOffer & { selectedDial?: Casualties; retreat?: DiplomatRetreatSelection };
|
||||
/** New battles allow physical withdrawal choice after reveal, never in the sealed plan. */
|
||||
harassAllocationVersion?: 1;
|
||||
harassAllocationEvent?: string;
|
||||
@@ -955,6 +957,7 @@ export type Decision =
|
||||
| { kind: 'discoveryDiscard'; player: string; event: string }
|
||||
| { kind: 'discoveryEntry'; player: string; event: string }
|
||||
| { kind: 'ecologicalStorm'; player: string; event: string }
|
||||
| { kind: 'jacurutuDial'; player: string; event: string }
|
||||
| { kind: 'greatMakerVote'; player: string; event: string }
|
||||
| { kind: 'greatMakerRide'; player: string; event: string }
|
||||
| { kind: 'wormRide'; player: string; territory: string }
|
||||
@@ -10342,7 +10345,7 @@ function startStormMovement(g: Game, n: number) {
|
||||
const offer = nexusRule(() => discoveryStormOffer(g));
|
||||
if (offer && !offer.blocked) {
|
||||
g.ecologicalStorm = nexusRule(() => createDiscoveryStorm(g,crypto.randomUUID()));
|
||||
g.decision = {kind:'ecologicalStorm',player:offer.owner,event:g.ecologicalStorm.event};
|
||||
g.decision = {kind:'ecologicalStorm',player:g.ecologicalStorm.owner,event:g.ecologicalStorm.event};
|
||||
} else moveStorm(g,n);
|
||||
}
|
||||
function choamStormOptions(g: Game) {
|
||||
@@ -10678,6 +10681,7 @@ function afterWorm(g: Game) {
|
||||
}
|
||||
function discoveryIntegrity(g: Game) {
|
||||
ecologicalStormIntegrity(g);
|
||||
jacurutuDialIntegrity(g);
|
||||
discoveryEntryIntegrity(g);
|
||||
if (!g.discoveryEnabled) {
|
||||
requireRule(!g.discoveries && !g.discoveryStash, 'Discovery custody requires its enabled module.');
|
||||
@@ -10742,7 +10746,7 @@ function ecologicalStormIntegrity(g: Game) {
|
||||
requireRule(source.kind !== 'card' || isStormCardDistance(source.distance),
|
||||
'The revealed Storm Card must retain a canonical distance.');
|
||||
requireRule(g.status === 'playing' && g.phase === 0 && source.turn === g.turn &&
|
||||
g.stormPending === (frame?.stage === 'traversal' ? source.distance+frame.delta! : source.distance),
|
||||
g.stormPending === (frame && frame.stage !== 'complete' ? discoveryStormDistance(frame) : source.distance),
|
||||
'The storm must retain its current turn and original movement source.');
|
||||
}
|
||||
const controls = [g,g.pendingExchange,g.pendingNullentropy?.resume,g.pendingRicheseGift?.resume,
|
||||
@@ -10758,7 +10762,7 @@ function ecologicalStormIntegrity(g: Game) {
|
||||
else {
|
||||
requireRule(!choices.length,'A completed station choice cannot remain open.');
|
||||
if (frame.stage === 'traversal') requireRule(g.stormResolution && g.stormResolution.discoveryStorm === frame.signature &&
|
||||
g.stormResolution.from === frame.from && g.stormResolution.distance === frame.base+frame.delta!,
|
||||
g.stormResolution.from === frame.from && g.stormResolution.distance === discoveryStormDistance(frame),
|
||||
'The storm traversal has lost its exact station adjustment.');
|
||||
else requireRule(!g.stormResolution?.discoveryStorm,'The completed station choice cannot retain a traversal.');
|
||||
}
|
||||
@@ -22055,6 +22059,51 @@ function advanceBattleOutcome(g: Game) {
|
||||
log(g, `${getPlayer(g, offer.player).name} may retreat surviving undialed forces with Diplomat before battle losses.`,
|
||||
{ faction: getPlayer(g, offer.player).faction, name: 'Diplomat retreat' });
|
||||
}
|
||||
function currentJacurutuIncome(g: Game, quote: ReturnType<typeof currentBattleResolutionQuote>, retreat?: DiplomatRetreatSelection) {
|
||||
const b = g.battle!;
|
||||
if (b.territory !== JACURUTU_SIETCH || !g.discoveryEnabled || !quote.winner ||
|
||||
!g.discoveries?.tokens.some(token => token.face === 'jacurutu-sietch' &&
|
||||
token.status === 'placed' && token.revealedTurn !== null)) return null;
|
||||
const opponent = quote.winner
|
||||
? getPlayer(g, quote.winner === b.attacker ? b.defender : b.attacker) : null;
|
||||
const forces = opponent
|
||||
? combatForces(g, opponent, b.territory, getPlayer(g, opponent.id === b.attacker ? b.defender : b.attacker)) : null;
|
||||
const plan = opponent ? b.plans[opponent.id] : null;
|
||||
const withdrawal = quote.harassWithdraw?.find(entry => entry.player === opponent?.id);
|
||||
const withdrawalDial = withdrawal && forces && plan
|
||||
? casualtyOptions(forces,plan.dial,plan.support).find(choice =>
|
||||
choice.normal === withdrawal.remaining.normal && choice.elite === withdrawal.remaining.elite)
|
||||
: undefined;
|
||||
requireRule(!withdrawal || withdrawalDial, 'Harass & Withdraw must retain its original physical dial.');
|
||||
return nexusRule(() => quoteJacurutuBattleIncome({
|
||||
territory: b.territory,
|
||||
jacurutuRevealed: true,
|
||||
winner: quote.winner,
|
||||
opponent: opponent?.id ?? null,
|
||||
opponentForces: forces,
|
||||
opponentDial: plan?.dial ?? null,
|
||||
opponentSupport: plan?.support ?? null,
|
||||
sentToTanks: forces ? {
|
||||
normal: forces.normal - (withdrawal?.returned.normal ?? 0) - (retreat?.normal ?? 0),
|
||||
elite: forces.elite - (withdrawal?.returned.elite ?? 0) - (retreat?.elite ?? 0),
|
||||
} : null,
|
||||
selectedDial: b.jacurutuDial?.selectedDial ?? withdrawalDial,
|
||||
}));
|
||||
}
|
||||
function jacurutuDialIntegrity(g: Game) {
|
||||
const frame = g.battle?.jacurutuDial;
|
||||
const decisions = homeworldSavedDecisions(g).filter(decision => decision.kind === 'jacurutuDial');
|
||||
if (!frame && !decisions.length) return;
|
||||
requireRule(frame && g.battle?.revealed && g.phase === 6 && frame.event.length > 0 &&
|
||||
!frame.selectedDial && decisions.length === 1 &&
|
||||
decisions[0].player === frame.owner && decisions[0].event === frame.event &&
|
||||
!!g.battle.diplomatRetreat === !!frame.retreat,
|
||||
'The Jacurutu dial choice must retain its original revealed battle and defeated owner.');
|
||||
const income = currentJacurutuIncome(g, currentBattleResolutionQuote(g), frame.retreat);
|
||||
requireRule(income?.kind === 'allocationRequired' && income.opponent === frame.owner &&
|
||||
income.winner === frame.winner && JSON.stringify(income.choices) === JSON.stringify(frame.choices),
|
||||
'The Jacurutu dial choice changed its original counters, support or Tanks losses.');
|
||||
}
|
||||
function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
|
||||
const b = g.battle!;
|
||||
requireRule(!!b.diplomatRetreat === !!retreat, 'Resolve the pending Diplomat retreat before battle losses.');
|
||||
@@ -22078,29 +22127,18 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
|
||||
? d
|
||||
: a
|
||||
: null;
|
||||
const losingForces = losingPlayer
|
||||
? combatForces(g, losingPlayer, b.territory, losingPlayer === a ? d : a)
|
||||
: null;
|
||||
const losingPlan = losingPlayer ? b.plans[losingPlayer.id] : null;
|
||||
const jacurutuIncome = quoteJacurutuBattleIncome({
|
||||
territory: b.territory,
|
||||
jacurutuRevealed:
|
||||
g.discoveryEnabled === true &&
|
||||
!!g.discoveries?.tokens.some(
|
||||
(token) =>
|
||||
token.face === 'jacurutu-sietch' &&
|
||||
token.status === 'placed' &&
|
||||
token.revealedTurn !== null,
|
||||
),
|
||||
winner: quote.winner,
|
||||
opponent: losingPlayer?.id ?? null,
|
||||
opponentForces: losingForces,
|
||||
opponentDial: losingPlan?.dial ?? null,
|
||||
opponentSupport: losingPlan?.support ?? null,
|
||||
sentToTanks: losingForces
|
||||
? { normal: losingForces.normal, elite: losingForces.elite }
|
||||
: null,
|
||||
});
|
||||
const jacurutuIncome = currentJacurutuIncome(g, quote, retreat);
|
||||
if (jacurutuIncome?.kind === 'allocationRequired') {
|
||||
b.jacurutuDial ??= { event: crypto.randomUUID(), owner: jacurutuIncome.opponent,
|
||||
winner: jacurutuIncome.winner, choices: jacurutuIncome.choices, ...(retreat ? {retreat} : {}) };
|
||||
g.decision = {kind:'jacurutuDial',player:b.jacurutuDial.owner,event:b.jacurutuDial.event};
|
||||
return;
|
||||
}
|
||||
if (b.jacurutuDial) {
|
||||
requireRule(b.jacurutuDial.selectedDial && jacurutuIncome?.kind === 'income',
|
||||
'Commit the original physical Jacurutu dial before settling its battle.');
|
||||
delete b.jacurutuDial;
|
||||
}
|
||||
for (const revelation of quote.revelations) {
|
||||
const holder = getPlayer(g, revelation.player),
|
||||
identity = revelation.identity;
|
||||
@@ -22308,14 +22346,6 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
|
||||
`${winner!.name} gained ${jacurutuIncome.amount} spice from the bank at Jacurutu Sietch for ${jacurutuIncome.amount} opposing undialed force${jacurutuIncome.amount === 1 ? '' : 's'} sent to the Tanks.`,
|
||||
{ faction: winner!.faction, name: 'Jacurutu Sietch income' },
|
||||
);
|
||||
} else if (jacurutuIncome?.kind === 'allocationRequired') {
|
||||
const amounts = [
|
||||
...new Set(jacurutuIncome.choices.map((choice) => choice.amount)),
|
||||
].sort((left, right) => left - right);
|
||||
log(
|
||||
g,
|
||||
`Jacurutu Sietch income remains unpaid: ${losingPlayer!.name}'s revealed dial and support permit ${amounts.join(' or ')} undialed physical forces among the counters sent to the Tanks. The physical dial allocation needs a player choice.`,
|
||||
);
|
||||
}
|
||||
for (const income of quote.strongholdIncome) {
|
||||
const player = getPlayer(g, income.player);
|
||||
@@ -29911,6 +29941,18 @@ function applyActionInner(
|
||||
if (destination === null)
|
||||
log(g, `${p.name} declined the Diplomat retreat.`, { faction: p.faction, name: 'Diplomat retreat' });
|
||||
resolveBattle(g, { destination: destination as string | null, normal: normal as number, elite: elite as number });
|
||||
} else if (decision.kind === 'jacurutuDial') {
|
||||
const frame = g.battle?.jacurutuDial;
|
||||
requireRule(frame && frame.owner === id && frame.event === decision.event && action.event === decision.event &&
|
||||
Object.keys(action).every(key => ['type','event','allocation'].includes(key)) &&
|
||||
typeof action.allocation === 'number' && Number.isSafeInteger(action.allocation) &&
|
||||
action.allocation >= 0 && action.allocation < frame.choices.length && !frame.selectedDial,
|
||||
'Identify the physical counters represented by your current revealed Jacurutu dial.');
|
||||
const chosen = frame.choices[action.allocation];
|
||||
frame.selectedDial = jacurutuDialSelection(frame,action.allocation)!;
|
||||
log(g, `${p.name} identified ${chosen.dialed.normal} normal and ${chosen.dialed.elite} elite physical counters represented by their Jacurutu dial; the battle winner receives ${chosen.amount} spice for opposing undialed counters actually sent to the Tanks.`,
|
||||
{faction:p.faction,name:'Jacurutu physical dial'});
|
||||
resolveBattle(g, frame.retreat);
|
||||
} else if (decision.kind === 'smugglerCollection') {
|
||||
const receipt = g.battle?.smugglerCollection;
|
||||
requireRule(receipt?.event === decision.event && receipt.player === id &&
|
||||
@@ -30347,9 +30389,11 @@ function applyActionInner(
|
||||
requireRule(g.ecologicalStorm && Object.keys(action).every(key => ['type','event','delta'].includes(key)),
|
||||
'Choose only the current station storm adjustment.');
|
||||
g.ecologicalStorm = nexusRule(() => chooseDiscoveryStorm(g,g.ecologicalStorm!,p.id,action.event,action.delta));
|
||||
g.stormPending = g.ecologicalStorm.base+g.ecologicalStorm.delta!;
|
||||
log(g, `${p.name} ${g.ecologicalStorm.delta === 0 ? 'kept' : g.ecologicalStorm.delta === 1 ? 'increased' : 'decreased'} storm movement ${g.ecologicalStorm.delta === 0 ? 'at' : 'by one, to'} ${g.stormPending} sectors using Ecological Testing Station.`, {faction:p.faction,name:'Ecological Testing Station'});
|
||||
moveStorm(g,g.stormPending);
|
||||
g.stormPending = nexusRule(() => discoveryStormDistance(g.ecologicalStorm!));
|
||||
log(g, `${p.name} ${action.delta === 0 ? 'kept' : action.delta === 1 ? 'increased' : 'decreased'} storm movement ${action.delta === 0 ? 'at' : 'by one, to'} ${g.stormPending} sectors using Ecological Testing Station.`, {faction:p.faction,name:'Ecological Testing Station'});
|
||||
if (g.ecologicalStorm.stage === 'choose')
|
||||
g.decision = {kind:'ecologicalStorm',player:g.ecologicalStorm.owner,event:g.ecologicalStorm.event};
|
||||
else moveStorm(g,g.stormPending);
|
||||
} else if (decision.kind === 'greatMakerVote') {
|
||||
decideGreatMakerVote(g,p,action);
|
||||
} else if (decision.kind === 'greatMakerRide') {
|
||||
@@ -33000,8 +33044,11 @@ export function viewGame(state: Game, id: string) {
|
||||
ecazOccupyPreview: !!g.ecazOccupyPreview,
|
||||
strongholdCards: g.strongholdCards ?? null,
|
||||
discoveries: discoveryChoices(g,id,g.phase === 7 && grummanCollectionAutomatic(g)),
|
||||
jacurutuDial: g.decision?.kind === 'jacurutuDial' && g.decision.player === id && g.battle?.jacurutuDial
|
||||
? {event:g.battle.jacurutuDial.event,owner:g.battle.jacurutuDial.owner,winner:g.battle.jacurutuDial.winner,
|
||||
choices:structuredClone(g.battle.jacurutuDial.choices)} : null,
|
||||
ecologicalStorm: g.decision?.kind === 'ecologicalStorm' && g.decision.player === id && g.ecologicalStorm?.stage === 'choose'
|
||||
? {...nexusRule(() => discoveryStormOffer(g))!,event:g.ecologicalStorm.event} : null,
|
||||
? {...nexusRule(() => discoveryStormOffer(g,g.ecologicalStorm))!,event:g.ecologicalStorm.event} : null,
|
||||
discoveryEntry: g.discoveryEntry?.stage === 'choose' && g.decision?.kind === 'discoveryEntry' && g.decision.player === id
|
||||
? {...nexusRule(() => quoteDiscoveryEntry({...g,discoveries:g.discoveries!},g.discoveryEntry!,id)),event:g.decision.event} : null,
|
||||
greatMaker: g.greatMaker ? { event:g.greatMaker.event, turn:g.greatMaker.turn, stage:g.greatMaker.stage, votes:structuredClone(g.greatMaker.votes), order:[...g.greatMaker.order], ride:greatMakerRideOptions(g,id) } : null,
|
||||
|
||||
+9
-8
@@ -4227,8 +4227,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Native typed forces enter as their actual ordinary and elite groups, never as passengers from the mobile stronghold or the concealed value of a No-Field. Richese may ship or move a concealed marker into an actually revealed nested location at sector zero; it remains one effective presence until its original reveal materializes available reserves. CHOAM Card Stash uses its five-card limit, drawing first and choosing any held discard afterward.',
|
||||
'At the start of the next turn, before the storm and mobile stronghold movement, each eligible faction may move any positive subset of its non-advisor groups from the surrounding territory into the newly revealed location. The signed choice preserves ordinary and elite forces across sectors, normal source storm and occupancy restrictions, and costs no spice, shipment or normal movement. The printed text gives no sequence and does not restate the source-storm restriction; processing tokens in reveal order and factions in storm order while excluding storm sources are source inferences.',
|
||||
'A sole Cistern occupant receives two bank spice during Collection. In Advanced, a sole Orgiz occupant takes one collected spice per territory where an unambiguous rival collected board spice. Multiple collected deposits in that territory do not add transfers. Basic retains its provisional one-per-positive-deposit interpretation. Contested ownership, different or shared payers leave only that uncertain transfer unpaid; ordinary Collection continues.',
|
||||
'Jacurutu automatically pays one bank spice for each opposing undialed physical force sent to the Tanks. Where Advanced normal/elite allocations would produce different rewards, this prototype records the unpaid gap pending a ruling; it never guesses from the numeric wheel alone.',
|
||||
'A sole non-advisor occupant of Ecological Testing Station may decrease ordinary storm movement by one, keep it, or increase it by one. Choose after the ordinary storm-card window, before movement and storm protection. Weather Control cannot be changed. Two-occupant adjustment order remains unresolved. An already saved pending storm without its original movement source keeps its distance until a new storm is produced.',
|
||||
'Jacurutu pays one bank spice for each opposing undialed physical force actually sent to the Tanks. Where an Advanced dial admits different physical counts, the defeated player chooses an exact ordinary/elite/paid reading before support, casualties or rewards settle; unique readings remain automatic. This choice timing is a visibly provisional implementation interpretation. Original Harass returns bind their physical dial and actual withdrawal/retreat counters do not earn spice.',
|
||||
'Each non-advisor Testing Station occupant may decrease ordinary storm movement by one, keep it or increase it by one. The provisional order is the original storm order, once per owner, with cumulative adjustments on current distance. Choose after the ordinary card window; traversal/protection starts only after all choices. Weather Control is unaffected. Original natural source/base and existing sole-frame history remain authoritative; legacy movement without a source is not invented.',
|
||||
'A current non-advisor Shrine occupant may play a physical Truthtrance card as Karama, or a physical Karama as Truthtrance, within the implemented timing and effect paths. The printed identity and actual card custody remain unchanged; a committed Truthtrance conversion survives departure. This does not create missing Karama powers or settle their pending rules.',
|
||||
'All four AI profiles use the same offered inspection, reveal, stash-discard, Great Maker, free-entry, Ornithopter and Testing Station choices as the player controls. Private knowledge and unfinished decisions survive saved JSON continuations. These focused paths do not establish complete variant compliance or combined-module acceptance.',
|
||||
],
|
||||
@@ -4236,28 +4236,29 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
checklist: [
|
||||
{
|
||||
area: 'Implementation', status: 'Partial',
|
||||
detail: 'Original classic and supported paired E1/E2 or standalone E3 Skills/Nexus/Discovery, and separate paired E1/E2 no-Skills Nexus/Discovery, retain selected Tech/Advanced Strongholds alongside existing original classic/native Discovery entries. Actual Maker votes/typed rides and settled-alliance closing deals, one-invoice signed markers, training, rescue, Cyborg substitution and cleanup/Tech/Face Dance use original consumers. Held benefits stay source-local; mandatory Ecaz coalition keeps owner-labelled armies. Smuggler policies use projected authoritative support. Advanced Orgiz aggregates actual territory Collection; Basic stays provisional per deposit. Pending sources and public starts remain guarded.',
|
||||
detail: 'Original classic and supported native Skills/Nexus/Discovery entries retain their original selected modules, Maker votes/typed rides, source-local held benefits, training and owner-labelled coalition aftermath. Mixed Jacurutu now pauses for an exact defeated-player physical reading before settlement, excluding actual returns from Tanks income. Testing Station uses a signed cumulative storm-order owner queue before one traversal; original source and sole-frame history remain. Advanced Orgiz aggregates territory Collection; Basic stays provisional. Other pending sources and public starts remain guarded.',
|
||||
evidence: ['game/discovery-module-profile.ts', 'game/richese-no-field.ts', 'game/discoveries.ts', 'game/discovery-actions.ts', 'game/discovery-entry.ts', 'game/discovery-collection.ts', 'game/discovery-flight.ts', 'game/discovery-battle.ts', 'game/discovery-storm.ts', 'game/shrine.ts', 'game/great-maker.ts', 'game/board.ts', 'game/engine.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Player controls', status: 'Partial',
|
||||
detail: 'Private backs and readable authorized faces, optional inspection/reveal, owned stash discard with card inspection, ordered Great Maker controls, multi-sector free entry and carried-Ornithopter movement use shared table controls. Testing Station offers three legal distances and Shrine uses existing card controls. Unambiguous Jacurutu and bounded Orgiz income are automatic and logged; unresolved rewards have no invented choice.',
|
||||
evidence: ['components/discoveries.tsx', 'components/discovery-entry.tsx', 'components/discovery-flight-movement.tsx', 'components/discovery-storm.tsx', 'components/truthtrance.tsx', 'components/great-maker.tsx', 'components/spice-card-inspector.tsx', 'components/game-table.tsx'],
|
||||
detail: 'Private backs and readable authorized faces, optional inspection/reveal, owned stash discard, ordered Great Maker, free entry and carried movement use existing controls. Jacurutu offers exact physical readings only to the defeated owner before settlement. Testing Station offers cumulative legal distances to each current owner in original storm order. Unique rewards remain automatic; Shrine uses original card controls.',
|
||||
evidence: ['components/discoveries.tsx', 'components/jacurutu-dial.tsx', 'components/discovery-entry.tsx', 'components/discovery-flight-movement.tsx', 'components/discovery-storm.tsx', 'components/truthtrance.tsx', 'components/great-maker.tsx', 'components/spice-card-inspector.tsx', 'components/game-table.tsx'],
|
||||
},
|
||||
{
|
||||
area: 'AI', status: 'Partial',
|
||||
detail: 'Four profiles share the private quoted token, discard, vote, reserve-ride, free-entry, carried-Ornithopter and conservative Testing Station actions. Shrine uses the existing legal Karama and Truthtrance policies. Complete Discovery strategy, combined games and strength calibration remain unfinished.',
|
||||
detail: 'Four profiles share private quoted inspection, discard, vote, ride, entry and carried-flight actions. Properly configured policies select a legal Jacurutu physical reading and each current Testing Station owner uses the same cumulative controls. Shrine uses original Karama/Truthtrance policies. Strategy, combined games and calibration remain unfinished.',
|
||||
evidence: ['game/discovery-options.ts', 'game/discovery-entry-options.ts', 'game/discovery-flight-options.ts', 'game/discovery-storm-options.ts', 'game/shrine.ts', 'game/great-maker-options.ts', 'game/bots.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Documentation', status: 'Partial',
|
||||
detail: 'Sourced identities and native/classic-skill boundaries are recorded. Advanced Orgiz follows the authorized territory-Collection trigger; Basic keeps its provisional deposit mapping. Great Maker first-turn/Sandtrout, exhausted supply, entry order and source-storm handling remain explicit inferences. Contested Cistern/Testing Station/Orgiz, mixed-force Jacurutu and shared-lot theft are unresolved.',
|
||||
detail: 'Sourced identities and native/classic-skill boundaries are recorded. Advanced Orgiz follows territory Collection; Basic remains provisional per deposit. Recommended defeated-player physical Jacurutu allocation and cumulative storm-order Testing Station choices are explicit, visible prototype interpretations, not new publisher rulings. Great Maker, entry and source-storm inferences remain; contested Cistern/Orgiz and shared-lot theft stay open.',
|
||||
evidence: ['docs/DISCOVERY_COMPONENTS.md', 'docs/DISCOVERY_PROTOTYPE.md'],
|
||||
},
|
||||
{
|
||||
area: 'Verification', status: 'Partial',
|
||||
detail: 'Mixed E1/E2 and paired E3 preserves Discovery7/8/all12 Nexus, optional all14/HW/Tech/Advanced Strongholds. New31 physical programmes plus2 stock regressions; final489/489 affected checks across42 files pass with types/lint. Across64 original families,258/288 games finish338931/339426 actions and9013 JSON across three frozen trees and preserved continuations. Native Ecaz assassination, actual separate Loyalty stock and existing selected overlap order are repaired without new printed rulings. Actual original Maker votes/both piles and native card draws retain physical sources; no human nested entry or winning Suk is invented. Source guards and full acceptance remain open.',
|
||||
detail: 'Current recommended Jacurutu/Testing Station batch passes104/104 in12 related files, types/lint and build. Genuine Advanced dial2/support1 produces1-or0 reward with the same original losses, support and later Collection; unique readings stay automatic. Original Basic/Advanced station sources, real alliance/garrisons and390px first/second cumulative choices preserve one final traversal, source and Weather exclusion. All four legal policies are exercised. Earlier native-family evidence remains source-bound; broader combinations, assurance, strategy and deployed acceptance stay open.',
|
||||
evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/homeworld-paired-nexus-runtime.test.ts', 'tests/homeworld-standalone-nexus-runtime.test.ts',
|
||||
'tests/fixture-jacurutu-mixed-dial.ts', 'tests/jacurutu-mixed-dial-runtime.test.ts', 'tests/fixture-testing-station-contest.ts', 'tests/testing-station-contest-runtime.test.ts',
|
||||
'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/homeworld-native-e1e2-modules-runtime.test.ts', 'tests/homeworld-native-e3-modules-runtime.test.ts', 'tests/homeworld-classic-discovery-runtime.test.ts', 'tests/homeworld-skills-discovery-runtime.test.ts', 'tests/homeworld-optional-modules-runtime.test.ts', 'tests/discovery-native-e1-skills-runtime.test.ts', 'tests/discovery-native-e2-skills-runtime.test.ts', 'tests/discovery-native-e3-skills-runtime.test.ts', 'tests/discovery-skills-strongholds-runtime.test.ts', 'tests/discovery-native-strongholds-runtime.test.ts', 'tests/ecaz-stronghold-runtime.test.ts', 'tests/e3-native-tech-runtime.test.ts', 'tests/discovery-classic-nexus-lifecycle.test.ts', 'tests/discovery-classic-nexus-effects.test.ts', 'tests/discovery-orgiz-advanced-runtime.test.ts', 'tests/discovery-classic-skills-runtime.test.ts', 'tests/discovery-module-profile.test.ts', 'tests/discovery-native-typed-runtime.test.ts', 'tests/discovery-native-marker-runtime.test.ts', 'tests/discovery-native-e3-runtime.test.ts', 'tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts', 'docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RICHESE_CARD_DEFINITIONS } from './richese-cards';
|
||||
// are envelope checks, not a duplicate implementation of each future choice.
|
||||
const DECISIONS = {
|
||||
ecazBattleLead: true,
|
||||
jacurutuDial: true,
|
||||
homeworldOccupiedIncome: true,
|
||||
homeworldOccupiedPercentage: true,
|
||||
homeworldOccupiedBonus: true,
|
||||
|
||||
Reference in new issue
Block a user