diff --git a/resources/sprites/samExplosion.png b/resources/sprites/samExplosion.png new file mode 100644 index 000000000..0e6cb8544 Binary files /dev/null and b/resources/sprites/samExplosion.png differ diff --git a/src/client/graphics/AnimatedSpriteLoader.ts b/src/client/graphics/AnimatedSpriteLoader.ts index 63cb67ff6..42f88f8de 100644 --- a/src/client/graphics/AnimatedSpriteLoader.ts +++ b/src/client/graphics/AnimatedSpriteLoader.ts @@ -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> = { originX: 30, originY: 30, }, + [FxType.SAMExplosion]: { + url: SAMExplosion, + frameWidth: 48, + frameCount: 9, + frameDuration: 70, + looping: false, + originX: 23, + originY: 19, + }, }; const animatedSpriteImageMap: Map = new Map(); diff --git a/src/client/graphics/fx/Fx.ts b/src/client/graphics/fx/Fx.ts index 8956c4ca4..a80bae2af 100644 --- a/src/client/graphics/fx/Fx.ts +++ b/src/client/graphics/fx/Fx.ts @@ -4,4 +4,5 @@ export interface Fx { export enum FxType { Nuke = "Nuke", + SAMExplosion = "SAMExplosion", } diff --git a/src/client/graphics/fx/SAMExplosionFx.ts b/src/client/graphics/fx/SAMExplosionFx.ts new file mode 100644 index 000000000..3be5c3a79 --- /dev/null +++ b/src/client/graphics/fx/SAMExplosionFx.ts @@ -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; + } +} diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index f19147b65..34b46b458 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -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 { diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 2439f4462..9910a6aa3 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -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( diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index fdfe635da..9e1ad5b0d 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -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); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 39956ac14..31f581586 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -345,6 +345,8 @@ export interface Unit { targetUnit(): Unit | undefined; setTargetedBySAM(targeted: boolean): void; targetedBySAM(): boolean; + setInterceptedBySam(): void; + interceptedBySam(): boolean; // Health hasHealth(): boolean; diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 8c3df8906..839ca330c 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -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 diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 060142dfe..5ce1266f5 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -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; } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 44564880c..a68f566d7 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -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(); }