Implement mixed Planetologist advisor gather continuation
This commit is contained in:
1 parent
bc37cd38fe
commit
c265f0638e
16 files changed
+512
-171
No files matched your search
+5
-8
@@ -97,7 +97,7 @@ import {
|
||||
fremenReserveEntry,
|
||||
guildTransportCost,
|
||||
} from './bot-mobility';
|
||||
import { planetologistMovementModeSupported, planetologistLeader } from './planetologist-movement';
|
||||
import { planetologistMovementModeSupported, planetologistLeader, planetologistGatherAdvisorEntry } from './planetologist-movement';
|
||||
import { quoteSandmasterStronghold, sandmasterDefaultChoice, sandmasterDestinationCollection } from './sandmaster-movement';
|
||||
import { sandmasterWormCollection } from './sandmaster-worm';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
@@ -3681,13 +3681,7 @@ function policyActions(g: GameView): Action[] {
|
||||
b = gatherSources[second],
|
||||
aTerritory = splitLocation(a.from).territory,
|
||||
bTerritory = splitLocation(b.from).territory;
|
||||
if (
|
||||
aTerritory === bTerritory ||
|
||||
(g.advanced &&
|
||||
me.faction === 'beneGesserit' &&
|
||||
isAdvisor(me, aTerritory) !== isAdvisor(me, bTerritory))
|
||||
)
|
||||
continue;
|
||||
if (aTerritory === bTerritory) continue;
|
||||
for (const to of usefulTargets
|
||||
.filter(
|
||||
(target) =>
|
||||
@@ -3709,10 +3703,13 @@ function policyActions(g: GameView): Action[] {
|
||||
),
|
||||
)
|
||||
.slice(0, 24)) {
|
||||
const entry = planetologistGatherAdvisorEntry(g, me, [aTerritory, bTerritory], to.t);
|
||||
if (entry.blocked) continue;
|
||||
groundMoves.push({
|
||||
action: {
|
||||
type: 'move',
|
||||
planetologist: 'gather',
|
||||
...(entry.requiresFlip ? { fighters: true } : {}),
|
||||
forces: { [a.from]: a.amount, [b.from]: b.amount },
|
||||
...(me.elites
|
||||
? {
|
||||
|
||||
+49
-25
@@ -18,7 +18,7 @@ import { quoteTleilaxuAmbassadorForces, TleilaxuAmbassadorForceError } from './t
|
||||
import { tleilaxuAmbassadorLeaderChoices, tleilaxuAmbassadorLeaderSelection } from './tleilaxu-ambassador-leaders';
|
||||
import { leaderSkillStrongholdCount, sandmasterVictorySpice, sandmasterVictoryPiles, type SandmasterVictorySpiceOffer } from './leader-skill-battle-board';
|
||||
import { beginRihani, chooseRihaniDraw, finishRihani, validateRihani, type RihaniReceipt, type RihaniSkill } from './rihani-decipherer';
|
||||
import { planetologistMovementModeSupported, planetologistLeader, planetologistRange, selectedOriginElites, groundMovementRange, type PlanetologistMovement } from './planetologist-movement';
|
||||
import { planetologistMovementModeSupported, planetologistLeader, planetologistRange, selectedOriginElites, groundMovementRange, planetologistGatherAdvisorEntry, advisorFlipMovementBlocked, type PlanetologistMovement } from './planetologist-movement';
|
||||
import { quoteSmugglerNoField, smugglerNoFieldModeSupported, type SmugglerNoFieldCompanion } from './smuggler-no-field';
|
||||
import { quoteSmugglerShipment, type SmugglerShipment } from './smuggler-shipment';
|
||||
import { createSmugglerBattle, settleSmugglerBattle, smugglerBattleModeSupported, smugglerBattlePlanBlock, smugglerBattlePile, smugglerBattleSignature, smugglerBattleAllocationRequired, chooseSmugglerBattleAllocation, type SmugglerBattleReceipt, type SmugglerBattleCollectionOffer } from './smuggler-battle';
|
||||
@@ -642,6 +642,7 @@ export type Player = {
|
||||
gholaBlocked?: Record<string, number>;
|
||||
ixMovementBlocked?: { turn: number; move: number };
|
||||
fremenMovementBlocked?: { turn: number; move: number };
|
||||
advisorFlipMovementBlocked?: { turn: number; move: number };
|
||||
fremenNexusMovementBlockedTurn?: number;
|
||||
fremenNexusWormBlockedTurn?: number;
|
||||
/** Permanent printed one-card reduction after Atomics; bound to its Aftermath receipt. */
|
||||
@@ -1032,7 +1033,8 @@ export type ResponseWindow = {
|
||||
/** Saved exact ordinary Guild-rate response; not the independent Karama rate. */
|
||||
guildRateEvent?: string;
|
||||
guildTransportEvent?: string;
|
||||
advisorResume?: 'wormRide' | 'declaration' | 'ambassador' | 'discoveryEntry';
|
||||
advisorResume?: 'wormRide' | 'declaration' | 'ambassador' | 'discoveryEntry' | 'planetologistGather';
|
||||
advisorGatherMove?: { turn: number; move: number; order: MovementOrder };
|
||||
discoveryEntry?: DiscoveryEntryArrivalChild;
|
||||
advisorAmbassadorEvent?: string;
|
||||
advisorRemaining?: string[];
|
||||
@@ -8481,7 +8483,8 @@ function requireFreshSetup(g: Game, allowIxElites = false) {
|
||||
!p.specialKaramaUsed && p.kwisatz === undefined && p.charityTurn === undefined &&
|
||||
p.noField === undefined && p.noFieldEvent === undefined && p.noFieldBlockedTurn === undefined &&
|
||||
p.advisors === undefined && p.gholaBlocked === undefined && p.ixMovementBlocked === undefined &&
|
||||
p.fremenMovementBlocked === undefined && p.fremenNexusMovementBlockedTurn === undefined &&
|
||||
p.fremenMovementBlocked === undefined && p.advisorFlipMovementBlocked === undefined &&
|
||||
p.fremenNexusMovementBlockedTurn === undefined &&
|
||||
p.fremenNexusWormBlockedTurn === undefined &&
|
||||
p.atomicsHandLimitPenalty === undefined && p.tupileHandSlot === undefined && p.faceDancers === undefined &&
|
||||
p.faceDancerReplacedTurn === undefined && p.revealedTraitors === undefined;
|
||||
@@ -15623,20 +15626,22 @@ function movementOrigins(move: Pick<MovementOrder, 'origin' | 'origins'>): strin
|
||||
return move.origins ?? [move.origin];
|
||||
}
|
||||
function movementStance(g: Game, p: Player, origins: string[], to: string, fighters: boolean) {
|
||||
requireRule(!g.advanced || p.faction !== 'beneGesserit' ||
|
||||
at(p, to) > 0 || !g.players.some(other => other.id !== p.id && at(other, to)) ||
|
||||
origins.every(origin => isAdvisor(p, origin) === isAdvisor(p, origins[0])),
|
||||
'Gathering advisors and fighters into an enemy-only territory still needs the combined flip response. Choose sources with the same stance or a destination with a determined stance.');
|
||||
const advisors = arrivalAsAdvisor(g, p, to, origins[0]);
|
||||
const gather = planetologistGatherAdvisorEntry(g, p, origins, to);
|
||||
requireRule(!gather.blocked, gather.blocked ?? 'The mixed gather cannot flip its advisors.');
|
||||
requireRule(!gather.requiresFlip || fighters,
|
||||
'Declare a common fighter arrival for a mixed advisor/fighter gather.');
|
||||
const advisors = !gather.requiresFlip && arrivalAsAdvisor(g, p, to, origins[0]);
|
||||
const lockedTurn = Math.max(0, ...origins.map(origin => p.advisors?.[origin]?.lockedTurn ?? 0)) || undefined;
|
||||
requireRule(advisors || lockedTurn !== g.turn ||
|
||||
!g.players.some(other => other.id !== p.id && at(other, to)),
|
||||
'New advisors cannot become fighters this turn.');
|
||||
const wantsFighters = advisors && fighters;
|
||||
requireRule(!wantsFighters || !advisorFlipMovementBlocked(g, p),
|
||||
'Advisor flipping was canceled for this movement. Choose a legal replacement without flipping advisors.');
|
||||
requireRule(!wantsFighters || (lockedTurn !== g.turn && !at(p, to) &&
|
||||
(!(p.ally && at(getPlayer(g, p.ally), to)) || sharesEcazOccupation(g, p, to))),
|
||||
'These advisors cannot flip to fighters on arrival.');
|
||||
return {advisors, lockedTurn, wantsFighters};
|
||||
return {advisors, lockedTurn, wantsFighters, advisorGather: gather.requiresFlip};
|
||||
}
|
||||
function validateFlightSelection(g: Game, p: Player, move: MovementOrder) {
|
||||
if (move.discoveryFlight)
|
||||
@@ -15772,7 +15777,8 @@ function validateMovementOrder(g: Game, p: Player, move: MovementOrder) {
|
||||
'The declared force group no longer matches its movement.',
|
||||
);
|
||||
if (move.planetologist) {
|
||||
const stance = movementStance(g, p, selected.origins, move.to, move.wantsFighters);
|
||||
const stance = movementStance(g, p, selected.origins, move.to, move.wantsFighters ||
|
||||
planetologistGatherAdvisorEntry(g, p, selected.origins, move.to).requiresFlip);
|
||||
requireRule(stance.advisors === move.advisors && stance.wantsFighters === move.wantsFighters &&
|
||||
stance.lockedTurn === move.lockedTurn, 'The declared Planetologist arrival stance is no longer current.');
|
||||
}
|
||||
@@ -19452,7 +19458,7 @@ function beginAdvisorBattle(g: Game, remaining = advisorBattleOptions(g)) {
|
||||
function finishAdvisorReaction(
|
||||
g: Game,
|
||||
details: {
|
||||
advisorResume?: 'wormRide' | 'declaration' | 'ambassador' | 'discoveryEntry';
|
||||
advisorResume?: ResponseWindow['advisorResume'];
|
||||
discoveryEntry?: DiscoveryEntryArrivalChild;
|
||||
advisorAmbassadorEvent?: string;
|
||||
advisorRemaining?: string[];
|
||||
@@ -26271,19 +26277,31 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
response.charityHomeworld ?? 0,
|
||||
);
|
||||
} else if (response.kind === 'advisorFlip') {
|
||||
if (canceled) movementCancellationQuote(g, response);
|
||||
const bg = getPlayer(g, response.owner),
|
||||
to = response.location!;
|
||||
if (!canceled) {
|
||||
bg.advisors ??= {};
|
||||
if (response.advisors) bg.advisors[to] = {};
|
||||
else delete bg.advisors[to];
|
||||
log(
|
||||
g,
|
||||
`${bg.name} changed all forces in ${territory(to).name} to ${response.advisors ? 'advisors' : 'fighters'}.`,
|
||||
);
|
||||
const bg = getPlayer(g, response.owner);
|
||||
if (response.advisorResume === 'planetologistGather') {
|
||||
movementCancellationQuote(g, response);
|
||||
const pending = response.advisorGatherMove!;
|
||||
if (canceled) {
|
||||
bg.advisorFlipMovementBlocked = { turn: pending.turn, move: pending.move };
|
||||
log(g, `${bg.name}'s mixed advisor/fighter gather was canceled before movement. Both groups remain in place and no movement was spent; advisor flipping is blocked for this movement opportunity.`);
|
||||
} else {
|
||||
validateMovementOrder(g, bg, pending.order);
|
||||
completeMove(g, pending.order);
|
||||
}
|
||||
} else {
|
||||
if (canceled) movementCancellationQuote(g, response);
|
||||
const to = response.location!;
|
||||
if (!canceled) {
|
||||
bg.advisors ??= {};
|
||||
if (response.advisors) bg.advisors[to] = {};
|
||||
else delete bg.advisors[to];
|
||||
log(
|
||||
g,
|
||||
`${bg.name} changed all forces in ${territory(to).name} to ${response.advisors ? 'advisors' : 'fighters'}.`,
|
||||
);
|
||||
}
|
||||
finishAdvisorReaction(g, response);
|
||||
}
|
||||
finishAdvisorReaction(g, response);
|
||||
} else if (response.kind === 'capture') {
|
||||
const capture = g.pendingCapture!;
|
||||
g.pendingCapture = null;
|
||||
@@ -33676,7 +33694,7 @@ function applyActionInner(
|
||||
} = forceGroup(g, p, action, planetologist?.mode === 'gather' ? 2 : 1);
|
||||
const to = stringField(action.territory),
|
||||
s = integer(action.sector, 0, 18, 'Sector');
|
||||
const {advisors, lockedTurn, wantsFighters} = movementStance(g, p, origins, to, action.fighters === true);
|
||||
const {advisors, lockedTurn, wantsFighters, advisorGather} = movementStance(g, p, origins, to, action.fighters === true);
|
||||
requireRule(
|
||||
origins.every(source => !balisetPrevents(g, id, source, to)),
|
||||
'Baliset prevents movement into this CHOAM territory; shipment remains possible.',
|
||||
@@ -33784,7 +33802,12 @@ function applyActionInner(
|
||||
: `That destination is blocked or more than ${speed} territories away.`,
|
||||
);
|
||||
validateFlightSelection(g, p, move);
|
||||
if (
|
||||
if (advisorGather) {
|
||||
g.response = {
|
||||
kind: 'advisorFlip', owner: id, location: to, advisors: false, amount: n, passed: [],
|
||||
advisorResume: 'planetologistGather', advisorGatherMove: { turn: g.turn, move: p.moved, order: move },
|
||||
};
|
||||
} else if (
|
||||
!move.ornithopterRange && !move.discoveryFlight &&
|
||||
p.faction === 'ixians' &&
|
||||
elite > 0 &&
|
||||
@@ -35167,6 +35190,7 @@ export function viewGame(state: Game, id: string) {
|
||||
ixMovementBlocked:
|
||||
p.ixMovementBlocked?.turn === g.turn &&
|
||||
p.ixMovementBlocked.move === p.moved,
|
||||
advisorFlipMovementBlocked: advisorFlipMovementBlocked(g, p),
|
||||
revealedTraitors: p.revealedTraitors ?? [],
|
||||
revealedFaceDancers: (p.faceDancers ?? [])
|
||||
.filter((c) => c.revealed)
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
MOBILE_STRONGHOLD,
|
||||
} from './board';
|
||||
import { presenceAt } from './force-presence';
|
||||
import { planetologistLeader } from './planetologist-movement';
|
||||
import { planetologistLeader, planetologistGatherAdvisorEntry } from './planetologist-movement';
|
||||
import { validateAmbassadors } from './ecaz-ambassadors';
|
||||
import {
|
||||
homeworldMobileStrongholdMovementBlock,
|
||||
@@ -53,7 +53,8 @@ export type MovementCancellationQuote =
|
||||
| 'advisorBattle'
|
||||
| 'movementTurn'
|
||||
| 'discoveryEntry'
|
||||
| 'ambassador';
|
||||
| 'ambassador'
|
||||
| 'planetologistGather';
|
||||
};
|
||||
|
||||
function sectorKey(g: Game, value: unknown): value is string {
|
||||
@@ -87,6 +88,7 @@ function movementDeclaration(
|
||||
g: Game,
|
||||
response: ResponseWindow,
|
||||
move: Move | null | undefined,
|
||||
territoryOnly = false,
|
||||
) {
|
||||
const owner = g.players.find((p) => p.id === response.owner)!;
|
||||
requireContext(
|
||||
@@ -97,7 +99,7 @@ function movementDeclaration(
|
||||
move.player === owner.id &&
|
||||
sectorKey(g, location(move.to, move.sector)) &&
|
||||
territoryId(g, move.origin) &&
|
||||
response.location === location(move.to, move.sector) &&
|
||||
response.location === (territoryOnly ? move.to : location(move.to, move.sector)) &&
|
||||
response.amount === move.total &&
|
||||
integer(move.total) &&
|
||||
move.total > 0 &&
|
||||
@@ -421,9 +423,28 @@ export function quoteMovementCancellation(
|
||||
response.advisorResume === 'declaration' ||
|
||||
response.advisorResume === 'wormRide' ||
|
||||
response.advisorResume === 'discoveryEntry' ||
|
||||
response.advisorResume === 'planetologistGather' ||
|
||||
response.advisorResume === 'ambassador'),
|
||||
'This advisor cancellation has no valid owner, territory or continuation.',
|
||||
);
|
||||
if (response.advisorResume === 'planetologistGather') {
|
||||
const pending = response.advisorGatherMove;
|
||||
requireContext(pending && record(pending) && pending.turn === g.turn &&
|
||||
pending.move === owner.moved && pending.order?.planetologist?.mode === 'gather' &&
|
||||
response.advisors === false && pending.order.advisors === false &&
|
||||
pending.order.wantsFighters === false && response.advisorFollowup === undefined &&
|
||||
response.advisorRemaining === undefined && response.advisorAmbassadorEvent === undefined &&
|
||||
response.discoveryEntry === undefined,
|
||||
'The mixed advisor gather has lost its original movement and flip declaration.');
|
||||
movementDeclaration(g, response, pending.order, true);
|
||||
const entry = planetologistGatherAdvisorEntry(g, owner, pending.order.origins!, pending.order.to);
|
||||
requireContext(entry.requiresFlip && !entry.blocked &&
|
||||
pending.order.group.every(([key, count]) => count <= (owner.forces[key] ?? 0)),
|
||||
entry.blocked ?? 'The mixed advisor gather no longer matches its original physical groups.');
|
||||
return { kind: 'advisorFlip', successor: 'planetologistGather' };
|
||||
}
|
||||
requireContext(response.advisorGatherMove === undefined,
|
||||
'Only the declared Planetologist gather may carry its original movement.');
|
||||
if (response.advisorResume === 'discoveryEntry') {
|
||||
requireContext(g.phase === 0 && g.discoveryEnabled && g.discoveries && g.discoveryEntry &&
|
||||
response.discoveryEntry && response.advisors === true &&
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
import { nativeExpansionLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import type { Game, GameView } from './engine';
|
||||
import type { Game, GameView, Player } from './engine';
|
||||
import { splitLocation } from './board';
|
||||
import { isAdvisor } from './advisors';
|
||||
import { presenceAt, type ForcePresence } from './force-presence';
|
||||
|
||||
type MovementPlayer = Pick<Player, 'id' | 'faction' | 'advisors'> & ForcePresence & {
|
||||
moved?: number;
|
||||
advisorFlipMovementBlocked?: boolean | { turn: number; move: number };
|
||||
};
|
||||
type MovementGame = Pick<Game, 'advanced' | 'turn'> & {
|
||||
players: readonly (Pick<Player, 'id'> & ForcePresence)[];
|
||||
};
|
||||
type AdvisorGatherEntry = Readonly<{ requiresFlip: boolean; blocked: string | null }>;
|
||||
const noAdvisorGather: AdvisorGatherEntry = { requiresFlip: false, blocked: null };
|
||||
const fighterAdvisorGather: AdvisorGatherEntry = { requiresFlip: true, blocked: null };
|
||||
const freshAdvisorGather: AdvisorGatherEntry = {
|
||||
requiresFlip: true, blocked: 'Fresh spiritual advisors cannot become fighters during this turn.',
|
||||
};
|
||||
const canceledAdvisorGather: AdvisorGatherEntry = {
|
||||
requiresFlip: true, blocked: 'Advisor flipping was canceled for this movement. Choose a legal replacement without flipping advisors.',
|
||||
};
|
||||
|
||||
/** The canceled native flip stays unavailable for this movement opportunity. */
|
||||
export function advisorFlipMovementBlocked(game: Pick<Game, 'turn'>, player: MovementPlayer): boolean {
|
||||
const blocked = player.advisorFlipMovementBlocked;
|
||||
return typeof blocked === 'boolean' ? blocked :
|
||||
blocked?.turn === game.turn && blocked.move === player.moved;
|
||||
}
|
||||
|
||||
/** Provisional simultaneous gather: declare one common fighter arrival before
|
||||
* either source moves; native cancellation keeps both original groups in place. */
|
||||
export function planetologistGatherAdvisorEntry(game: MovementGame, player: MovementPlayer,
|
||||
origins: readonly string[], destination: string): AdvisorGatherEntry {
|
||||
if (!game.advanced || player.faction !== 'beneGesserit' || origins.length !== 2 ||
|
||||
isAdvisor(player, origins[0]) === isAdvisor(player, origins[1]) ||
|
||||
presenceAt(player, destination) ||
|
||||
!game.players.some(other => other.id !== player.id && presenceAt(other, destination)))
|
||||
return noAdvisorGather;
|
||||
if (origins.some(origin => player.advisors?.[origin]?.lockedTurn === game.turn))
|
||||
return freshAdvisorGather;
|
||||
return advisorFlipMovementBlocked(game, player) ? canceledAdvisorGather : fighterAdvisorGather;
|
||||
}
|
||||
export type PlanetologistMoveMode = 'range' | 'gather';
|
||||
export type PlanetologistMovement = {
|
||||
leader: string;
|
||||
|
||||
+2
-2
@@ -4944,7 +4944,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Warmaster, Master of Assassins, Swordmaster of Ginaz, Killer Medic and Prana-Bindu Adept are connected. They give another leader +1 for the matching physical card role, or the surviving assigned leader +3. These bonuses do not stack to +4. They change battle strength, not the printed disc value or leader bounty.',
|
||||
'The assigned leader keeps its skill through capture and returns with it if it survives its captive battle. The known skill identifies that captured leader; Harkonnen receives only its lower battle effect. Normal effects remain unavailable to both the captor and original faction while it is captured. A killed skilled leader returns its card to the shuffled skill deck.',
|
||||
'When reviving an own leader while you have no Skill Card, decide whether to draw before seeing two private cards. Once drawn, keep one and assign it to that revived leader. Under the 13 October 2026 ruling the original faction keeps its assignment while its trained leader is captured, so reviving a different own leader is legal but draws no replacement while that card exists.',
|
||||
'Planetologist chooses either one extra movement range (maximum three) or forces from two territories to one destination at normal range. These alternatives do not stack. The actual skilled leader can use an eligible green Special in the weapon slot for +2 if it survives; discard that Special after battle without activating its ordinary effect. Mixed advisor/fighter entry into an enemy-only territory and combined expansion modules remain unfinished.',
|
||||
'Planetologist chooses either one extra movement range (maximum three) or forces from two territories to one destination at normal range. These alternatives do not stack. Provisional mixed BG advisor/fighter gather into an enemy-only territory declares one common fighter arrival before either group moves. Native Karama cancellation keeps both sources and the movement allowance unchanged, and blocks another advisor flip for that movement opportunity; fresh-advisor locks remain. Empty or already BG-occupied destinations keep their original common stance. The actual skilled leader can use an eligible green Special in the weapon slot for +2 if it survives; discard that Special without its ordinary effect. Other combined modules remain unfinished.',
|
||||
'Suk Graduate returns one selected casualty to reserves while its leader stays face up, or lets its surviving battle leader save up to three: one stays in its original sector and the rest return to reserves. The effects do not stack. Rescued counters still count toward Advanced Atreides’ seven battle losses for Kwisatz Haderach; combined modules remain unfinished.',
|
||||
'In the supported classic Nexus/Skills composition, Emperor Cunning counts five actual ordinary counters as temporary Sardaukar. Normal or trained Suk rescue still saves physical ordinary casualties. The original casualty allocation remains pending while the rescue choice is open; physical counters move only when rescue settles. Original support is already paid and is not refunded or charged twice. A normal save returns one to reserves; a trained save keeps one and returns the rest. Actual starred reserves and Tanks remain unchanged.',
|
||||
'A surviving selected Mentat adds two battle strength. A surviving selected Bureaucrat subtracts one from the opponent’s total per distinct stronghold occupied by that opponent; advisors do not count. Printed disc values and bounties stay unchanged.',
|
||||
@@ -4958,7 +4958,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Bureaucrat may redirect two spice when one other player pays at least five to a player other than the skill owner, once per phase. Payer costs stay unchanged; declining preserves later use. Bribes qualify at transfer and remain unavailable until Mentat Pause. Supported ordinary/Richese auctions, Guild shipments and paid Tleilaxu revival have connected paths; a separate free-revival award cannot make a smaller price qualify. Allied payer shares never pool to reach five. Trainer or recipient contributions cannot qualify but do not hide an eligible other payer. Public prompts show gross and recipient, not private shares or which payer qualified. Supported classic and native E1/E2/E3 profiles and Homeworld Skills retain original costs and callbacks. Native Guild/Emperor/revival Karama cancellation resolves before Bureaucrat; Richese retains direct seller credit with no invented cancellation window. Basic has no Black Market; free Karama purchases remain normal-only. By user ruling Bureaucrat takes two off gross before occupied Homeworld allocation. Fresh classic occupation plus Skills now connects this: eight becomes two to Bank, three native and three occupied, with full original payer contributions retained. Decline gives four plus four; native income cancellation preserves the separate occupied share and creates no Bureaucrat offer. The low/occupied Guild two-odd contributor rounding boundary is unchanged. By user ruling Guild Nexus Betrayal also carries Bureaucrat on its holder receipt in the bounded classic Nexus plus Skills composition; the holder receives the fee minus two. Emperor gifts remain excluded by user ruling, without threshold disclosure. Other profiles, overlays and full-mode acceptance remain unfinished.',
|
||||
'Smuggler records existing positive battle-territory piles at public reveal. Provisional modifier policy: cap by copied/printed disc plus declared Leader Skill bonuses and Kwisatz2. Pending spice cannot fund plans. A surviving controller collects even when losing or no forces remain, unless an actual zero-strength weapon effect resolves. Artillery Strike supplies that effect; the ordinary projectile Stunner does not. A sole own Traitor bypasses weapons and retains collection. Partial multi-pile collection asks its owner for exact sectors; all/none and single piles stay automatic. Both the modifier and allocation policies are explicit interpretations, not publisher rulings. Collection precedes Sandmaster; a drained pile receives none. Other module guards remain.',
|
||||
'In classic and supported E1/E2 native Tech composition, actual Smuggler prices and normal Banker qualifying fees remain distinct from phase-end Tech income. Tech piles cannot fund their triggering action; Banker front-shield grants still wait for actual Mentat. Winning physical Suk rescue and Sandmaster board spice precede original played-card cleanup and mandatory winner token reward. One token transfers automatically, several require a selection, and no token skips without an invented decline. A later Face Dancer retains the original winner’s cards, bounty and Tech reward, kills the matching trainer without another bounty and replaces only actual survivors from real sources. Native Tleilaxu-only free revival excludes Axlotl; CHOAM initial income is not Charity. Richese’s free companion does not change its marker price or create the concealed counters early.',
|
||||
'Physical Fremen Ambassador relocation now offers optional Sandmaster destination collection before arrival reactions, without an invented route or ordinary movement use. Same-territory repositioning earns none; concealed relocation and other special movement still retain their own limits. Normal Banker income is mandatory in fresh supported ordinary Skills setups, with provisional original-earned-faction custody and no retrofit of existing games. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz skills follow their bounded contracts; shared-Duke assignment, combined Occupy skills and other combined modules remain unfinished. This prototype does not enable public module starts.',
|
||||
'Physical Fremen Ambassador relocation offers optional Sandmaster destination collection before arrival reactions, without an invented route or ordinary movement use. Same-territory repositioning earns none; concealed relocation and other special movement retain their own limits. Normal Banker income is mandatory in fresh supported ordinary Skills setups, with provisional original-earned-faction custody and no retrofit. Diplomat retreat retains its conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz coalition skills follow their bounded contracts; shared-Duke assignment, exceptional roles and other combined modules remain unfinished. Normal module starts are labelled previews, not complete-mode acceptance.',
|
||||
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
|
||||
],
|
||||
related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'richese-leader-skills', 'ecaz-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'],
|
||||
|
||||
Reference in new issue
Block a user