Count mobile stronghold passengers for spice collection
This commit is contained in:
1 parent
3edebecc89
commit
7d26929abd
10 files changed
+262
-11
No files matched your search
+6
-5
@@ -12,6 +12,7 @@ import { createVictoryContinuation, validateVictoryContinuation, voteVictoryCont
|
||||
VictoryContinuationError, MAX_VICTORY_EXTRA_STRONGHOLDS, type VictoryContinuation,
|
||||
type VictoryContinuationResume } from './victory-continuation';
|
||||
import { SabotageGiftError, quoteSabotageGift, quoteSabotageGiftDiscard } from './sabotage-gift';
|
||||
import { mobileStrongholdCollectionCapacity } from './mobile-stronghold-collection';
|
||||
import { kullDefaultEligible } from './kull-default-profile';
|
||||
import { advancedKullLobbyEligible } from './kull-advanced-profile';
|
||||
import { SaphoTruthOrderError, quoteSaphoTruthOrder, reorderSaphoTruthOrder, validateSaphoTruthOrder } from './sapho-truth-order';
|
||||
@@ -38076,7 +38077,7 @@ function validateMobileMove(g: Game, p: Player, input: unknown, max: number) {
|
||||
type MobileCollection = { location: string; amount: number };
|
||||
/** Quote each route occurrence against the remaining physical pile after any
|
||||
* Sandmaster collection. Absent selections retain the older automatic route. */
|
||||
function quoteMobileCollections(g: Game, p: Player, route: string[], input: unknown,
|
||||
function quoteMobileCollections(g: Game, route: string[], input: unknown,
|
||||
collect: boolean, skillPiles: readonly string[]): MobileCollection[] | undefined {
|
||||
if (input === undefined) return undefined;
|
||||
requireRule(Array.isArray(input) && input.length === route.length,
|
||||
@@ -38090,7 +38091,7 @@ function quoteMobileCollections(g: Game, p: Player, route: string[], input: unkn
|
||||
const available = Math.max(0, remaining[key] ?? 0);
|
||||
requireRule(Object.keys(item).sort().join(',') === 'amount,location' && item.location === key &&
|
||||
typeof item.amount === 'number' && Number.isSafeInteger(item.amount) && item.amount >= 0 &&
|
||||
item.amount <= (collect ? Math.min(available, at(p, MOBILE_STRONGHOLD) * 2) : 0),
|
||||
item.amount <= (collect ? Math.min(available, mobileStrongholdCollectionCapacity(g.players)) : 0),
|
||||
'Choose an available spice amount within the stronghold forces’ collection limit for each route sector.');
|
||||
remaining[key] = available - item.amount;
|
||||
return { location: key, amount: item.amount };
|
||||
@@ -38108,7 +38109,7 @@ function relocateMobileStronghold(
|
||||
: sandmasterLeader(g, p.id)
|
||||
? Object.values(sandmasterCollectionPiles(g, { interior: move.route })).filter(piles => piles.length === 1).map(piles => piles[0])
|
||||
: [];
|
||||
const collections = quoteMobileCollections(g, p, move.route, move.collections, move.collect, skillPiles);
|
||||
const collections = quoteMobileCollections(g, move.route, move.collections, move.collect, skillPiles);
|
||||
let collected = 0;
|
||||
if (move.collect) {
|
||||
// User ruling 7 October 2026: passengers remaining inside a relocated HMS
|
||||
@@ -39405,7 +39406,7 @@ export function prepareSpecialKaramaIntent(
|
||||
const route = validateMobileMove(g, p, action.route, 2);
|
||||
const sandmasterPiles = nexusRule(() => validateSandmasterStronghold(
|
||||
g, p.id, route, action.sandmasterPiles, action.collect !== false));
|
||||
const collections = quoteMobileCollections(g, p, route, action.collections, action.collect !== false, sandmasterPiles);
|
||||
const collections = quoteMobileCollections(g, route, action.collections, action.collect !== false, sandmasterPiles);
|
||||
return {
|
||||
...base,
|
||||
kind: 'ixians',
|
||||
@@ -42120,7 +42121,7 @@ function applyActionInner(
|
||||
const route = validateMobileMove(g, p, action.route, 3);
|
||||
const sandmasterPiles = nexusRule(() => validateSandmasterStronghold(
|
||||
g, p.id, route, action.sandmasterPiles, action.collect !== false));
|
||||
const collections = quoteMobileCollections(g, p, route, action.collections, action.collect !== false, sandmasterPiles);
|
||||
const collections = quoteMobileCollections(g, route, action.collections, action.collect !== false, sandmasterPiles);
|
||||
g.pendingMobileMove = {
|
||||
player: p.id,
|
||||
route,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { isAdvisor } from './advisors';
|
||||
import { MOBILE_STRONGHOLD, splitLocation } from './board';
|
||||
import type { Player } from './engine';
|
||||
|
||||
/** Only public board presence is needed; concealed token values are never read. */
|
||||
export type MobileCollectionPlayer = Pick<Player, 'id' | 'faction' | 'advisors'> & {
|
||||
forces: Readonly<Record<string, number>>;
|
||||
noField?: {
|
||||
deployed: {
|
||||
controller: string;
|
||||
location: { territory: string; sector: number };
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provisional interpretation: all fighters aboard contribute, including
|
||||
* passengers; advisors do not. A concealed No-Field contributes one force,
|
||||
* with its controller's stance, even though Richese holds the marker record.
|
||||
*/
|
||||
export function mobileStrongholdCollectionCapacity(
|
||||
players: readonly MobileCollectionPlayer[],
|
||||
): number {
|
||||
let fighters = 0;
|
||||
for (const player of players) {
|
||||
if (!isAdvisor(player, MOBILE_STRONGHOLD)) {
|
||||
fighters += Object.entries(player.forces).reduce(
|
||||
(total, [key, count]) =>
|
||||
total + (splitLocation(key).territory === MOBILE_STRONGHOLD ? count : 0),
|
||||
0,
|
||||
);
|
||||
}
|
||||
const marker = player.noField?.deployed;
|
||||
if (marker?.location.territory === MOBILE_STRONGHOLD) {
|
||||
const controller = players.find((seat) => seat.id === marker.controller);
|
||||
if (!controller || !isAdvisor(controller, MOBILE_STRONGHOLD)) fighters++;
|
||||
}
|
||||
}
|
||||
return fighters * 2;
|
||||
}
|
||||
+1
-1
@@ -59,7 +59,7 @@ export const HELP = {
|
||||
'Ixians privately choose a starting card before the rest are shuffled and dealt. Before bidding, inspect one extra card and return one to the top or bottom, then shuffle the auction pool. Advanced Ixians can swap one upcoming card with a hand card before Atreides looks, once per round. An ally may replace the card it just purchased with the deck’s top card. Each bidding advantage has its own Karama response before the effect; canceled inspection reveals nothing.',
|
||||
|
||||
mobileStronghold:
|
||||
'The Hidden Mobile Stronghold is a separate stronghold whose occupants survive storms and worms. It cannot relocate into, out of or through the storm; battles wait while its pointing sector is in storm. After the first storm, Ixians place it at a non-stronghold sector. While Ixian forces occupy it, they may relocate it up to three territories before later storm dials or reveals. Declare the sector route to collect spice along it. Other factions enter by movement through the pointing sector; only Ixians ship directly inside, with Bene Gesserit accompaniment allowed. It counts toward victory for its controller. Further movement and collection interaction audits are pending.',
|
||||
'The Hidden Mobile Stronghold is a separate stronghold whose occupants survive storms and worms. It cannot relocate into, out of or through the storm; battles wait while its pointing sector is in storm. After the first storm, Ixians place it at a non-stronghold sector. While Ixian forces occupy it, they may relocate it up to three territories before later storm dials or reveals. Declare the sector route and select up to two spice per eligible force in the stronghold at each visited pile; Ixians collect it. The preview provisionally counts foreign fighters and a concealed No-Field as one force, but excludes advisors. Other factions enter by movement through the pointing sector; only Ixians ship directly inside, with Bene Gesserit accompaniment allowed. It counts toward victory for its controller. Further movement and collection interaction audits are pending.',
|
||||
|
||||
ixTransport:
|
||||
'A selected cyborg lets its movement group travel two territories; a cyborg left behind does not. Ornithopters allow three. Cyborgs collect three spice each and cost three to revive; suboids collect normally and cost two. Choose either type for the free revival.',
|
||||
|
||||
Reference in new issue
Block a user