Resolve Family Atomics with concealed No-Field forces

This commit is contained in:
Quintine committed 2026-10-27 11:54:38 +11:00
1 parent 28df687bf9
commit 57a59c1663
13 files changed
+511 -24

No files matched your search

+14 -4
View File
@@ -1,6 +1,7 @@
import { fighterCount, isAdvisor } from './advisors';
import { splitLocation, TERRITORIES } from './board';
import type { OrdinaryCardPlayer } from './card-availability';
import { presenceByLocation } from './force-presence';
export type FamilyAtomicsState = {
advanced?: boolean;
@@ -12,7 +13,7 @@ export type FamilyAtomicsState = {
stormCardWindowOpen?: boolean;
stormResolution?: unknown;
};
export type FamilyAtomicsPlayer = Pick<OrdinaryCardPlayer, 'id' | 'faction' | 'forces' | 'advisors'>;
export type FamilyAtomicsPlayer = Pick<OrdinaryCardPlayer, 'id' | 'faction' | 'forces' | 'advisors' | 'noField'>;
const wall = TERRITORIES.find(territory => territory.id === 'shield_wall')!;
const nearby = TERRITORIES.filter(territory => territory.id === wall.id || territory.neighbors.includes(wall.id));
@@ -24,8 +25,17 @@ const nearby = TERRITORIES.filter(territory => territory.id === wall.id || terri
export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtomicsPlayer): string | null {
if (state.shieldWallDestroyed || state.phase !== 0 || state.stormCardWindowOpen === false || state.stormResolution)
return 'Play Family Atomics before storm movement.';
// A concealed No-Field is one force for all effects (supplied physical36 B),
// including denomination0. Its public location suffices; never read private
// value, reserve capacity or token inventory merely to qualify this card.
const marker = player.noField?.deployed;
const markerHere = marker?.controller === player.id && nearby.some(territory =>
territory.id === marker.location.territory && territory.sectors.includes(marker.location.sector));
const forces = presenceByLocation({ forces: player.forces,
noField: markerHere ? { deployed: { location: marker!.location } } : null });
const effective = { faction: player.faction, advisors: player.advisors, forces };
if (!state.advanced)
return nearby.some(territory => fighterCount(player, territory.id) > 0)
return nearby.some(territory => fighterCount(effective, territory.id) > 0)
? null : 'You need forces on or adjacent to the Shield Wall.';
if (!Number.isSafeInteger(state.turn) || (state.turn ?? 0) <= 1)
return 'Play Family Atomics after the first game turn.';
@@ -35,7 +45,7 @@ export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtom
if (!Number.isSafeInteger(state.storm) || (state.storm ?? -1) < 0 || (state.storm ?? 19) > 18)
return 'Family Atomics needs the current storm position.';
const storm = state.storm!;
const qualified = Object.entries(player.forces).some(([key, amount]) => {
const qualified = Object.entries(forces).some(([key, amount]) => {
if (!Number.isSafeInteger(amount) || amount <= 0) return false;
const source = splitLocation(key);
const territory = nearby.find(candidate => candidate.id === source.territory);
@@ -48,5 +58,5 @@ export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtom
return !(storm > first && storm < last);
});
});
return qualified ? null : 'You need physical fighters on or adjacent to the Shield Wall with no intervening storm.';
return qualified ? null : 'You need fighters on or adjacent to the Shield Wall with no intervening storm.';
}