mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
Add ruins and desolation FX on nuke explosions (#847)
## Description: Add a few animations after a nuke exploded: - small fire - big fire - small smoke - big smokes https://github.com/user-attachments/assets/6ef7c1e3-ae3e-4420-aab2-3a3a3630ad98 ## 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:
Binary file not shown.
|
After Width: | Height: | Size: 647 B |
Binary file not shown.
|
After Width: | Height: | Size: 256 B |
Binary file not shown.
|
After Width: | Height: | Size: 345 B |
Binary file not shown.
|
After Width: | Height: | Size: 748 B |
@@ -9,7 +9,7 @@ export class AnimatedSprite {
|
||||
private frameWidth: number,
|
||||
private frameCount: number,
|
||||
private frameDuration: number, // in milliseconds
|
||||
private looping: boolean = true,
|
||||
private looping: boolean = false,
|
||||
private originX: number,
|
||||
private originY: number,
|
||||
) {
|
||||
@@ -42,6 +42,13 @@ export class AnimatedSprite {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
lifeTime(): number | undefined {
|
||||
if (this.looping) {
|
||||
return undefined;
|
||||
}
|
||||
return this.frameDuration * this.frameCount;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D, x: number, y: number) {
|
||||
const drawX = x - this.originX;
|
||||
const drawY = y - this.originY;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
||||
import miniFire from "../../../resources/sprites/minifire.png";
|
||||
import nuke from "../../../resources/sprites/nukeExplosion.png";
|
||||
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
|
||||
import miniSmoke from "../../../resources/sprites/smoke.png";
|
||||
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
|
||||
import { AnimatedSprite } from "./AnimatedSprite";
|
||||
import { FxType } from "./fx/Fx";
|
||||
|
||||
@@ -14,6 +18,42 @@ type AnimatedSpriteConfig = {
|
||||
};
|
||||
|
||||
const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
[FxType.MiniFire]: {
|
||||
url: miniFire,
|
||||
frameWidth: 7,
|
||||
frameCount: 6,
|
||||
frameDuration: 100,
|
||||
looping: true,
|
||||
originX: 3,
|
||||
originY: 11,
|
||||
},
|
||||
[FxType.MiniSmoke]: {
|
||||
url: miniSmoke,
|
||||
frameWidth: 11,
|
||||
frameCount: 4,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 2,
|
||||
originY: 10,
|
||||
},
|
||||
[FxType.MiniBigSmoke]: {
|
||||
url: miniBigSmoke,
|
||||
frameWidth: 24,
|
||||
frameCount: 5,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
},
|
||||
[FxType.MiniSmokeAndFire]: {
|
||||
url: miniSmokeAndFire,
|
||||
frameWidth: 24,
|
||||
frameCount: 5,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
},
|
||||
[FxType.Nuke]: {
|
||||
url: nuke,
|
||||
frameWidth: 60,
|
||||
|
||||
@@ -3,6 +3,10 @@ export interface Fx {
|
||||
}
|
||||
|
||||
export enum FxType {
|
||||
MiniFire = "MiniFire",
|
||||
MiniSmoke = "MiniSmoke",
|
||||
MiniBigSmoke = "MiniBigSmoke",
|
||||
MiniSmokeAndFire = "MiniSmokeAndFire",
|
||||
Nuke = "Nuke",
|
||||
SAMExplosion = "SAMExplosion",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AnimatedSprite } from "../AnimatedSprite";
|
||||
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
import { SpriteFX } from "./SpriteFx";
|
||||
|
||||
/**
|
||||
* Shockwave effect: draw a growing 1px white circle
|
||||
@@ -31,32 +31,72 @@ export class ShockwaveFx implements Fx {
|
||||
}
|
||||
|
||||
/**
|
||||
* Explosion effect: sprite animation of an explosion
|
||||
* Spawn @p number of @p type animation within a perimeter
|
||||
*/
|
||||
export class NukeExplosionFx implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
private nukeExplosionSprite: AnimatedSprite | null;
|
||||
constructor(
|
||||
private x: number,
|
||||
private y: number,
|
||||
private duration: number,
|
||||
) {
|
||||
this.nukeExplosionSprite = createAnimatedSpriteForUnit(FxType.Nuke);
|
||||
}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
if (this.nukeExplosionSprite) {
|
||||
this.lifeTime += frameTime;
|
||||
if (this.lifeTime >= this.duration) {
|
||||
return false;
|
||||
}
|
||||
if (this.nukeExplosionSprite.isActive()) {
|
||||
this.nukeExplosionSprite.update(frameTime);
|
||||
this.nukeExplosionSprite.draw(ctx, this.x, this.y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
function addSpriteInCircle(
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
num: number,
|
||||
type: FxType,
|
||||
result: Fx[],
|
||||
game: GameView,
|
||||
) {
|
||||
const count = Math.max(0, Math.floor(num));
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const distance = Math.random() * (radius / 2);
|
||||
const spawnX = Math.floor(x + Math.cos(angle) * distance);
|
||||
const spawnY = Math.floor(y + Math.sin(angle) * distance);
|
||||
if (
|
||||
game.isValidCoord(spawnX, spawnY) &&
|
||||
game.isLand(game.ref(spawnX, spawnY))
|
||||
) {
|
||||
const sprite = new SpriteFX(spawnX, spawnY, type, 6000, 0.1, 0.8);
|
||||
result.push(sprite as Fx);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explosion effect:
|
||||
* - explosion animation
|
||||
* - shockwave
|
||||
* - ruins and desolation fx
|
||||
*/
|
||||
export function nukeFxFactory(
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
game: GameView,
|
||||
): Fx[] {
|
||||
const nukeFx: Fx[] = [];
|
||||
// Explosion animation
|
||||
nukeFx.push(new SpriteFX(x, y, FxType.Nuke) as Fx);
|
||||
// Shockwave animation
|
||||
nukeFx.push(new ShockwaveFx(x, y, 1500, radius * 1.5));
|
||||
// Ruins and desolation sprites
|
||||
const debrisPlan: Array<{
|
||||
type: FxType;
|
||||
radiusFactor: number;
|
||||
density: number;
|
||||
}> = [
|
||||
{ type: FxType.MiniFire, radiusFactor: 1.0, density: 1 / 25 },
|
||||
{ type: FxType.MiniSmoke, radiusFactor: 1.0, density: 1 / 28 },
|
||||
{ type: FxType.MiniBigSmoke, radiusFactor: 0.9, density: 1 / 70 },
|
||||
{ type: FxType.MiniSmokeAndFire, radiusFactor: 0.9, density: 1 / 70 },
|
||||
];
|
||||
|
||||
for (const { type, radiusFactor, density } of debrisPlan) {
|
||||
addSpriteInCircle(
|
||||
x,
|
||||
y,
|
||||
radius * radiusFactor,
|
||||
radius * density,
|
||||
type,
|
||||
nukeFx,
|
||||
game,
|
||||
);
|
||||
}
|
||||
return nukeFx;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { consolex } from "../../../core/Consolex";
|
||||
import { AnimatedSprite } from "../AnimatedSprite";
|
||||
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
|
||||
function fadeInOut(
|
||||
t: number,
|
||||
fadeIn: number = 0.3,
|
||||
fadeOut: number = 0.7,
|
||||
): number {
|
||||
if (t < fadeIn) {
|
||||
const f = t / fadeIn; // Map to [0, 1]
|
||||
return f * f;
|
||||
} else if (t < fadeOut) {
|
||||
return 1;
|
||||
} else {
|
||||
const f = (t - fadeOut) / (1 - fadeOut); // Map to [0, 1]
|
||||
return 1 - f * f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple FX displaying an animated sprite
|
||||
*/
|
||||
export class SpriteFX implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
private animatedSprite: AnimatedSprite | null;
|
||||
private totalLifeTime: number = 0;
|
||||
constructor(
|
||||
private x: number,
|
||||
private y: number,
|
||||
fxType: FxType,
|
||||
duration?: number,
|
||||
private fadeIn?: number,
|
||||
private fadeOut?: number,
|
||||
) {
|
||||
this.animatedSprite = createAnimatedSpriteForUnit(fxType);
|
||||
if (!this.animatedSprite) {
|
||||
consolex.error("Could not load animated sprite ", fxType);
|
||||
this.totalLifeTime = 0;
|
||||
} else if (!duration) {
|
||||
// When no duration set, rely on the sprite lifetime
|
||||
this.totalLifeTime = this.animatedSprite.lifeTime() ?? 1000; // 1s by default
|
||||
} else {
|
||||
this.totalLifeTime = duration;
|
||||
}
|
||||
}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
if (this.animatedSprite) {
|
||||
this.lifeTime += frameTime;
|
||||
if (this.lifeTime >= this.totalLifeTime) {
|
||||
return false;
|
||||
}
|
||||
if (this.animatedSprite.isActive()) {
|
||||
ctx.save();
|
||||
const t = this.lifeTime / this.totalLifeTime;
|
||||
ctx.globalAlpha = fadeInOut(t, this.fadeIn ?? 0, this.fadeOut ?? 0.7);
|
||||
this.animatedSprite.update(frameTime);
|
||||
this.animatedSprite.draw(ctx, this.x, this.y);
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { loadAllAnimatedSpriteImages } from "../AnimatedSpriteLoader";
|
||||
import { Fx } from "../fx/Fx";
|
||||
import { NukeExplosionFx, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { SAMExplosionFx } from "../fx/SAMExplosionFx";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@@ -36,41 +36,39 @@ export class FxLayer implements Layer {
|
||||
switch (unit.type()) {
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.MIRVWarhead:
|
||||
this.handleNukes(unit, 70);
|
||||
this.onNukeEvent(unit, 70);
|
||||
break;
|
||||
case UnitType.HydrogenBomb:
|
||||
this.handleNukes(unit, 250);
|
||||
this.onNukeEvent(unit, 160);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleNukes(unit: UnitView, shockwaveRadius: number) {
|
||||
onNukeEvent(unit: UnitView, radius: number) {
|
||||
if (!unit.isActive()) {
|
||||
if (unit.wasInterceptedBySAM()) {
|
||||
this.handleSAMInterception(unit);
|
||||
} else {
|
||||
// Kaboom
|
||||
this.handleNukeExplosion(unit, shockwaveRadius);
|
||||
this.handleNukeExplosion(unit, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNukeExplosion(unit: UnitView, shockwaveRadius: number) {
|
||||
handleNukeExplosion(unit: UnitView, radius: 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);
|
||||
const nukeFx = nukeFxFactory(x, y, radius, this.game);
|
||||
this.allFx = this.allFx.concat(nukeFx);
|
||||
}
|
||||
|
||||
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);
|
||||
this.allFx.push(interception);
|
||||
const shockwave = new ShockwaveFx(x, y, 800, 40);
|
||||
this.allFx.push(shockwave as Fx);
|
||||
this.allFx.push(shockwave);
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
||||
Reference in New Issue
Block a user