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. nukepr ## 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),