Add SAM interception FX (#830)

## Description:
Add SAM interception animation:


https://github.com/user-attachments/assets/5bfae4f2-f040-41cb-8fba-790538091807

Previously an intercepted nuke detonated with the regular nuke
explosion, which was confusing.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

IngloriousTom
This commit is contained in:
DevelopingTom
2025-05-22 23:11:31 +02:00
committed by GitHub
parent 9302af868d
commit 85c03d659c
11 changed files with 94 additions and 10 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,4 +1,5 @@
import nuke from "../../../resources/sprites/nukeExplosion.png";
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
import { AnimatedSprite } from "./AnimatedSprite";
import { FxType } from "./fx/Fx";
@@ -22,6 +23,15 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 30,
originY: 30,
},
[FxType.SAMExplosion]: {
url: SAMExplosion,
frameWidth: 48,
frameCount: 9,
frameDuration: 70,
looping: false,
originX: 23,
originY: 19,
},
};
const animatedSpriteImageMap: Map<FxType, CanvasImageSource> = new Map();
+1
View File
@@ -4,4 +4,5 @@ export interface Fx {
export enum FxType {
Nuke = "Nuke",
SAMExplosion = "SAMExplosion",
}
+34
View File
@@ -0,0 +1,34 @@
import { AnimatedSprite } from "../AnimatedSprite";
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
/**
* Explosion effect: sprite animation of an explosion
*/
export class SAMExplosionFx implements Fx {
private lifeTime: number = 0;
private explosionSprite: AnimatedSprite | null;
constructor(
private x: number,
private y: number,
private duration: number,
) {
this.explosionSprite = createAnimatedSpriteForUnit(FxType.SAMExplosion);
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
if (this.explosionSprite) {
this.lifeTime += frameTime;
if (this.lifeTime >= this.duration) {
return false;
}
if (this.explosionSprite.isActive()) {
this.explosionSprite.update(frameTime);
this.explosionSprite.draw(ctx, this.x, this.y);
return true;
}
return false;
}
return false;
}
}
+28 -9
View File
@@ -4,6 +4,7 @@ import { GameView, UnitView } from "../../../core/game/GameView";
import { loadAllAnimatedSpriteImages } from "../AnimatedSpriteLoader";
import { Fx } from "../fx/Fx";
import { NukeExplosionFx, ShockwaveFx } from "../fx/NukeFx";
import { SAMExplosionFx } from "../fx/SAMExplosionFx";
import { Layer } from "./Layer";
export class FxLayer implements Layer {
@@ -35,25 +36,43 @@ export class FxLayer implements Layer {
switch (unit.type()) {
case UnitType.AtomBomb:
case UnitType.MIRVWarhead:
this.handleNukeExplosion(unit, 70);
this.handleNukes(unit, 70);
break;
case UnitType.HydrogenBomb:
this.handleNukeExplosion(unit, 250);
this.handleNukes(unit, 250);
break;
}
}
handleNukeExplosion(unit: UnitView, shockwaveRadius: number) {
handleNukes(unit: UnitView, shockwaveRadius: number) {
if (!unit.isActive()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const nuke = new NukeExplosionFx(x, y, 1000);
this.allFx.push(nuke as Fx);
const shockwave = new ShockwaveFx(x, y, 1500, shockwaveRadius);
this.allFx.push(shockwave as Fx);
if (unit.wasInterceptedBySAM()) {
this.handleSAMInterception(unit);
} else {
// Kaboom
this.handleNukeExplosion(unit, shockwaveRadius);
}
}
}
handleNukeExplosion(unit: UnitView, shockwaveRadius: number) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const nuke = new NukeExplosionFx(x, y, 1000);
this.allFx.push(nuke as Fx);
const shockwave = new ShockwaveFx(x, y, 1500, shockwaveRadius);
this.allFx.push(shockwave as Fx);
}
handleSAMInterception(unit: UnitView) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const interception = new SAMExplosionFx(x, y, 1000);
this.allFx.push(interception as Fx);
const shockwave = new ShockwaveFx(x, y, 800, 40);
this.allFx.push(shockwave as Fx);
}
async init() {
this.redraw();
try {
+4 -1
View File
@@ -180,7 +180,10 @@ export class SAMLauncherExecution implements Execution {
this.sam.owner().id(),
);
// Delete warheads
mirvWarheadTargets.forEach((u) => u.delete());
mirvWarheadTargets.forEach((u) => {
u.setInterceptedBySam();
u.delete();
});
} else if (target !== null) {
target.setTargetedBySAM(true);
this.mg.addExecution(
@@ -66,6 +66,7 @@ export class SAMMissileExecution implements Execution {
this._owner.id(),
);
this.active = false;
this.target.setInterceptedBySam();
this.target.delete(true, this._owner);
this.SAMMissile.delete(false);
+2
View File
@@ -345,6 +345,8 @@ export interface Unit {
targetUnit(): Unit | undefined;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
setInterceptedBySam(): void;
interceptedBySam(): boolean;
// Health
hasHealth(): boolean;
+1
View File
@@ -73,6 +73,7 @@ export interface UnitUpdate {
pos: TileRef;
lastPos: TileRef;
isActive: boolean;
wasIntercepted: boolean;
retreating: boolean;
targetUnitId?: number; // Only for trade ships
targetTile?: TileRef; // Only for nukes
+3
View File
@@ -93,6 +93,9 @@ export class UnitView {
isActive(): boolean {
return this.data.isActive;
}
wasInterceptedBySAM(): boolean {
return this.data.wasIntercepted;
}
hasHealth(): boolean {
return this.data.health !== undefined;
}
+10
View File
@@ -21,6 +21,7 @@ export class UnitImpl implements Unit {
private _lastTile: TileRef;
private _retreating: boolean = false;
private _targetedBySAM = false;
private _interceptedBySAM = false;
private _lastSetSafeFromPirates: number; // Only for trade ships
private _constructionType: UnitType | undefined;
private _lastOwner: PlayerImpl | null = null;
@@ -85,6 +86,7 @@ export class UnitImpl implements Unit {
ownerID: this._owner.smallID(),
lastOwnerID: this._lastOwner?.smallID(),
isActive: this._active,
wasIntercepted: this._interceptedBySAM,
retreating: this._retreating,
pos: this._tile,
lastPos: this._lastTile,
@@ -304,6 +306,14 @@ export class UnitImpl implements Unit {
return this._targetedBySAM;
}
setInterceptedBySam(): void {
this._interceptedBySAM = true;
}
interceptedBySam(): boolean {
return this._interceptedBySAM;
}
setSafeFromPirates(): void {
this._lastSetSafeFromPirates = this.mg.ticks();
}