Merge branch 'v28'

This commit is contained in:
evanpelle
2026-01-01 14:25:45 -08:00
38 changed files with 140 additions and 365 deletions
+3 -13
View File
@@ -69,13 +69,6 @@
position: absolute;
width: 100%;
height: 100vh;
background: linear-gradient(
to bottom,
rgba(139, 26, 26, 0.5),
rgba(255, 255, 255, 0.5),
rgba(39, 174, 96, 0.5)
);
}
.dark .bg-image {
@@ -187,7 +180,6 @@
<body
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col"
>
<div class="snow"></div>
<header class="l-header">
<div class="l-header__content">
<svg
@@ -206,10 +198,8 @@
x2="100%"
y2="0%"
>
<stop offset="0%" style="stop-color: #e53935" />
<!-- Vibrant Red -->
<stop offset="100%" style="stop-color: #388e3c" />
<!-- Deep Green -->
<stop offset="0%" style="stop-color: #2563eb" />
<stop offset="100%" style="stop-color: #3b82f6" />
</linearGradient>
</defs>
<g>
@@ -332,7 +322,7 @@
id="settings-button"
title="Settings"
class="fixed bottom-4 right-4 z-50 rounded-full p-2 shadow-lg transition-colors duration-300 flex items-center justify-center"
style="width: 80px; height: 80px; background-color: var(--primaryColor)"
style="width: 80px; height: 80px; background-color: #0075ff"
>
<img
src="/images/SettingIconWhite.svg"
+1 -31
View File
@@ -34,6 +34,7 @@
"obscenity": "^0.4.3",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"tsx": "^4.17.0",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"ws": "^8.18.0",
@@ -89,7 +90,6 @@
"sinon-chai": "^4.0.0",
"tailwindcss": "^3.4.17",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.26.0",
"vite": "^7.3.0",
@@ -1300,7 +1300,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1317,7 +1316,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1334,7 +1332,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1351,7 +1348,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1368,7 +1364,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1385,7 +1380,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1402,7 +1396,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1419,7 +1412,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1436,7 +1428,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1453,7 +1444,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1470,7 +1460,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1487,7 +1476,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1504,7 +1492,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1521,7 +1508,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1538,7 +1524,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1555,7 +1540,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1572,7 +1556,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1589,7 +1572,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1606,7 +1588,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1623,7 +1604,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1640,7 +1620,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1674,7 +1653,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1691,7 +1669,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1708,7 +1685,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1725,7 +1701,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -6956,7 +6931,6 @@
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -7703,7 +7677,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -7787,7 +7760,6 @@
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -10406,7 +10378,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
@@ -11832,7 +11803,6 @@
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

+1 -1
View File
@@ -379,7 +379,7 @@ export class AccountButton extends LitElement {
<div class="fixed top-4 right-4 z-[9998]">
<button
@click="${this.open}"
class="w-12 h-12 bg-red-600 hover:bg-red-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-red-500 focus:ring-offset-4"
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
title="${buttonTitle}"
>
${this.renderIcon()}
+1 -55
View File
@@ -43,7 +43,6 @@ import { UsernameInput } from "./UsernameInput";
import { incrementGamesPlayed, isInIframe } from "./Utils";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./snow.css";
import "./styles.css";
import "./styles/components/button.css";
import "./styles/components/controls.css";
@@ -55,7 +54,6 @@ import "./styles/core/variables.css";
import "./styles/layout/container.css";
import "./styles/layout/header.css";
import "./styles/modal/chat.css";
import Snowflake3Png from "/images/Snowflake.webp?url";
declare global {
interface Window {
turnstile: any;
@@ -570,9 +568,6 @@ class Client {
document.querySelectorAll(".ad").forEach((ad) => {
(ad as HTMLElement).style.display = "none";
});
// Hide snowflakes when joining lobby
document.documentElement.classList.add("in-game");
removeSnowflakes(); // Stop snowflakes when joining a game
crazyGamesSDK.loadingStart();
@@ -618,9 +613,6 @@ class Client {
this.gutterAds.hide();
this.publicLobby.leaveLobby();
// Show snowflakes when leaving lobby (back to homepage)
document.documentElement.classList.remove("in-game");
enableSnowflakes(); // Restart snowflakes when leaving a game
}
private handleKickPlayer(event: CustomEvent) {
@@ -688,58 +680,12 @@ class Client {
}
}
}
function enableSnowflakes() {
// Respect user's motion preferences
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReducedMotion) {
return;
}
const snowContainer = document.querySelector(".snow") as HTMLElement;
if (!snowContainer) {
console.warn("Snow container element not found");
return;
}
// Clear existing snowflakes if any
removeSnowflakes();
const isMobile = window.innerWidth <= 768;
const numberOfSnowflakes = isMobile ? 30 : 75; // Increased count
for (let i = 0; i < numberOfSnowflakes; i++) {
const snowflake = document.createElement("div");
snowflake.classList.add("snowflake");
snowflake.style.left = `${Math.random() * 100}vw`; // Random horizontal position
snowflake.style.animationDuration = `${Math.random() * 10 + 5}s`; // Random duration between 5-15s
snowflake.style.animationDelay = `${Math.random() * -10}s`; // Random delay
snowflake.style.opacity = `${Math.random() * 0.5 + 0.5}`; // Random opacity between 0.5-1
const size = Math.random() * 20 + 10; // Random size between 10-30px
snowflake.style.width = `${size}px`;
snowflake.style.height = `${size}px`;
snowflake.style.backgroundImage = `url(${Snowflake3Png})`;
snowContainer.appendChild(snowflake);
}
}
function removeSnowflakes() {
const snowContainer = document.querySelector(".snow") as HTMLElement;
if (snowContainer) {
snowContainer.replaceChildren();
}
}
// Initialize the client when the DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
new Client().initialize();
// Initially enable snowflakes if not in-game
if (!document.documentElement.classList.contains("in-game")) {
enableSnowflakes();
}
});
async function getTurnstileToken(): Promise<{
token: string;
createdAt: number;
+1 -1
View File
@@ -173,7 +173,7 @@ export class MatchmakingButton extends LitElement {
<div class="z-[9999]">
<button
@click="${this.open}"
class="w-full h-16 bg-red-600 hover:bg-red-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-red-500 focus:ring-offset-4"
class="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
title="${translateText("matchmaking_modal.title")}"
>
Matchmaking
-5
View File
@@ -7,7 +7,6 @@ import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import changelog from "/changelog.md?url";
import megaphone from "/images/Megaphone.svg?url";
import santaHatIcon from "/images/SantaHat.webp?url";
@customElement("news-modal")
export class NewsModal extends LitElement {
@@ -167,10 +166,6 @@ export class NewsButton extends LitElement {
src="${megaphone}"
alt=${translateText("news.title")}
/>
<div
class="santa-hat-overlay absolute bg-contain bg-no-repeat pointer-events-none"
style="background-image: url('${santaHatIcon}')"
></div>
</button>
</div>
<news-modal></news-modal>
+17 -14
View File
@@ -116,8 +116,8 @@ export class PublicLobby extends LitElement {
?disabled=${this.isButtonDebounced}
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
.isLobbyHighlighted
? "bg-gradient-to-r from-emerald-600 to-emerald-500"
: "bg-gradient-to-r from-red-800 to-red-700"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
? "bg-gradient-to-r from-green-600 to-green-500"
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
.isButtonDebounced
? "opacity-70 cursor-not-allowed"
: ""}"
@@ -147,24 +147,27 @@ export class PublicLobby extends LitElement {
: translateText("public_lobby.join")}
</div>
<div class="text-md font-medium text-white-400">
<span class="text-sm text-red-800 bg-white rounded-sm px-1 mr-1">
${fullModeLabel}
</span>
<span>
${translateText(
`map.${lobby.gameConfig.gameMap
.toLowerCase()
.replace(/[\s.]+/g, "")}`,
)}
</span>
${fullModeLabel
? html`<span
class="text-sm ${this.isLobbyHighlighted
? "text-green-600"
: "text-blue-600"} bg-white rounded-sm px-1 ml-1"
>${fullModeLabel}</span
>`
: ""}
<span
>${translateText(
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/[\s.]+/g, "")}`,
)}</span
>
</div>
</div>
<div>
<div class="text-md font-medium text-white-400">
<div class="text-md font-medium text-blue-100">
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
</div>
<div class="text-md font-medium text-white-400">${timeDisplay}</div>
<div class="text-md font-medium text-blue-100">${timeDisplay}</div>
</div>
</div>
</button>
+3 -3
View File
@@ -80,7 +80,7 @@ export class StatsModal extends LitElement {
${translateText("stats_modal.loading")}
</p>
<div
class="w-6 h-6 border-4 border-red-500 border-t-transparent rounded-full animate-spin"
class="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"
></div>
</div>
`;
@@ -91,7 +91,7 @@ export class StatsModal extends LitElement {
<div class="flex flex-col items-center justify-center p-6 text-white">
<p class="mb-4 text-center">${this.error}</p>
<button
class="px-4 py-2 bg-red-600 hover:bg-red-700 rounded text-sm font-medium"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium"
@click=${() => this.loadLeaderboard()}
>
Retry
@@ -225,7 +225,7 @@ export class StatsButton extends LitElement {
<div class="fixed top-20 right-4 z-[9998]">
<button
@click="${this.open}"
class="w-12 h-12 bg-red-600 hover:bg-red-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-red-500 focus:ring-offset-4"
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
title="${translateText("stats_modal.title")}"
>
<img src="/icons/stats.svg" alt="Stats" class="w-6 h-6" />
@@ -71,10 +71,10 @@ export class OModal extends LitElement {
backdrop-filter: blur(8px);
}
`;
public open() {
this.isModalOpen = true;
}
public close() {
if (this.isModalOpen) {
this.isModalOpen = false;
+52 -52
View File
@@ -1,25 +1,25 @@
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 conquestChampagne from "../../../resources/sprites/nyeve/conquest.png";
import nukeEve from "../../../resources/sprites/nyeve/firework.png";
import nukeEveCyan from "../../../resources/sprites/nyeve/firework_cyan.png";
import nukeEveRed from "../../../resources/sprites/nyeve/firework_red.png";
import nukeEveYellow from "../../../resources/sprites/nyeve/firework_yellow.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 { Theme } from "../../core/configuration/Config";
import { PlayerView } from "../../core/game/GameView";
import { AnimatedSprite } from "./AnimatedSprite";
import { FxType } from "./fx/Fx";
import { colorizeCanvas } from "./SpriteLoader";
import miniBigSmoke from "/sprites/bigsmoke.png?url";
import buildingExplosion from "/sprites/buildingExplosion.png?url";
import happyElf from "/sprites/christmas/happy_elf.png?url";
import sadElf from "/sprites/christmas/sad_elf.png?url";
import santa from "/sprites/christmas/santa.png?url";
import snowman from "/sprites/christmas/snowman.png?url";
import sparks from "/sprites/christmas/sparks.png?url";
import conquestSword from "/sprites/conquestSword.png?url";
import dust from "/sprites/dust.png?url";
import miniExplosion from "/sprites/miniExplosion.png?url";
import miniFire from "/sprites/minifire.png?url";
import nuke from "/sprites/nukeExplosion.png?url";
import SAMExplosion from "/sprites/samExplosion.png?url";
import sinkingShip from "/sprites/sinkingShip.png?url";
import miniSmoke from "/sprites/smoke.png?url";
import miniSmokeAndFire from "/sprites/smokeAndFire.png?url";
import unitExplosion from "/sprites/unitExplosion.png?url";
type AnimatedSpriteConfig = {
url: string;
@@ -140,50 +140,50 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
originX: 10,
originY: 16,
},
[FxType.Santa]: {
url: santa,
frameWidth: 34,
[FxType.ConquestChampagne]: {
url: conquestChampagne,
frameWidth: 28,
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,
originX: 14,
originY: 23,
},
[FxType.HappyElf]: {
url: happyElf,
frameWidth: 7,
frameCount: 5,
[FxType.FireworkAll]: {
url: nukeEve,
frameWidth: 60,
frameCount: 15,
frameDuration: 90,
looping: true,
originX: 3,
originY: 7,
looping: false,
originX: 30,
originY: 30,
},
[FxType.SadElf]: {
url: sadElf,
frameWidth: 14,
frameCount: 10,
frameDuration: 90,
looping: true,
originX: 6,
originY: 10,
[FxType.FireworkRed]: {
url: nukeEveRed,
frameWidth: 30,
frameCount: 9,
frameDuration: 100,
looping: false,
originX: 15,
originY: 20,
},
[FxType.Sparks]: {
url: sparks,
frameWidth: 13,
[FxType.FireworkCyan]: {
url: nukeEveCyan,
frameWidth: 30,
frameCount: 13,
frameDuration: 60,
frameDuration: 100,
looping: false,
originX: 6,
originY: 6,
originX: 15,
originY: 20,
},
[FxType.FireworkYellow]: {
url: nukeEveYellow,
frameWidth: 30,
frameCount: 15,
frameDuration: 100,
looping: false,
originX: 15,
originY: 20,
},
};
+1 -1
View File
@@ -25,7 +25,7 @@ export function conquestFxFactory(
animatedSpriteLoader,
x,
y,
FxType.Conquest,
FxType.ConquestChampagne,
2500,
);
const fadeAnimation = new FadeFx(swordAnimation, 0.1, 0.6);
+5 -5
View File
@@ -16,9 +16,9 @@ export enum FxType {
UnderConstruction = "UnderConstruction",
Dust = "Dust",
Conquest = "Conquest",
Santa = "Santa",
Snowman = "Snowman",
HappyElf = "HappyElf",
SadElf = "SadElf",
Sparks = "Sparks",
FireworkAll = "FireworkAll",
FireworkRed = "FireworkRed",
FireworkYellow = "FireworkYellow",
FireworkCyan = "FireworkCyan",
ConquestChampagne = "ConquestChampagne",
}
+5 -8
View File
@@ -55,7 +55,7 @@ function addSpriteInCircle(
game.isLand(game.ref(spawnX, spawnY))
) {
const sprite = new FadeFx(
new SpriteFx(animatedSpriteLoader, spawnX, spawnY, type, 6000),
new SpriteFx(animatedSpriteLoader, spawnX, spawnY, type),
0.1,
0.8,
);
@@ -79,19 +79,16 @@ export function nukeFxFactory(
): Fx[] {
const nukeFx: Fx[] = [];
// Explosion animation
nukeFx.push(new SpriteFx(animatedSpriteLoader, x, y, FxType.Nuke));
// Shockwave animation
nukeFx.push(new ShockwaveFx(x, y, 1500, radius * 1.5));
nukeFx.push(new SpriteFx(animatedSpriteLoader, x, y, FxType.FireworkAll));
// Ruins and desolation sprites
const debrisPlan: Array<{
type: FxType;
radiusFactor: number;
density: number;
}> = [
{ 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.Snowman, radiusFactor: 0.9, density: 1 / 70 },
{ type: FxType.FireworkRed, radiusFactor: 1.0, density: 1 / 28 },
{ type: FxType.FireworkCyan, radiusFactor: 0.9, density: 1 / 70 },
{ type: FxType.FireworkYellow, radiusFactor: 0.9, density: 1 / 70 },
];
for (const { type, radiusFactor, density } of debrisPlan) {
-46
View File
@@ -1,46 +0,0 @@
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);
}
}
-40
View File
@@ -14,7 +14,6 @@ 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";
@@ -34,9 +33,6 @@ 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();
}
@@ -47,7 +43,6 @@ export class FxLayer implements Layer {
tick() {
this.manageBoatTargetFx();
this.spawnSantaIfNeeded();
this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
@@ -76,24 +71,6 @@ 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(
@@ -191,9 +168,6 @@ 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;
@@ -328,20 +302,6 @@ 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());
-58
View File
@@ -1,58 +0,0 @@
.snow {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1050;
}
.snowflake {
position: absolute;
background-size: contain;
background-repeat: no-repeat;
pointer-events: none;
animation-name: snowflake-fall;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: 10s; /* Adjust duration to taste */
z-index: 10;
opacity: 0.8; /* Default opacity */
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); /* Subtle glow */
will-change: transform, opacity;
}
.santa-hat-overlay {
position: absolute;
width: 55px;
height: 55px;
background-image: url("/resources/images/SantaHat.webp"); /* Use direct path for CSS background */
background-size: contain;
background-repeat: no-repeat;
top: -15px;
right: -15px;
z-index: 1000;
}
@keyframes snowflake-fall {
from {
transform: translateY(-100%) rotate(0deg); /* Start off-screen */
}
to {
transform: translateY(105vh) rotate(360deg); /* Fall completely out of view */
}
}
html.in-game .santa-hat-overlay {
display: none;
}
html.modal-active .santa-hat-overlay {
display: none;
}
@media (max-width: 768px) {
.snowflake {
filter: none;
}
}
+15 -15
View File
@@ -41,7 +41,7 @@
padding: 15px 20px;
font-size: 16px;
cursor: pointer;
background-color: var(--primaryColor);
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
@@ -51,7 +51,7 @@
}
.start-game-button:not(:disabled):hover {
background-color: var(--primaryColorHover);
background-color: #0056b3;
}
.start-game-button:disabled {
@@ -98,8 +98,8 @@
}
.option-card.selected {
border-color: var(--primaryColor);
background: rgba(229, 57, 53, 0.1);
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.option-card-title {
@@ -143,8 +143,8 @@ label.option-card:hover {
}
.option-card.selected .checkbox-icon {
border-color: var(--primaryColor);
background: var(--primaryColor);
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
@@ -242,14 +242,14 @@ label.option-card:hover {
#bots-count::-moz-range-progress,
#private-lobby-bots-count::-moz-range-progress {
height: 8px;
background-color: var(--primaryColor);
background-color: #0075ff;
}
#bots-count::-moz-range-thumb,
#private-lobby-bots-count::-moz-range-thumb {
height: 16px;
width: 16px;
background: var(--primaryColor);
background: #0075ff;
border: none;
border-radius: 50%;
}
@@ -260,7 +260,7 @@ label.option-card:hover {
height: 8px;
background: linear-gradient(
to right,
var(--primaryColor) var(--progress, 0%),
#0075ff var(--progress, 0%),
white var(--progress, 0%)
);
}
@@ -270,7 +270,7 @@ label.option-card:hover {
-webkit-appearance: none;
height: 16px;
width: 16px;
background: var(--primaryColor);
background: #0075ff;
border: none;
border-radius: 50%;
margin-top: -4px;
@@ -282,8 +282,8 @@ label.option-card:hover {
}
.random-map.selected {
border: 2px solid var(--primaryColor);
background: rgba(229, 57, 53, 0.1);
border: 2px solid #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
#helpModal table {
@@ -592,11 +592,11 @@ label.option-card:hover {
/* News Button Notification */
news-button .active button {
position: relative;
border-color: var(--primaryColor) !important;
border-color: #2563eb !important;
border-width: 2px !important;
box-shadow:
0 0 0 1px rgba(139, 26, 26, 0.5),
0 0 8px rgba(139, 26, 26, 0.4);
0 0 0 1px rgba(37, 99, 235, 0.5),
0 0 8px rgba(37, 99, 235, 0.4);
}
news-button .active button::after {
+1 -1
View File
@@ -32,7 +32,7 @@
.c-button--secondary {
background: var(--secondaryColor);
color: var(--fontColorLight);
color: var(--fontColor);
}
.c-button--secondary:hover,
+11 -11
View File
@@ -6,21 +6,21 @@
--boxBackgroundColor: #111827cc;
--fontColor: #202020;
--fontColorLight: #fff;
--primaryColor: #8b1a1a;
--primaryColorHover: #6b1515;
--primaryColor: #2563eb;
--primaryColorHover: #1d4ed8;
--primaryColorDisabled: linear-gradient(
to right,
rgba(139, 26, 26, 0.7),
rgba(107, 21, 21, 0.7)
rgb(74, 74, 74),
rgb(61, 61, 61)
);
--secondaryColor: #27ae60;
--secondaryColorHover: #1e8449;
--secondaryColor: #dbeafe;
--secondaryColorHover: #bfdbfe;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--primaryColorDark: #8b1a1a;
--primaryColorHoverDark: #6b1515;
--primaryColorDisabledDark: rgba(139, 26, 26, 0.5);
--secondaryColorDark: #27ae60;
--secondaryColorHoverDark: #1e8449;
--primaryColorDark: #3b82f6;
--primaryColorHoverDark: #2563eb;
--primaryColorDisabledDark: #4b5563;
--secondaryColorDark: #374151;
--secondaryColorHoverDark: #4b5563;
--fontColorDark: #f3f4f6;
}
+1 -1
View File
@@ -30,7 +30,7 @@
}
.l-header__highlightText {
color: var(--primaryColor);
color: #2563eb;
font-weight: 700;
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
+8
View File
@@ -94,6 +94,14 @@ export class GameManager {
return totalClients;
}
desyncCount(): number {
let totalDesyncs = 0;
this.games.forEach((game: GameServer) => {
totalDesyncs += game.desyncCount;
});
return totalDesyncs;
}
tick() {
const active = new Map<GameID, GameServer>();
for (const [id, game] of this.games) {
+4
View File
@@ -71,6 +71,8 @@ export class GameServer {
{ winner: ClientSendWinnerMessage; ips: Set<string> }
> = new Map();
public desyncCount = 0;
constructor(
public readonly id: string,
readonly log_: Logger,
@@ -831,6 +833,8 @@ export class GameServer {
const { mostCommonHash, outOfSyncClients } =
this.findOutOfSyncClients(lastHashTurn);
this.desyncCount += outOfSyncClients.length;
if (outOfSyncClients.length === 0) {
this.turns[lastHashTurn].hash = mostCommonHash;
return;
+9 -3
View File
@@ -59,6 +59,10 @@ export function initWorkerMetrics(gameManager: GameManager): void {
},
);
const desyncsGauge = meter.createObservableGauge("openfront.desyncs.gauge", {
description: "Number of detected desyncs on active games on this worker",
});
const memoryUsageGauge = meter.createObservableGauge(
"openfront.memory_usage.bytes",
{
@@ -66,19 +70,21 @@ export function initWorkerMetrics(gameManager: GameManager): void {
},
);
// Register callback for active games metric
activeGamesGauge.addCallback((result) => {
const count = gameManager.activeGames();
result.observe(count, getPromLabels());
});
// Register callback for connected clients metric
connectedClientsGauge.addCallback((result) => {
const count = gameManager.activeClients();
result.observe(count, getPromLabels());
});
// Register callback for memory usage metric
desyncsGauge.addCallback((result) => {
const count = gameManager.desyncCount();
result.observe(count, getPromLabels());
});
memoryUsageGauge.addCallback((result) => {
const memoryUsage = process.memoryUsage();
result.observe(memoryUsage.heapUsed, getPromLabels());