Files
Dune/game/battle-card-slots.ts
T

101 lines
3.6 KiB
TypeScript

import type { Card } from './cards';
import { isDefenseCard, isWeaponCard } from './battle-cards';
import { ecazTreacheryDefinition } from './ecaz-cards';
import {
isPlanetologistBattleSpecialCard,
validLeaderSkillBattleCardPair,
} from './leader-skill-combat';
export type BattleCardSlot = 'weapon' | 'defense';
export type BattlePlanInspectionField = BattleCardSlot | 'leader' | 'dial';
export type BattleCategoryInspectionValue = string | number | null;
function canonicalEcazBattleSpecial(card: Card | undefined) {
if (
!card ||
Object.keys(card).sort().join(',') !== 'effect,id,kind,name'
)
return false;
const definition = ecazTreacheryDefinition(card);
return (
definition?.card.effect === 'reinforcements' ||
definition?.card.effect === 'harassWithdraw'
);
}
/**
* Single-slot role eligibility only. The caller still applies paired-card rules
* such as Chemistry/Weirding Way dependencies and validates the selected
* Planetologist leader before setting planetologistWeapon.
*/
export function battleCardSlotEligible(
slot: BattleCardSlot,
card: Card | undefined,
options: Readonly<{ planetologistWeapon?: boolean }> = {},
): boolean {
if (!card) return true;
if (canonicalEcazBattleSpecial(card)) return true;
if (slot === 'defense') return isDefenseCard(card);
return (
isWeaponCard(card) ||
(!!options.planetologistWeapon && isPlanetologistBattleSpecialCard(card))
);
}
/** Slot-only specials do not supply the opposite category for alternate roles. */
export function validBattleSlotPair(
weapon?: Card,
defense?: Card,
planetologistWeapon = false,
): boolean {
if (!canonicalEcazBattleSpecial(weapon) && !canonicalEcazBattleSpecial(defense))
return validLeaderSkillBattleCardPair(weapon, defense, planetologistWeapon);
return (
battleCardSlotEligible('weapon', weapon, { planetologistWeapon }) &&
battleCardSlotEligible('defense', defense) &&
(!weapon || weapon.id !== defense?.id) &&
(weapon?.kind !== 'chemistry' || (!!defense && isDefenseCard(defense))) &&
(defense?.kind !== 'weirdingWay' || (!!weapon && isWeaponCard(weapon)))
);
}
/** A Planetologist substitution suppresses only the selected weapon's native effect. */
export function usesNativeBattleSpecial(
plan: Readonly<{ weapon?: unknown; defense?: unknown; planetologistSpecial?: unknown }>,
card: 'ecaz-reinforcements' | 'ecaz-harass-withdraw',
): boolean {
return plan.defense === card || (plan.weapon === card && plan.planetologistSpecial !== true);
}
/**
* Prescience asks for the selected category rather than physical slot
* occupancy. Exact Ecaz battle specials answer null without revealing their
* identity. Existing Planetologist inspection behavior is a separate contract.
*/
export function battleCategoryInspectionValue(
field: BattlePlanInspectionField,
value: BattleCategoryInspectionValue | undefined,
card?: Card,
): BattleCategoryInspectionValue | undefined {
if (value === undefined || field === 'leader' || field === 'dial' || value === null)
return value;
if (card && card.id !== value)
throw new Error('The inspected card does not match the selected battle slot.');
if (canonicalEcazBattleSpecial(card))
return null;
return value;
}
/** Partial plan search leaves an unspecified candidate unconstrained. */
export function fixedBattleInspectionMatches(
field: BattlePlanInspectionField,
fixed: BattleCategoryInspectionValue,
candidate: BattleCategoryInspectionValue | undefined,
card?: Card,
): boolean {
return (
candidate === undefined ||
battleCategoryInspectionValue(field, candidate, card) === fixed
);
}