Files
Dune/game/homeworld-substitution.ts
T
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

117 lines
3.6 KiB
TypeScript

import {
HomeworldCombatLossError,
quoteHomeworldCombatLoss,
type HomeworldCombatLossContext,
} from './homeworld-combat-loss';
import {
HomeworldCustodyError,
quoteHomeworldCustody,
type HomeworldCustody,
} from './homeworld-custody';
export type HomeworldSubstitutionRequest = {
location: string;
player: string;
amount: number;
/** Bound to the resolved battle by the caller, never all historical deaths. */
cyborgsLost: number;
};
export class HomeworldSubstitutionError extends Error {
constructor(message: string) {
super(message);
this.name = 'HomeworldSubstitutionError';
}
}
function requireSubstitution(
condition: unknown,
message: string,
): asserts condition {
if (!condition) throw new HomeworldSubstitutionError(message);
}
/** Exact post-casualty exchange. Event identity, declaration timing and Karama
* permission are caller-owned. The lost Cyborgs stay at this battle location;
* surviving Suboids take their place in the Tanks, without revival or transport. */
export function quoteHomeworldSubstitution(
context: HomeworldCombatLossContext,
custody: HomeworldCustody,
request: HomeworldSubstitutionRequest,
) {
try {
requireSubstitution(
!!request &&
typeof request === 'object' &&
!Array.isArray(request) &&
Number.isSafeInteger(request.amount) &&
request.amount > 0 &&
Number.isSafeInteger(request.cyborgsLost) &&
request.cyborgsLost <= 7 &&
request.amount <= request.cyborgsLost,
'Exchange a positive number of Suboids for Cyborgs lost in this battle.',
);
// A zero loss validates the entire physical inventory and detaches custody
// without temporarily increasing an accumulated battle-loss counter.
const before = quoteHomeworldCombatLoss(context, custody, {
location: request.location,
player: request.player,
losses: { normal: 0, elite: 0 },
});
const owner = before.players.find(
(player) => player.id === request.player,
)!;
requireSubstitution(
owner.faction === 'ixians' &&
owner.eliteTanks >= request.cyborgsLost &&
owner.battleLosses >= request.cyborgsLost &&
before.receipt.before.normal >= request.amount,
'Use surviving Ixian Suboids here and the Cyborg casualties of this battle.',
);
const exchange = quoteHomeworldCustody(
{ advanced: context.advanced, players: before.players },
before.custody,
[
{
homeworld: request.location,
player: request.player,
withdraw: { normal: request.amount, elite: 0 },
deposit: { normal: 0, elite: request.amount },
},
],
);
const players = before.players.map((player, index) => ({
...player,
...exchange.players[index],
eliteTanks:
player.eliteTanks - (player.id === request.player ? request.amount : 0),
}));
const after = quoteHomeworldCombatLoss(
{ advanced: context.advanced, players },
exchange.state,
{
location: request.location,
player: request.player,
losses: { normal: 0, elite: 0 },
},
);
return {
custody: after.custody,
players: after.players,
receipt: {
location: request.location,
player: request.player,
amount: request.amount,
cyborgsLost: request.cyborgsLost,
before: { ...before.receipt.before },
after: { ...after.receipt.after },
},
};
} catch (error) {
if (
error instanceof HomeworldCustodyError ||
error instanceof HomeworldCombatLossError
)
throw new HomeworldSubstitutionError(error.message);
throw error;
}
}