mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-27 22:04:15 +00:00
6307a2702b
## Description: Resize the target fx. It's still a little too big in my opinion, but it becomes very blurry when shrinked down. https://github.com/user-attachments/assets/c3cda98d-ed57-4933-93b4-1cc7f1cb8e50 The UI Layer should probably not be bound to the zoom level so we can have a sharper UI. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom
340 lines
9.2 KiB
TypeScript
340 lines
9.2 KiB
TypeScript
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 SoundManager, { SoundEffect } from "../../sound/SoundManager";
|
|
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";
|
|
import { TargetFx } from "../fx/TargetFx";
|
|
import { TextFx } from "../fx/TextFx";
|
|
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
|
import { Layer } from "./Layer";
|
|
export class FxLayer implements Layer {
|
|
private canvas: HTMLCanvasElement;
|
|
private context: CanvasRenderingContext2D;
|
|
|
|
private lastRefresh: number = 0;
|
|
private refreshRate: number = 10;
|
|
private theme: Theme;
|
|
private animatedSpriteLoader: AnimatedSpriteLoader =
|
|
new AnimatedSpriteLoader();
|
|
|
|
private allFx: Fx[] = [];
|
|
private boatTargetFxByUnitId: Map<number, TargetFx> = new Map();
|
|
|
|
constructor(private game: GameView) {
|
|
this.theme = this.game.config().theme();
|
|
}
|
|
|
|
shouldTransform(): boolean {
|
|
return true;
|
|
}
|
|
|
|
tick() {
|
|
this.manageBoatTargetFx();
|
|
this.game
|
|
.updatesSinceLastTick()
|
|
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
|
|
?.forEach((unitView) => {
|
|
if (unitView === undefined) return;
|
|
this.onUnitEvent(unitView);
|
|
});
|
|
this.game
|
|
.updatesSinceLastTick()
|
|
?.[GameUpdateType.BonusEvent]?.forEach((bonusEvent) => {
|
|
if (bonusEvent === undefined) return;
|
|
this.onBonusEvent(bonusEvent);
|
|
});
|
|
|
|
this.game
|
|
.updatesSinceLastTick()
|
|
?.[GameUpdateType.RailroadEvent]?.forEach((update) => {
|
|
if (update === undefined) return;
|
|
this.onRailroadEvent(update);
|
|
});
|
|
this.game
|
|
.updatesSinceLastTick()
|
|
?.[GameUpdateType.ConquestEvent]?.forEach((update) => {
|
|
if (update === undefined) return;
|
|
this.onConquestEvent(update);
|
|
});
|
|
}
|
|
|
|
private manageBoatTargetFx() {
|
|
// End markers for boats that arrived or retreated
|
|
for (const [unitId, fx] of Array.from(
|
|
this.boatTargetFxByUnitId.entries(),
|
|
)) {
|
|
const unit = this.game.unit(unitId);
|
|
if (
|
|
!unit ||
|
|
!unit.isActive() ||
|
|
unit.reachedTarget() ||
|
|
unit.retreating()
|
|
) {
|
|
(fx as any).end?.();
|
|
this.boatTargetFxByUnitId.delete(unitId);
|
|
}
|
|
}
|
|
}
|
|
|
|
onBonusEvent(bonus: BonusEventUpdate) {
|
|
if (this.game.player(bonus.player) !== this.game.myPlayer()) {
|
|
// Only display text fx for the current player
|
|
return;
|
|
}
|
|
const tile = bonus.tile;
|
|
const x = this.game.x(tile);
|
|
let y = this.game.y(tile);
|
|
const gold = bonus.gold;
|
|
const troops = bonus.troops;
|
|
|
|
if (gold > 0) {
|
|
const shortened = renderNumber(gold, 0);
|
|
this.addTextFx(`+ ${shortened}`, x, y);
|
|
y += 10; // increase y so the next popup starts bellow
|
|
}
|
|
|
|
if (troops > 0) {
|
|
const shortened = renderNumber(troops, 0);
|
|
this.addTextFx(`+ ${shortened} troops`, x, y);
|
|
y += 10;
|
|
}
|
|
}
|
|
|
|
addTextFx(text: string, x: number, y: number) {
|
|
const textFx = new TextFx(text, x, y, 1000, 20);
|
|
this.allFx.push(textFx);
|
|
}
|
|
|
|
onUnitEvent(unit: UnitView) {
|
|
switch (unit.type()) {
|
|
case UnitType.TransportShip: {
|
|
const my = this.game.myPlayer();
|
|
if (!my) return;
|
|
if (unit.owner() !== my) return;
|
|
if (!unit.isActive()) return;
|
|
if (this.boatTargetFxByUnitId.has(unit.id())) return;
|
|
const t = unit.targetTile();
|
|
if (t !== undefined) {
|
|
const x = this.game.x(t);
|
|
const y = this.game.y(t);
|
|
// persistent until boat finishes or retreats
|
|
const fx = new TargetFx(x, y, 0, true);
|
|
this.allFx.push(fx);
|
|
this.boatTargetFxByUnitId.set(unit.id(), fx);
|
|
}
|
|
break;
|
|
}
|
|
case UnitType.AtomBomb:
|
|
case UnitType.MIRVWarhead:
|
|
this.onNukeEvent(unit, 70);
|
|
break;
|
|
case UnitType.HydrogenBomb:
|
|
this.onNukeEvent(unit, 160);
|
|
break;
|
|
case UnitType.Warship:
|
|
this.onWarshipEvent(unit);
|
|
break;
|
|
case UnitType.Shell:
|
|
this.onShellEvent(unit);
|
|
break;
|
|
case UnitType.Train:
|
|
this.onTrainEvent(unit);
|
|
break;
|
|
}
|
|
}
|
|
|
|
onShellEvent(unit: UnitView) {
|
|
if (!unit.isActive()) {
|
|
if (unit.reachedTarget()) {
|
|
const x = this.game.x(unit.lastTile());
|
|
const y = this.game.y(unit.lastTile());
|
|
const explosion = new SpriteFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
FxType.MiniExplosion,
|
|
);
|
|
this.allFx.push(explosion);
|
|
}
|
|
}
|
|
}
|
|
|
|
onTrainEvent(unit: UnitView) {
|
|
if (!unit.isActive()) {
|
|
if (!unit.reachedTarget()) {
|
|
const x = this.game.x(unit.lastTile());
|
|
const y = this.game.y(unit.lastTile());
|
|
const explosion = new SpriteFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
FxType.MiniExplosion,
|
|
);
|
|
this.allFx.push(explosion);
|
|
}
|
|
}
|
|
}
|
|
|
|
onRailroadEvent(railroad: RailroadUpdate) {
|
|
const railTiles = railroad.railTiles;
|
|
for (const rail of railTiles) {
|
|
// No need for pseudorandom, this is fx
|
|
const chanceFx = Math.floor(Math.random() * 3);
|
|
if (chanceFx === 0) {
|
|
const x = this.game.x(rail.tile);
|
|
const y = this.game.y(rail.tile);
|
|
const animation = new SpriteFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
FxType.Dust,
|
|
);
|
|
this.allFx.push(animation);
|
|
}
|
|
}
|
|
}
|
|
|
|
onConquestEvent(conquest: ConquestUpdate) {
|
|
// Only display fx for the current player
|
|
const conqueror = this.game.player(conquest.conquerorId);
|
|
if (conqueror !== this.game.myPlayer()) {
|
|
return;
|
|
}
|
|
|
|
SoundManager.playSoundEffect(SoundEffect.KaChing);
|
|
|
|
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());
|
|
const y = this.game.y(unit.lastTile());
|
|
const shipExplosion = new UnitExplosionFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
this.game,
|
|
);
|
|
this.allFx.push(shipExplosion);
|
|
const sinkingShip = new SpriteFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
FxType.SinkingShip,
|
|
undefined,
|
|
unit.owner(),
|
|
this.theme,
|
|
);
|
|
this.allFx.push(sinkingShip);
|
|
}
|
|
}
|
|
|
|
onNukeEvent(unit: UnitView, radius: number) {
|
|
if (!unit.isActive()) {
|
|
if (!unit.reachedTarget()) {
|
|
this.handleSAMInterception(unit);
|
|
} else {
|
|
// Kaboom
|
|
this.handleNukeExplosion(unit, radius);
|
|
}
|
|
}
|
|
}
|
|
|
|
handleNukeExplosion(unit: UnitView, radius: number) {
|
|
const x = this.game.x(unit.lastTile());
|
|
const y = this.game.y(unit.lastTile());
|
|
const nukeFx = nukeFxFactory(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
radius,
|
|
this.game,
|
|
);
|
|
this.allFx = this.allFx.concat(nukeFx);
|
|
}
|
|
|
|
handleSAMInterception(unit: UnitView) {
|
|
const x = this.game.x(unit.lastTile());
|
|
const y = this.game.y(unit.lastTile());
|
|
const explosion = new SpriteFx(
|
|
this.animatedSpriteLoader,
|
|
x,
|
|
y,
|
|
FxType.SAMExplosion,
|
|
);
|
|
this.allFx.push(explosion);
|
|
const shockwave = new ShockwaveFx(x, y, 800, 40);
|
|
this.allFx.push(shockwave);
|
|
}
|
|
|
|
async init() {
|
|
this.redraw();
|
|
try {
|
|
this.animatedSpriteLoader.loadAllAnimatedSpriteImages();
|
|
console.log("FX sprites loaded successfully");
|
|
} catch (err) {
|
|
console.error("Failed to load FX sprites:", err);
|
|
}
|
|
}
|
|
|
|
redraw(): void {
|
|
this.canvas = document.createElement("canvas");
|
|
const context = this.canvas.getContext("2d");
|
|
if (context === null) throw new Error("2d context not supported");
|
|
this.context = context;
|
|
this.context.imageSmoothingEnabled = false;
|
|
this.canvas.width = this.game.width();
|
|
this.canvas.height = this.game.height();
|
|
}
|
|
|
|
renderLayer(context: CanvasRenderingContext2D) {
|
|
const now = Date.now();
|
|
if (this.game.config().userSettings()?.fxLayer()) {
|
|
if (now > this.lastRefresh + this.refreshRate) {
|
|
const delta = now - this.lastRefresh;
|
|
this.renderAllFx(context, delta);
|
|
this.lastRefresh = now;
|
|
}
|
|
context.drawImage(
|
|
this.canvas,
|
|
-this.game.width() / 2,
|
|
-this.game.height() / 2,
|
|
this.game.width(),
|
|
this.game.height(),
|
|
);
|
|
}
|
|
}
|
|
|
|
renderAllFx(context: CanvasRenderingContext2D, delta: number) {
|
|
if (this.allFx.length > 0) {
|
|
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.renderContextFx(delta);
|
|
}
|
|
}
|
|
|
|
renderContextFx(duration: number) {
|
|
for (let i = this.allFx.length - 1; i >= 0; i--) {
|
|
if (!this.allFx[i].renderTick(duration, this.context)) {
|
|
this.allFx.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|