diff --git a/resources/sprites/conquestSword.png b/resources/sprites/conquestSword.png new file mode 100644 index 000000000..518789ff4 Binary files /dev/null and b/resources/sprites/conquestSword.png differ diff --git a/src/client/graphics/AnimatedSpriteLoader.ts b/src/client/graphics/AnimatedSpriteLoader.ts index 91f7254d3..d8158847f 100644 --- a/src/client/graphics/AnimatedSpriteLoader.ts +++ b/src/client/graphics/AnimatedSpriteLoader.ts @@ -1,4 +1,5 @@ import miniBigSmoke from "../../../resources/sprites/bigsmoke.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"; @@ -115,6 +116,15 @@ const ANIMATED_SPRITE_CONFIG: Partial> = { originX: 23, originY: 19, }, + [FxType.Conquest]: { + url: conquestSword, + frameWidth: 21, + frameCount: 10, + frameDuration: 90, + looping: false, + originX: 10, + originY: 16, + }, }; export class AnimatedSpriteLoader { diff --git a/src/client/graphics/fx/ConquestFx.ts b/src/client/graphics/fx/ConquestFx.ts new file mode 100644 index 000000000..7fa8d0690 --- /dev/null +++ b/src/client/graphics/fx/ConquestFx.ts @@ -0,0 +1,46 @@ +import { ConquestUpdate } from "../../../core/game/GameUpdates"; +import { GameView } from "../../../core/game/GameView"; +import { renderNumber } from "../../Utils"; +import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader"; +import { Fx, FxType } from "./Fx"; +import { FadeFx, SpriteFx } from "./SpriteFx"; +import { TextFx } from "./TextFx"; + +/** + * Conquest FX: + * - conquest sprite + * - gold displayed + */ +export function conquestFxFactory( + animatedSpriteLoader: AnimatedSpriteLoader, + conquest: ConquestUpdate, + game: GameView, +): Fx[] { + const conquestFx: 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, + ); + const fadeAnimation = new FadeFx(swordAnimation, 0.1, 0.6); + conquestFx.push(fadeAnimation); + + const shortenedGold = renderNumber(conquest.gold); + const goldText = new TextFx( + `+ ${shortenedGold}`, + x, + y + 8, + 2500, + 0, + "11px sans-serif", + ); + conquestFx.push(goldText); + + return conquestFx; +} diff --git a/src/client/graphics/fx/Fx.ts b/src/client/graphics/fx/Fx.ts index 3640c743f..2aeb3ccf6 100644 --- a/src/client/graphics/fx/Fx.ts +++ b/src/client/graphics/fx/Fx.ts @@ -14,4 +14,5 @@ export enum FxType { SAMExplosion = "SAMExplosion", UnderConstruction = "UnderConstruction", Dust = "Dust", + Conquest = "Conquest", } diff --git a/src/client/graphics/fx/SpriteFx.ts b/src/client/graphics/fx/SpriteFx.ts index 9293dd663..919533386 100644 --- a/src/client/graphics/fx/SpriteFx.ts +++ b/src/client/graphics/fx/SpriteFx.ts @@ -45,15 +45,16 @@ export class FadeFx implements Fx { export class SpriteFx implements Fx { protected animatedSprite: AnimatedSprite | null; protected elapsedTime = 0; - protected duration = 1000; + protected duration: number; + protected waitToTheEnd = false; constructor( animatedSpriteLoader: AnimatedSpriteLoader, protected x: number, protected y: number, fxType: FxType, duration?: number, - private owner?: PlayerView, - private theme?: Theme, + owner?: PlayerView, + theme?: Theme, ) { this.animatedSprite = animatedSpriteLoader.createAnimatedSprite( fxType, @@ -63,6 +64,7 @@ export class SpriteFx implements Fx { if (!this.animatedSprite) { console.error("Could not load animated sprite", fxType); } else { + this.waitToTheEnd = duration ? true : false; this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000; } } @@ -73,7 +75,7 @@ export class SpriteFx implements Fx { this.elapsedTime += frameTime; if (this.elapsedTime >= this.duration) return false; - if (!this.animatedSprite.isActive()) return false; + if (!this.animatedSprite.isActive() && !this.waitToTheEnd) return false; const t = this.elapsedTime / this.duration; this.animatedSprite.update(frameTime); diff --git a/src/client/graphics/fx/TextFx.ts b/src/client/graphics/fx/TextFx.ts index 200a2ac1e..c1a82e212 100644 --- a/src/client/graphics/fx/TextFx.ts +++ b/src/client/graphics/fx/TextFx.ts @@ -9,12 +9,12 @@ export class TextFx implements Fx { private y: number, private duration: number, private riseDistance: number = 30, + private font: string = "11px sans-serif", private color: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255, }, - private font: string = "11px sans-serif", ) {} renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean { diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index 9e8057073..8b0364c7c 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -2,12 +2,14 @@ import { Theme } from "../../../core/configuration/Config"; import { UnitType } from "../../../core/game/Game"; import { BonusEventUpdate, + ConquestUpdate, GameUpdateType, RailroadUpdate, } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; import { renderNumber } from "../../Utils"; import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader"; +import { conquestFxFactory } from "../fx/ConquestFx"; import { Fx, FxType } from "../fx/Fx"; import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx"; import { SpriteFx } from "../fx/SpriteFx"; @@ -55,6 +57,12 @@ export class FxLayer implements Layer { if (update === undefined) return; this.onRailroadEvent(update); }); + this.game + .updatesSinceLastTick() + ?.[GameUpdateType.ConquestEvent]?.forEach((update) => { + if (update === undefined) return; + this.onConquestEvent(update); + }); } onBonusEvent(bonus: BonusEventUpdate) { @@ -164,6 +172,21 @@ export class FxLayer implements Layer { } } + onConquestEvent(conquest: ConquestUpdate) { + // Only display fx for the current player + const conqueror = this.game.player(conquest.conquerorId); + if (conqueror !== this.game.myPlayer()) { + return; + } + + const conquestFx = conquestFxFactory( + this.animatedSpriteLoader, + conquest, + this.game, + ); + this.allFx = this.allFx.concat(conquestFx); + } + onWarshipEvent(unit: UnitView) { if (!unit.isActive()) { const x = this.game.x(unit.lastTile()); diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 5038c28ad..03ace5829 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -1,4 +1,4 @@ -import { renderNumber, renderTroops } from "../../client/Utils"; +import { renderTroops } from "../../client/Utils"; import { Attack, Execution, @@ -331,17 +331,7 @@ export class AttackExecution implements Execution { private handleDeadDefender() { if (!(this.target.isPlayer() && this.target.numTilesOwned() < 100)) return; - const gold = this.target.gold(); - this.mg.displayMessage( - `Conquered ${this.target.displayName()} received ${renderNumber( - gold, - )} gold`, - MessageType.CONQUERED_PLAYER, - this._owner.id(), - gold, - ); - this.target.removeGold(gold); - this._owner.addGold(gold); + this.mg.conquerPlayer(this._owner, this.target); for (let i = 0; i < 10; i++) { for (const tile of this.target.tiles()) { diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index c93bcf44f..447885c3c 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -1,6 +1,5 @@ -import { renderNumber } from "../../client/Utils"; import { Config } from "../configuration/Config"; -import { Execution, Game, MessageType, Player, UnitType } from "../game/Game"; +import { Execution, Game, Player, UnitType } from "../game/Game"; import { GameImpl } from "../game/GameImpl"; import { TileRef } from "../game/GameMap"; import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; @@ -199,20 +198,7 @@ export class PlayerExecution implements Execution { const tiles = this.mg.bfs(firstTile, filter); if (this.player.numTilesOwned() === tiles.size) { - const gold = this.player.gold(); - this.mg.displayMessage( - `Conquered ${this.player.displayName()} received ${renderNumber( - gold, - )} gold`, - MessageType.CONQUERED_PLAYER, - capturing.id(), - gold, - ); - capturing.addGold(gold); - this.player.removeGold(gold); - - // Record stats - this.mg.stats().goldWar(capturing, this.player, gold); + this.mg.conquerPlayer(capturing, this.player); } for (const tile of tiles) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 04b823a4e..ab38731b1 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -696,6 +696,7 @@ export interface Game extends GameMap { addUpdate(update: GameUpdate): void; railNetwork(): RailNetwork; + conquerPlayer(conqueror: Player, conquered: Player); } export interface PlayerActions { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 1cb78faf8..f074cabef 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,3 +1,4 @@ +import { renderNumber } from "../../client/Utils"; import { Config } from "../configuration/Config"; import { AllPlayersStats, ClientID, Winner } from "../Schemas"; import { simpleHash } from "../Util"; @@ -875,6 +876,28 @@ export class GameImpl implements Game { railNetwork(): RailNetwork { return this._railNetwork; } + conquerPlayer(conqueror: Player, conquered: Player) { + const gold = conquered.gold(); + this.displayMessage( + `Conquered ${conquered.displayName()} received ${renderNumber( + gold, + )} gold`, + MessageType.CONQUERED_PLAYER, + conqueror.id(), + gold, + ); + conqueror.addGold(gold); + conquered.removeGold(gold); + this.addUpdate({ + type: GameUpdateType.ConquestEvent, + conquerorId: conqueror.id(), + conqueredId: conquered.id(), + gold, + }); + + // Record stats + this.stats().goldWar(conqueror, conquered, gold); + } } // Or a more dynamic approach that will catch new enum values: diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 02f9ffbd6..87495e976 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -44,6 +44,7 @@ export enum GameUpdateType { UnitIncoming, BonusEvent, RailroadEvent, + ConquestEvent, } export type GameUpdate = @@ -63,7 +64,8 @@ export type GameUpdate = | UnitIncomingUpdate | AllianceExtensionUpdate | BonusEventUpdate - | RailroadUpdate; + | RailroadUpdate + | ConquestUpdate; export interface BonusEventUpdate { type: GameUpdateType.BonusEvent; @@ -86,12 +88,20 @@ export interface RailTile { tile: TileRef; railType: RailType; } + export interface RailroadUpdate { type: GameUpdateType.RailroadEvent; isActive: boolean; railTiles: RailTile[]; } +export interface ConquestUpdate { + type: GameUpdateType.ConquestEvent; + conquerorId: PlayerID; + conqueredId: PlayerID; + gold: Gold; +} + export interface TileUpdateWrapper { type: GameUpdateType.Tile; update: TileUpdate;