From be182bb7f71ad4fe46b5ccf635f8b2e3ca977e71 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 17 May 2026 13:02:00 -0700 Subject: [PATCH] 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. --- src/client/graphics/AnimatedSprite.ts | 86 ------- src/client/graphics/AnimatedSpriteLoader.ts | 236 -------------------- src/client/graphics/fx/ConquestFx.ts | 28 --- src/client/graphics/fx/Fx.ts | 19 -- src/client/graphics/fx/NukeFx.ts | 110 --------- src/client/graphics/fx/SpriteFx.ts | 97 -------- src/client/graphics/fx/Timeline.ts | 33 --- src/client/graphics/fx/UnitExplosionFx.ts | 47 ---- 8 files changed, 656 deletions(-) delete mode 100644 src/client/graphics/AnimatedSprite.ts delete mode 100644 src/client/graphics/AnimatedSpriteLoader.ts delete mode 100644 src/client/graphics/fx/ConquestFx.ts delete mode 100644 src/client/graphics/fx/Fx.ts delete mode 100644 src/client/graphics/fx/NukeFx.ts delete mode 100644 src/client/graphics/fx/SpriteFx.ts delete mode 100644 src/client/graphics/fx/Timeline.ts delete mode 100644 src/client/graphics/fx/UnitExplosionFx.ts diff --git a/src/client/graphics/AnimatedSprite.ts b/src/client/graphics/AnimatedSprite.ts deleted file mode 100644 index 66648951c..000000000 --- a/src/client/graphics/AnimatedSprite.ts +++ /dev/null @@ -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; - } -} diff --git a/src/client/graphics/AnimatedSpriteLoader.ts b/src/client/graphics/AnimatedSpriteLoader.ts deleted file mode 100644 index d32dfe87b..000000000 --- a/src/client/graphics/AnimatedSpriteLoader.ts +++ /dev/null @@ -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> = { - [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 = new Map(); - // Do not color the same sprite twice - private coloredAnimatedSpriteCache: Map = - new Map(); - - public async loadAllAnimatedSpriteImages(): Promise { - 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((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); - } -} diff --git a/src/client/graphics/fx/ConquestFx.ts b/src/client/graphics/fx/ConquestFx.ts deleted file mode 100644 index 9f45d2106..000000000 --- a/src/client/graphics/fx/ConquestFx.ts +++ /dev/null @@ -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); -} diff --git a/src/client/graphics/fx/Fx.ts b/src/client/graphics/fx/Fx.ts deleted file mode 100644 index d4b206614..000000000 --- a/src/client/graphics/fx/Fx.ts +++ /dev/null @@ -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", -} diff --git a/src/client/graphics/fx/NukeFx.ts b/src/client/graphics/fx/NukeFx.ts deleted file mode 100644 index 479d68e18..000000000 --- a/src/client/graphics/fx/NukeFx.ts +++ /dev/null @@ -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; -} diff --git a/src/client/graphics/fx/SpriteFx.ts b/src/client/graphics/fx/SpriteFx.ts deleted file mode 100644 index cf625d797..000000000 --- a/src/client/graphics/fx/SpriteFx.ts +++ /dev/null @@ -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; - } -} diff --git a/src/client/graphics/fx/Timeline.ts b/src/client/graphics/fx/Timeline.ts deleted file mode 100644 index 32310244e..000000000 --- a/src/client/graphics/fx/Timeline.ts +++ /dev/null @@ -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); - } -} diff --git a/src/client/graphics/fx/UnitExplosionFx.ts b/src/client/graphics/fx/UnitExplosionFx.ts deleted file mode 100644 index b77d5f4fa..000000000 --- a/src/client/graphics/fx/UnitExplosionFx.ts +++ /dev/null @@ -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(); - } -}