Connect provisional Basic current-sole Homeworld benefits and repair stale rule guards
This commit is contained in:
1 parent
66e061ccca
commit
b892967eb3
23 files changed
+657
-344
No files matched your search
+14
-1
@@ -3101,13 +3101,26 @@ function policyActions(g: GameView): Action[] {
|
||||
actions.push({ type: 'tleilaxuRevivalLimit', target: player.id });
|
||||
} else if (tleilaxu && !me.leaderRevived) {
|
||||
const request = g.revivalRequests[me.id];
|
||||
// The server re-validates that the requested leader is still a dead,
|
||||
// uncaptured native disc (or a dead Kwisatz Haderach) at acceptance, so
|
||||
// never keep proposing an acceptance the authoritative guard rejects.
|
||||
const requestedLeader = request?.leader === undefined
|
||||
? undefined
|
||||
: me.leaders.find((l) => l.id === request.leader);
|
||||
const stillRevivable = !!request && (request.leader === 'kwisatz'
|
||||
? me.kwisatz?.dead === true
|
||||
: !!requestedLeader?.dead && !requestedLeader.capturedBy);
|
||||
if (
|
||||
request &&
|
||||
!request.declined &&
|
||||
request.price !== null &&
|
||||
request.price <= (me.spice ?? 0)
|
||||
request.price <= (me.spice ?? 0) &&
|
||||
stillRevivable
|
||||
)
|
||||
actions.push({ type: 'acceptLeaderRevival' });
|
||||
else if (request && !stillRevivable)
|
||||
// Clear an unhonourable request so the phase can request again.
|
||||
actions.push({ type: 'cancelLeaderRequest' });
|
||||
if (
|
||||
!request &&
|
||||
me.leaders.filter((l) => l.faction === me.faction && nativeAvailable(l))
|
||||
|
||||
@@ -108,7 +108,7 @@ export function quoteOccupiedTupileAuthority(game: OccupiedTupileContext): Occup
|
||||
if (!game.homeworldTupilePreview || !native) return none;
|
||||
if (!game.homeworldOccupationPreview) return Object.freeze({ ...base, status: 'unknown',
|
||||
lowIntelligence: 'unknown', blocked: 'Tupile slots require the original fresh occupation profile.' });
|
||||
const quote = quoteStableHomeworldOccupation(game, 'tupile');
|
||||
const quote = quoteStableHomeworldOccupation(game, 'tupile', { basic: 'epoch' });
|
||||
const entitlement = quote.entitlement;
|
||||
const blocked = quote.blocked ?? (entitlement?.ally === native.id
|
||||
? 'The native CHOAM cannot ally with its own Homeworld occupier.' : null);
|
||||
|
||||
@@ -21,14 +21,30 @@ export type StableHomeworldOccupationQuote = {
|
||||
blocked: string | null;
|
||||
};
|
||||
type SourceWorld = [string, [string, number, number][]];
|
||||
type HomeworldCard = (typeof HOMEWORLD_CARDS)[number];
|
||||
type HomeworldWorld = ReturnType<typeof homeworldForceGroups>[number];
|
||||
type History = NonNullable<Game['homeworldOccupationHistory']>;
|
||||
type Player = Game['players'][number];
|
||||
|
||||
export type StableHomeworldOccupationOptions = {
|
||||
/** Basic-only policy selector. `currentSole` is the provisional default used
|
||||
* by occupied income, Bidding, defenses and percentage effects. `epoch` keeps
|
||||
* the conservative original-epoch gate for Basic Tupile slot leasing while
|
||||
* the retention/turnover ruling stays open. Advanced ignores this option. */
|
||||
basic?: 'currentSole' | 'epoch';
|
||||
};
|
||||
|
||||
/** Original source history supplies entitlement, never a current-controller
|
||||
* shortcut. Advanced uses the authorized supplied rulebook p.22: qualification
|
||||
* requires sole foreign presence and survives until that occupier leaves.
|
||||
* Basic retains the publisher-only pending lifecycle guards below. */
|
||||
* Basic defaults to a visibly provisional current-sole-controller policy; the
|
||||
* sole-foreign-controller entitlement expires immediately on departure,
|
||||
* contest or native repopulation. Tupile keeps the conservative Basic epoch
|
||||
* gate documented in its own module. Neither mode invents an unobserved event. */
|
||||
export function quoteStableHomeworldOccupation(
|
||||
game: StableHomeworldOccupationContext,
|
||||
cardId: HomeworldId,
|
||||
options: StableHomeworldOccupationOptions = {},
|
||||
): StableHomeworldOccupationQuote {
|
||||
const none: StableHomeworldOccupationQuote = { entitlement: null, blocked: null };
|
||||
if (!game.homeworldOccupationPreview) return none;
|
||||
@@ -69,6 +85,57 @@ export function quoteStableHomeworldOccupation(
|
||||
return { entitlement: { world: world.id, card: cardId, native: native.id, occupier: occupier.id,
|
||||
ally, qualification: retained.event, turn: game.turn, spice: card.occupied.spiceIcons }, blocked: null };
|
||||
}
|
||||
return options.basic === 'epoch'
|
||||
? basicEpochOccupation(game, card, world, native, history)
|
||||
: basicCurrentSoleOccupation(game, card, world, native, history);
|
||||
}
|
||||
|
||||
/** Provisional Basic default: the currently present sole foreign faction
|
||||
* controls the occupied benefit, expiring immediately on departure, contest or
|
||||
* native repopulation. Entitlement still requires an observed qualification;
|
||||
* the quote never manufactures one from a physical write. */
|
||||
function basicCurrentSoleOccupation(
|
||||
game: StableHomeworldOccupationContext,
|
||||
card: HomeworldCard,
|
||||
world: HomeworldWorld,
|
||||
native: Player,
|
||||
history: History,
|
||||
): StableHomeworldOccupationQuote {
|
||||
if (!history.qualifications.some(f => f.world === world.id)) return { entitlement: null, blocked: null };
|
||||
let nativeHigh = false;
|
||||
const foreign: string[] = [];
|
||||
for (const [id, forces] of Object.entries(world.forces)) {
|
||||
const amount = forces.normal + forces.elite;
|
||||
if (id === native.id) {
|
||||
const population = card.reserveType === 'sardaukar' ? forces.elite : amount;
|
||||
if (population >= card.high.reserves.min) nativeHigh = true;
|
||||
} else if (amount > 0) foreign.push(id);
|
||||
}
|
||||
if (nativeHigh || foreign.length !== 1) return { entitlement: null, blocked: null };
|
||||
const qualified = history.qualifications.filter(f => f.world === world.id && f.player === foreign[0]).at(-1);
|
||||
if (!qualified)
|
||||
return { entitlement: null,
|
||||
blocked: 'The original Basic sole-occupation change must be observed before its benefit settles.' };
|
||||
const occupier = game.players.find(p => p.id === foreign[0])!;
|
||||
const ally = occupier.ally && game.players.some(p => p.id === occupier.ally && p.ally === occupier.id)
|
||||
? occupier.ally : null;
|
||||
return { entitlement: { world: world.id, card: card.id, native: native.id, occupier: occupier.id,
|
||||
ally, qualification: qualified.event, turn: game.turn, spice: card.occupied.spiceIcons }, blocked: null };
|
||||
}
|
||||
|
||||
/** Conservative Basic epoch gate retained for Tupile slot leasing while the
|
||||
* retention, composition and turnover ruling is open: the original first
|
||||
* qualifier must be alone and have qualified this turn, and departure, contest
|
||||
* or native repopulation reports an explicit unresolved reason instead of a
|
||||
* silent hand-off. */
|
||||
function basicEpochOccupation(
|
||||
game: StableHomeworldOccupationContext,
|
||||
card: HomeworldCard,
|
||||
world: HomeworldWorld,
|
||||
native: Player,
|
||||
history: History,
|
||||
): StableHomeworldOccupationQuote {
|
||||
const none: StableHomeworldOccupationQuote = { entitlement: null, blocked: null };
|
||||
const first = history.qualifications.find(f => f.world === world.id);
|
||||
if (!first) return none;
|
||||
const blocked = (detail: string): StableHomeworldOccupationQuote => ({ entitlement: null,
|
||||
@@ -115,6 +182,6 @@ export function quoteStableHomeworldOccupation(
|
||||
const occupier = game.players.find(p => p.id === first.player)!;
|
||||
const ally = occupier.ally && game.players.some(p => p.id === occupier.ally && p.ally === occupier.id)
|
||||
? occupier.ally : null;
|
||||
return { entitlement: { world: world.id, card: cardId, native: native.id, occupier: occupier.id,
|
||||
return { entitlement: { world: world.id, card: card.id, native: native.id, occupier: occupier.id,
|
||||
ally, qualification: first.event, turn: game.turn, spice: card.occupied.spiceIcons }, blocked: null };
|
||||
}
|
||||
+1
-1
@@ -3903,7 +3903,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When several Terror tokens share a stronghold, Moritani privately selects one for an eligible entry, then chooses whether to reveal it or offer an alliance. The other tokens remain hidden. The board shows the public stack count; refresh preserves the selected token and original arrival.',
|
||||
'While Junction has high population, Guild may offer another faction transport at half or full price during that faction’s shipment. The recipient can choose exact forces from one territory or Homeworld, including a return to their own world. An allied Homeworld remains forbidden. Half-price totals round up; five forces to a Homeworld cost three spice. The Junction route and offered rate resist Karama.',
|
||||
'Homeworld shipment, battle and occupation have special rules. Occupiers receive the printed Collection income and card effects. An alliance can gain a special Homeworld victory through high-population Ecaz; this is separate from ordinary stronghold victory.',
|
||||
'The fresh local homeworld-occupation profile uses original selected decks and no other modules. Advanced occupation begins with sole foreign presence and its benefits remain until the occupier’s last force leaves, including across contests, native population recovery and later turns. A later sole arrival after departure creates a new source epoch. Basic still guards unresolved lifecycle cases; original history, not a current garrison shortcut, proves entitlement.',
|
||||
'The fresh local homeworld-occupation profile uses original selected decks and no other modules. Advanced occupation begins with sole foreign presence and its benefits remain until the occupier’s last force leaves, including across contests, native population recovery and later turns. A later sole arrival after departure creates a new source epoch. Basic uses a visibly provisional current-sole policy: the present sole foreign faction controls occupied income, Bidding effects, defenses, percentage shares and the Ecaz-victory holding, and that entitlement ends immediately on departure, contested presence or native repopulation. Basic Tupile slot leasing keeps its own conservative original-epoch gate. Recorded history, never a live garrison snapshot, always proves entitlement.',
|
||||
'Printed occupied bank income is collected once and can be immediately allocated to a reciprocal ally. Separate Kaitain and Junction paid-receipt shares and Richese’s actual net sale income use frozen original invoices without charging the payer again. Southern Hemisphere rounds one completed actual native Collection total, including settled shared and occupied-bank credits; it redistributes rather than collecting twice. Ordinary native income cancellation remains distinct from printed occupied effects.',
|
||||
'Proved Wallach IX occupation protects the occupier and ally from native Voice; Tleilax prevents native Face Dancers against them; Grumman prevents revealing Terror on their entry. Occupied Salusa removes Sardaukar advantage without changing starred physical identity. These printed effects cannot be restored or canceled with Karama. Other occupied powers and full Homeworld acceptance remain open.',
|
||||
'Caladan shares the admitted original Bidding inspection with the occupier, not its ally; native inspection can still be prevented independently. Ix gives the occupier the actual private drawn pool and selection, not unrelated setup or Technology. Richese sets its own method/direction before the occupier chooses only the physical cache card; seller, payment and other decisions stay native. Giedi gives one original purchase bonus to the occupier or its reciprocal ally with real receiver capacity, even when Harkonnen’s purchased card fills its hand.',
|
||||
|
||||
Reference in new issue
Block a user