import { HomeworldCustodyError, homeworldForceGroups, type HomeworldCustody, } from './homeworld-custody'; import { quoteHomeworldShipment, type HomeworldShipmentContext } from './homeworld-shipment'; import type { HomeworldForces } from './homeworld-custody'; import { deployRicheseNoField, isHomeworldNoFieldLocation, moveRicheseNoField, type RicheseNoField } from './richese-no-field'; export type RicheseHomeworldMarkerShipmentIntent = { player: string; destination: string; tokenId: string; /** Optional physical companions withdrawn from native Richese reserves. */ forces?: HomeworldForces; }; export type RicheseHomeworldMarkerTransferIntent = RicheseHomeworldMarkerShipmentIntent & { source: string; forces?: HomeworldForces; }; const record = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); function requireMarker(condition: unknown, message: string): asserts condition { if (!condition) throw new HomeworldCustodyError(message); } /** A fresh concealed marker counts as one for the whole shipment invoice. Any * physical companions are independently withdrawn from native Richese reserves; * the marker's denomination never enters the price or physical custody. */ export function quoteRicheseHomeworldMarkerShipment( context: HomeworldShipmentContext, custody: HomeworldCustody, intent: RicheseHomeworldMarkerShipmentIntent, noField: RicheseNoField, ) { // This validates all physical reserve and foreign garrison custody first. const worlds = homeworldForceGroups(context, custody); requireMarker(record(intent) && (Object.keys(intent).sort().join(',') === 'destination,player,tokenId' || Object.keys(intent).sort().join(',') === 'destination,forces,player,tokenId') && typeof intent.player === 'string' && typeof intent.destination === 'string' && typeof intent.tokenId === 'string', 'Choose a Richese player, one active Homeworld and one physical No-Field.'); if ('forces' in intent) { requireMarker(record(intent.forces) && Object.keys(intent.forces).sort().join(',') === 'elite,normal' && Number.isSafeInteger(intent.forces.normal) && Number.isSafeInteger(intent.forces.elite) && intent.forces.normal >= 0 && intent.forces.elite >= 0, 'Choose a valid physical normal and special counter group.'); } requireMarker(context.players.every(seat => seat.ally === null || typeof seat.ally === 'string' && seat.ally !== seat.id && context.players.some(other => other.id === seat.ally)), 'Homeworld shipment needs valid seated alliance identities.'); const player = context.players.find(seat => seat.id === intent.player); const source = worlds.find(world => world.id === 'homeworld:richese'); const destination = worlds.find(world => world.id === intent.destination); requireMarker(player?.faction === 'richese' && source?.native === player.id, 'Only native Richese can ship its concealed No-Field.'); requireMarker(destination && destination.native !== player.id, 'Choose another seated faction’s Homeworld.'); const native = context.players.find(seat => seat.id === destination.native)!; requireMarker(!(player.ally === native.id && native.ally === player.id), 'You cannot ship to your ally’s Homeworld.'); let deployed: RicheseNoField; try { deployed = deployRicheseNoField(noField, { tokenId: intent.tokenId, controller: player.id, location: { homeworld: destination.id }, }); } catch (error) { throw new HomeworldCustodyError(error instanceof Error ? error.message : 'Invalid No-Field shipment.'); } if (intent.forces && intent.forces.normal + intent.forces.elite > 0) { const physical = quoteHomeworldShipment(context, custody, { player: player.id, destination: destination.id, sources: { [source.id]: intent.forces }, }); const amount = 1 + physical.amount; return { player: player.id, tokenId: intent.tokenId, source: source.id, destination: destination.id, amount, elite: physical.elite, cost: context.purchasedGuildRate ? Math.ceil(amount / 2) : amount, noField: deployed, state: physical.state, players: physical.players, sources: physical.sources }; } return { player: player.id, tokenId: intent.tokenId, source: source.id, destination: destination.id, amount: 1, elite: 0, cost: 1, noField: deployed, }; } /** A deployed marker can be transported like one force between eligible * Homeworlds, provisionally alongside physical forces from that same source. * Advanced permits a native Homeworld endpoint; Basic remains foreign-only. * Its denomination never enters the price or physical custody. */ export function quoteRicheseHomeworldMarkerTransfer( context: HomeworldShipmentContext, custody: HomeworldCustody, intent: RicheseHomeworldMarkerTransferIntent, noField: RicheseNoField, ) { const worlds = homeworldForceGroups(context, custody); requireMarker(record(intent) && (Object.keys(intent).sort().join(',') === 'destination,player,source,tokenId' || Object.keys(intent).sort().join(',') === 'destination,forces,player,source,tokenId') && typeof intent.player === 'string' && typeof intent.source === 'string' && typeof intent.destination === 'string' && typeof intent.tokenId === 'string', 'Choose the current Homeworld marker, its source and another Homeworld.'); if ('forces' in intent) { requireMarker(record(intent.forces) && Object.keys(intent.forces).sort().join(',') === 'elite,normal' && Number.isSafeInteger(intent.forces.normal) && Number.isSafeInteger(intent.forces.elite) && intent.forces.normal >= 0 && intent.forces.elite >= 0, 'Choose a valid physical normal and special counter group.'); } requireMarker(context.players.every(seat => seat.ally === null || typeof seat.ally === 'string' && seat.ally !== seat.id && context.players.some(other => other.id === seat.ally)), 'Homeworld shipment needs valid seated alliance identities.'); const player = context.players.find(seat => seat.id === intent.player); const source = worlds.find(world => world.id === intent.source); const destination = worlds.find(world => world.id === intent.destination); requireMarker(player?.faction === 'richese' && source && destination && source.id !== destination.id && (context.advanced || (source.native !== player.id && destination.native !== player.id)), 'Choose two different active Homeworlds; Basic marker transfers require foreign Homeworlds.'); const native = context.players.find(seat => seat.id === destination.native)!; requireMarker(!(player.ally === native.id && native.ally === player.id), 'You cannot ship to your ally’s Homeworld.'); const marker = noField?.deployed; requireMarker(marker?.controller === player.id && marker.tokenId === intent.tokenId && isHomeworldNoFieldLocation(marker.location) && marker.location.homeworld === source.id, 'The chosen concealed marker is no longer on its original Homeworld.'); let moved: RicheseNoField; try { moved = moveRicheseNoField(noField, intent.tokenId, { homeworld: destination.id }); } catch (error) { throw new HomeworldCustodyError(error instanceof Error ? error.message : 'Invalid No-Field transfer.'); } const forces = intent.forces; if (forces && forces.normal + forces.elite > 0) { const physical = quoteHomeworldShipment(context, custody, { player: player.id, destination: destination.id, sources: { [source.id]: forces }, }); const amount = 1 + physical.amount; return { player: player.id, tokenId: intent.tokenId, source: source.id, destination: destination.id, amount, elite: physical.elite, cost: context.purchasedGuildRate ? Math.ceil(amount / 2) : amount, noField: moved, state: physical.state, players: physical.players, sources: physical.sources }; } return { player: player.id, tokenId: intent.tokenId, source: source.id, destination: destination.id, amount: 1, elite: 0, cost: 1, noField: moved }; }