Route Emperor Face Dance returns to typed native reserves

This commit is contained in:
Quintine committed 2026-10-26 09:19:54 +11:00
1 parent 4ee51917bc
commit 2c3dde51d4
14 files changed
+1346 -98

No files matched your search

+15 -21
View File
@@ -105,7 +105,7 @@ import { createHomeworldOccupationHistory, observeHomeworldOccupation, validateH
import { tupileIntelligenceTargets, quoteTupileIntelligenceRequest, type TupileIntelligenceCategory } from './tupile-intelligence';
import { quoteTupileIntelligenceAnswer } from './tupile-intelligence-answer';
import { createTupileIntelligenceState, appendTupileIntelligenceObservation, validateTupileIntelligenceState, type TupileIntelligenceState } from './tupile-intelligence-state';
import { homeworldForceGroups, quoteHomeworldCustody } from './homeworld-custody';
import { homeworldForceGroups } from './homeworld-custody';
import { quoteHomeworldShipment, type HomeworldShipmentIntent } from './homeworld-shipment';
import { quoteGuildHomeworldShipment } from './guild-homeworld-shipment';
import { quoteJunctionTransport } from './junction-transport';
@@ -9311,8 +9311,7 @@ function faceDanceReturnBlock(g: Game, winner: string): string | null {
const quote = homeworldRule(() => quoteOccupiedHomeworldFaceDancer(g, target));
if (quote.blocked) return quote.blocked;
}
return g.advanced && g.homeworlds && getPlayer(g, winner).faction === 'emperor'
? 'Face Dance returning the Emperor’s army awaits the Kaitain/Salusa reserve-placement ruling.' : null;
return null;
}
function homeworldSubstitutionIntegrity(g: Game) {
const pending = g.pendingIxSubstitution;
@@ -36150,7 +36149,11 @@ function applyActionInner(
requireRule(home || validGameLocation(g, decision.territory, sector), 'Choose a sector in the battle territory.');
// This is replacement, not shipment or movement: no transport cost or worm/storm transit.
for (const source of selected) {
if (source.key === 'reserves') { if (!home) p.reserves -= source.count; }
if (source.key === 'reserves') {
// Native reserves already at the Homeworld stay there. An Arrakis
// replacement withdraws the actual stock before placing it.
if (!home) withdrawNativeReserves(g, p, source.count, 0);
}
else {
p.forces[source.key] -= source.count;
if (!p.forces[source.key]) delete p.forces[source.key];
@@ -36163,30 +36166,21 @@ function applyActionInner(
: winningArmies.map(owner => [owner.id, at(owner, decision.territory)] as const))
.filter(([, count]) => count > 0));
if (home) {
const destination = `homeworld:${winner.faction}`;
const external = selected.filter((source) => source.key !== 'reserves').reduce((n, source) => n + source.count, 0);
const quote = quoteHomeworldCustody(homeworldContext(g), g.homeworlds!.custody!, [
{ homeworld: home.id, player: winner.id, withdraw: army, deposit: {normal: 0, elite: 0} },
{ homeworld: destination, player: winner.id, withdraw: {normal: 0, elite: 0}, deposit: army },
{ homeworld: home.id, player: p.id, withdraw: {normal: 0, elite: 0}, deposit: {normal: external, elite: 0} },
]);
g.homeworlds!.custody = quote.state;
for (const update of quote.players) {
const owner = getPlayer(g, update.id);
owner.reserves = update.reserves;
if (owner.elites) owner.elites.reserves = update.eliteReserves;
}
// Selected provisional composition: return each physical type to its
// native stock (Emperor normal→Kaitain, Sardaukar→Salusa). This is not
// revival and does not restore a historical shipment origin.
addNativeReserves(g, winner, army.normal + army.elite, army.elite, home.id);
if (external) addNativeReserves(g, p, external, 0);
} else {
for (const owner of winningArmies)
for (const key of Object.keys(owner.forces).filter(
(key) => splitLocation(key).territory === decision.territory,
)) {
owner.reserves += owner.forces[key];
if (owner.elites) {
owner.elites.reserves += owner.elites.forces[key] ?? 0;
delete owner.elites.forces[key];
}
const amount = owner.forces[key], elite = owner.elites?.forces[key] ?? 0;
if (owner.elites) delete owner.elites.forces[key];
delete owner.forces[key];
addNativeReserves(g, owner, amount, elite);
}
}
const leader = g.players
+2 -1
View File
@@ -164,7 +164,8 @@ function nativeReserveDeposits(
/** Authorized native-reserve deposits: E3 p.10 restores Advanced Emperor's
* normal forces to Kaitain and Sardaukar to Salusa. Used by revival and by the
* Nexus Secret Ally reserve return (user ruling 7 October 2026); it does not
* Nexus Secret Ally reserve return (user ruling 7 October 2026) and selected
* provisional Face Dance return composition (26 October 2026); it does not
* validate Tanks, cost, revival allowances or route permission. */
export function quoteNativeReserveDeposit(
context: HomeworldCustodyContext,
+13
View File
@@ -2224,12 +2224,25 @@ export const RULE_TOPICS: RuleTopic[] = [
'Tleilaxu take no ordinary traitors. Once everyone else has selected their traitors, shuffle the unused cards and deal Tleilaxu three private Face Dancers.',
'After another faction wins, that winner resolves losses, leader bounties, card choices and tech-token rewards. Tleilaxu then have a separate opportunity to reveal an unrevealed Face Dancer matching the winning leader.',
'Return the winner’s surviving forces to reserves, then replace up to that number with Tleilaxu forces from reserves or other board locations. The winning leader goes to the tanks with no additional bounty. A leader already killed does not die again.',
'With Advanced Homeworlds, the Emperor’s normal survivors provisionally return to Kaitain and Sardaukar to Salusa Secundus, preserving their exact types and existing casualties. This selected reserve-return composition is not revival and grants no revival income, fee or second reward. The original victory remains the winner’s.',
'Arrakis replacements withdraw actual Tleilaxu native stocks. On Tleilax, existing native forces remain and selecting those reserves adds no counters; only selected external Arrakis replacements increase that pool. Occupied-Tleilax protection for its occupier or ally still blocks a reveal. Zero replacements or decline remain available.',
'A winning Cheap Hero may be a Face Dancer. Kwisatz Haderach does not protect an accompanying leader against Face Dancing. Karama cannot cancel the reveal or the replacement army.',
'Keep revealed Face Dancers face up until all three have been used. Shuffle those cards back into the unused deck and draw three new ones. During Mentat Pause, you may instead replace one unrevealed Face Dancer; this once-per-turn exchange has a Karama response.',
'Zoal has variable battle strength: he copies the opposing leader disc, without any KH bonus, or counts zero against a Cheap Hero. His death bounty uses that copied strength. His ordinary revival value is three spice before discounts.',
'After completed replacement, advisors left alone at remote sources or a zero-replacement target have a separate BG conversion response. Face Dance remains complete and immune; prevention keeps its returns, replacement, leader death and revelation/cycle, then resumes the original aftermath once.',
'The core setup, reveal sequence, card cycling, Zoal effects and AI controls are tested. Tleilaxu revival commerce also has tested support. Foreign ghola acquisition and buyback controls are also implemented. Their special Karama now also has tested support; its timing and the complete expansion interaction audit are still unfinished, so starts remain labelled previews.',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Advanced Emperor typed returns use native reserve quotes from Arrakis and visitor Homeworlds. Actual replacement stocks, existing immunity, original victory and single leader death remain. Broader combinations stay open.' },
{ area: 'Player controls', status: 'Partial', detail: 'Persistent provisional routing, public survivor maximum and own selectable sources feed the original choices and guarded reveal/decline callbacks.' },
{ area: 'AI', status: 'Partial', detail: 'All four minimal profiles use the unblocked projected opportunity and legal native replacement sources; no strategic refinement or calibration.' },
{ area: 'Documentation', status: 'Partial', detail: 'Distinguishes selected normal/Kaitain and Sardaukar/Salusa reserve return from revival, historical origin and a confirmed publisher destination ruling.' },
{ area: 'Verification', status: 'Partial', detail: 'Genuine public Advanced shipment/battle sources, labelled controlled Homeworld visitor cases and control supplements cover the route. Full modes, browser/mobile/deployed and final assurance remain due.', evidence: [
'game/homeworld-native-reserves.ts', 'components/face-dance-actions.tsx', 'components/face-dance-decision.tsx',
'tests/fixture-emperor-face-dance-return.ts', 'tests/emperor-face-dance-return-engine.test.ts',
'tests/fixture-emperor-face-dance-homeworld.ts', 'tests/emperor-face-dance-homeworld.test.ts', 'tests/emperor-face-dance-return-controls.test.ts', 'docs/HOMEWORLD_REPLACEMENT_RULES.md',
] },
],
related: [
'battle',
'cheap-hero-traitor',