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:
1 parent
9381ef140d
commit
1380271bb6
22 files changed
+1166
-25
No files matched your search
+32
-3
@@ -11,6 +11,7 @@ import { leaderSkillStrongholdCount, sandmasterVictorySpice } from './leader-ski
|
||||
import { beginRihani, chooseRihaniDraw, finishRihani, validateRihani, type RihaniReceipt, type RihaniSkill } from './rihani-decipherer';
|
||||
import { planetologistLeader, planetologistRange, type PlanetologistMovement } from './planetologist-movement';
|
||||
import { quoteSmugglerShipment, type SmugglerShipment } from './smuggler-shipment';
|
||||
import { spiceBankerModeSupported, validateSpiceBankerSpend } from './spice-banker';
|
||||
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
|
||||
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
|
||||
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
|
||||
@@ -561,6 +562,8 @@ export type Player = {
|
||||
prediction?: { faction: FactionId; turn: number };
|
||||
};
|
||||
export type Plan = {
|
||||
/** Separate own-spice commitment for the selected Spice Banker; absent is zero. */
|
||||
bankerSpice?: number;
|
||||
dial: number;
|
||||
leader: string | null;
|
||||
weapon: string | null;
|
||||
@@ -571,7 +574,7 @@ export type Plan = {
|
||||
};
|
||||
export type PlanField = Exclude<
|
||||
keyof Plan,
|
||||
'support' | 'kwisatz' | 'allyPayment'
|
||||
'support' | 'kwisatz' | 'allyPayment' | 'bankerSpice'
|
||||
>;
|
||||
export type Battle = {
|
||||
/** Public face-up/behind-shield choice precedes faction battle powers. */
|
||||
@@ -4917,6 +4920,15 @@ function leaderSkillAssignmentUnavailable(
|
||||
function leaderSkillsIntegrity(g: Game) {
|
||||
sukRescueIntegrity(g);
|
||||
rihaniIntegrity(g);
|
||||
for (const [owner, plan] of Object.entries(g.battle?.plans ?? {})) {
|
||||
if (plan.bankerSpice === undefined) continue;
|
||||
const p = getPlayer(g, owner);
|
||||
if (plan.bankerSpice) requireRule(spiceBankerModeSupported(g), 'Spice Banker with expansion modules is still being integrated.');
|
||||
const leader = controlledLeaders(g, p).find(l => l.id === plan.leader && !l.dead);
|
||||
requireRule([g.battle!.attacker, g.battle!.defender].includes(owner), 'Spice Banker belongs to a current combatant.');
|
||||
nexusRule(() => validateSpiceBankerSpend(battleLeaderSkills(g, p), leader?.id, plan.bankerSpice,
|
||||
p.spice, battleSupportCost(g, p, plan.support) - (plan.allyPayment ?? 0)));
|
||||
}
|
||||
if (g.leaderSkills || g.pendingShipment?.smuggler) {
|
||||
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'guildShipment');
|
||||
requireRule(decisions.length === (g.pendingShipment ? 1 : 0),
|
||||
@@ -6950,6 +6962,7 @@ function uncommittedSpice(g: Game, p: Player) {
|
||||
: 0) -
|
||||
(battleSupportCost(g, p, g.battle?.plans[p.id]?.support ?? 0) -
|
||||
(g.battle?.plans[p.id]?.allyPayment ?? 0)) -
|
||||
(g.battle?.plans[p.id]?.bankerSpice ?? 0) -
|
||||
(g.pendingHomeworldShipment?.player === p.id
|
||||
? g.pendingHomeworldShipment.cost - g.pendingHomeworldShipment.allyPayment : 0)
|
||||
);
|
||||
@@ -12723,6 +12736,9 @@ function validatePlan(
|
||||
const opponent = getPlayer(g, b.attacker === p.id ? b.defender : b.attacker);
|
||||
const forces = combatForces(g, p, b.territory, opponent);
|
||||
const typedForces = g.advanced || p.faction === 'ixians';
|
||||
const bankerSpice = input.bankerSpice === undefined ? 0 : input.bankerSpice;
|
||||
requireRule(typeof bankerSpice === 'number' && Number.isSafeInteger(bankerSpice) && bankerSpice >= 0 && bankerSpice <= 3,
|
||||
'Choose zero to decline, or one through three spice for Spice Banker.');
|
||||
const dial = typedForces
|
||||
? Number(input.dial)
|
||||
: integer(input.dial, 0, forces.normal + forces.elite, 'Forces dialed');
|
||||
@@ -12737,12 +12753,13 @@ function validatePlan(
|
||||
);
|
||||
const allyPayment = integer(
|
||||
input.allyPayment ??
|
||||
Math.max(0, battleSupportCost(g, p, support) - p.spice),
|
||||
Math.max(0, battleSupportCost(g, p, support) - p.spice),
|
||||
Math.max(0, battleSupportCost(g, p, support) - (p.spice - bankerSpice)),
|
||||
Math.max(0, battleSupportCost(g, p, support) - (p.spice - bankerSpice)),
|
||||
Math.min(battleSupportCost(g, p, support), battleAidFor(g, p)?.amount ?? 0),
|
||||
'Battle ally payment',
|
||||
);
|
||||
const plan: Plan = {
|
||||
...(bankerSpice ? { bankerSpice } : {}),
|
||||
...(allyPayment ? { allyPayment } : {}),
|
||||
dial,
|
||||
leader:
|
||||
@@ -12777,6 +12794,10 @@ function validatePlan(
|
||||
(l) => !l.dead && (!l.usedAt || l.usedAt === b.territory),
|
||||
);
|
||||
const l = available.find((l) => l.id === plan.leader);
|
||||
nexusRule(() => validateSpiceBankerSpend(battleLeaderSkills(g, p), l?.id, bankerSpice,
|
||||
p.spice, battleSupportCost(g, p, support) - allyPayment));
|
||||
if (bankerSpice) requireRule(spiceBankerModeSupported(g),
|
||||
'Spice Banker with expansion modules is still being integrated.');
|
||||
requireRule(!l || !g.leaderSkills?.assignments.some((assignment) => assignment.owner === p.id && assignment.leader === l.id) || !!b.leaderSkillHidden?.[p.id], 'Move your skilled leader behind the shield before using it in your battle plan.');
|
||||
const hero = cardOf(p, plan.leader)?.kind === 'hero';
|
||||
requireRule(
|
||||
@@ -14229,6 +14250,14 @@ function resolveBattle(g: Game) {
|
||||
{ faction: player.faction, name: 'Stronghold support' },
|
||||
);
|
||||
}
|
||||
for (const payment of quote.spiceBankerPayments ?? []) {
|
||||
const player = getPlayer(g, payment.player);
|
||||
player.spice -= payment.amount;
|
||||
log(g, payment.waivedByTraitor
|
||||
? `${player.name}'s successful traitor call waived the committed Spice Banker payment along with other Battle Plan spice.`
|
||||
: `${player.name} paid ${payment.amount} committed Spice Banker spice to the bank, separate from force support. The payment is spent win or lose; only a surviving skilled leader receives its strength bonus.`,
|
||||
{ faction: player.faction, name: 'Spice Banker payment' });
|
||||
}
|
||||
if (quote.choamIncome) g.pendingChoamBattleIncome = { ...quote.choamIncome };
|
||||
const explosion = quote.explosion;
|
||||
if (ac && dc) {
|
||||
|
||||
Reference in new issue
Block a user