From b7519ab9b1ce8d229c47ad734489496c6d30dca5 Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Wed, 29 Oct 2025 21:02:16 +0100 Subject: [PATCH] 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> --- src/client/graphics/fx/NukeAreaFx.ts | 76 +++++++++++++++++++++++++++ src/client/graphics/layers/FxLayer.ts | 11 ++-- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/client/graphics/fx/NukeAreaFx.ts diff --git a/src/client/graphics/fx/NukeAreaFx.ts b/src/client/graphics/fx/NukeAreaFx.ts new file mode 100644 index 000000000..7aca6460b --- /dev/null +++ b/src/client/graphics/fx/NukeAreaFx.ts @@ -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; + } +} diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index 55ef97a4b..d0df2d340 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -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 = new Map(); - private nukeTargetFxByUnitId: Map = new Map(); + private nukeTargetFxByUnitId: Map = 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: