Christmas Themed FX (#2624)

## Description:

### Santa:
- spawn randomly on the map every minute:


![santa_is_coming](https://github.com/user-attachments/assets/0c40f983-7c5e-4fd9-93cd-5601008cb2ef)

### Nuke changes:
- Atom: small gift
- Hydro: big gift
- MIRV: shooting star


![all_christmas_nukes](https://github.com/user-attachments/assets/db80d765-6292-44e0-a5a3-fe08c0516993)

### Nuke fallout FX:
- melting snowman
- happy elves
- elves needing assistance


![christmastroph](https://github.com/user-attachments/assets/92270357-a0f6-43bf-9b95-cc5b2427a542)

## 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
This commit is contained in:
DevelopingTom
2025-12-16 05:24:23 +01:00
committed by GitHub
parent f96fd6dc12
commit 648ae1943f
14 changed files with 149 additions and 3 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 B

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 303 B

@@ -1,5 +1,10 @@
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
import happyElf from "../../../resources/sprites/christmas/happy_elf.png";
import sadElf from "../../../resources/sprites/christmas/sad_elf.png";
import santa from "../../../resources/sprites/christmas/santa.png";
import snowman from "../../../resources/sprites/christmas/snowman.png";
import sparks from "../../../resources/sprites/christmas/sparks.png";
import conquestSword from "../../../resources/sprites/conquestSword.png";
import dust from "../../../resources/sprites/dust.png";
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
@@ -135,6 +140,51 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 10,
originY: 16,
},
[FxType.Santa]: {
url: santa,
frameWidth: 34,
frameCount: 8,
frameDuration: 90,
looping: true,
originX: 16,
originY: 15,
},
[FxType.Snowman]: {
url: snowman,
frameWidth: 16,
frameCount: 19,
frameDuration: 200,
looping: false,
originX: 8,
originY: 12,
},
[FxType.HappyElf]: {
url: happyElf,
frameWidth: 7,
frameCount: 5,
frameDuration: 90,
looping: true,
originX: 3,
originY: 7,
},
[FxType.SadElf]: {
url: sadElf,
frameWidth: 14,
frameCount: 10,
frameDuration: 90,
looping: true,
originX: 6,
originY: 10,
},
[FxType.Sparks]: {
url: sparks,
frameWidth: 13,
frameCount: 13,
frameDuration: 60,
looping: false,
originX: 6,
originY: 6,
},
};
export class AnimatedSpriteLoader {
+5
View File
@@ -16,4 +16,9 @@ export enum FxType {
UnderConstruction = "UnderConstruction",
Dust = "Dust",
Conquest = "Conquest",
Santa = "Santa",
Snowman = "Snowman",
HappyElf = "HappyElf",
SadElf = "SadElf",
Sparks = "Sparks",
}
+3 -3
View File
@@ -88,10 +88,10 @@ export function nukeFxFactory(
radiusFactor: number;
density: number;
}> = [
{ type: FxType.MiniFire, radiusFactor: 1.0, density: 1 / 25 },
{ type: FxType.MiniSmoke, radiusFactor: 1.0, density: 1 / 28 },
{ type: FxType.HappyElf, radiusFactor: 1.0, density: 1 / 25 },
{ type: FxType.SadElf, radiusFactor: 1.0, density: 1 / 28 },
{ type: FxType.MiniBigSmoke, radiusFactor: 0.9, density: 1 / 70 },
{ type: FxType.MiniSmokeAndFire, radiusFactor: 0.9, density: 1 / 70 },
{ type: FxType.Snowman, radiusFactor: 0.9, density: 1 / 70 },
];
for (const { type, radiusFactor, density } of debrisPlan) {
+46
View File
@@ -0,0 +1,46 @@
import { Theme } from "../../../core/configuration/Config";
import { PlayerView } from "../../../core/game/GameView";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { SpriteFx } from "./SpriteFx";
export class SantaFx implements Fx {
private spriteFx: SpriteFx;
private speed: number = 0.05; // px / ms
constructor(
animatedSpriteLoader: AnimatedSpriteLoader,
private startX: number,
private startY: number,
private endX: number,
owner?: PlayerView,
theme?: Theme,
) {
const distance = Math.abs(endX - startX);
const duration = Math.max(distance / this.speed, 1);
this.spriteFx = new SpriteFx(
animatedSpriteLoader,
startX,
startY,
FxType.Santa,
duration,
owner,
theme,
);
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
const elapsed = this.spriteFx.getElapsedTime();
const duration = this.spriteFx.getDuration();
const t = elapsed / duration;
if (t >= 1) return false;
const x = this.startX + Math.floor((this.endX - this.startX) * t);
const y = this.startY;
this.spriteFx.setPosition(x, y);
return this.spriteFx.renderTick(frameTime, ctx);
}
}
+5
View File
@@ -69,6 +69,11 @@ export class SpriteFx implements Fx {
}
}
public setPosition(x: number, y: number): void {
this.x = x;
this.y = y;
}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
if (!this.animatedSprite) return false;
+40
View File
@@ -14,6 +14,7 @@ import { conquestFxFactory } from "../fx/ConquestFx";
import { Fx, FxType } from "../fx/Fx";
import { NukeAreaFx } from "../fx/NukeAreaFx";
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
import { SantaFx } from "../fx/SantaFx";
import { SpriteFx } from "../fx/SpriteFx";
import { TargetFx } from "../fx/TargetFx";
import { TextFx } from "../fx/TextFx";
@@ -33,6 +34,9 @@ export class FxLayer implements Layer {
private boatTargetFxByUnitId: Map<number, TargetFx> = new Map();
private nukeTargetFxByUnitId: Map<number, NukeAreaFx> = new Map();
private lastSantaTick = 0;
private santaIntervalTicks = 60 * 10; // one each minute
constructor(private game: GameView) {
this.theme = this.game.config().theme();
}
@@ -43,6 +47,7 @@ export class FxLayer implements Layer {
tick() {
this.manageBoatTargetFx();
this.spawnSantaIfNeeded();
this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
@@ -71,6 +76,24 @@ export class FxLayer implements Layer {
});
}
private spawnSantaIfNeeded() {
const currentTick = this.game.ticks();
if (currentTick - this.lastSantaTick < this.santaIntervalTicks) {
return;
}
this.lastSantaTick = currentTick;
// Santa enters left side, exits right
const margin = 50;
const startX = -margin;
const endX = this.game.width() + margin;
const startY = Math.floor(
margin + Math.random() * (this.game.height() - 2 * margin),
);
const santa = new SantaFx(this.animatedSpriteLoader, startX, startY, endX);
this.allFx.push(santa);
}
private manageBoatTargetFx() {
// End markers for boats that arrived or retreated
for (const [unitId, fx] of Array.from(
@@ -168,6 +191,9 @@ export class FxLayer implements Layer {
this.onNukeEvent(unit, 70);
break;
}
case UnitType.MIRV:
this.addSparks(unit);
break;
case UnitType.MIRVWarhead:
this.onNukeEvent(unit, 70);
break;
@@ -302,6 +328,20 @@ export class FxLayer implements Layer {
}
}
addSparks(unit: UnitView) {
if (unit.isActive()) {
const x = this.game.x(unit.lastTile());
const y = this.game.y(unit.lastTile());
const sparks = new SpriteFx(
this.animatedSpriteLoader,
x,
y,
FxType.Sparks,
);
this.allFx.push(sparks);
}
}
onNukeEvent(unit: UnitView, radius: number) {
if (!unit.isActive()) {
const fx = this.nukeTargetFxByUnitId.get(unit.id());