Files
Dune/components/spice-banker.tsx
T
Quintine 1380271bb6 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
2026-09-13 20:45:42 +10:00

100 lines
2.9 KiB
TypeScript

'use client';
import type { GameView } from '@/game/engine';
import { controlsLeader } from '@/game/leader-control';
import {
spiceBankerBattleMaximum,
spiceBankerModeSupported,
} from '@/game/spice-banker';
import { Input } from '@/components/ui/input';
export function spiceBankerControlState(
game: GameView,
selectedLeader: string,
ownSupport: number,
) {
if (!spiceBankerModeSupported(game)) return { eligible: false, maximum: 0 };
const me = game.players.find((player) => player.id === game.me);
if (!me) return { eligible: false, maximum: 0 };
const leader = me.leaders.find(
(candidate) =>
candidate.id === selectedLeader &&
!candidate.dead &&
controlsLeader(me, candidate) &&
(!candidate.usedAt || candidate.usedAt === game.battle?.territory),
);
const assignments =
game.leaderSkills?.assignments.filter(
(assignment) => assignment.controller === game.me,
) ?? [];
const eligible =
!!leader && spiceBankerBattleMaximum(assignments, leader.id, 3, 0) > 0;
return {
eligible,
maximum: eligible
? spiceBankerBattleMaximum(
assignments,
leader.id,
me.spice ?? 0,
ownSupport,
)
: 0,
};
}
export function SpiceBankerControl({
game,
selectedLeader,
ownSupport,
value,
onChange,
disabled = false,
}: {
game: GameView;
selectedLeader: string;
ownSupport: number;
value: number;
onChange: (amount: number) => void;
disabled?: boolean;
}) {
const state = spiceBankerControlState(game, selectedLeader, ownSupport);
if (!state.eligible) return null;
const commitment = Number.isFinite(value)
? Math.max(0, Math.min(Math.trunc(value), state.maximum))
: 0;
return (
<div className="notice" aria-label="Spice Banker battle commitment">
<label htmlFor="battle-banker-spice">
Spice Banker commitment
<Input
id="battle-banker-spice"
type="number"
min={0}
max={state.maximum}
value={commitment}
disabled={disabled || state.maximum === 0}
onChange={(event) =>
onChange(
Math.max(
0,
Math.min(Math.trunc(Number(event.target.value)), state.maximum),
),
)
}
/>
</label>
<p>
Plan funding: {ownSupport} own spice for force support + {commitment}{' '}
committed to Spice Banker = {ownSupport + commitment} own spice total.
</p>
<p className="fine">
You may commit up to {state.maximum} own spice after your own
force-support payment. Bank and CHOAM support are separate. Each
committed spice adds 1 strength if this skilled leader survives. The
commitment goes to the Bank after a win or loss, including if the leader
dies. A sole winner through a traitor call spends no Battle Plan spice.
</p>
</div>
);
}