Files
OpenFrontIO/tests/client/controllers/BuildPreviewController.test.ts
T
Evan 117fa43947 Fix nuke preview showing teammate SAMs as threats (#4342)
## Problem

In the nuke trajectory preview, the SAM-intercept **"X"** marker was
drawn over **teammates'** SAMs — implying their SAM would shoot down
your missile. It shouldn't: like allies, a teammate's SAM never engages
your nuke. The bug only affected teammates; allies already worked.

## Cause

The preview built its threat set from `myPlayer.allies()` only — formal
alliances — and never considered teammates. That diverged from the sim
([`SAMLauncherExecution.ts`](src/core/execution/SAMLauncherExecution.ts#L118-L134)),
which skips any nuke whose owner it's `isFriendly()` with (**same team
OR allied**).

## Fix

`samThreatensNukePreview` now takes a teammate set and excludes
teammates **unconditionally**.

The subtlety: allies keep the existing *betrayal* exception — a strike
close enough to break the alliance makes that ally's SAM engage at
launch (`listNukeBreakAlliance`, the same function the sim uses).
Teammates get **no** such exception, because a strike can break an
alliance but never a team relationship. So even a player who is both a
teammate *and* a betrayed ally is correctly left off the threat set.

## Notes

- The sim has an "aftergame fun" exception where teammate SAMs *do*
target teammate nukes once there's a winner. The preview only appears
while aiming a buildable mid-game (no winner yet), so that case doesn't
apply here.

## Tests

Updated `samThreatensNukePreview` unit tests for the new signature and
added coverage for: teammate excluded, and teammate stays excluded even
when listed as betrayed. All 11 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:37:45 -07:00

75 lines
2.8 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
samThreatensNukePreview,
shouldPreserveGhostAfterBuild,
} from "../../../src/client/controllers/BuildPreviewController";
import { UnitType } from "../../../src/core/game/Game";
describe("BuildPreviewController ghost preservation (locked nuke / Enter confirm)", () => {
describe("shouldPreserveGhostAfterBuild", () => {
test("returns true for AtomBomb so ghost is not cleared after placement", () => {
expect(shouldPreserveGhostAfterBuild(UnitType.AtomBomb)).toBe(true);
});
test("returns true for HydrogenBomb so ghost is not cleared after placement", () => {
expect(shouldPreserveGhostAfterBuild(UnitType.HydrogenBomb)).toBe(true);
});
test("returns false for City so ghost is cleared after placement", () => {
expect(shouldPreserveGhostAfterBuild(UnitType.City)).toBe(false);
});
test("returns false for Factory so ghost is cleared after placement", () => {
expect(shouldPreserveGhostAfterBuild(UnitType.Factory)).toBe(false);
});
test("returns false for other buildable types (Port, DefensePost, MissileSilo, SAMLauncher, Warship, MIRV)", () => {
expect(shouldPreserveGhostAfterBuild(UnitType.Port)).toBe(false);
expect(shouldPreserveGhostAfterBuild(UnitType.DefensePost)).toBe(false);
expect(shouldPreserveGhostAfterBuild(UnitType.MissileSilo)).toBe(false);
expect(shouldPreserveGhostAfterBuild(UnitType.SAMLauncher)).toBe(false);
expect(shouldPreserveGhostAfterBuild(UnitType.Warship)).toBe(false);
expect(shouldPreserveGhostAfterBuild(UnitType.MIRV)).toBe(false);
});
});
});
describe("samThreatensNukePreview (nuke trajectory threat set, #4226)", () => {
const teammates = new Set([7, 8]);
const allies = new Set([2, 3]);
test("non-friendly SAM threatens the trajectory", () => {
expect(samThreatensNukePreview(5, teammates, allies, new Set())).toBe(true);
});
test("allied SAM does not threaten when the strike breaks no alliance", () => {
expect(samThreatensNukePreview(2, teammates, allies, new Set())).toBe(
false,
);
});
test("would-be-betrayed ally's SAM threatens (alliance breaks at launch)", () => {
expect(samThreatensNukePreview(2, teammates, allies, new Set([2]))).toBe(
true,
);
});
test("other allies' SAMs still excluded when a different ally is betrayed", () => {
expect(samThreatensNukePreview(3, teammates, allies, new Set([2]))).toBe(
false,
);
});
test("teammate SAM does not threaten the trajectory", () => {
expect(samThreatensNukePreview(7, teammates, new Set(), new Set())).toBe(
false,
);
});
test("teammate SAM stays excluded even if listed as betrayed (a strike never breaks a team)", () => {
expect(
samThreatensNukePreview(7, teammates, new Set([7]), new Set([7])),
).toBe(false);
});
});