mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-12 09:43:56 +00:00
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>
This commit is contained in:
@@ -43,15 +43,21 @@ export function shouldPreserveGhostAfterBuild(unitType: UnitType): boolean {
|
||||
|
||||
/**
|
||||
* Whether a SAM belongs in the nuke trajectory preview's threat set.
|
||||
* Allied SAMs are excluded unless the strike would betray that ally —
|
||||
* the alliance breaks at launch, so their SAMs will engage the nuke.
|
||||
* Mirrors SAMLauncherExecution: a SAM ignores a nuke whose owner it's
|
||||
* friendly with (same team OR allied).
|
||||
* Teammates are excluded unconditionally — a strike can break an alliance
|
||||
* but never a team relationship, so a teammate's SAM never engages.
|
||||
* Allied SAMs are excluded unless the strike would betray that ally — the
|
||||
* alliance breaks at launch, so their SAMs will engage the nuke.
|
||||
* (Own SAMs never threaten; the caller filters those out first.)
|
||||
*/
|
||||
export function samThreatensNukePreview(
|
||||
samOwnerSmallId: number,
|
||||
teammateSmallIds: ReadonlySet<number>,
|
||||
allySmallIds: ReadonlySet<number>,
|
||||
betrayedSmallIds: ReadonlySet<number>,
|
||||
): boolean {
|
||||
if (teammateSmallIds.has(samOwnerSmallId)) return false;
|
||||
return (
|
||||
!allySmallIds.has(samOwnerSmallId) || betrayedSmallIds.has(samOwnerSmallId)
|
||||
);
|
||||
@@ -331,10 +337,15 @@ export class BuildPreviewController implements Controller {
|
||||
const srcX = this.game.x(bestSilo.tile());
|
||||
const srcY = this.game.y(bestSilo.tile());
|
||||
|
||||
// Non-allied SAMs threaten the trajectory; own + allied SAMs don't —
|
||||
// except allies this strike would betray: the alliance breaks at launch
|
||||
// (NukeExecution.maybeBreakAlliances), so their SAMs will intercept.
|
||||
// Non-friendly SAMs threaten the trajectory; own + teammate + allied SAMs
|
||||
// don't — except allies this strike would betray: the alliance breaks at
|
||||
// launch (NukeExecution.maybeBreakAlliances), so their SAMs will intercept.
|
||||
// Teammates have no such exception (a strike never breaks a team).
|
||||
// listNukeBreakAlliance is the same function the sim uses there.
|
||||
const teammateIds = new Set<number>();
|
||||
for (const p of this.game.players()) {
|
||||
if (myPlayer.isOnSameTeam(p)) teammateIds.add(p.smallID());
|
||||
}
|
||||
const allyIds = new Set<number>();
|
||||
for (const a of myPlayer.allies()) allyIds.add(a.smallID());
|
||||
const betrayedIds: ReadonlySet<number> =
|
||||
@@ -351,7 +362,14 @@ export class BuildPreviewController implements Controller {
|
||||
if (!s.isActive()) continue;
|
||||
const owner = s.owner();
|
||||
if (owner === myPlayer) continue;
|
||||
if (!samThreatensNukePreview(owner.smallID(), allyIds, betrayedIds)) {
|
||||
if (
|
||||
!samThreatensNukePreview(
|
||||
owner.smallID(),
|
||||
teammateIds,
|
||||
allyIds,
|
||||
betrayedIds,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const r = this.game.config().samRange(s.level());
|
||||
|
||||
Reference in New Issue
Block a user