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

+53 -3
View File
@@ -147,6 +147,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { BattlePreparation, battlePlanCommitments, bindBattlePlanCommitments, NexusInspectionHistory } from './battle-preparation';
import { PrivateBattlePlan } from './private-battle-plan';
import { SpiceBankerControl, spiceBankerControlState } from './spice-banker';
import { canUsePlanetologistBattleSpecial, validLeaderSkillBattleCardPair } from '../game/leader-skill-combat';
import type { Card } from '../game/cards';
import { RevealedBattle } from './revealed-battle';
@@ -379,6 +380,7 @@ export function GameTable({
)
: 0;
const [support, setSupport] = useState(0);
const [bankerSpice, setBankerSpice] = useState(0);
const strongholdBankSupport =
g.battle?.strongholdEffects[me.id] === 'arrakeen' ? 2 : 0;
const battleSupportMaximum = Math.min(
@@ -413,6 +415,34 @@ export function GameTable({
const [discardCards, setDiscardCards] = useState<Record<string, boolean>>({});
const stormLosses = g.decision?.kind === 'stormLosses' ? g.decision : null;
const committed = battlePlanCommitments(g);
const selectedBattleLeader = committed.leader
? String(committed.leader.value ?? '')
: leader;
const paidBattleSupport = Math.max(0, battleSupport - strongholdBankSupport);
const selectedBattleAllyPayment =
allyPayment === ''
? Math.max(0, paidBattleSupport - (me.spice ?? 0))
: Math.max(
0,
Math.min(
Math.trunc(Number(allyPayment)),
paidBattleSupport,
g.aid.available,
),
);
const ownBattleSupport = Math.max(
0,
paidBattleSupport - selectedBattleAllyPayment,
);
const bankerControl = spiceBankerControlState(
g,
selectedBattleLeader,
ownBattleSupport,
);
const battleBankerSpice = Math.max(
0,
Math.min(Math.trunc(bankerSpice), bankerControl.maximum),
);
const planetologistSpecial = (card: Card) => canUsePlanetologistBattleSpecial({
assignments: g.leaderSkills?.assignments.filter(a => a.controller === me.id) ?? [],
selectedLeader: committed.leader ? String(committed.leader.value ?? '') : leader,
@@ -4239,6 +4269,7 @@ export function GameTable({
fill={(plan) => {
setDial(plan.dial);
setSupport(plan.support);
setBankerSpice(plan.bankerSpice ?? 0);
setAllyPayment(
plan.allyPayment === undefined
? ''
@@ -4450,6 +4481,14 @@ export function GameTable({
))}
</select>
</label>
<SpiceBankerControl
game={g}
selectedLeader={selectedBattleLeader}
ownSupport={ownBattleSupport}
value={battleBankerSpice}
onChange={setBankerSpice}
disabled={busy}
/>
{[
['Weapon', weapon, setWeapon],
['Defense', defense, setDefense],
@@ -4556,6 +4595,9 @@ export function GameTable({
: 'Seal battle plan',
bindBattlePlanCommitments(g, {
type: 'battlePlan',
...(battleBankerSpice
? { bankerSpice: battleBankerSpice }
: {}),
...(g.aid.available > 0 && allyPayment !== ''
? { allyPayment: Number(allyPayment) }
: {}),
@@ -4580,9 +4622,17 @@ export function GameTable({
)}
</>
) : (
<p className="muted">
Your plan is sealed. Waiting for your opponent.
</p>
<>
<p className="muted">
Your plan is sealed. Waiting for your opponent.
</p>
{!!g.battle.plans[me.id]?.bankerSpice && (
<p className="fine">
Spice Banker: {g.battle.plans[me.id].bankerSpice}{' '}
spice committed.
</p>
)}
</>
)}
</>
)