mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
delete dead canvas2D FX system
graphics/fx/ (6 files) and the AnimatedSprite/AnimatedSpriteLoader pair were the canvas2D-era visual-effects pipeline. WebGL has its own FX stack now (render/gl/passes/fx-pass/), so nothing outside the dead cluster imported any of these. The only "reference" left was a stale comment in fx-sprite-pass.ts.
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
export class AnimatedSprite {
|
||||
private frameHeight: number;
|
||||
private frameWidth: number;
|
||||
private currentFrame: number = 0;
|
||||
private elapsedTime: number = 0;
|
||||
private active: boolean = true;
|
||||
|
||||
constructor(
|
||||
private image: CanvasImageSource,
|
||||
private frameCount: number,
|
||||
private frameDuration: number, // in milliseconds
|
||||
private looping: boolean = false,
|
||||
private originX: number,
|
||||
private originY: number,
|
||||
) {
|
||||
if (frameCount <= 0) {
|
||||
throw new Error("Animated sprite should at least have one frame");
|
||||
}
|
||||
if ("height" in image && "width" in image) {
|
||||
this.frameHeight = (image as HTMLImageElement | HTMLCanvasElement).height;
|
||||
this.frameWidth = Math.floor(
|
||||
(image as HTMLImageElement | HTMLCanvasElement).width / frameCount,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Image source must have 'width' and 'height' properties.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (!this.active) return;
|
||||
this.elapsedTime += deltaTime;
|
||||
if (this.elapsedTime >= this.frameDuration) {
|
||||
this.elapsedTime -= this.frameDuration;
|
||||
this.currentFrame++;
|
||||
|
||||
if (this.currentFrame >= this.frameCount) {
|
||||
if (this.looping) {
|
||||
this.currentFrame = 0;
|
||||
} else {
|
||||
this.currentFrame = this.frameCount - 1;
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
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;
|
||||
|
||||
ctx.drawImage(
|
||||
this.image,
|
||||
this.currentFrame * this.frameWidth,
|
||||
0,
|
||||
this.frameWidth,
|
||||
this.frameHeight,
|
||||
drawX,
|
||||
drawY,
|
||||
this.frameWidth,
|
||||
this.frameHeight,
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.currentFrame = 0;
|
||||
this.elapsedTime = 0;
|
||||
}
|
||||
|
||||
setOrigin(xRatio: number, yRatio: number) {
|
||||
this.originX = xRatio;
|
||||
this.originY = yRatio;
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { Theme } from "src/core/configuration/Theme";
|
||||
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
||||
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
|
||||
import conquestSword from "../../../resources/sprites/conquestSword.png";
|
||||
import dust from "../../../resources/sprites/dust.png";
|
||||
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
|
||||
import miniFire from "../../../resources/sprites/minifire.png";
|
||||
import nuke from "../../../resources/sprites/nukeExplosion.png";
|
||||
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
|
||||
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
|
||||
import miniSmoke from "../../../resources/sprites/smoke.png";
|
||||
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
|
||||
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
|
||||
import { PlayerView } from "../../core/game/GameView";
|
||||
import { AnimatedSprite } from "./AnimatedSprite";
|
||||
import { FxType } from "./fx/Fx";
|
||||
import { colorizeCanvas } from "./SpriteLoader";
|
||||
|
||||
type AnimatedSpriteConfig = {
|
||||
url: string;
|
||||
frameCount: number;
|
||||
frameDuration: number; // ms per frame
|
||||
looping?: boolean;
|
||||
originX: number;
|
||||
originY: number;
|
||||
};
|
||||
|
||||
const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
[FxType.MiniFire]: {
|
||||
url: miniFire,
|
||||
frameCount: 6,
|
||||
frameDuration: 100,
|
||||
looping: true,
|
||||
originX: 3,
|
||||
originY: 11,
|
||||
},
|
||||
[FxType.MiniSmoke]: {
|
||||
url: miniSmoke,
|
||||
frameCount: 4,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 2,
|
||||
originY: 10,
|
||||
},
|
||||
[FxType.MiniBigSmoke]: {
|
||||
url: miniBigSmoke,
|
||||
frameCount: 5,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
},
|
||||
[FxType.MiniSmokeAndFire]: {
|
||||
url: miniSmokeAndFire,
|
||||
frameCount: 6,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
},
|
||||
[FxType.MiniExplosion]: {
|
||||
url: miniExplosion,
|
||||
frameCount: 4,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 6,
|
||||
originY: 6,
|
||||
},
|
||||
[FxType.Dust]: {
|
||||
url: dust,
|
||||
frameCount: 3,
|
||||
frameDuration: 100,
|
||||
looping: false,
|
||||
originX: 4,
|
||||
originY: 5,
|
||||
},
|
||||
[FxType.UnitExplosion]: {
|
||||
url: unitExplosion,
|
||||
frameCount: 4,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 9,
|
||||
originY: 9,
|
||||
},
|
||||
[FxType.BuildingExplosion]: {
|
||||
url: buildingExplosion,
|
||||
frameCount: 10,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 8,
|
||||
originY: 8,
|
||||
},
|
||||
[FxType.SinkingShip]: {
|
||||
url: sinkingShip,
|
||||
frameCount: 14,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 7,
|
||||
originY: 7,
|
||||
},
|
||||
[FxType.Nuke]: {
|
||||
url: nuke,
|
||||
frameCount: 9,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 30,
|
||||
originY: 30,
|
||||
},
|
||||
[FxType.SAMExplosion]: {
|
||||
url: SAMExplosion,
|
||||
frameCount: 9,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 23,
|
||||
originY: 19,
|
||||
},
|
||||
[FxType.Conquest]: {
|
||||
url: conquestSword,
|
||||
frameCount: 10,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 10,
|
||||
originY: 16,
|
||||
},
|
||||
};
|
||||
|
||||
export class AnimatedSpriteLoader {
|
||||
private animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
|
||||
// Do not color the same sprite twice
|
||||
private coloredAnimatedSpriteCache: Map<string, HTMLCanvasElement> =
|
||||
new Map();
|
||||
|
||||
public async loadAllAnimatedSpriteImages(): Promise<void> {
|
||||
const entries = Object.entries(ANIMATED_SPRITE_CONFIG);
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async ([fxType, config]) => {
|
||||
const typedFxType = fxType as FxType;
|
||||
if (!config?.url) return;
|
||||
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = config.url;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = (e) => reject(e);
|
||||
});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
canvas.getContext("2d")!.drawImage(img, 0, 0);
|
||||
|
||||
this.animatedSpriteImageMap.set(typedFxType, canvas);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load sprite for ${typedFxType}:`, err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private createRegularAnimatedSprite(fxType: FxType): AnimatedSprite | null {
|
||||
const config = ANIMATED_SPRITE_CONFIG[fxType];
|
||||
const image = this.animatedSpriteImageMap.get(fxType);
|
||||
if (!config || !image) return null;
|
||||
|
||||
return new AnimatedSprite(
|
||||
image,
|
||||
config.frameCount,
|
||||
config.frameDuration,
|
||||
config.looping ?? true,
|
||||
config.originX,
|
||||
config.originY,
|
||||
);
|
||||
}
|
||||
|
||||
private getColoredAnimatedSprite(
|
||||
owner: PlayerView,
|
||||
fxType: FxType,
|
||||
theme: Theme,
|
||||
): HTMLCanvasElement | null {
|
||||
const baseImage = this.animatedSpriteImageMap.get(fxType);
|
||||
const config = ANIMATED_SPRITE_CONFIG[fxType];
|
||||
if (!baseImage || !config) return null;
|
||||
const territoryColor = owner.territoryColor();
|
||||
const borderColor = owner.borderColor();
|
||||
const spawnHighlightColor = theme.spawnHighlightColor();
|
||||
const key = `${fxType}-${owner.id()}`;
|
||||
let coloredCanvas: HTMLCanvasElement;
|
||||
if (this.coloredAnimatedSpriteCache.has(key)) {
|
||||
coloredCanvas = this.coloredAnimatedSpriteCache.get(key)!;
|
||||
} else {
|
||||
coloredCanvas = colorizeCanvas(
|
||||
baseImage,
|
||||
territoryColor,
|
||||
borderColor,
|
||||
spawnHighlightColor,
|
||||
);
|
||||
|
||||
this.coloredAnimatedSpriteCache.set(key, coloredCanvas);
|
||||
}
|
||||
return coloredCanvas;
|
||||
}
|
||||
|
||||
private createColoredAnimatedSpriteForUnit(
|
||||
fxType: FxType,
|
||||
owner: PlayerView,
|
||||
theme: Theme,
|
||||
): AnimatedSprite | null {
|
||||
const config = ANIMATED_SPRITE_CONFIG[fxType];
|
||||
const image = this.getColoredAnimatedSprite(owner, fxType, theme);
|
||||
if (!config || !image) return null;
|
||||
|
||||
return new AnimatedSprite(
|
||||
image,
|
||||
config.frameCount,
|
||||
config.frameDuration,
|
||||
config.looping ?? true,
|
||||
config.originX,
|
||||
config.originY,
|
||||
);
|
||||
}
|
||||
|
||||
public createAnimatedSprite(
|
||||
fxType: FxType,
|
||||
owner?: PlayerView,
|
||||
theme?: Theme,
|
||||
): AnimatedSprite | null {
|
||||
if (owner && theme) {
|
||||
return this.createColoredAnimatedSpriteForUnit(fxType, owner, theme);
|
||||
}
|
||||
return this.createRegularAnimatedSprite(fxType);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ConquestUpdate } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
import { FadeFx, SpriteFx } from "./SpriteFx";
|
||||
|
||||
/**
|
||||
* Conquest FX:
|
||||
* - conquest sprite
|
||||
*/
|
||||
export function conquestFxFactory(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
conquest: ConquestUpdate,
|
||||
game: GameView,
|
||||
): Fx {
|
||||
const conquered = game.player(conquest.conqueredId);
|
||||
const x = conquered.nameLocation().x;
|
||||
const y = conquered.nameLocation().y;
|
||||
|
||||
const swordAnimation = new SpriteFx(
|
||||
animatedSpriteLoader,
|
||||
x,
|
||||
y,
|
||||
FxType.Conquest,
|
||||
2500,
|
||||
);
|
||||
return new FadeFx(swordAnimation, 0.1, 0.6);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface Fx {
|
||||
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean;
|
||||
}
|
||||
|
||||
export enum FxType {
|
||||
MiniFire = "MiniFire",
|
||||
MiniSmoke = "MiniSmoke",
|
||||
MiniBigSmoke = "MiniBigSmoke",
|
||||
MiniSmokeAndFire = "MiniSmokeAndFire",
|
||||
MiniExplosion = "MiniExplosion",
|
||||
UnitExplosion = "UnitExplosion",
|
||||
BuildingExplosion = "BuildingExplosion",
|
||||
SinkingShip = "SinkingShip",
|
||||
Nuke = "Nuke",
|
||||
SAMExplosion = "SAMExplosion",
|
||||
UnderConstruction = "UnderConstruction",
|
||||
Dust = "Dust",
|
||||
Conquest = "Conquest",
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
import { FadeFx, SpriteFx } from "./SpriteFx";
|
||||
|
||||
/**
|
||||
* Shockwave effect: draw a growing 1px white circle
|
||||
*/
|
||||
export class ShockwaveFx implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
constructor(
|
||||
private x: number,
|
||||
private y: number,
|
||||
private duration: number,
|
||||
private maxRadius: number,
|
||||
) {}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
this.lifeTime += frameTime;
|
||||
if (this.lifeTime >= this.duration) {
|
||||
return false;
|
||||
}
|
||||
const t = this.lifeTime / this.duration;
|
||||
const radius = t * this.maxRadius;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, radius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, " + (1 - t) + ")";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn @p number of @p type animation within a perimeter
|
||||
*/
|
||||
function addSpriteInCircle(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
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 FadeFx(
|
||||
new SpriteFx(animatedSpriteLoader, spawnX, spawnY, type, 6000),
|
||||
0.1,
|
||||
0.8,
|
||||
);
|
||||
result.push(sprite as Fx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explosion effect:
|
||||
* - explosion animation
|
||||
* - shockwave
|
||||
* - ruins and desolation fx
|
||||
*/
|
||||
export function nukeFxFactory(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
game: GameView,
|
||||
): Fx[] {
|
||||
const nukeFx: Fx[] = [];
|
||||
// Explosion animation
|
||||
nukeFx.push(new SpriteFx(animatedSpriteLoader, x, y, FxType.Nuke));
|
||||
// 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(
|
||||
animatedSpriteLoader,
|
||||
x,
|
||||
y,
|
||||
radius * radiusFactor,
|
||||
radius * density,
|
||||
type,
|
||||
nukeFx,
|
||||
game,
|
||||
);
|
||||
}
|
||||
return nukeFx;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Theme } from "src/core/configuration/Theme";
|
||||
import { PlayerView } from "../../../core/game/GameView";
|
||||
import { AnimatedSprite } from "../AnimatedSprite";
|
||||
import { AnimatedSpriteLoader } 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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fade in/out another FX
|
||||
*/
|
||||
export class FadeFx implements Fx {
|
||||
constructor(
|
||||
private fxToFade: SpriteFx,
|
||||
private fadeIn: number,
|
||||
private fadeOut: number,
|
||||
) {}
|
||||
|
||||
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
const t = this.fxToFade.getElapsedTime() / this.fxToFade.getDuration();
|
||||
ctx.save();
|
||||
ctx.globalAlpha = fadeInOut(t, this.fadeIn, this.fadeOut);
|
||||
const result = this.fxToFade.renderTick(duration, ctx);
|
||||
ctx.restore();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated sprite. Can be colored if provided an owner/theme
|
||||
*/
|
||||
export class SpriteFx implements Fx {
|
||||
protected animatedSprite: AnimatedSprite | null;
|
||||
protected elapsedTime = 0;
|
||||
protected duration: number;
|
||||
protected waitToTheEnd = false;
|
||||
constructor(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
protected x: number,
|
||||
protected y: number,
|
||||
fxType: FxType,
|
||||
duration?: number,
|
||||
owner?: PlayerView,
|
||||
theme?: Theme,
|
||||
) {
|
||||
this.animatedSprite = animatedSpriteLoader.createAnimatedSprite(
|
||||
fxType,
|
||||
owner,
|
||||
theme,
|
||||
);
|
||||
if (!this.animatedSprite) {
|
||||
console.error("Could not load animated sprite", fxType);
|
||||
} else {
|
||||
this.waitToTheEnd = duration ? true : false;
|
||||
this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000;
|
||||
}
|
||||
}
|
||||
|
||||
public setPosition(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
if (!this.animatedSprite) return false;
|
||||
|
||||
this.elapsedTime += frameTime;
|
||||
if (this.elapsedTime >= this.duration) return false;
|
||||
|
||||
if (!this.animatedSprite.isActive() && !this.waitToTheEnd) return false;
|
||||
|
||||
this.animatedSprite.update(frameTime);
|
||||
this.animatedSprite.draw(ctx, this.x, this.y);
|
||||
return true;
|
||||
}
|
||||
|
||||
getElapsedTime(): number {
|
||||
return this.elapsedTime;
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
return this.duration;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
type TimedTask = {
|
||||
delay: number;
|
||||
action: () => void;
|
||||
triggered: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic timeline to chain actions
|
||||
*/
|
||||
export class Timeline {
|
||||
private tasks: TimedTask[] = [];
|
||||
private timeElapsed = 0;
|
||||
|
||||
add(delay: number, action: () => void): Timeline {
|
||||
this.tasks.push({ delay, action, triggered: false });
|
||||
return this;
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
this.timeElapsed += dt;
|
||||
|
||||
for (const task of this.tasks) {
|
||||
if (!task.triggered && this.timeElapsed >= task.delay) {
|
||||
task.action();
|
||||
task.triggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isComplete() {
|
||||
return this.tasks.every((t) => t.triggered);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
import { SpriteFx } from "./SpriteFx";
|
||||
import { Timeline } from "./Timeline";
|
||||
|
||||
/**
|
||||
* Explosion Effect: a few timed explosions
|
||||
*/
|
||||
export class UnitExplosionFx implements Fx {
|
||||
private timeline = new Timeline();
|
||||
private explosions: Fx[] = [];
|
||||
|
||||
constructor(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
private x: number,
|
||||
private y: number,
|
||||
game: GameView,
|
||||
) {
|
||||
const config = [
|
||||
{ dx: 0, dy: 0, delay: 0, type: FxType.UnitExplosion },
|
||||
{ dx: 4, dy: -6, delay: 80, type: FxType.UnitExplosion },
|
||||
{ dx: -6, dy: 4, delay: 160, type: FxType.UnitExplosion },
|
||||
];
|
||||
for (const { dx, dy, delay, type } of config) {
|
||||
this.timeline.add(delay, () => {
|
||||
if (game.isValidCoord(x + dx, y + dy)) {
|
||||
this.explosions.push(
|
||||
new SpriteFx(animatedSpriteLoader, x + dx, y + dy, type),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
this.timeline.update(frameTime);
|
||||
let allDone = true;
|
||||
for (const fx of this.explosions) {
|
||||
if (fx.renderTick(frameTime, ctx)) {
|
||||
allDone = false;
|
||||
}
|
||||
}
|
||||
|
||||
return !allDone || !this.timeline.isComplete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user