From 3dadfbd23d731e450e042720430b65caa5e5699e Mon Sep 17 00:00:00 2001
From: bibizu <104801209+bibizu@users.noreply.github.com>
Date: Sun, 18 Jan 2026 23:56:43 -0500
Subject: [PATCH] feat: Nuke trajectory prediction now accounts for alliance
breakage. (#2912)
## Description:
Nuke trajectory prediction now will show interception with allied SAMs
if the alliance will break on nuke launch.
Code was also refactored to be shared a bit more.
In addition, if an incoming alliance would break if accepted, the nuke
launch will break the alliance.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
bibizu
---
.../layers/NukeTrajectoryPreviewLayer.ts | 16 +++++++-
src/core/execution/NukeExecution.ts | 40 ++++++-------------
src/core/execution/Util.ts | 36 ++++++++++++++++-
3 files changed, 63 insertions(+), 29 deletions(-)
diff --git a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
index 36bf818c7..0d1dfc9d0 100644
--- a/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
+++ b/src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
@@ -1,4 +1,5 @@
import { EventBus } from "../../../core/EventBus";
+import { listNukeBreakAlliance } from "../../../core/execution/Util";
import { UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
@@ -258,6 +259,18 @@ export class NukeTrajectoryPreviewLayer implements Layer {
break;
}
}
+ const playersToBreakAllianceWith = listNukeBreakAlliance({
+ game: this.game,
+ targetTile,
+ magnitude: this.game.config().nukeMagnitudes(ghostStructure),
+ allySmallIds: new Set(
+ this.game
+ .myPlayer()
+ ?.allies()
+ .map((a) => a.smallID()),
+ ),
+ threshold: this.game.config().nukeAllianceBreakThreshold(),
+ });
// Find the point where SAM can intercept
this.targetedIndex = this.trajectoryPoints.length;
// Check trajectory
@@ -270,7 +283,8 @@ export class NukeTrajectoryPreviewLayer implements Layer {
)) {
if (
sam.unit.owner().isMe() ||
- this.game.myPlayer()?.isFriendly(sam.unit.owner())
+ (this.game.myPlayer()?.isFriendly(sam.unit.owner()) &&
+ !playersToBreakAllianceWith.has(sam.unit.owner().smallID()))
) {
continue;
}
diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts
index 277057c5f..68db6bdf4 100644
--- a/src/core/execution/NukeExecution.ts
+++ b/src/core/execution/NukeExecution.ts
@@ -4,7 +4,6 @@ import {
isStructureType,
MessageType,
Player,
- StructureTypes,
TerraNullius,
TrajectoryTile,
Unit,
@@ -16,7 +15,7 @@ import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola"
import { PathStatus } from "../pathfinding/types";
import { PseudoRandom } from "../PseudoRandom";
import { NukeType } from "../StatsSchemas";
-import { computeNukeBlastCounts } from "./Util";
+import { listNukeBreakAlliance } from "./Util";
const SPRITE_RADIUS = 16;
@@ -85,36 +84,22 @@ export class NukeExecution implements Execution {
}
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
- const threshold = this.mg.config().nukeAllianceBreakThreshold();
- // Use shared utility to compute weighted tile counts per player
- const blastCounts = computeNukeBlastCounts({
- gm: this.mg,
+ const playersToBreakAllianceWith = listNukeBreakAlliance({
+ game: this.mg,
targetTile: this.dst,
magnitude,
+ allySmallIds: new Set(this.player.allies().map((a) => a.smallID())),
+ threshold: this.mg.config().nukeAllianceBreakThreshold(),
});
- // Collect all players that should have alliance broken:
- // either exceeds tile threshold OR has a structure in blast radius
- const playersToBreakAllianceWith = new Set();
-
- for (const [playerSmallId, totalWeight] of blastCounts) {
- if (totalWeight > threshold) {
- playersToBreakAllianceWith.add(playerSmallId);
+ // Automatically reject incoming alliance requests.
+ for (const incoming of this.player.incomingAllianceRequests()) {
+ if (playersToBreakAllianceWith.has(incoming.requestor().smallID())) {
+ incoming.reject();
}
}
- // Also check if any allied structures would be destroyed
- this.mg
- .nearbyUnits(this.dst, magnitude.outer, [...StructureTypes])
- .filter(
- ({ unit }) =>
- unit.owner().isPlayer() && this.player.isAlliedWith(unit.owner()),
- )
- .forEach(({ unit }) =>
- playersToBreakAllianceWith.add(unit.owner().smallID()),
- );
-
for (const playerSmallId of playersToBreakAllianceWith) {
const attackedPlayer = this.mg.playerBySmallID(playerSmallId);
if (!attackedPlayer.isPlayer()) {
@@ -123,11 +108,12 @@ export class NukeExecution implements Execution {
// Resolves exploit of alliance breaking in which a pending alliance request
// was accepted in the middle of a missile attack.
- const allianceRequest = attackedPlayer
+ const outgoingAllianceRequest = attackedPlayer
.incomingAllianceRequests()
.find((ar) => ar.requestor() === this.player);
- if (allianceRequest) {
- allianceRequest.reject();
+ if (outgoingAllianceRequest) {
+ outgoingAllianceRequest.reject();
+ continue;
}
const alliance = this.player.allianceWith(attackedPlayer);
diff --git a/src/core/execution/Util.ts b/src/core/execution/Util.ts
index f0b5b95b1..a4a414e89 100644
--- a/src/core/execution/Util.ts
+++ b/src/core/execution/Util.ts
@@ -36,7 +36,7 @@ export function computeNukeBlastCounts(
}
export interface NukeAllianceCheckParams {
- game: GameView;
+ game: Game | GameView;
targetTile: TileRef;
magnitude: NukeMagnitude;
allySmallIds: Set;
@@ -93,6 +93,40 @@ export function wouldNukeBreakAlliance(
return result;
}
+// Same as wouldNukeBreakAlliance(), but takes time to find every player
+// that would be "angered" from this nuke.
+// This includes unallied players!
+export function listNukeBreakAlliance(
+ params: NukeAllianceCheckParams,
+): Set {
+ const { game, targetTile, magnitude, threshold } = params;
+
+ // Collect all players that should have alliance broken:
+ // either exceeds tile threshold OR has a structure in blast radius
+ const playersToBreakAllianceWith = new Set();
+
+ // compute tile breakage threshold
+ const blastCounts = computeNukeBlastCounts({
+ gm: game,
+ targetTile,
+ magnitude,
+ });
+ for (const [playerSmallId, totalWeight] of blastCounts) {
+ if (totalWeight > threshold) {
+ playersToBreakAllianceWith.add(playerSmallId);
+ }
+ }
+
+ // Also check if any allied structures would be destroyed
+ game
+ .nearbyUnits(targetTile, magnitude.outer, [...StructureTypes])
+ .forEach(({ unit }) =>
+ playersToBreakAllianceWith.add(unit.owner().smallID()),
+ );
+
+ return playersToBreakAllianceWith;
+}
+
export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] {
return Array.from(gm.bfs(tile, euclDistFN(tile, 4, true))).filter(
(t) => !gm.hasOwner(t) && gm.isLand(t),