Files
Dune/game/homeworld-shipment.ts
T

145 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
HomeworldCustodyError,
homeworldForceGroups,
quoteHomeworldCustody,
type HomeworldCustody,
type HomeworldCustodyContext,
type HomeworldForces,
type HomeworldReserveSeat,
} from './homeworld-custody';
export type HomeworldShipmentContext = Omit<
HomeworldCustodyContext,
'players'
> & {
players: (HomeworldReserveSeat & { ally: string | null })[];
/** Only the native Guild faction rate; independent transport grants survive. */
nativeGuildRateCanceled?: boolean;
/** Independently purchased ordinary Karama price; never a route permission. */
purchasedGuildRate?: boolean;
};
export type HomeworldShipmentIntent = {
player: string;
destination: string;
sources: Record<string, HomeworldForces>;
};
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
const whole = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
function requireShipment(
condition: unknown,
message: string,
): asserts condition {
if (!condition) throw new HomeworldCustodyError(message);
}
/** E3 world-to-world transport: use the actual native or foreign source pool.
* Guild may also return a foreign garrison to its own world; other own-world
* exceptions need separately established permission. The caller owns timing, usage,
* funding, bank payment, responses and arrival effects. A purchased ordinary
* Karama price takes precedence over the native Guild half rate or its
* cancellation; an alliance alone does not grant either price. Independent
* Nexus and Junction prices belong to their own grants. */
export function quoteHomeworldShipment(
context: HomeworldShipmentContext,
custody: HomeworldCustody,
intent: HomeworldShipmentIntent,
) {
// Validate the entire public custody before using any selected source.
const worlds = homeworldForceGroups(context, custody);
requireShipment(
context.players.every(
(p) =>
p.ally === null ||
(typeof p.ally === 'string' &&
p.ally !== p.id &&
context.players.some((other) => other.id === p.ally)),
),
'Homeworld shipment needs valid seated alliance identities.',
);
requireShipment(
record(intent) &&
Object.keys(intent).length === 3 &&
typeof intent.player === 'string' &&
typeof intent.destination === 'string' &&
record(intent.sources),
'Choose a seated shipper, a destination Homeworld and typed source groups.',
);
const player = context.players.find((p) => p.id === intent.player);
const destination = worlds.find((w) => w.id === intent.destination);
requireShipment(
player && destination,
'Choose an active Homeworld and seated shipper.',
);
const native = context.players.find((p) => p.id === destination.native)!;
requireShipment(
native.id !== player.id || player.faction === 'guild',
'Shipment to your own Homeworld needs its specific return permission.',
);
requireShipment(
!(player.ally === native.id && native.ally === player.id),
'You cannot ship to your ally’s Homeworld.',
);
const sources = Object.entries(intent.sources);
requireShipment(
sources.length === 1 ||
(sources.length === 2 &&
context.advanced &&
player.faction === 'emperor' &&
sources.every(([id]) =>
['homeworld:emperor', 'homeworld:emperor:salusa'].includes(id),
)),
'Use one source Homeworld, or combine Emperor’s own Kaitain and Salusa forces in Advanced play.',
);
let normal = 0;
let elite = 0;
for (const [id, forces] of sources) {
requireShipment(
id !== destination.id && worlds.some((w) => w.id === id),
'Shipment needs a distinct active source Homeworld.',
);
requireShipment(
record(forces) &&
Object.keys(forces).length === 2 &&
whole(forces.normal) &&
whole(forces.elite) &&
forces.normal + forces.elite > 0 &&
forces.normal + forces.elite <= 20,
'Each source needs a positive physical normal and special counter group.',
);
normal += forces.normal;
elite += forces.elite;
}
const amount = normal + elite;
requireShipment(
amount > 0 && amount <= 20,
'A shipment cannot exceed the faction’s twenty physical counters.',
);
const transferred = quoteHomeworldCustody(context, custody, [
...sources.map(([homeworld, forces]) => ({
homeworld,
player: player.id,
withdraw: { normal: forces.normal, elite: forces.elite },
deposit: { normal: 0, elite: 0 },
})),
{
homeworld: destination.id,
player: player.id,
withdraw: { normal: 0, elite: 0 },
deposit: { normal, elite },
},
]);
return {
...transferred,
amount,
elite,
cost:
context.purchasedGuildRate ||
(player.faction === 'guild' && !context.nativeGuildRateCanceled)
? Math.ceil(amount / 2)
: amount,
sources: transferred.receipts.filter((r) => r.homeworld !== destination.id),
};
}