Prototype Planetologist movement and Special-card battles
Connect capped extra range, two-origin gathering, Fremen cancellation and saved replacement choices. Add human controls and actual first-choice AI paths for all four profiles. Admit the exact nine base green Specials in the actual skilled leader's weapon slot for +2 on survival, with mandatory physical discard and no native utility effect. Reject stale dead-disc plans. Independent movement/battle review closed after the saved-dead-disc fix. 84 focused tests passed. Two four-profile Basic/Advanced samples completed (194/186 accepted actions, six JSON restorations each, zero rejected actions). Basic used both movement branches; Advanced used added range. Browser genuine setup/gather/refresh and a separate targeted Special/traitor-loss fixture passed. Required verification: types, lint, 4435 offline tests, build and 43 HTTP tests passed. First attempt stopped at missing explicit test promise markers; corrected before the successful broad run. Final check/build/HTTP durations: 144157/6638/25048 ms. Source fingerprint (1260 files): bb7be69cf3e925bf7338ea444f77279ed7b37faadc3c44ed7db01b941f27b861 Private report: checkpoints/20260913-planetologist/checks-final/report.json. Preserved all 325 opening games and all original application/seat/recovery records; 359 rooms after 34 new QA/HTTP rooms. SQLite quick_check ok and zero foreign-key violations. Healthy existing server reused; no reset/restart. Boundaries: eight other Leader Skills, enemy-only mixed BG gather response, expansion roster/green-card/optional-module combinations remain unfinished. Winning traitor caller discard remains a documented card-specific inference. No mode or publication gates opened. Efficiency measure: aggregate goal counter 67321978 -> 68710948 (1388970), including context and coordination; not billing tokens or a savings claim. One skill's three connected effects, selective implementation/research/review, one interrupted lint attempt followed by the required complete verification.
This commit is contained in:
1 parent
6e6fecc9d4
commit
5082c81353
22 files changed
+2473
-93
No files matched your search
@@ -34,6 +34,9 @@ import {
|
||||
type HomeworldExplosionLosses,
|
||||
} from './homeworld-battle-rules';
|
||||
import {
|
||||
PLANETOLOGIST_BASE_SPECIALS,
|
||||
canUsePlanetologistBattleSpecial,
|
||||
isPlanetologistBattleSpecialCard,
|
||||
leaderSkillBattleBonus,
|
||||
type BattleLeaderSkill,
|
||||
type LeaderSkillBattleBonus,
|
||||
@@ -213,6 +216,35 @@ function validateCombatant(
|
||||
(side.leader.id === p.leader && Number.isFinite(side.leader.strength)),
|
||||
'The controlled leader must match the revealed plan.',
|
||||
);
|
||||
const selectedPlanetologistCards = [
|
||||
p.weapon,
|
||||
p.defense,
|
||||
p.leader,
|
||||
side.lateDefense,
|
||||
].filter(
|
||||
(selectedId): selectedId is string =>
|
||||
!!selectedId &&
|
||||
PLANETOLOGIST_BASE_SPECIALS.some(
|
||||
(card) => card.id === selectedId,
|
||||
),
|
||||
);
|
||||
requireQuote(
|
||||
selectedPlanetologistCards.every((selectedId) => {
|
||||
const selectedCard = side.hand.find((card) => card.id === selectedId);
|
||||
return (
|
||||
selectedId === p.weapon &&
|
||||
side.leader?.id === p.leader &&
|
||||
!side.leader.dead &&
|
||||
isPlanetologistBattleSpecialCard(selectedCard) &&
|
||||
canUsePlanetologistBattleSpecial({
|
||||
assignments: side.leaderSkills ?? [],
|
||||
selectedLeader: p.leader,
|
||||
card: selectedCard,
|
||||
})
|
||||
);
|
||||
}),
|
||||
'A green Special may replace only the assigned Planetologist leader’s weapon.',
|
||||
);
|
||||
return played;
|
||||
}
|
||||
/**
|
||||
@@ -614,16 +646,30 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
|
||||
auditCount(other.hand, auditor.usedCards, auditor.survived);
|
||||
}
|
||||
let retention: MoritaniRetention | null = null;
|
||||
const mandatoryPlanetologistDiscard = (
|
||||
side: ResolutionCombatant,
|
||||
selected: string,
|
||||
) =>
|
||||
selected === side.plan.weapon &&
|
||||
canUsePlanetologistBattleSpecial({
|
||||
assignments: side.leaderSkills ?? [],
|
||||
selectedLeader: side.plan.leader,
|
||||
card: card(side, selected),
|
||||
});
|
||||
const moritani = input.participants.find((p) => p.faction === 'moritani');
|
||||
if (winner && moritani) {
|
||||
const loser = winner === a ? d : a,
|
||||
loserPlayed = loser === a ? played.attacker : played.defender;
|
||||
const eligible = loserPlayed.filter((selected) =>
|
||||
canRetainBattleCard(
|
||||
card(loser, selected)!,
|
||||
ac || dc,
|
||||
!!loser.poisonTooth,
|
||||
),
|
||||
const retainablePlayed = loserPlayed.filter(
|
||||
(selected) => !mandatoryPlanetologistDiscard(loser, selected),
|
||||
);
|
||||
const eligible = retainablePlayed.filter(
|
||||
(selected) =>
|
||||
canRetainBattleCard(
|
||||
card(loser, selected)!,
|
||||
ac || dc,
|
||||
!!loser.poisonTooth,
|
||||
),
|
||||
);
|
||||
if (
|
||||
loser.id !== moritani.id &&
|
||||
@@ -640,7 +686,7 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
|
||||
player: loser.id,
|
||||
territory: input.territory,
|
||||
turn: input.turn,
|
||||
played: [...loserPlayed],
|
||||
played: [...retainablePlayed],
|
||||
eligible,
|
||||
stage: 'choose',
|
||||
};
|
||||
@@ -651,9 +697,12 @@ function calculate(input: BattleResolutionInput): BattleResolutionQuote {
|
||||
.filter(
|
||||
(selected) =>
|
||||
!(
|
||||
retention?.player === s.id && retention.played.includes(selected)
|
||||
retention?.player === s.id &&
|
||||
retention.played.includes(selected) &&
|
||||
!mandatoryPlanetologistDiscard(s, selected)
|
||||
) &&
|
||||
(winner !== s ||
|
||||
(mandatoryPlanetologistDiscard(s, selected) ||
|
||||
winner !== s ||
|
||||
!canRetainBattleCard(
|
||||
card(s, selected)!,
|
||||
ac || dc,
|
||||
|
||||
Reference in new issue
Block a user