Add red warning circle when nuke would break alliance (#2728)

## Description:

When placing a nuke (Atom Bomb or Hydrogen Bomb), the range circle now
turns red to warn players when the attack would break an alliance.

<img width="456" height="333" alt="Screenshot 2025-12-28 211927"
src="https://github.com/user-attachments/assets/dfe6f874-3f8b-4662-8877-0af30aa20139"
/>


## 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:

abodcraft1

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
Abdallah Bahrawi
2025-12-30 19:55:15 +02:00
committed by GitHub
parent 4f3d9df46a
commit 09a1cf885f
4 changed files with 142 additions and 41 deletions
@@ -454,6 +454,7 @@ export class SpriteFactory {
stage: PIXI.Container,
pos: { x: number; y: number },
level?: number,
targetingAlly: boolean = false,
): PIXI.Container | null {
if (stage === undefined) throw new Error("Not initialized");
const parentContainer = new PIXI.Container();
@@ -478,10 +479,18 @@ export class SpriteFactory {
default:
return null;
}
// Add warning colors (red/orange) when targeting an ally to indicate alliance will break
const isNuke = type === UnitType.AtomBomb || type === UnitType.HydrogenBomb;
const fillColor = targetingAlly && isNuke ? 0xff6b35 : 0xffffff;
const fillAlpha = targetingAlly && isNuke ? 0.35 : 0.2;
const strokeColor = targetingAlly && isNuke ? 0xff4444 : 0xffffff;
const strokeAlpha = targetingAlly && isNuke ? 0.8 : 0.5;
const strokeWidth = targetingAlly && isNuke ? 2 : 1;
circle
.circle(0, 0, radius)
.fill({ color: 0xffffff, alpha: 0.2 })
.stroke({ width: 1, color: 0xffffff, alpha: 0.5 });
.fill({ color: fillColor, alpha: fillAlpha })
.stroke({ width: strokeWidth, color: strokeColor, alpha: strokeAlpha });
parentContainer.addChild(circle);
parentContainer.position.set(pos.x, pos.y);
parentContainer.scale.set(this.transformHandler.scale);
@@ -4,6 +4,7 @@ import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { wouldNukeBreakAlliance } from "../../../core/execution/Util";
import {
BuildableUnit,
Cell,
@@ -65,6 +66,7 @@ export class StructureIconsLayer implements Layer {
priceBox: { height: number; y: number; paddingX: number; minWidth: number };
range: PIXI.Container | null;
rangeLevel?: number;
targetingAlly?: boolean;
buildableUnit: BuildableUnit;
} | null = null;
private pixicanvas: HTMLCanvasElement;
@@ -258,6 +260,29 @@ export class StructureIconsLayer implements Layer {
tileRef = this.game.ref(tile.x, tile.y);
}
// Check if targeting an ally (for nuke warning visual)
// Uses shared logic with NukeExecution.maybeBreakAlliances()
let targetingAlly = false;
const myPlayer = this.game.myPlayer();
const nukeType = this.ghostUnit.buildableUnit.type;
if (
tileRef &&
myPlayer &&
(nukeType === UnitType.AtomBomb || nukeType === UnitType.HydrogenBomb)
) {
// Only check if player has allies
const allies = myPlayer.allies();
if (allies.length > 0) {
targetingAlly = wouldNukeBreakAlliance({
gm: this.game,
targetTile: tileRef,
magnitude: this.game.config().nukeMagnitudes(nukeType),
allySmallIds: new Set(allies.map((a) => a.smallID())),
threshold: this.game.config().nukeAllianceBreakThreshold(),
});
}
}
this.game
?.myPlayer()
?.actions(tileRef)
@@ -292,7 +317,7 @@ export class StructureIconsLayer implements Layer {
this.updateGhostPrice(unit.cost ?? 0, showPrice);
const targetLevel = this.resolveGhostRangeLevel(unit);
this.updateGhostRange(targetLevel);
this.updateGhostRange(targetLevel, targetingAlly);
if (unit.canUpgrade) {
this.potentialUpgrade = this.renders.find(
@@ -470,18 +495,23 @@ export class StructureIconsLayer implements Layer {
return 1;
}
private updateGhostRange(level?: number) {
private updateGhostRange(level?: number, targetingAlly: boolean = false) {
if (!this.ghostUnit) {
return;
}
if (this.ghostUnit.range && this.ghostUnit.rangeLevel === level) {
if (
this.ghostUnit.range &&
this.ghostUnit.rangeLevel === level &&
this.ghostUnit.targetingAlly === targetingAlly
) {
return;
}
this.ghostUnit.range?.destroy();
this.ghostUnit.range = null;
this.ghostUnit.rangeLevel = level;
this.ghostUnit.targetingAlly = targetingAlly;
const position = this.ghostUnit.container.position;
const range = this.factory.createRange(
@@ -489,6 +519,7 @@ export class StructureIconsLayer implements Layer {
this.ghostStage,
{ x: position.x, y: position.y },
level,
targetingAlly,
);
if (range) {
this.ghostUnit.range = range;