Connect actual occupied Homeworld Bureaucrat split receipts
This commit is contained in:
1 parent
df9bbc5e04
commit
304849f8d9
18 files changed
+1019
-38
No files matched your search
@@ -31,6 +31,7 @@ export type BureaucratPaymentView = {
|
||||
amount: number;
|
||||
kind: BureaucratPaymentKind;
|
||||
redirect: 2;
|
||||
homeworldAllocation?: true;
|
||||
};
|
||||
usedThisPhase: boolean;
|
||||
};
|
||||
|
||||
+20
-10
@@ -8596,7 +8596,9 @@ function projectedBureaucrat(g: Game,viewer: string): BureaucratPaymentView {
|
||||
![pending.owner,pending.source.payer,pending.source.payee,pending.resume.response?.recipient].includes(viewer);
|
||||
return {pending:pending && !privateRevival ? {event:pending.source.event,owner:pending.owner,
|
||||
payer:pending.source.privateFunding ? null : pending.source.payer,payee:pending.source.payee,
|
||||
amount:pending.source.amount,kind:pending.source.kind,redirect:2} : null,
|
||||
amount:pending.source.amount,kind:pending.source.kind,redirect:2,
|
||||
...(g.homeworlds && (pending.source.kind === 'auction' || pending.source.kind === 'shipment')
|
||||
? { homeworldAllocation: true as const } : {})} : null,
|
||||
usedThisPhase:bureaucratUsed(g.bureaucratPayments?.used ?? [],g.turn,g.phase)};
|
||||
}
|
||||
function actBureaucratPayment(g: Game,action:Action) {
|
||||
@@ -8951,7 +8953,7 @@ export function initializeHomeworldGameForAudit(state: Game): Game {
|
||||
return initializeSetupGameForAudit(state, true);
|
||||
}
|
||||
/** Fresh occupation prototype; ambiguity remains source-gated rather than assigned a controller. */
|
||||
export function initializeHomeworldOccupationGameForAudit(state: Game): Game {
|
||||
export function initializeHomeworldOccupationGameForAudit(state: Game, leaderSkills = false): Game {
|
||||
requireRule(state.homeworlds?.custody === null && !state.homeworldOccupationPreview &&
|
||||
!state.homeworldOccupationHistory && !state.nexusCards && !state.leaderSkills &&
|
||||
!state.techTokens && !state.strongholdCards && !state.discoveryEnabled &&
|
||||
@@ -8964,7 +8966,10 @@ export function initializeHomeworldOccupationGameForAudit(state: Game): Game {
|
||||
'Occupation requires a fresh Homeworld lobby with original selected faction decks and no other modules.');
|
||||
requireFreshBaseRuntime(state);
|
||||
requireFreshFactionInventory(state);
|
||||
const g = initializeSetupGameForAudit(state, true, false, false, false, false, false, true);
|
||||
requireRule(!leaderSkills || !state.expansions.length && homeworldLeaderSkillsProfile(state),
|
||||
'Occupation with Leader Skills requires original classic Homeworlds without other modules.');
|
||||
const g = leaderSkills ? initializeLeaderSkillsGameForAudit(state)
|
||||
: initializeSetupGameForAudit(state, true, false, false, false, false, false, true);
|
||||
g.homeworldOccupationPreview = true;
|
||||
g.pendingHomeworldOccupiedIncome = null;
|
||||
g.homeworldOccupiedIncomeHistory = [];
|
||||
@@ -16493,7 +16498,8 @@ function admitOccupiedPercentage(g: Game, source: OccupiedPercentageSource, alre
|
||||
g.decision = owner ? { kind: 'homeworldOccupiedPercentage', player: owner, event: source.event } : null;
|
||||
return true;
|
||||
}
|
||||
function occupiedPaymentSource(g: Game, response: ResponseWindow, gross: number, canceled: boolean): OccupiedPercentageSource {
|
||||
function occupiedPaymentSource(g: Game, response: ResponseWindow, gross: number, canceled: boolean,
|
||||
diversion = 0): OccupiedPercentageSource {
|
||||
const kind = response.kind === 'guildIncome' ? 'guild-shipping' : 'emperor-treachery', sale = g.currentAuctionSale;
|
||||
const buyer = response.source === 'ambassador' ? g.pendingAmbassador!.purchaseReceipt!.buyer :
|
||||
response.kind === 'richesePurchaseIncome' ? g.pendingRichesePurchaseIncome!.buyer : sale?.winner;
|
||||
@@ -16503,19 +16509,22 @@ function occupiedPaymentSource(g: Game, response: ResponseWindow, gross: number,
|
||||
sale.occupiedContributions!;
|
||||
}
|
||||
const paid = contributions.reduce((sum, leg) => sum + leg.amount, 0);
|
||||
if (paid !== gross) {
|
||||
requireRule(diversion === 0 || diversion === 2 && !canceled && paid === gross + diversion,
|
||||
'The occupied receipt lost its original Bureaucrat diversion.');
|
||||
if (!diversion && paid !== gross) {
|
||||
requireRule(contributions.length === 1 && paid >= gross, 'The routed payment lost its eligible paid contributions.');
|
||||
contributions = [{ ...contributions[0], amount: gross }];
|
||||
}
|
||||
const id = response.occupiedPayment?.event ?? (response.source === 'ambassador' ? g.pendingAmbassador!.event :
|
||||
response.kind === 'richesePurchaseIncome' ? g.pendingRichesePurchaseIncome!.event! : sale!.occupiedEvent!);
|
||||
return { kind, event: crypto.randomUUID(), native: response.owner, amount: gross,
|
||||
payment: { id, binding: occupiedProducerProof(g, kind, response), paid: gross, contributions, recipient: response.owner },
|
||||
payment: { id, binding: occupiedProducerProof(g, kind, response), paid: diversion ? paid : gross,
|
||||
contributions, recipient: response.owner, ...(diversion ? { bankDiversion: 2 as const } : {}) },
|
||||
nativeIncome: canceled ? 'karama-canceled' : 'received' };
|
||||
}
|
||||
function settleOccupiedPayment(g: Game, response: ResponseWindow, gross: number, canceled: boolean,
|
||||
suffix: OccupiedPercentageSuffix): boolean {
|
||||
return !!g.homeworldOccupationPreview && admitOccupiedPercentage(g, occupiedPaymentSource(g, response, gross, canceled), 0, suffix);
|
||||
suffix: OccupiedPercentageSuffix, diversion = 0): boolean {
|
||||
return !!g.homeworldOccupationPreview && admitOccupiedPercentage(g, occupiedPaymentSource(g, response, gross, canceled, diversion), 0, suffix);
|
||||
}
|
||||
function decideOccupiedPercentage(g: Game, p: Player, action: Action) {
|
||||
occupiedPercentageIntegrity(g);
|
||||
@@ -19081,7 +19090,8 @@ function homeworldOccupiedIncomeIntegrity(g: Game) {
|
||||
return;
|
||||
}
|
||||
requireRule(g.homeworldOccupationPreview === true && g.homeworlds && !g.nexusCards &&
|
||||
!g.leaderSkills && !g.techTokens && !g.strongholdCards && !g.discoveryEnabled &&
|
||||
(!g.leaderSkills || !g.expansions.length && homeworldLeaderSkillsProfile(g)) &&
|
||||
!g.techTokens && !g.strongholdCards && !g.discoveryEnabled &&
|
||||
Array.isArray(history) && pending !== undefined,
|
||||
'The fresh occupation profile lost its original Homeworld module or income ledger.');
|
||||
const turns = new Set<number>();
|
||||
@@ -24110,7 +24120,7 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
|
||||
? { kind: 'richesePurchase', resume: g.pendingRichesePurchaseIncome!.resume } : { kind: 'none', decision: g.decision };
|
||||
if (response.kind === 'emperorIncome') awardBankerIncome(g, g.currentAuctionSale?.spiceBankerIncomePayment, canceled);
|
||||
if (response.kind === 'guildIncome') awardBankerIncome(g, response.spiceBankerIncomePayment, canceled);
|
||||
const pending = settleOccupiedPayment(g, response, gross, canceled, suffix);
|
||||
const pending = settleOccupiedPayment(g, response, gross, canceled, suffix, bureaucratDiversion ?? 0);
|
||||
if (!pending) runOccupiedPercentageSuffix(g, suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export type OccupiedPercentagePayment = Readonly<{
|
||||
paid: number;
|
||||
contributions: readonly OccupiedPercentageContribution[];
|
||||
recipient: string;
|
||||
/** Ruled Bureaucrat diversion; original paid amounts and payer shares remain intact. */
|
||||
bankDiversion?: 2;
|
||||
}>;
|
||||
export type OccupiedPercentageSource = Readonly<{
|
||||
event: string;
|
||||
@@ -127,10 +129,14 @@ function freezeSource(source: OccupiedPercentageSource, game: OccupiedPercentage
|
||||
}) });
|
||||
}
|
||||
const richese = source.kind === 'richese-income';
|
||||
const diverted = !!source.payment && Object.hasOwn(source.payment, 'bankDiversion');
|
||||
requireReceipt(keys(source, richese ? ['kind', 'event', 'native', 'amount', 'payment'] :
|
||||
['kind', 'event', 'native', 'amount', 'payment', 'nativeIncome']) &&
|
||||
keys(source.payment, richese ? ['id', 'binding', 'paid', 'contributions', 'recipient', 'received'] :
|
||||
['id', 'binding', 'paid', 'contributions', 'recipient']) && text(source.payment.id) && text(source.payment.binding) &&
|
||||
diverted ? ['id', 'binding', 'paid', 'contributions', 'recipient', 'bankDiversion'] :
|
||||
['id', 'binding', 'paid', 'contributions', 'recipient']) &&
|
||||
(!diverted || !richese && source.payment.bankDiversion === 2 && source.nativeIncome === 'received') &&
|
||||
text(source.payment.id) && text(source.payment.binding) &&
|
||||
whole(source.payment.paid) && source.payment.recipient === source.native && Array.isArray(source.payment.contributions),
|
||||
'payment lost its original receipt, actual native recipient or paid provenance.');
|
||||
const seen: Record<string, true> = Object.create(null);
|
||||
@@ -144,7 +150,8 @@ function freezeSource(source: OccupiedPercentageSource, game: OccupiedPercentage
|
||||
requireReceipt(total(contributions.map(leg => leg.amount)) === source.payment.paid,
|
||||
'original payer contributions do not conserve the paid amount.');
|
||||
const payment = { id: source.payment.id, binding: source.payment.binding, paid: source.payment.paid,
|
||||
contributions: Object.freeze(contributions), recipient: source.payment.recipient };
|
||||
contributions: Object.freeze(contributions), recipient: source.payment.recipient,
|
||||
...(diverted ? { bankDiversion: 2 as const } : {}) };
|
||||
if (source.kind === 'richese-income') {
|
||||
requireReceipt(whole(source.payment.received) && source.payment.received <= source.payment.paid &&
|
||||
source.amount === source.payment.received, 'Richese percentage requires actual net received income, not the gross sale.');
|
||||
@@ -152,7 +159,8 @@ function freezeSource(source: OccupiedPercentageSource, game: OccupiedPercentage
|
||||
}
|
||||
requireReceipt(source.nativeIncome === 'received' || source.nativeIncome === 'karama-canceled', 'invalid native income outcome.');
|
||||
const eligible = contributions.filter(leg => source.kind !== 'guild-shipping' || leg.payer !== source.native);
|
||||
requireReceipt(total(eligible.map(leg => leg.amount)) === source.amount, 'eligible receipt includes excluded or unpaid spice.');
|
||||
requireReceipt(total(eligible.map(leg => leg.amount)) === source.amount + (diverted ? 2 : 0),
|
||||
'eligible receipt includes excluded or unpaid spice.');
|
||||
return Object.freeze({ ...base, kind: source.kind, payment: Object.freeze(payment), nativeIncome: source.nativeIncome });
|
||||
}
|
||||
function amounts(source: OccupiedPercentageSource, nativeLow: boolean, occupied: boolean) {
|
||||
@@ -239,7 +247,7 @@ export function quoteOccupiedPercentageSource(
|
||||
// comparing rounding; no new transaction-versus-contribution policy is invented.
|
||||
if (source.kind === 'guild-shipping' && (payment?.low || stable.entitlement || stable.blocked)) {
|
||||
const eligible = source.payment.contributions.filter(leg => leg.payer !== source.native && leg.amount > 0).map(leg => leg.amount);
|
||||
requireReceipt(quoteGuildPaymentRounding(source.amount, eligible).unambiguous,
|
||||
requireReceipt(quoteGuildPaymentRounding(source.amount + (source.payment.bankDiversion ?? 0), eligible).unambiguous,
|
||||
'multiple-contributor shipping rounding awaits its original ruling.');
|
||||
}
|
||||
const candidate = Math.floor(source.amount / 2);
|
||||
|
||||
+1
-1
@@ -4802,7 +4802,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic or supported native banker-income profiles, including the bounded Advanced Moritani roster; ordinary/default starts are unchanged.',
|
||||
'Diplomat may use one committed Worthless as the opponent’s canonical printed defensive role when you played no actual defense: Shield, Snooper, Shield Snooper, defensive Weirding Way or Chemistry, or sealed Portable Snooper. Hybrid protection, Weirding’s non-Shield behavior and Chemistry’s Poison Tooth exception remain distinct. Copying the used role without its activation or own-slot prerequisite is the explicit first-version interpretation. Choose or decline after reveal, before traitors; the Worthless must be discarded even if you win. Already-open base-only battles keep that source window. A native Diplomat can protect its own selected disc; a captive grants no upper band. A surviving trained loser may retreat selected undialed physical counters up to printed/copied disc strength to one empty adjacent non-stronghold before losses. Late/mirror/numeric modifications remain separate.',
|
||||
'Normal Mentat questions use the user-approved uniform private card-show step before posture and faction powers in supported Leader Skills games. The opponent shows the named held weapon or chooses another held card, even for an exact match or sole eligible card. Weirding Way is a nameable default weapon; Chemistry is not. Cards stay held and need not enter a Battle Plan. New battles activate the question without an extra opt-in; already-opened unversioned battles retain their original flow. Empty hands skip without an answer. Private history records an observation, not a later hand guarantee.',
|
||||
'Bureaucrat may redirect two spice when one other player pays at least five to a player other than the skill owner, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with their remaining share unavailable until Mentat Pause. Supported auctions, Guild income, Richese sales and paid Tleilaxu revival have connected paths; a separate free-revival award does not make a smaller price qualify. Allied payer shares below five each, such as three plus three, never pool to qualify. Supported classic, E1, E2 and E3 games without Homeworlds additionally connect qualifying allied-funded ordinary auctions, Richese cache/Advanced Black Market and shipments, including fully ally-funded five: one optional two-spice diversion from the gross recipient receipt, unchanged full contributor costs. Supported Homeworlds plus Leader Skills games connect the same allied-funded auction and Guild-shipment income. Original Guild/Emperor income cancellation resolves first; Richese proceeds use their original direct seller-credit callback without an invented cancellation window. Basic still has no Black Market and free Karama purchases remain normal-only. A trainer or payee contribution cannot qualify, but does not hide an eligible other payer. The prompt shows public gross and recipient, not private shares or which payer qualified. By user ruling the diversion comes off the gross before any Homeworld occupation split, but that split-recipient composition remains fenced from Leader Skills. By user ruling a Guild Nexus Betrayal receipt still carries Bureaucrat: the holder receives the fee minus two, and the bounded classic Nexus plus Leader Skills Betrayal composition now connects that offer from the invoice’s own payment legs. By user ruling, Emperor gifts are not payments: no Bureaucrat prompt/use or threshold disclosure, and exact gift amounts remain private. Other payment-family/module interactions remain unfinished.',
|
||||
'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 all existing positive battle-territory piles when plans reveal, capped in total by the selected leader’s unmodified strength. A surviving controller collects even when losing or no forces remain. A partial collection across several piles asks that owner for exact sector amounts; all/none and single-pile collection stay automatic. This allocation policy is provisional. Pending spice cannot fund the plan. Collection precedes Sandmaster addition; a drained pile receives none. Modified strength and other combined-module limits remain guarded. Older saved battles retain their recorded behavior.',
|
||||
'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 has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. 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.',
|
||||
|
||||
Reference in new issue
Block a user