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
This commit is contained in:
DevelopingTom
2025-07-25 19:52:05 -04:00
committed by GitHub
parent 71fe6a81a0
commit c738648460
12 changed files with 126 additions and 34 deletions
@@ -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<Record<FxType, AnimatedSpriteConfig>> = {
originX: 23,
originY: 19,
},
[FxType.Conquest]: {
url: conquestSword,
frameWidth: 21,
frameCount: 10,
frameDuration: 90,
looping: false,
originX: 10,
originY: 16,
},
};
export class AnimatedSpriteLoader {
+46
View File
@@ -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;
}
+1
View File
@@ -14,4 +14,5 @@ export enum FxType {
SAMExplosion = "SAMExplosion",
UnderConstruction = "UnderConstruction",
Dust = "Dust",
Conquest = "Conquest",
}
+6 -4
View File
@@ -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);
+1 -1
View File
@@ -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 {
+23
View File
@@ -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());