From 648ae1943f21da63b2c7e835c8ac575213e5c609 Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Tue, 16 Dec 2025 05:24:23 +0100 Subject: [PATCH] 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 --- resources/sprites/atombomb.png | Bin 126 -> 185 bytes resources/sprites/christmas/happy_elf.png | Bin 0 -> 190 bytes resources/sprites/christmas/sad_elf.png | Bin 0 -> 411 bytes resources/sprites/christmas/santa.png | Bin 0 -> 1383 bytes resources/sprites/christmas/snowman.png | Bin 0 -> 573 bytes resources/sprites/christmas/sparks.png | Bin 0 -> 283 bytes resources/sprites/hydrogenbomb.png | Bin 140 -> 307 bytes resources/sprites/mirv2.png | Bin 177 -> 303 bytes src/client/graphics/AnimatedSpriteLoader.ts | 50 ++++++++++++++++++++ src/client/graphics/fx/Fx.ts | 5 ++ src/client/graphics/fx/NukeFx.ts | 6 +-- src/client/graphics/fx/SantaFx.ts | 46 ++++++++++++++++++ src/client/graphics/fx/SpriteFx.ts | 5 ++ src/client/graphics/layers/FxLayer.ts | 40 ++++++++++++++++ 14 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 resources/sprites/christmas/happy_elf.png create mode 100644 resources/sprites/christmas/sad_elf.png create mode 100644 resources/sprites/christmas/santa.png create mode 100644 resources/sprites/christmas/snowman.png create mode 100644 resources/sprites/christmas/sparks.png create mode 100644 src/client/graphics/fx/SantaFx.ts diff --git a/resources/sprites/atombomb.png b/resources/sprites/atombomb.png index 353fbadebbd8cde9916529c2efc1f89361c0ae88..cec7cd0c3d502b44f381af706d4dda15c894af90 100644 GIT binary patch delta 168 zcmV;Z09XHhxdD(Ee+UKu007{T1`q%M0DwtEK~xx(ZI4S4fG`Y1U%4RxRMbIjR7(Y5 z00*360|%%m5;%qfr465*$z+o3CL1Ww9R*MkwKYJ8K_wajfU{}|1}i`HhW@j>*}=T=ON`Cutuq@brB7 zfBkFkf60^o^s`)$?OMY>+qrONXRu%5EZa@B5^aZ`Ts+EYDKdR#%bUa53wL*3sZW^| oQTyc7!>t1H2bTB8>~v&gP|iO(e@&fyI?xRap00i_>zopr0JYIeW&i*H literal 0 HcmV?d00001 diff --git a/resources/sprites/christmas/sad_elf.png b/resources/sprites/christmas/sad_elf.png new file mode 100644 index 0000000000000000000000000000000000000000..26e36e6b65a27b13061d88f2fca7e74c15ec6460 GIT binary patch literal 411 zcmV;M0c8G(P)Px$RY^oaR9J=Wl`*b@KoExK8BSqExB-bhrPrXP@-|c!TX-028@UE0HvelJgZuW$ z%%H+1`zJJpf4eyX5kKf z^TgJ2L%(Zk`0r|hXK!eJb9xGSCm9?7Khok{*y6Qv$H7~Bru$$lJ^Nm1=su6nq4AL5 zrWkvqw^69Kmxiv)z_-QidQyM6d0UyhL*FdIrP1wZ@!qZ{T#5pDTVd!MQ7G);WtZF( zV~+GMg*{w%z)4E_SHIit4rwjhCJX=o002ovPDHLk FV1m=azyJUM literal 0 HcmV?d00001 diff --git a/resources/sprites/christmas/santa.png b/resources/sprites/christmas/santa.png new file mode 100644 index 0000000000000000000000000000000000000000..13f1669c638604ec72e3f8e5524599c66cfebf27 GIT binary patch literal 1383 zcmV-t1(^DYP)Px)AxT6*RCt{2n=xn`R~X0tPdFVj1Q!CCOdN=pxLH!^WVo%Rm@L`kVlZT}L4nL7 zSz6~Hn@tPOXi$olk|ha+YznQVOmeaa5(y2#U_;2DZVl+`aQDvdPIvF#-My0(Nq-QM zrMvfj_kI8Gd+%%+m}@MuN;w)g!(4NvRc_n>vnH`|OhOY;VIY9spO{1dAy0%n5drqk z0ElSp*MSM*m?bGSkV#3**UVv+a^x@8N^QWT^JG}KaT?i&6VvnT7h{ZZrZkG;h>C|U zjK~j5skelT0&5r8Z!^VOXUcl{apo|2(gyaO1EfX=K-~)ZORKfnE&r`SC0RVh=|5NA~1ki^Q6W6ZT#Tv*VtagksWfp$@<&9eafQz@A zO6NX3Jfvo`iF`hf{L)e?4$+W2C$eUy6ps{nQSk<5$fqhS3vgBzo|r!ld+-|sfZf-e zBqlMcsIbJ$0*7C+#GbVaxY+*>XYB%-Z$dbBe@rxQr8Zz5^#t+GQKM-Z-By@83P!c-O$rsZ>W*k7ZN`&alC;U@~H|71AxQU<&w^PG0NiO8JcII1Mnv@Q3-4!Kyo1SBJ6#-bw#DP* z2)g|cb@%$8?rRx~$irD=x@0ZmLbZxD!?4G`aYK5%$PvaOi${NJ3!?3!@`8}ujM^VT z94ZW>;811J$&sXXLyZ#Gs8J?`VAQDOObZ8J`T2^jDp>)4+1ykbc~NO%;xYU-N;gN)TxpetLnq59IffcYQlrX_^WR&m zNCyXF%r&f7H2gjBzCQ~NtCXXkzQ3e$s;jVKDH;Z?0r=aQL#8#uK+!Qq(J;s=Gr_PL=-y z(HRe23LD%1eijUA;-T|@e{}maY@R63>h?!#Z_$m@IO;{@;E%+4(_z%44MZ1*&MoRB z6mFb{ZiXNxmG`q|NyDL19I6GRJ09bxm$(c=+ma;7_TNK^62G1t^;P98IO5EiCC9XA pQsOZS9OI~$r1Q@;*IaYO*8hg>>CMbf0bl?C002ovPDHLkV1neem0bV; literal 0 HcmV?d00001 diff --git a/resources/sprites/christmas/snowman.png b/resources/sprites/christmas/snowman.png new file mode 100644 index 0000000000000000000000000000000000000000..00a3df1d1e5b74d921d9ce95c9bfec7734de5f73 GIT binary patch literal 573 zcmV-D0>b@?P)+00001b5ch_0Itp) z=>Px$_en%SRCt{2n?G*CFcgPhx^o7kc4y=QnK=lmREdh7B2pIa(WX1S03&;Q3aWN{ z9f;A3l{j|X*!lDOq$q^&Js{XW+aQPl0AQpfO()VUXLbAI<+<^3@Q3LGFk8|rXNl}g z@Q3fK8ftTO0T?Z5mhAppUQxHFwhlBBYC1%7xx&2oO+ZY%d4_d9Y8 zz~g>LYD0*Dq0$&&Z{NSf%tY@Aap7JVeq5g-1|VSu2SRe#EGv!dX|Z4?V!3a6Ed2Ps zL@hyY%g}y4x1&bjmw71%-7>`Z|XeoPx#)k#D_R9J=WR@)JPAPjR)r(rt>agX?+-f>7D^gvqA{3+Iyl7=qJkFEf)8e+CL zc*0<05sm+bAttrPgn_3b0$6cEl~eY8(#8SU?p&@oQEcUZhjeVOPLYaO+sx=9dJ>r# zvo|rDzXx31sZs5Q<7K*@xGP6@8?%KM-J|DR$czU`>Oo0%^mI_z9Q%P}9F2WrE z=OUCqI2X|fM4MkYjI#0kz1GI_gSg`r16eAbU0gHM;bq{_PSVAiD3Au8Z*-~^Mj`BP h@L1!*-l;$Q;Q)=k7q6|ZmX-hj002ovPDHLkV1m_id!GOR literal 0 HcmV?d00001 diff --git a/resources/sprites/hydrogenbomb.png b/resources/sprites/hydrogenbomb.png index fd3c0de34da7253a710b1b70d0afa7be78428870..683e9f9d1e58730ae65ddd38ace009a84d7f9cd9 100644 GIT binary patch delta 292 zcmV+<0o(qJ0kZ;-7=H)`0001T>*Ra@008<)L_t(2k)@F#ZvsIOg}(vx2?B^7L6aH` z;ykGP1Fa@OfvbjG=wG0Uq#FJJ3jB#nlOitFmw+ibfkQNl*xq%Q95uXTcW2+cw>!H7 z_hSBvQhPnFV^dc#4Wj{k?EaEjJQ@I?#uEh4({~UmfK}NuMSlRiz3cVqCj_7#cp?BB zrV5}F_|%5lcwB^5eEv!SXfAyznYGr29$R%2sg)CvlUVySKHvh?#|KU9jNi-9J++9Oghs55x%j%gB4d#_L+NJbA5;MP%mhGT qciu#E=}X~F;vFK{0j5}7Ebamps8yq-y^_2D0000;tcQ!aRt&%O-)<2Y`HYQdkc`mSQ6wH%;50sMjDV~ z=;`7Z!f`!0At5m#A%TY%NbvF|CJ6BIvIPx#=}AOER5*>jld%qh zFc60SGc34C+=wGHF=1ur348!YU%~fq^f}yFNeDA>bR)XKcn(;oZJ{+W+;;T%?!W(9 zVCmrI!Y*~Ie>SzkxK0?3>XuU`R3XUv+@fw7WrHg9`;0ozb${(vV32_Tpe;l8<9s}L z-t5GWj{t!C0TdNPB5%uZ=r##f03s+V2mlCD$iM--JCDihstFVzs0&!{AEZ^wu{lnH zG-W1*ew?GL(tdPlWZIsze`oKR3b2LqrR}QpdjY?|OsZ@^ZEmg;mW`IZ5nhT7tG;=Y nT?__`ZF2*+R~qB`yi9d(90FbsQg0k_00000NkvXXu0mjfT!no{ delta 162 zcmZ3_w2^Uwc)c(aGXnzyzumofAjK2l6XFV_o0^*5%BXDFvgPj9v;rWXu_VYZn8D%M zjWi%9*we)^gyVX0LV`d71JlI|3<{D`Qil#4kdTr(b>M*1)~r>`%^BBLEoC$)`ucmS zL($*g(;0adz7F5d(|GzF)5OHpPL3&36H-`bJzy|nmrhcztsR-$3&j NJYD@<);T3K0RUa7I~xE1 diff --git a/src/client/graphics/AnimatedSpriteLoader.ts b/src/client/graphics/AnimatedSpriteLoader.ts index 03e49e0cc..2f4ce53eb 100644 --- a/src/client/graphics/AnimatedSpriteLoader.ts +++ b/src/client/graphics/AnimatedSpriteLoader.ts @@ -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> = { 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 { diff --git a/src/client/graphics/fx/Fx.ts b/src/client/graphics/fx/Fx.ts index d4b206614..842514b78 100644 --- a/src/client/graphics/fx/Fx.ts +++ b/src/client/graphics/fx/Fx.ts @@ -16,4 +16,9 @@ export enum FxType { UnderConstruction = "UnderConstruction", Dust = "Dust", Conquest = "Conquest", + Santa = "Santa", + Snowman = "Snowman", + HappyElf = "HappyElf", + SadElf = "SadElf", + Sparks = "Sparks", } diff --git a/src/client/graphics/fx/NukeFx.ts b/src/client/graphics/fx/NukeFx.ts index 479d68e18..79f70c2cd 100644 --- a/src/client/graphics/fx/NukeFx.ts +++ b/src/client/graphics/fx/NukeFx.ts @@ -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) { diff --git a/src/client/graphics/fx/SantaFx.ts b/src/client/graphics/fx/SantaFx.ts new file mode 100644 index 000000000..88c7034d9 --- /dev/null +++ b/src/client/graphics/fx/SantaFx.ts @@ -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); + } +} diff --git a/src/client/graphics/fx/SpriteFx.ts b/src/client/graphics/fx/SpriteFx.ts index 2926121a8..24818da7c 100644 --- a/src/client/graphics/fx/SpriteFx.ts +++ b/src/client/graphics/fx/SpriteFx.ts @@ -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; diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index a2e0990a2..6b7aaf790 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -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 = new Map(); private nukeTargetFxByUnitId: Map = 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());