From c73864846033f2c600529da99125d90c47e8cd2f Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Sat, 26 Jul 2025 01:52:05 +0200 Subject: [PATCH] Add conquest FX (#1390) ## Description: Add an animation when the player conquer another one. The FX consists of two parts: short animation, and the gold won. It is only displayed to the conquering player, so everybody knows who won the money if multiple people fighted over the last pixel of a player. Changes: - Add new update `ConquestUpdate` - Add new fx `ConquestFx` - Merge conquest logic in `Game` https://github.com/user-attachments/assets/9f985e41-baa4-48a6-927e-3216274f758c ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [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 --- resources/sprites/conquestSword.png | Bin 0 -> 627 bytes src/client/graphics/AnimatedSpriteLoader.ts | 10 +++++ src/client/graphics/fx/ConquestFx.ts | 46 ++++++++++++++++++++ src/client/graphics/fx/Fx.ts | 1 + src/client/graphics/fx/SpriteFx.ts | 10 +++-- src/client/graphics/fx/TextFx.ts | 2 +- src/client/graphics/layers/FxLayer.ts | 23 ++++++++++ src/core/execution/AttackExecution.ts | 14 +----- src/core/execution/PlayerExecution.ts | 18 +------- src/core/game/Game.ts | 1 + src/core/game/GameImpl.ts | 23 ++++++++++ src/core/game/GameUpdates.ts | 12 ++++- 12 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 resources/sprites/conquestSword.png create mode 100644 src/client/graphics/fx/ConquestFx.ts diff --git a/resources/sprites/conquestSword.png b/resources/sprites/conquestSword.png new file mode 100644 index 0000000000000000000000000000000000000000..518789ff4970345d732d30a2dee28e5ab974e1eb GIT binary patch literal 627 zcmV-(0*w8MP)Px%ElET{RCt{2m_LrAFc`%@kM=fOxf?0nR=ES0h-4RYgegFpL%nCFG&e1w3`pbHS4} zF~q*gO=@n&&{(d?u`n$S5jCkXTDiNZgR*8U8 z^R+cct9EWs^-{*FWK&H3e*e?Yx7JDsK}1BVs?v5`FDE&k!=lQ_^>PyLy<9J+CDVKU z{XV?+Vy%^`s-){W2_eW*e?v{Zc51$c=3A;U4pG;2H+_0u*R{-fiX6|u=t9>;*))v_ zAxPJCeE^o+fc?(mcn(H?LyvP`?XH)Tgb>6SWAtX`qMCZ`)O=CRdzFfuSF-)iLLZ+) zP3@cSxID2BJx;adXaYEt)?C9UH5X`3E%z-6(@%zSZvY_=LA=Sj7b0biDj6ZqO1~e` zVt{V4rW6vKbI|cQMFK*eott7qyvdp}x#R|qkf$eSkHY;8A%u`3`~}ukj45PdSIqzb N002ovPDHLkV1jkoDd_+J literal 0 HcmV?d00001 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;