Halloween Event (#2285)

## Description:

Changed theme colors for an "autumn" ambiance:

<img width="395" height="254" alt="image"
src="https://github.com/user-attachments/assets/67bd07d3-e74b-49e2-ba0b-c87958767d45"
/>

Changed structures pixel art:

<img width="756" height="605" alt="image"
src="https://github.com/user-attachments/assets/5158ae81-0641-413c-8862-367259496a9a"
/>


Change existing FX with new halloween-themed ones:


https://github.com/user-attachments/assets/fb99be49-43cd-4d85-ad77-8c153070edaf

Added new FX playing randomly on the map:


https://github.com/user-attachments/assets/16631113-77e6-4b8c-b1b6-c147f5f1d275

Added a couple of new emojis, which are used by the bots when attacked:
👻🎃

## 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-10-25 23:06:15 +02:00
committed by GitHub
parent 4ed1076166
commit b69adf70b3
28 changed files with 210 additions and 61 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+79 -38
View File
@@ -1,15 +1,21 @@
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
import buildingExplosion from "../../../resources/sprites/buildingExplosion.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";
import nuke from "../../../resources/sprites/nukeExplosion.png";
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
import miniSmoke from "../../../resources/sprites/smoke.png";
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
import bats from "../../../resources/sprites/halloween/bats.png";
import bubble from "../../../resources/sprites/halloween/bubble.png";
import ghost from "../../../resources/sprites/halloween/ghost.png";
import minifireGreen from "../../../resources/sprites/halloween/minifireGreen.png";
import shark from "../../../resources/sprites/halloween/shark.png";
import skull from "../../../resources/sprites/halloween/skull.png";
import skullNuke from "../../../resources/sprites/halloween/skullNuke.png";
import miniSmokeAndFireGreen from "../../../resources/sprites/halloween/smokeAndFireGreen.png";
import tentacle from "../../../resources/sprites/halloween/tentacle.png";
import tornado from "../../../resources/sprites/halloween/tornado.png";
import { Theme } from "../../core/configuration/Config";
import { PlayerView } from "../../core/game/GameView";
import { AnimatedSprite } from "./AnimatedSprite";
@@ -28,7 +34,7 @@ type AnimatedSpriteConfig = {
const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
[FxType.MiniFire]: {
url: miniFire,
url: minifireGreen,
frameWidth: 7,
frameCount: 6,
frameDuration: 100,
@@ -37,28 +43,28 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originY: 11,
},
[FxType.MiniSmoke]: {
url: miniSmoke,
frameWidth: 11,
frameCount: 4,
frameDuration: 120,
url: ghost,
frameWidth: 10,
frameCount: 5,
frameDuration: 100,
looping: true,
originX: 2,
originX: 4,
originY: 10,
},
[FxType.MiniBigSmoke]: {
url: miniBigSmoke,
frameWidth: 24,
frameCount: 5,
url: bats,
frameWidth: 21,
frameCount: 6,
frameDuration: 120,
looping: true,
originX: 9,
originY: 14,
},
[FxType.MiniSmokeAndFire]: {
url: miniSmokeAndFire,
url: miniSmokeAndFireGreen,
frameWidth: 24,
frameCount: 5,
frameDuration: 120,
frameDuration: 90,
looping: true,
originX: 9,
originY: 14,
@@ -90,15 +96,6 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 9,
originY: 9,
},
[FxType.BuildingExplosion]: {
url: buildingExplosion,
frameWidth: 17,
frameCount: 10,
frameDuration: 70,
looping: false,
originX: 8,
originY: 8,
},
[FxType.SinkingShip]: {
url: sinkingShip,
frameWidth: 16,
@@ -108,14 +105,23 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 7,
originY: 7,
},
[FxType.Nuke]: {
url: nuke,
frameWidth: 60,
frameCount: 9,
[FxType.BuildingExplosion]: {
url: buildingExplosion,
frameWidth: 17,
frameCount: 10,
frameDuration: 70,
looping: false,
originX: 30,
originY: 30,
originX: 8,
originY: 8,
},
[FxType.Nuke]: {
url: skullNuke,
frameWidth: 42,
frameCount: 19,
frameDuration: 50,
looping: false,
originX: 20,
originY: 21,
},
[FxType.SAMExplosion]: {
url: SAMExplosion,
@@ -127,16 +133,51 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originY: 19,
},
[FxType.Conquest]: {
url: conquestSword,
frameWidth: 21,
frameCount: 10,
url: skull,
frameWidth: 14,
frameCount: 14,
frameDuration: 90,
looping: false,
originX: 10,
originY: 16,
originX: 7,
originY: 23,
},
[FxType.Tentacle]: {
url: tentacle,
frameWidth: 22,
frameCount: 26,
frameDuration: 90,
looping: false,
originX: 13,
originY: 28,
},
[FxType.Shark]: {
url: shark,
frameWidth: 25,
frameCount: 14,
frameDuration: 90,
looping: false,
originX: 13,
originY: 8,
},
[FxType.Bubble]: {
url: bubble,
frameWidth: 22,
frameCount: 13,
frameDuration: 80,
looping: false,
originX: 13,
originY: 8,
},
[FxType.Tornado]: {
url: tornado,
frameWidth: 30,
frameCount: 10,
frameDuration: 80,
looping: true,
originX: 11,
originY: 22,
},
};
export class AnimatedSpriteLoader {
private animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
// Do not color the same sprite twice
+4 -4
View File
@@ -1,6 +1,6 @@
import { Colord } from "colord";
import atomBombSprite from "../../../resources/sprites/atombomb.png";
import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png";
import miniPumpkin from "../../../resources/sprites/halloween/miniPumpkin.png";
import pumpkin from "../../../resources/sprites/halloween/pumpkin.png";
import mirvSprite from "../../../resources/sprites/mirv2.png";
import samMissileSprite from "../../../resources/sprites/samMissile.png";
import tradeShipSprite from "../../../resources/sprites/tradeship.png";
@@ -26,8 +26,8 @@ const SPRITE_CONFIG: Partial<Record<UnitType | TrainTypeSprite, string>> = {
[UnitType.TransportShip]: transportShipSprite,
[UnitType.Warship]: warshipSprite,
[UnitType.SAMMissile]: samMissileSprite,
[UnitType.AtomBomb]: atomBombSprite,
[UnitType.HydrogenBomb]: hydrogenBombSprite,
[UnitType.AtomBomb]: miniPumpkin,
[UnitType.HydrogenBomb]: pumpkin,
[UnitType.TradeShip]: tradeShipSprite,
[UnitType.MIRV]: mirvSprite,
[TrainTypeSprite.Engine]: trainEngineSprite,
-1
View File
@@ -26,7 +26,6 @@ export function conquestFxFactory(
x,
y,
FxType.Conquest,
2500,
);
const fadeAnimation = new FadeFx(swordAnimation, 0.1, 0.6);
conquestFx.push(fadeAnimation);
+4
View File
@@ -16,4 +16,8 @@ export enum FxType {
UnderConstruction = "UnderConstruction",
Dust = "Dust",
Conquest = "Conquest",
Tentacle = "Tentacle",
Shark = "Shark",
Bubble = "Bubble",
Tornado = "Tornado",
}
+31 -2
View File
@@ -19,6 +19,35 @@ function fadeInOut(
return 1 - f * f;
}
}
/**
* Move a sprite around
*/
export class MoveSpriteFx implements Fx {
private originX: number;
private originY: number;
constructor(
private fxToMove: SpriteFx,
private toX: number,
private toY: number,
private fadeIn: number = 0.1,
private fadeOut: number = 0.9,
) {
this.originX = fxToMove.x;
this.originY = fxToMove.y;
}
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
const t = this.fxToMove.getElapsedTime() / this.fxToMove.getDuration();
this.fxToMove.x = Math.floor(this.originX * (1 - t) + this.toX * t);
this.fxToMove.y = Math.floor(this.originY * (1 - t) + this.toY * t);
ctx.save();
ctx.globalAlpha = fadeInOut(t, this.fadeIn, this.fadeOut);
const result = this.fxToMove.renderTick(duration, ctx);
ctx.restore();
return result;
}
}
/**
* Fade in/out another FX
*/
@@ -49,8 +78,8 @@ export class SpriteFx implements Fx {
protected waitToTheEnd = false;
constructor(
animatedSpriteLoader: AnimatedSpriteLoader,
protected x: number,
protected y: number,
public x: number,
public y: number,
fxType: FxType,
duration?: number,
owner?: PlayerView,
+77 -1
View File
@@ -13,7 +13,7 @@ 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 { FadeFx, MoveSpriteFx, SpriteFx } from "../fx/SpriteFx";
import { TargetFx } from "../fx/TargetFx";
import { TextFx } from "../fx/TextFx";
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
@@ -21,6 +21,8 @@ import { Layer } from "./Layer";
export class FxLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private lastRandomEvent: number = 0;
private randomEventRate: number = 8;
private lastRefresh: number = 0;
private refreshRate: number = 10;
@@ -40,6 +42,14 @@ export class FxLayer implements Layer {
}
tick() {
if (!this.game.config().userSettings()?.fxLayer()) {
return;
}
this.lastRandomEvent += 1;
if (this.lastRandomEvent > this.randomEventRate) {
this.lastRandomEvent = 0;
this.randomEvent();
}
this.manageBoatTargetFx();
this.game
.updatesSinceLastTick()
@@ -116,6 +126,72 @@ export class FxLayer implements Layer {
this.allFx.push(textFx);
}
randomEvent() {
const randX = Math.floor(Math.random() * this.game.width());
const randY = Math.floor(Math.random() * this.game.height());
const ref = this.game.ref(randX, randY);
if (this.game.isOcean(ref) && !this.game.isShoreline(ref)) {
const animation = Math.floor(Math.random() * 4);
if (animation === 0) {
const fx = new SpriteFx(
this.animatedSpriteLoader,
randX,
randY,
FxType.Shark,
);
this.allFx.push(fx);
} else if (animation === 1) {
const fx = new SpriteFx(
this.animatedSpriteLoader,
randX,
randY,
FxType.Bubble,
);
this.allFx.push(fx);
} else if (animation === 2) {
const fx = new MoveSpriteFx(
new SpriteFx(
this.animatedSpriteLoader,
randX,
randY,
FxType.Tornado,
6000,
),
randX - 40,
randY,
0.1,
0.8,
);
this.allFx.push(fx);
} else if (animation === 3) {
const fx = new FadeFx(
new SpriteFx(
this.animatedSpriteLoader,
randX,
randY,
FxType.Tentacle,
),
0.1,
0.8,
);
this.allFx.push(fx);
}
} else {
const ghost = new FadeFx(
new SpriteFx(
this.animatedSpriteLoader,
randX,
randY,
FxType.MiniSmoke,
4000,
),
0.1,
0.8,
);
this.allFx.push(ghost);
}
}
onUnitEvent(unit: UnitView) {
switch (unit.type()) {
case UnitType.TransportShip: {
+5 -5
View File
@@ -289,17 +289,17 @@ export function createRandomName(
}
export const emojiTable = [
["😀", "😊", "🥰", "😇", "😎"],
["😀", "😊", "😇", "😎", "😈"],
["😞", "🥺", "😭", "😱", "😡"],
["😈", "🤡", "🖕", "🥱", "🤦‍♂️"],
["👋", "👏", "🤌", "💪", "🫡"],
["", "🥱", "🤦‍♂️", "🖕", "🤡"],
["👋", "👏", "👻", "💪", "🎃"],
["👍", "👎", "❓", "🐔", "🐀"],
["🤝", "🆘", "🕊️", "🏳️", ""],
["🆘", "🤝", "🕊️", "🏳️", "🛡️"],
["🔥", "💥", "💀", "☢️", "⚠️"],
["↖️", "⬆️", "↗️", "👑", "🥇"],
["⬅️", "🎯", "➡️", "🥈", "🥉"],
["↙️", "⬇️", "↘️", "❤️", "💔"],
["💰", "", "", "🏡", "🛡️"],
["💰", "🏭", "🚂", "", ""],
] as const;
// 2d to 1d array
export const flattenedEmojiTable = emojiTable.flat();
+9 -9
View File
@@ -18,7 +18,7 @@ export class PastelTheme implements Theme {
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
private background = colord({ r: 60, g: 60, b: 60 });
private shore = colord({ r: 204, g: 203, b: 158 });
private shore = colord({ r: 223, g: 187, b: 132 });
private falloutColors = [
colord({ r: 120, g: 255, b: 71 }), // Original color
colord({ r: 130, g: 255, b: 85 }), // Slightly lighter
@@ -26,8 +26,8 @@ export class PastelTheme implements Theme {
colord({ r: 125, g: 255, b: 75 }), // Warmer tint
colord({ r: 115, g: 250, b: 68 }), // Cooler tint
];
private water = colord({ r: 70, g: 132, b: 180 });
private shorelineWater = colord({ r: 100, g: 143, b: 255 });
private water = colord({ r: 80, g: 76, b: 179 });
private shorelineWater = colord({ r: 100, g: 110, b: 255 });
private _selfColor = colord({ r: 0, g: 255, b: 0 });
private _allyColor = colord({ r: 255, g: 255, b: 0 });
@@ -97,15 +97,15 @@ export class PastelTheme implements Theme {
}
case TerrainType.Plains:
return colord({
r: 190,
g: 220 - 2 * mag,
b: 138,
r: 216,
g: 205 - 2 * mag,
b: 127,
});
case TerrainType.Highland:
return colord({
r: 200 + 2 * mag,
g: 183 + 2 * mag,
b: 138 + 2 * mag,
r: 223 + 2 * mag,
g: 187 + 2 * mag,
b: 132 + 2 * mag,
});
case TerrainType.Mountain:
return colord({
+1 -1
View File
@@ -20,7 +20,7 @@ const EMOJI_ASSIST_ACCEPT = (["👍", "⛵", "🤝", "🎯"] as const).map(emoji
const EMOJI_RELATION_TOO_LOW = (["🥱", "🤦‍♂️"] as const).map(emojiId);
const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
export const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
export const EMOJI_HECKLE = (["👻", "🎃"] as const).map(emojiId);
export class BotBehavior {
private enemy: Player | null = null;