Connect Spice Banker sealed battle spending and saved settlement

Prototype one through three own spice as a separate sealed battle commitment. Reserve funds alongside actual force support, add strength only for the surviving skilled leader, and settle once through existing versioned writes. Apply the documented card/core-rule interpretation: payment survives leader death or loss, except for the sole successful traitor caller. Keep printed disc strength and bounty unchanged. Normal income awaits its deferred-custody ruling; combined modules remain gated.

Validation: 54 focused tests; typecheck, lint, 4490 offline tests, production build and 43 HTTP tests passed. Independent rules, privacy, persistence and UI review completed. SQLite tests cover reserved-spice diversion, concurrent settlement, traitor waiver and legacy plans. Browser exercised a three-spice commitment, pending refresh, actual +3 strength and payment, and restored private cards and the next decision. Two genuine four-profile Basic/Advanced samples completed without rejected candidates but did not use Banker spending.

Following another reported outage, the server remained healthy. Preserved all 461 opening rooms and original seat/recovery rows (495 rooms after QA/HTTP). SQLite integrity and foreign keys passed. A fresh online backup captured all 495 rooms, and browser refresh restored the QA seat. No restart or reset was needed.

Verified source tree: 208bf9324ab994b5e59ed50d78998b4dbf8810b3b6799b1a577ef3b84479335f
This commit is contained in:
Quintine committed 2026-09-13 20:45:42 +10:00
1 parent 9381ef140d
commit 1380271bb6
22 files changed
+1166 -25

No files matched your search

+16
View File
@@ -29,6 +29,7 @@ import {
import { matchingTraitor } from './traitors';
import { auditCount } from './choam-auditor';
import { sukGraduateSkill, type SukGraduateSkill } from './suk-graduate';
import { validateSpiceBankerSpend } from './spice-banker';
import { rihaniVictorySkill, type RihaniSkill } from './rihani-decipherer';
import {
quoteHomeworldBattleRules,
@@ -53,6 +54,7 @@ export class BattleResolutionQuoteError extends Error {
}
}
export type ResolutionPlan = {
bankerSpice?: number;
dial: number;
support: number;
allyPayment?: number;
@@ -136,6 +138,7 @@ export type BattleResolutionQuote = {
effects: ReturnType<typeof strongholdBattleEffects> | null;
explosion: boolean;
payments: BattleSupportPayment[];
spiceBankerPayments?: { player: string; amount: number; waivedByTraitor: boolean }[];
choamIncome: { owner: string; amount: number } | null;
bounty: { player: string; amount: number } | null;
strongholdIncome: { player: string; amount: number }[];
@@ -197,6 +200,12 @@ function validateCombatant(
whole(p.allyPayment ?? 0),
'The revealed battle needs valid force, dial and payment quantities.',
);
try {
validateSpiceBankerSpend(side.leaderSkills ?? [], side.leader && !side.leader.dead ? side.leader.id : undefined,
p.bankerSpice, side.spice, strongholdSupportCost(side.stronghold, p.support) - (p.allyPayment ?? 0));
} catch (error) {
throw new BattleResolutionQuoteError(error instanceof Error ? error.message : 'Invalid Spice Banker commitment.');
}
const selected = [p.weapon, p.defense, p.leader, side.lateDefense].filter(
(value): value is string => value != null,
);
@@ -406,6 +415,10 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
(v) => v.called && !a.plan.kwisatz && v.beneficiary === d.id,
);
const payments: BattleSupportPayment[] = [];
const spiceBankerPayments = [a, d].filter(side => (side.plan.bankerSpice ?? 0) > 0).map(side => {
const waivedByTraitor = side === a ? ac && !dc : dc && !ac;
return { player: side.id, amount: waivedByTraitor ? 0 : side.plan.bankerSpice!, waivedByTraitor };
});
const credits = new Map<string, number>();
if (input.advanced)
for (const [side, free] of [
@@ -524,6 +537,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
weapon: aw,
defense: ad,
skilledLeaderSurvives: !deaths.attacker,
bankerSpice: a.plan.bankerSpice,
}),
defender: leaderSkillBattleBonus({
assignments: d.leaderSkills ?? [],
@@ -531,6 +545,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
weapon: dw,
defense: dd,
skilledLeaderSurvives: !deaths.defender,
bankerSpice: d.plan.bankerSpice,
}),
};
const attackerPenalty = bureaucratBattlePenalty(d.leaderSkills ?? [], d.leader?.id,
@@ -759,6 +774,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
effects,
explosion,
payments,
...(spiceBankerPayments.length ? { spiceBankerPayments } : {}),
choamIncome:
choam && choamAmount > 0
? { owner: choam.id, amount: choamAmount }