140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
import type { Game } from './engine';
|
|
import {
|
|
homeworldForceGroups,
|
|
HomeworldCustodyError,
|
|
type HomeworldCustody,
|
|
type HomeworldCustodyContext,
|
|
} from './homeworld-custody';
|
|
import { homeworldContext } from './homeworld-game';
|
|
import { quoteNativeReserveDeposit } from './homeworld-native-reserves';
|
|
import { homeworldPopulations } from './homeworld-population';
|
|
|
|
export type HomeworldRevivalDeploymentContext = Pick<
|
|
Game,
|
|
'advanced' | 'players' | 'homeworlds'
|
|
>;
|
|
export type HomeworldRevivalDeploymentSource =
|
|
| 'normal'
|
|
| 'emperorExtra'
|
|
| 'ghola'
|
|
| 'nexusEmperor'
|
|
| 'nexusTleilaxu';
|
|
export type HomeworldRevivalDeploymentGroup = {
|
|
amount: number;
|
|
elite: number;
|
|
free: number;
|
|
};
|
|
export type HomeworldRevivalDeploymentQuote = {
|
|
kind: 'fedaykin' | 'tleilax';
|
|
normal: number;
|
|
elite: number;
|
|
beforePopulation: number;
|
|
afterPopulation: number;
|
|
blocked: string | null;
|
|
};
|
|
|
|
function physicalWorlds(
|
|
context: HomeworldCustodyContext,
|
|
custody: HomeworldCustody,
|
|
) {
|
|
return homeworldForceGroups(context, custody)
|
|
.map((home) => ({
|
|
id: home.id,
|
|
native: home.native,
|
|
forces: Object.entries(home.forces)
|
|
.filter(([, forces]) => forces.normal + forces.elite > 0)
|
|
.map(([player, forces]) => [player, forces.normal, forces.elite])
|
|
.sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
|
|
}))
|
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
}
|
|
|
|
/** Quote the eligible subset of one actual typed native revival deposit.
|
|
* Timing, destinations, splitting and durable completion belong to the caller.
|
|
* Provisional first version: the high-threshold placement ability is evaluated
|
|
* on the population *resulting* from this return, so the redeployment that
|
|
* itself reaches the high side qualifies. This is a visibly provisional
|
|
* composition policy, not a located publisher timing ruling. */
|
|
export function quoteHomeworldRevivalDeployment(
|
|
before: HomeworldRevivalDeploymentContext,
|
|
after: HomeworldRevivalDeploymentContext,
|
|
player: string,
|
|
source: HomeworldRevivalDeploymentSource,
|
|
group: HomeworldRevivalDeploymentGroup,
|
|
): HomeworldRevivalDeploymentQuote | null {
|
|
if (
|
|
!['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu', 'nexusEmperor'].includes(source) ||
|
|
!group ||
|
|
typeof group !== 'object' ||
|
|
Array.isArray(group) ||
|
|
!Number.isSafeInteger(group.amount) ||
|
|
group.amount <= 0 ||
|
|
group.amount > 20 ||
|
|
!Number.isSafeInteger(group.elite) ||
|
|
group.elite < 0 ||
|
|
group.elite > group.amount ||
|
|
!Number.isSafeInteger(group.free) ||
|
|
group.free < 0 ||
|
|
group.free > group.amount ||
|
|
(source === 'nexusEmperor' &&
|
|
(group.amount !== 3 || group.free !== 3))
|
|
)
|
|
throw new HomeworldCustodyError(
|
|
'Revival deployment needs a valid source and exact newly returned physical group.',
|
|
);
|
|
if (!before.homeworlds?.custody && !after.homeworlds?.custody) return null;
|
|
if (
|
|
!before.homeworlds?.custody ||
|
|
!after.homeworlds?.custody ||
|
|
before.advanced !== after.advanced
|
|
)
|
|
throw new HomeworldCustodyError(
|
|
'The Homeworld module cannot change during a revival deposit.',
|
|
);
|
|
const beforeContext = homeworldContext(before);
|
|
const afterContext = homeworldContext(after);
|
|
const expected = quoteNativeReserveDeposit(
|
|
beforeContext,
|
|
before.homeworlds.custody,
|
|
player,
|
|
{ normal: group.amount - group.elite, elite: group.elite },
|
|
);
|
|
if (
|
|
JSON.stringify(
|
|
physicalWorlds(
|
|
{ ...beforeContext, players: expected.players },
|
|
expected.state,
|
|
),
|
|
) !== JSON.stringify(physicalWorlds(afterContext, after.homeworlds.custody))
|
|
)
|
|
throw new HomeworldCustodyError(
|
|
'The revival deployment does not match its original typed native reserve deposit.',
|
|
);
|
|
const owner = beforeContext.players.find((seat) => seat.id === player)!;
|
|
const kind =
|
|
owner.faction === 'fremen' && group.elite > 0
|
|
? 'fedaykin'
|
|
: owner.faction === 'tleilaxu' && source === 'normal' && group.free > 0
|
|
? 'tleilax'
|
|
: null;
|
|
if (!kind) return null;
|
|
const card = kind === 'fedaykin' ? 'southern_hemisphere' : 'tleilax';
|
|
const prior = homeworldPopulations(
|
|
beforeContext,
|
|
before.homeworlds.custody,
|
|
).find((home) => home.native === player && home.card === card)!;
|
|
const current = homeworldPopulations(
|
|
afterContext,
|
|
after.homeworlds.custody,
|
|
).find((home) => home.native === player && home.card === card)!;
|
|
if (current.side !== 'high') return null;
|
|
return {
|
|
kind,
|
|
normal: kind === 'tleilax' ? group.free : 0,
|
|
elite: kind === 'fedaykin' ? group.elite : 0,
|
|
beforePopulation: prior.population,
|
|
afterPopulation: current.population,
|
|
blocked: null,
|
|
};
|
|
}
|