Add new nuke preview FX (#2322)

## Description:

New FX played to preview the nuke explosion area (follow up of
https://github.com/openfrontio/OpenFrontIO/pull/2309)
Will prevent over-nuking the same area in team games



![nuke_area](https://github.com/user-attachments/assets/cbff20b0-3650-43ab-9d93-43211856f12e)

Visuals inspired from
https://github.com/openfrontio/OpenFrontIO/pull/1814 (@ryanbarlow97)

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

IngloriousTom

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
DevelopingTom
2025-10-29 21:02:16 +01:00
committed by GitHub
parent ecced3c6d0
commit b7519ab9b1
2 changed files with 84 additions and 3 deletions
+76
View File
@@ -0,0 +1,76 @@
import { NukeMagnitude } from "../../../core/configuration/Config";
import { Fx } from "./Fx";
export class NukeAreaFx implements Fx {
private lifeTime = 0;
private ended = false;
private readonly endAnimationDuration = 300; // in ms
private readonly startAnimationDuration = 200; // in ms
private readonly innerDiameter: number;
private readonly outerDiameter: number;
private offset = 0;
private readonly dashSize: number;
private readonly rotationSpeed = 20; // px per seconds
private readonly baseAlpha = 0.9;
constructor(
private x: number,
private y: number,
magnitude: NukeMagnitude,
) {
this.innerDiameter = magnitude.inner;
this.outerDiameter = magnitude.outer;
const numDash = Math.max(1, Math.floor(this.outerDiameter / 3));
this.dashSize = (Math.PI / numDash) * this.outerDiameter;
}
end() {
this.ended = true;
this.lifeTime = 0; // reset for fade-out timing
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
this.lifeTime += frameTime;
if (this.ended && this.lifeTime >= this.endAnimationDuration) return false;
let t: number;
if (this.ended) {
t = Math.max(0, 1 - this.lifeTime / this.endAnimationDuration);
} else {
t = Math.min(1, this.lifeTime / this.startAnimationDuration);
}
const alpha = Math.max(0, Math.min(1, this.baseAlpha * t));
ctx.save();
ctx.globalAlpha = alpha;
ctx.lineWidth = 1;
ctx.strokeStyle = `rgba(255,0,0,${alpha})`;
ctx.fillStyle = `rgba(255,0,0,${Math.max(0, alpha - 0.6)})`;
// Inner circle
ctx.beginPath();
ctx.lineWidth = 1;
const innerDiameter =
(this.innerDiameter / 2) * (1 - t) + this.innerDiameter * t;
ctx.arc(this.x, this.y, innerDiameter, 0, Math.PI * 2);
ctx.stroke();
ctx.fill();
// Outer circle
this.offset += this.rotationSpeed * (frameTime / 1000);
ctx.beginPath();
ctx.strokeStyle = `rgba(255,0,0,${alpha})`;
ctx.lineWidth = 1;
ctx.lineDashOffset = this.offset;
ctx.setLineDash([this.dashSize]);
const outerDiameter =
(this.outerDiameter + 20) * (1 - t) + this.outerDiameter * t;
ctx.arc(this.x, this.y, outerDiameter, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
return true;
}
}
+8 -3
View File
@@ -12,6 +12,7 @@ import { renderNumber } from "../../Utils";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { conquestFxFactory } from "../fx/ConquestFx";
import { Fx, FxType } from "../fx/Fx";
import { NukeAreaFx } from "../fx/NukeAreaFx";
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
import { FadeFx, MoveSpriteFx, SpriteFx } from "../fx/SpriteFx";
import { TargetFx } from "../fx/TargetFx";
@@ -32,7 +33,7 @@ export class FxLayer implements Layer {
private allFx: Fx[] = [];
private boatTargetFxByUnitId: Map<number, TargetFx> = new Map();
private nukeTargetFxByUnitId: Map<number, TargetFx> = new Map();
private nukeTargetFxByUnitId: Map<number, NukeAreaFx> = new Map();
constructor(private game: GameView) {
this.theme = this.game.config().theme();
@@ -112,7 +113,11 @@ export class FxLayer implements Layer {
if (t !== undefined) {
const x = this.game.x(t);
const y = this.game.y(t);
const fx = new TargetFx(x, y, 0, true);
const fx = new NukeAreaFx(
x,
y,
this.game.config().nukeMagnitudes(unit.type()),
);
this.allFx.push(fx);
this.nukeTargetFxByUnitId.set(unit.id(), fx);
}
@@ -236,7 +241,7 @@ export class FxLayer implements Layer {
}
case UnitType.AtomBomb: {
this.createNukeTargetFxIfOwned(unit);
this.onNukeEvent(unit, 160);
this.onNukeEvent(unit, 70);
break;
}
case UnitType.MIRVWarhead: