mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:40:46 +00:00
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:
Binary file not shown.
|
After Width: | Height: | Size: 627 B |
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -14,4 +14,5 @@ export enum FxType {
|
||||
SAMExplosion = "SAMExplosion",
|
||||
UnderConstruction = "UnderConstruction",
|
||||
Dust = "Dust",
|
||||
Conquest = "Conquest",
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -696,6 +696,7 @@ export interface Game extends GameMap {
|
||||
|
||||
addUpdate(update: GameUpdate): void;
|
||||
railNetwork(): RailNetwork;
|
||||
conquerPlayer(conqueror: Player, conquered: Player);
|
||||
}
|
||||
|
||||
export interface PlayerActions {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user