build: migrate build system to Vite and test runner to Vitest & Remove depracated husky usage (#2703)

- Replace Webpack with Vite for faster client bundling and HMR.
- Migrate tests from Jest to Vitest and update configuration.
- Update Web Worker instantiation to standard ESM syntax.
- Implement Env utility in `src/core` for safe, hybrid environment
variable access (Vite vs Node).
- Refactor configuration loaders to remove direct `process.env`
dependencies in shared code.
- Update TypeScript environment definitions and project scripts for the
new toolchain.
- Remove the [depracated usage of the
husky](https://github.com/typicode/husky/releases/tag/v9.0.1).

## Description:

migrate build system to Vite and test runner to Vitest & Remove
depracated husky usage

## 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
- [ ] 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:

wraith4081

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Wraith
2025-12-29 09:10:26 +03:00
committed by GitHub
parent f6412a5979
commit 26f5d40819
75 changed files with 2765 additions and 10503 deletions
+2
View File
@@ -11,3 +11,5 @@ resources/.DS_Store
.clinic/
CLAUDE.md
.idea/
# this is autogenerated by script
src/assets/
Executable → Regular
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Deprecated with husky v9
#. "$(dirname -- "$0")/_/husky.sh"
# Add PATH setup to ensure npx is found
export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
+2 -3
View File
@@ -12,8 +12,7 @@ RUN --mount=type=cache,target=/root/.npm \
# Copy only what's needed for build
COPY tsconfig.json ./
COPY tsconfig.jest.json ./
COPY webpack.config.js ./
COPY vite.config.ts ./
COPY tailwind.config.js ./
COPY postcss.config.js ./
COPY eslint.config.js ./
@@ -93,4 +92,4 @@ ENV CF_CONFIG_PATH=/etc/cloudflared/config.yml
ENV CF_CREDS_PATH=/etc/cloudflared/creds.json
# Use the startup script as the entrypoint
ENTRYPOINT ["/usr/local/bin/startup.sh"]
ENTRYPOINT ["/usr/local/bin/startup.sh"]
+1 -2
View File
@@ -26,10 +26,9 @@ export default [
allowDefaultProject: [
"__mocks__/fileMock.js",
"eslint.config.js",
"jest.config.ts",
"postcss.config.js",
"tailwind.config.js",
"webpack.config.js",
"scripts/sync-assets.mjs",
],
},
tsconfigRootDir: import.meta.dirname,
+6 -8
View File
@@ -7,12 +7,8 @@
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>OpenFront (ALPHA)</title>
<link rel="manifest" href="../../resources/manifest.json" />
<link
rel="icon"
type="image/x-icon"
href="../../resources/images/Favicon.svg"
/>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/x-icon" href="/images/Favicon.svg" />
<!-- SEO -->
<link rel="canonical" href="https://openfront.io/" />
@@ -119,6 +115,7 @@
from {
transform: translateY(-100%) rotate(0deg); /* Start off-screen */
}
to {
transform: translateY(105vh) rotate(360deg); /* Fall completely out of view */
}
@@ -338,7 +335,7 @@
style="width: 80px; height: 80px; background-color: var(--primaryColor)"
>
<img
src="../../resources/images/SettingIconWhite.svg"
src="/images/SettingIconWhite.svg"
alt="Settings"
style="width: 72px; height: 72px"
/>
@@ -416,7 +413,7 @@
>
© OpenFront™ and Contributors
<img
src="../../resources/icons/github-mark-white.svg"
src="/icons/github-mark-white.svg"
alt="GitHub"
width="20"
height="20"
@@ -511,5 +508,6 @@
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'
></script>
<script type="module" src="/src/client/Main.ts"></script>
</body>
</html>
-30
View File
@@ -1,30 +0,0 @@
export default {
testEnvironment: "node",
testRegex: "/tests/.*\\.(test|spec)?\\.(ts|tsx)$",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/fileMock.js",
},
transform: {
"^.+\\.tsx?$": ["@swc/jest"],
"^.+\\.mjs$": ["@swc/jest"],
"^.+\\.js$": ["@swc/jest"],
},
transformIgnorePatterns: [
"node_modules/(?!(nanoid|@jsep|fastpriorityqueue|@datastructures-js|jose|lit.*|@lit)/)",
],
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
coverageThreshold: {
global: {
statements: 21,
branches: 16,
lines: 21.0,
functions: 20.5,
},
},
coverageReporters: ["text", "lcov", "html"],
};
+1897 -9527
View File
File diff suppressed because it is too large Load Diff
+23 -32
View File
@@ -1,26 +1,33 @@
{
"name": "openfront-client",
"scripts": {
"build-dev": "webpack --config webpack.config.js --mode development",
"build-prod": "webpack --config webpack.config.js --mode production",
"start:client": "webpack serve --open --node-env development",
"start:server": "node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
"start:server-dev": "cross-env GAME_ENV=dev node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
"build-dev": "concurrently \"tsc --noEmit\" \"vite build --mode development\"",
"build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
"start:client": "vite",
"start:server": "tsx src/server/Server.ts",
"start:server-dev": "cross-env GAME_ENV=dev tsx src/server/Server.ts",
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"docs:map-generator": "cd map-generator && go doc -cmd -u -all",
"tunnel": "npm run build-prod && npm run start:server",
"test": "jest",
"test": "vitest run",
"perf": "npx tsx tests/perf/*.ts",
"test:coverage": "jest --coverage",
"test:coverage": "vitest run --coverage",
"format": "prettier --ignore-unknown --write .",
"format:map-generator": "cd map-generator && go fmt .",
"lint": "eslint",
"lint:fix": "eslint --fix",
"prepare": "husky",
"gen-maps": "cd map-generator && go run . && npm run format",
"inst": "npm ci --ignore-scripts"
"inst": "npm ci --ignore-scripts",
"sync-assets": "node scripts/sync-assets.mjs",
"predev": "npm run sync-assets",
"prebuild-dev": "npm run sync-assets",
"prebuild-prod": "npm run sync-assets",
"prestart:client": "npm run sync-assets",
"pretest": "npm run sync-assets",
"pretest:coverage": "npm run sync-assets"
},
"lint-staged": {
"**/*": [
@@ -29,13 +36,9 @@
]
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/preset-typescript": "^7.24.7",
"@datastructures-js/priority-queue": "^6.3.3",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@swc/jest": "^0.2.39",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
@@ -43,7 +46,6 @@
"@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/jest": "^30.0.0",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
"@types/msgpack5": "^3.4.6",
@@ -53,29 +55,21 @@
"@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.20",
"benchmark": "^2.1.4",
"binary-base64-loader": "^1.0.0",
"binary-loader": "^0.0.1",
"canvas": "^3.1.0",
"chai": "^5.1.1",
"concurrently": "^8.2.2",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"d3": "^7.9.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2",
"eslint-webpack-plugin": "^5.0.0",
"file-loader": "^6.2.0",
"globals": "^16.0.0",
"html-inline-script-webpack-plugin": "^3.2.1",
"html-loader": "^5.1.0",
"html-webpack-plugin": "^5.6.3",
"husky": "^9.1.7",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"jsdom": "^27.4.0",
"lint-staged": "^16.1.2",
"lit": "^3.3.1",
"lit-markdown": "^1.3.2",
@@ -83,25 +77,22 @@
"pixi-filters": "^6.1.4",
"pixi.js": "^8.11.0",
"postcss": "^8.5.1",
"postcss-loader": "^8.1.1",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-sh": "^0.17.4",
"protobufjs": "^7.5.3",
"raw-loader": "^4.0.2",
"sinon": "^21.0.0",
"sinon-chai": "^4.0.0",
"style-loader": "^4.0.0",
"tailwindcss": "^3.4.17",
"ts-loader": "^9.5.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.26.0",
"webpack": "^5.100.2",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"worker-loader": "^3.0.8"
"vite": "^7.3.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
+59
View File
@@ -0,0 +1,59 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.resolve(__dirname, "..");
const resourcesDir = path.join(root, "resources");
const assetsDir = path.join(root, "src", "assets");
const dataDir = path.join(assetsDir, "data");
const langDir = path.join(assetsDir, "lang");
const dataFiles = ["version.txt", "countries.json", "QuickChat.json"];
async function ensureDir(dir) {
await fs.mkdir(dir, { recursive: true });
}
async function copyFile(src, dest) {
await ensureDir(path.dirname(dest));
await fs.copyFile(src, dest);
}
async function copyDataFiles() {
await Promise.all(
dataFiles.map((name) =>
copyFile(path.join(resourcesDir, name), path.join(dataDir, name)),
),
);
}
async function copyLangFiles() {
const sourceDir = path.join(resourcesDir, "lang");
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
await Promise.all(
entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
.map((entry) =>
copyFile(
path.join(sourceDir, entry.name),
path.join(langDir, entry.name),
),
),
);
}
async function main() {
await ensureDir(dataDir);
await ensureDir(langDir);
await copyDataFiles();
await copyLangFiles();
console.log("Synced resources to src/assets.");
}
main().catch((error) => {
console.error("sync-assets failed:", error);
process.exit(1);
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import Countries from "./data/countries.json";
import Countries from "../assets/data/countries.json" with { type: "json" };
import { translateText } from "./Utils";
@customElement("flag-input-modal")
+1 -1
View File
@@ -1,6 +1,5 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import {
@@ -32,6 +31,7 @@ import { crazyGamesSDK } from "./CrazyGamesSDK";
import { JoinLobbyEvent } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import randomMap from "/images/RandomMap.webp?url";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
+34 -34
View File
@@ -2,40 +2,40 @@ import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./LanguageModal";
import ar from "../../resources/lang/ar.json";
import bg from "../../resources/lang/bg.json";
import bn from "../../resources/lang/bn.json";
import cs from "../../resources/lang/cs.json";
import da from "../../resources/lang/da.json";
import de from "../../resources/lang/de.json";
import el from "../../resources/lang/el.json";
import en from "../../resources/lang/en.json";
import eo from "../../resources/lang/eo.json";
import es from "../../resources/lang/es.json";
import fa from "../../resources/lang/fa.json";
import fi from "../../resources/lang/fi.json";
import fr from "../../resources/lang/fr.json";
import gl from "../../resources/lang/gl.json";
import he from "../../resources/lang/he.json";
import hi from "../../resources/lang/hi.json";
import hu from "../../resources/lang/hu.json";
import it from "../../resources/lang/it.json";
import ja from "../../resources/lang/ja.json";
import ko from "../../resources/lang/ko.json";
import mk from "../../resources/lang/mk.json";
import nl from "../../resources/lang/nl.json";
import pl from "../../resources/lang/pl.json";
import pt_BR from "../../resources/lang/pt-BR.json";
import pt_PT from "../../resources/lang/pt-PT.json";
import ru from "../../resources/lang/ru.json";
import sh from "../../resources/lang/sh.json";
import sk from "../../resources/lang/sk.json";
import sl from "../../resources/lang/sl.json";
import sv_SE from "../../resources/lang/sv-SE.json";
import tp from "../../resources/lang/tp.json";
import tr from "../../resources/lang/tr.json";
import uk from "../../resources/lang/uk.json";
import zh_CN from "../../resources/lang/zh-CN.json";
import ar from "../assets/lang/ar.json";
import bg from "../assets/lang/bg.json";
import bn from "../assets/lang/bn.json";
import cs from "../assets/lang/cs.json";
import da from "../assets/lang/da.json";
import de from "../assets/lang/de.json";
import el from "../assets/lang/el.json";
import en from "../assets/lang/en.json";
import eo from "../assets/lang/eo.json";
import es from "../assets/lang/es.json";
import fa from "../assets/lang/fa.json";
import fi from "../assets/lang/fi.json";
import fr from "../assets/lang/fr.json";
import gl from "../assets/lang/gl.json";
import he from "../assets/lang/he.json";
import hi from "../assets/lang/hi.json";
import hu from "../assets/lang/hu.json";
import it from "../assets/lang/it.json";
import ja from "../assets/lang/ja.json";
import ko from "../assets/lang/ko.json";
import mk from "../assets/lang/mk.json";
import nl from "../assets/lang/nl.json";
import pl from "../assets/lang/pl.json";
import pt_BR from "../assets/lang/pt-BR.json";
import pt_PT from "../assets/lang/pt-PT.json";
import ru from "../assets/lang/ru.json";
import sh from "../assets/lang/sh.json";
import sk from "../assets/lang/sk.json";
import sl from "../assets/lang/sl.json";
import sv_SE from "../assets/lang/sv-SE.json";
import tp from "../assets/lang/tp.json";
import tr from "../assets/lang/tr.json";
import uk from "../assets/lang/uk.json";
import zh_CN from "../assets/lang/zh-CN.json";
@customElement("lang-selector")
export class LangSelector extends LitElement {
+12 -3
View File
@@ -1,5 +1,4 @@
import Snowflake3Png from "../../resources/images/Snowflake.webp";
import version from "../../resources/version.txt";
import version from "../assets/data/version.txt?raw";
import { UserMeResponse } from "../core/ApiSchemas";
import { EventBus } from "../core/EventBus";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
@@ -46,7 +45,17 @@ import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./snow.css";
import "./styles.css";
import "./styles/components/button.css";
import "./styles/components/controls.css";
import "./styles/components/modal.css";
import "./styles/components/setting.css";
import "./styles/core/flag-animation.css";
import "./styles/core/typography.css";
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;
+4 -4
View File
@@ -1,13 +1,13 @@
import { LitElement, css, html } from "lit";
import { resolveMarkdown } from "lit-markdown";
import { customElement, property, query } from "lit/decorators.js";
import changelog from "../../resources/changelog.md";
import megaphone from "../../resources/images/Megaphone.svg";
import santaHatIcon from "../../resources/images/SantaHat.webp";
import version from "../../resources/version.txt";
import version from "../assets/data/version.txt?raw";
import { translateText } from "../client/Utils";
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 {
+1 -1
View File
@@ -1,6 +1,5 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import {
Difficulty,
@@ -28,6 +27,7 @@ import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main";
import { UsernameInput } from "./UsernameInput";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import randomMap from "/images/RandomMap.webp?url";
@customElement("single-player-modal")
export class SinglePlayerModal extends LitElement {
+1 -1
View File
@@ -1,4 +1,4 @@
import version from "../../resources/version.txt";
import version from "../assets/data/version.txt?raw";
import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader";
export const terrainMapFileLoader = new FetchGameMapLoader(`/maps`, version);
+17 -17
View File
@@ -1,25 +1,25 @@
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";
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 { 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;
+13 -13
View File
@@ -1,18 +1,18 @@
import allianceIcon from "../../../resources/images/AllianceIcon.svg";
import allianceIconFaded from "../../../resources/images/AllianceIconFaded.svg";
import allianceRequestBlackIcon from "../../../resources/images/AllianceRequestBlackIcon.svg";
import allianceRequestWhiteIcon from "../../../resources/images/AllianceRequestWhiteIcon.svg";
import crownIcon from "../../../resources/images/CrownIcon.svg";
import disconnectedIcon from "../../../resources/images/DisconnectedIcon.svg";
import embargoBlackIcon from "../../../resources/images/EmbargoBlackIcon.svg";
import embargoWhiteIcon from "../../../resources/images/EmbargoWhiteIcon.svg";
import nukeRedIcon from "../../../resources/images/NukeIconRed.svg";
import nukeWhiteIcon from "../../../resources/images/NukeIconWhite.svg";
import questionMarkIcon from "../../../resources/images/QuestionMarkIcon.svg";
import targetIcon from "../../../resources/images/TargetIcon.svg";
import traitorIcon from "../../../resources/images/TraitorIcon.svg";
import { AllPlayers, nukeTypes } from "../../core/game/Game";
import { GameView, PlayerView } from "../../core/game/GameView";
import allianceIcon from "/images/AllianceIcon.svg?url";
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.svg?url";
import allianceRequestWhiteIcon from "/images/AllianceRequestWhiteIcon.svg?url";
import crownIcon from "/images/CrownIcon.svg?url";
import disconnectedIcon from "/images/DisconnectedIcon.svg?url";
import embargoBlackIcon from "/images/EmbargoBlackIcon.svg?url";
import embargoWhiteIcon from "/images/EmbargoWhiteIcon.svg?url";
import nukeRedIcon from "/images/NukeIconRed.svg?url";
import nukeWhiteIcon from "/images/NukeIconWhite.svg?url";
import questionMarkIcon from "/images/QuestionMarkIcon.svg?url";
import targetIcon from "/images/TargetIcon.svg?url";
import traitorIcon from "/images/TraitorIcon.svg?url";
export type PlayerIconId =
| "crown"
+10 -10
View File
@@ -1,17 +1,17 @@
import { Colord } from "colord";
import atomBombSprite from "../../../resources/sprites/atombomb.png";
import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png";
import mirvSprite from "../../../resources/sprites/mirv2.png";
import samMissileSprite from "../../../resources/sprites/samMissile.png";
import tradeShipSprite from "../../../resources/sprites/tradeship.png";
import trainCarriageSprite from "../../../resources/sprites/trainCarriage.png";
import trainLoadedCarriageSprite from "../../../resources/sprites/trainCarriageLoaded.png";
import trainEngineSprite from "../../../resources/sprites/trainEngine.png";
import transportShipSprite from "../../../resources/sprites/transportship.png";
import warshipSprite from "../../../resources/sprites/warship.png";
import { Theme } from "../../core/configuration/Config";
import { TrainType, UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
import atomBombSprite from "/sprites/atombomb.png?url";
import hydrogenBombSprite from "/sprites/hydrogenbomb.png?url";
import mirvSprite from "/sprites/mirv2.png?url";
import samMissileSprite from "/sprites/samMissile.png?url";
import tradeShipSprite from "/sprites/tradeship.png?url";
import trainCarriageSprite from "/sprites/trainCarriage.png?url";
import trainLoadedCarriageSprite from "/sprites/trainCarriageLoaded.png?url";
import trainEngineSprite from "/sprites/trainEngine.png?url";
import transportShipSprite from "/sprites/transportship.png?url";
import warshipSprite from "/sprites/warship.png?url";
// Can't reuse TrainType because "loaded" is not a type, just an attribute
const TrainTypeSprite = {
+11 -11
View File
@@ -1,16 +1,5 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
@@ -34,6 +23,17 @@ import {
import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samlauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
export interface BuildItemDisplay {
unitType: UnitType;
+1 -1
View File
@@ -4,7 +4,7 @@ import { customElement, query } from "lit/decorators.js";
import { PlayerType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import quickChatData from "../../../../resources/QuickChat.json";
import quickChatData from "../../../assets/data/QuickChat.json" with { type: "json" };
import { EventBus } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { SendQuickChatEvent } from "../../Transport";
+5 -5
View File
@@ -2,11 +2,6 @@ import { html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js";
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import nukeIcon from "../../../../resources/images/NukeIconWhite.svg";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import {
AllPlayers,
@@ -50,6 +45,11 @@ import {
import { getMessageTypeClasses, translateText } from "../../Utils";
import { UIState } from "../UIState";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import nukeIcon from "/images/NukeIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
interface GameEvent {
description: string;
@@ -1,14 +1,14 @@
import { Colord } from "colord";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import leaderboardRegularIcon from "../../../../resources/images/LeaderboardIconRegularWhite.svg";
import leaderboardSolidIcon from "../../../../resources/images/LeaderboardIconSolidWhite.svg";
import teamRegularIcon from "../../../../resources/images/TeamIconRegularWhite.svg";
import teamSolidIcon from "../../../../resources/images/TeamIconSolidWhite.svg";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import leaderboardRegularIcon from "/images/LeaderboardIconRegularWhite.svg?url";
import leaderboardSolidIcon from "/images/LeaderboardIconSolidWhite.svg?url";
import teamRegularIcon from "/images/TeamIconRegularWhite.svg?url";
import teamSolidIcon from "/images/TeamIconSolidWhite.svg?url";
@customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Layer {
@@ -1,10 +1,5 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
import FastForwardIconSolid from "../../../../resources/images/FastForwardIconSolidWhite.svg";
import pauseIcon from "../../../../resources/images/PauseIconWhite.svg";
import playIcon from "../../../../resources/images/PlayIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
@@ -15,6 +10,11 @@ import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import exitIcon from "/images/ExitIconWhite.svg?url";
import FastForwardIconSolid from "/images/FastForwardIconSolidWhite.svg?url";
import pauseIcon from "/images/PauseIconWhite.svg?url";
import playIcon from "/images/PlayIconWhite.svg?url";
import settingsIcon from "/images/SettingIconWhite.svg?url";
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
+2 -2
View File
@@ -19,9 +19,9 @@ import {
MenuElementParams,
rootMenuElement,
} from "./RadialMenuElements";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import { ContextMenuEvent } from "../../InputHandler";
@customElement("main-radial-menu")
+1 -1
View File
@@ -1,4 +1,3 @@
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
@@ -17,6 +16,7 @@ import {
} from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import shieldIcon from "/images/ShieldIconBlack.svg?url";
class RenderInfo {
public icons: Map<PlayerIconId, HTMLElement> = new Map(); // Track icon elements
@@ -1,14 +1,6 @@
import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
@@ -32,6 +24,14 @@ import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
import allianceIcon from "/images/AllianceIcon.svg?url";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
function euclideanDistWorld(
coord: { x: number; y: number },
+12 -12
View File
@@ -1,15 +1,6 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import stopTradingIcon from "../../../../resources/images/StopIconWhite.png";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import startTradingIcon from "../../../../resources/images/TradingIconWhite.png";
import traitorIcon from "../../../../resources/images/TraitorIconLightRed.svg";
import breakAllianceIcon from "../../../../resources/images/TraitorIconWhite.svg";
import Countries from "../../../assets/data/countries.json" with { type: "json" };
import { EventBus } from "../../../core/EventBus";
import {
AllPlayers,
@@ -23,7 +14,6 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
import { actionButton } from "../../components/ui/ActionButton";
import "../../components/ui/Divider";
import Countries from "../../data/countries.json";
import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
import {
SendAllianceRequestIntentEvent,
@@ -44,6 +34,16 @@ import { ChatModal } from "./ChatModal";
import { EmojiTable } from "./EmojiTable";
import { Layer } from "./Layer";
import "./SendResourceModal";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import stopTradingIcon from "/images/StopIconWhite.png?url";
import targetIcon from "/images/TargetIconWhite.svg?url";
import startTradingIcon from "/images/TradingIconWhite.png?url";
import traitorIcon from "/images/TraitorIconLightRed.svg?url";
import breakAllianceIcon from "/images/TraitorIconWhite.svg?url";
@customElement("player-panel")
export class PlayerPanel extends LitElement implements Layer {
@@ -445,7 +445,7 @@ export class PlayerPanel extends LitElement implements Layer {
${country && typeof flagCode === "string"
? html`<img
src="/flags/${encodeURIComponent(flagCode)}.svg"
alt=${country?.name || "Flag"}
alt=${country?.name ?? "Flag"}
class="h-10 w-10 rounded-full object-cover"
@error=${(e: Event) => {
(e.target as HTMLImageElement).style.display = "none";
+1 -1
View File
@@ -1,5 +1,4 @@
import * as d3 from "d3";
import backIcon from "../../../../resources/images/BackIconWhite.svg";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { getSvgAspectRatio, translateText } from "../../Utils";
@@ -10,6 +9,7 @@ import {
MenuElementParams,
TooltipKey,
} from "./RadialMenuElements";
import backIcon from "/images/BackIconWhite.svg?url";
export class CloseRadialMenuEvent implements GameEvent {
constructor() {}
@@ -12,19 +12,19 @@ import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel";
import { TooltipItem } from "./RadialMenu";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
import buildIcon from "../../../../resources/images/BuildIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import infoIcon from "../../../../resources/images/InfoIcon.svg";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import xIcon from "../../../../resources/images/XIcon.svg";
import { EventBus } from "../../../core/EventBus";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import boatIcon from "/images/BoatIconWhite.svg?url";
import buildIcon from "/images/BuildIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import infoIcon from "/images/InfoIcon.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
import targetIcon from "/images/TargetIconWhite.svg?url";
import traitorIcon from "/images/TraitorIconWhite.svg?url";
import xIcon from "/images/XIcon.svg?url";
export interface MenuElementParams {
myPlayer: PlayerView;
+12 -12
View File
@@ -1,17 +1,5 @@
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
import cursorPriceIcon from "../../../../resources/images/CursorPriceIconWhite.svg";
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import sirenIcon from "../../../../resources/images/SirenIconWhite.svg";
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import musicIcon from "../../../../resources/images/music.svg";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
@@ -19,6 +7,18 @@ import { PauseGameIntentEvent } from "../../Transport";
import { translateText } from "../../Utils";
import SoundManager from "../../sound/SoundManager";
import { Layer } from "./Layer";
import structureIcon from "/images/CityIconWhite.svg?url";
import cursorPriceIcon from "/images/CursorPriceIconWhite.svg?url";
import darkModeIcon from "/images/DarkModeIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import exitIcon from "/images/ExitIconWhite.svg?url";
import explosionIcon from "/images/ExplosionIconWhite.svg?url";
import mouseIcon from "/images/MouseIconWhite.svg?url";
import ninjaIcon from "/images/NinjaIconWhite.svg?url";
import settingsIcon from "/images/SettingIconWhite.svg?url";
import sirenIcon from "/images/SirenIconWhite.svg?url";
import treeIcon from "/images/TreeIconWhite.svg?url";
import musicIcon from "/images/music.svg?url";
export class ShowSettingsModalEvent {
constructor(
@@ -3,13 +3,12 @@ import { Theme } from "../../../core/configuration/Config";
import { Cell, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "../../../../resources/images/AnchorIcon.png";
import cityIcon from "../../../../resources/images/CityIcon.png";
import factoryIcon from "../../../../resources/images/FactoryUnit.png";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import SAMMissileIcon from "../../../../resources/images/SamLauncherUnit.png";
import shieldIcon from "../../../../resources/images/ShieldIcon.png";
import anchorIcon from "/images/AnchorIcon.png?url";
import cityIcon from "/images/CityIcon.png?url";
import factoryIcon from "/images/FactoryUnit.png?url";
import missileSiloIcon from "/images/MissileSiloUnit.png?url";
import SAMMissileIcon from "/images/SamLauncherUnit.png?url";
import shieldIcon from "/images/ShieldIcon.png?url";
export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
[UnitType.City]: "circle",
@@ -2,7 +2,6 @@ import { extend } from "colord";
import a11yPlugin from "colord/plugins/a11y";
import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import {
@@ -40,6 +39,7 @@ import {
STRUCTURE_SHAPES,
ZOOM_THRESHOLD,
} from "./StructureDrawingUtils";
import bitmapFont from "/fonts/round_6x6_modified.xml?url";
extend([a11yPlugin]);
@@ -75,7 +75,8 @@ export class StructureIconsLayer implements Layer {
public playerActions: PlayerActions | null = null;
private dotsStage: PIXI.Container;
private readonly theme: Theme;
private renderer: PIXI.Renderer;
private renderer: PIXI.Renderer | null = null;
private rendererInitialized: boolean = false;
private renders: StructureRenderInfo[] = [];
private readonly seenUnits: Set<UnitView> = new Set();
private readonly mousePos = { x: 0, y: 0 };
@@ -113,7 +114,7 @@ export class StructureIconsLayer implements Layer {
} catch (error) {
console.error("Failed to load bitmap font:", error);
}
this.renderer = new PIXI.WebGLRenderer();
const renderer = new PIXI.WebGLRenderer();
this.pixicanvas = document.createElement("canvas");
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
@@ -143,7 +144,7 @@ export class StructureIconsLayer implements Layer {
this.rootStage.position.set(0, 0);
this.rootStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
await this.renderer.init({
await renderer.init({
canvas: this.pixicanvas,
resolution: 1,
width: this.pixicanvas.width,
@@ -153,6 +154,9 @@ export class StructureIconsLayer implements Layer {
backgroundAlpha: 0,
backgroundColor: 0x00000000,
});
this.renderer = renderer;
this.rendererInitialized = true;
}
shouldTransform(): boolean {
@@ -202,7 +206,7 @@ export class StructureIconsLayer implements Layer {
}
renderLayer(mainContext: CanvasRenderingContext2D) {
if (!this.renderer) {
if (!this.renderer || !this.rendererInitialized) {
return;
}
+6 -6
View File
@@ -4,16 +4,16 @@ import { EventBus } from "../../../core/EventBus";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import cityIcon from "../../../../resources/images/buildings/cityAlt1.png";
import factoryIcon from "../../../../resources/images/buildings/factoryAlt1.png";
import shieldIcon from "../../../../resources/images/buildings/fortAlt3.png";
import anchorIcon from "../../../../resources/images/buildings/port1.png";
import missileSiloIcon from "../../../../resources/images/buildings/silo1.png";
import SAMMissileIcon from "../../../../resources/images/buildings/silo4.png";
import { Cell, UnitType } from "../../../core/game/Game";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import cityIcon from "/images/buildings/cityAlt1.png?url";
import factoryIcon from "/images/buildings/factoryAlt1.png?url";
import shieldIcon from "/images/buildings/fortAlt3.png?url";
import anchorIcon from "/images/buildings/port1.png?url";
import missileSiloIcon from "/images/buildings/silo1.png?url";
import SAMMissileIcon from "/images/buildings/silo4.png?url";
const underConstructionColor = colord("rgb(150,150,150)");
+10 -10
View File
@@ -1,15 +1,5 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { Gold, PlayerActions, UnitType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
@@ -20,6 +10,16 @@ import {
import { renderNumber, translateText } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import defensePostIcon from "/images/ShieldIconWhite.svg?url";
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Howl } from "howler";
import of4 from "../../../proprietary/sounds/music/of4.mp3";
import openfront from "../../../proprietary/sounds/music/openfront.mp3";
import war from "../../../proprietary/sounds/music/war.mp3";
import kaChingSound from "../../../resources/sounds/effects/ka-ching.mp3";
import kaChingSound from "/sounds/effects/ka-ching.mp3?url";
export enum SoundEffect {
KaChing = "ka-ching",
+1 -10
View File
@@ -1,16 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("./styles/core/flag-animation.css");
@import url("./styles/core/variables.css");
@import url("./styles/core/typography.css");
@import url("./styles/layout/header.css");
@import url("./styles/layout/container.css");
@import url("./styles/components/button.css");
@import url("./styles/components/modal.css");
@import url("./styles/modal/chat.css");
@import url("./styles/components/setting.css");
@import url("./styles/components/controls.css");
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
+26
View File
@@ -0,0 +1,26 @@
/// <reference types="vite/client" />
declare module "*.bin" {
const binContent: string;
export default binContent;
}
declare module "*.md" {
const mdContent: string;
export default mdContent;
}
declare module "*.html" {
const htmlContent: string;
export default htmlContent;
}
declare module "*.xml" {
const xmlContent: string;
export default xmlContent;
}
declare module "*.webp" {
const webpContent: string;
export default webpContent;
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
import countries from "../client/data/countries.json" with { type: "json" };
import countries from "../assets/data/countries.json" with { type: "json" };
import quickChatData from "../assets/data/QuickChat.json" with { type: "json" };
import {
ColorPaletteSchema,
PatternDataSchema,
+3 -2
View File
@@ -3,6 +3,7 @@ import { GameConfig } from "../Schemas";
import { Config, GameEnv, ServerConfig } from "./Config";
import { DefaultConfig } from "./DefaultConfig";
import { DevConfig, DevServerConfig } from "./DevConfig";
import { Env } from "./Env";
import { preprodConfig } from "./PreprodConfig";
import { prodConfig } from "./ProdConfig";
@@ -22,7 +23,7 @@ export async function getConfig(
console.log("using prod config");
return new DefaultConfig(sc, gameConfig, userSettings, isReplay);
default:
throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`);
throw Error(`unsupported server configuration: ${Env.GAME_ENV}`);
}
}
export async function getServerConfigFromClient(): Promise<ServerConfig> {
@@ -44,7 +45,7 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
return cachedSC;
}
export function getServerConfigFromServer(): ServerConfig {
const gameEnv = process.env.GAME_ENV ?? "dev";
const gameEnv = Env.GAME_ENV;
return getServerConfig(gameEnv);
}
export function getServerConfig(gameEnv: string) {
+11 -10
View File
@@ -27,6 +27,7 @@ import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas";
import { assertNever, sigmoid, simpleHash, within } from "../Util";
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
import { Env } from "./Env";
import { PastelTheme } from "./PastelTheme";
import { PastelThemeDark } from "./PastelThemeDark";
@@ -88,20 +89,20 @@ const numPlayersConfig = {
export abstract class DefaultServerConfig implements ServerConfig {
turnstileSecretKey(): string {
return process.env.TURNSTILE_SECRET_KEY ?? "";
return Env.TURNSTILE_SECRET_KEY ?? "";
}
abstract turnstileSiteKey(): string;
allowedFlares(): string[] | undefined {
return;
}
stripePublishableKey(): string {
return process.env.STRIPE_PUBLISHABLE_KEY ?? "";
return Env.STRIPE_PUBLISHABLE_KEY ?? "";
}
domain(): string {
return process.env.DOMAIN ?? "";
return Env.DOMAIN ?? "";
}
subdomain(): string {
return process.env.SUBDOMAIN ?? "";
return Env.SUBDOMAIN ?? "";
}
private publicKey: JWK;
@@ -134,24 +135,24 @@ export abstract class DefaultServerConfig implements ServerConfig {
);
}
otelEndpoint(): string {
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
return Env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
}
otelAuthHeader(): string {
return process.env.OTEL_AUTH_HEADER ?? "";
return Env.OTEL_AUTH_HEADER ?? "";
}
gitCommit(): string {
return process.env.GIT_COMMIT ?? "";
return Env.GIT_COMMIT ?? "";
}
apiKey(): string {
return process.env.API_KEY ?? "";
return Env.API_KEY ?? "";
}
adminHeader(): string {
return "x-admin-key";
}
adminToken(): string {
const token = process.env.ADMIN_TOKEN;
const token = Env.ADMIN_TOKEN;
if (!token) {
throw new Error("ADMIN_TOKEN not set");
}
@@ -225,7 +226,7 @@ export class DefaultConfig implements Config {
) {}
stripePublishableKey(): string {
return process.env.STRIPE_PUBLISHABLE_KEY ?? "";
return Env.STRIPE_PUBLISHABLE_KEY ?? "";
}
isReplay(): boolean {
+92
View File
@@ -0,0 +1,92 @@
/**
* Safely access environment variables in both Node.js and Vite environments.
* - In Vite (Browser), it uses `import.meta.env`.
* - In Node.js (Server), it uses `process.env`.
*/
declare global {
interface ImportMetaEnv {
[key: string]: string | boolean | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
}
function getEnv(key: string, viteKey?: string): string | undefined {
const vKey = viteKey ?? key;
// Try import.meta.env (Vite/Browser)
// We use a try-catch block or check existence to avoid ReferenceErrors
try {
if (typeof import.meta !== "undefined" && import.meta.env) {
const val = import.meta.env[vKey] ?? import.meta.env[key];
if (val !== undefined) {
return String(val);
}
}
} catch {
// Ignore errors accessing import.meta
}
// Try process.env (Node.js)
try {
if (typeof process !== "undefined" && process.env) {
const val = process.env[key];
if (val !== undefined) {
return val;
}
}
} catch {
// Ignore errors accessing process
}
return undefined;
}
export const Env = {
get GAME_ENV(): string {
// Check MODE for Vite, GAME_ENV for Node
try {
if (
typeof import.meta !== "undefined" &&
import.meta.env &&
import.meta.env.MODE
) {
return import.meta.env.MODE;
}
} catch {
// Ignore errors accessing import.meta
}
return getEnv("GAME_ENV") ?? "dev";
},
get TURNSTILE_SECRET_KEY() {
return getEnv("TURNSTILE_SECRET_KEY");
},
get STRIPE_PUBLISHABLE_KEY() {
return getEnv("STRIPE_PUBLISHABLE_KEY");
},
get DOMAIN() {
return getEnv("DOMAIN");
},
get SUBDOMAIN() {
return getEnv("SUBDOMAIN");
},
get OTEL_EXPORTER_OTLP_ENDPOINT() {
return getEnv("OTEL_EXPORTER_OTLP_ENDPOINT");
},
get OTEL_AUTH_HEADER() {
return getEnv("OTEL_AUTH_HEADER");
},
get GIT_COMMIT() {
return getEnv("GIT_COMMIT");
},
get API_KEY() {
return getEnv("API_KEY");
},
get ADMIN_TOKEN() {
return getEnv("ADMIN_TOKEN");
},
};
+20 -49
View File
@@ -2,14 +2,6 @@ import { GameMapType } from "./Game";
import { GameMapLoader, MapData } from "./GameMapLoader";
import { MapManifest } from "./TerrainMapLoader";
export interface BinModule {
default: string;
}
interface NationMapModule {
default: MapManifest;
}
export class BinaryLoaderGameMapLoader implements GameMapLoader {
private maps: Map<GameMapType, MapData>;
@@ -36,59 +28,38 @@ export class BinaryLoaderGameMapLoader implements GameMapLoader {
);
const fileName = key?.toLowerCase();
const loadBinary = (url: string) =>
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`Failed to load ${url}`);
return res.arrayBuffer();
})
.then((buf) => new Uint8Array(buf));
const mapBasePath = `/maps/${fileName}`;
const mapData = {
mapBin: this.createLazyLoader(() =>
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/map.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
),
mapBin: this.createLazyLoader(() => loadBinary(`${mapBasePath}/map.bin`)),
map4xBin: this.createLazyLoader(() =>
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/map4x.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
loadBinary(`${mapBasePath}/map4x.bin`),
),
map16xBin: this.createLazyLoader(() =>
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/map16x.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
loadBinary(`${mapBasePath}/map16x.bin`),
),
manifest: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/manifest.json`
) as Promise<NationMapModule>
).then((m) => m.default),
fetch(`${mapBasePath}/manifest.json`).then((res) => {
if (!res.ok) {
throw new Error(`Failed to load ${mapBasePath}/manifest.json`);
}
return res.json() as Promise<MapManifest>;
}),
),
webpPath: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/thumbnail.webp`
) as Promise<{ default: string }>
).then((m) => m.default),
Promise.resolve(`${mapBasePath}/thumbnail.webp`),
),
} satisfies MapData;
this.maps.set(map, mapData);
return mapData;
}
/**
* Converts a given string into a UInt8Array where each character in the string
* is represented as an 8-bit unsigned integer.
*/
private toUInt8Array(data: string) {
const rawData = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
rawData[i] = data.charCodeAt(i);
}
return rawData;
}
}
+3 -1
View File
@@ -42,7 +42,9 @@ export class FetchGameMapLoader implements GameMapLoader {
let url = `${this.prefix}/${map}/${path}`;
if (this.cacheBuster) {
url += `${url.includes("?") ? "&" : "?"}v=${this.cacheBuster}`;
url += `${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(
this.cacheBuster.trim(),
)}`;
}
return url;
+1 -1
View File
@@ -1,4 +1,4 @@
import version from "../../../resources/version.txt";
import version from "../../assets/data/version.txt?raw";
import { createGameRunner, GameRunner } from "../GameRunner";
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
+3 -1
View File
@@ -23,7 +23,9 @@ export class WorkerClient {
private gameStartInfo: GameStartInfo,
private clientID: ClientID,
) {
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url), {
type: "module",
});
this.messageHandlers = new Map();
// Set up global message handler
-47
View File
@@ -1,47 +0,0 @@
declare module "*.png" {
const content: string;
export default content;
}
declare module "*.jpg" {
const value: string;
export default value;
}
declare module "*.webp" {
const value: string;
export default value;
}
declare module "*.jpeg" {
const value: string;
export default value;
}
declare module "*.svg" {
const value: string;
export default value;
}
declare module "*.bin" {
const value: string;
export default value;
}
declare module "*.md" {
const value: string;
export default value;
}
declare module "*.txt" {
const value: string;
export default value;
}
declare module "*.html" {
const content: string;
export default content;
}
declare module "*.xml" {
const value: string;
export default value;
}
declare module "*.mp3" {
const value: string;
export default value;
}
Regular → Executable
+1 -1
View File
@@ -1,7 +1,7 @@
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{html,ts,js}"],
content: ["./index.html", "./src/**/*.{html,ts,js}"],
theme: {
extend: {},
},
+12 -12
View File
@@ -35,9 +35,9 @@ describe("AllianceExtensionExecution", () => {
});
test("Successfully extends existing alliance between Humans", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player2, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
vi.spyOn(player2, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "isAlive").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
game.executeNextTick();
@@ -53,7 +53,7 @@ describe("AllianceExtensionExecution", () => {
expect(player2.allianceWith(player1)).toBeTruthy();
const allianceBefore = player1.allianceWith(player2)!;
const allianceSpy = jest.spyOn(allianceBefore, "extend");
const allianceSpy = vi.spyOn(allianceBefore, "extend");
const expirationBefore = allianceBefore.expiresAt();
@@ -82,9 +82,9 @@ describe("AllianceExtensionExecution", () => {
});
test("Successfully extends existing alliance between Human and non-Human", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player3, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
vi.spyOn(player3, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "isAlive").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(player1, player3.id()));
game.executeNextTick();
@@ -100,7 +100,7 @@ describe("AllianceExtensionExecution", () => {
expect(player3.allianceWith(player1)).toBeTruthy();
const allianceBefore = player1.allianceWith(player3)!;
const allianceSpy = jest.spyOn(allianceBefore, "extend");
const allianceSpy = vi.spyOn(allianceBefore, "extend");
const expirationBefore = allianceBefore.expiresAt();
game.addExecution(new AllianceExtensionExecution(player1, player3.id()));
@@ -120,9 +120,9 @@ describe("AllianceExtensionExecution", () => {
});
test("Sends message to other player when one player requests renewal", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player2, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
vi.spyOn(player2, "isAlive").mockReturnValue(true);
vi.spyOn(player1, "isAlive").mockReturnValue(true);
// Create alliance between player1 and player2
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
@@ -139,7 +139,7 @@ describe("AllianceExtensionExecution", () => {
expect(player2.allianceWith(player1)).toBeTruthy();
// Spy on displayMessage to verify it's called
const displayMessageSpy = jest.spyOn(game, "displayMessage");
const displayMessageSpy = vi.spyOn(game, "displayMessage");
// Player1 requests renewal
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
+6 -9
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { AutoUpgradeEvent } from "../src/client/InputHandler";
import { EventBus } from "../src/core/EventBus";
@@ -19,7 +16,7 @@ describe("AutoUpgrade Feature", () => {
});
test("should emit AutoUpgradeEvent when created", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const event = new AutoUpgradeEvent(150, 250);
eventBus.emit(event);
@@ -36,7 +33,7 @@ describe("AutoUpgrade Feature", () => {
describe("AutoUpgradeEvent Integration", () => {
test("should handle multiple AutoUpgradeEvents", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const event1 = new AutoUpgradeEvent(100, 200);
const event2 = new AutoUpgradeEvent(300, 400);
@@ -70,7 +67,7 @@ describe("AutoUpgrade Feature", () => {
describe("AutoUpgradeEvent Event Bus Integration", () => {
test("should allow event listeners to subscribe to AutoUpgradeEvent", () => {
const mockListener = jest.fn();
const mockListener = vi.fn();
const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener);
@@ -80,8 +77,8 @@ describe("AutoUpgrade Feature", () => {
});
test("should allow multiple listeners for AutoUpgradeEvent", () => {
const mockListener1 = jest.fn();
const mockListener2 = jest.fn();
const mockListener1 = vi.fn();
const mockListener2 = vi.fn();
const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener1);
@@ -93,7 +90,7 @@ describe("AutoUpgrade Feature", () => {
});
test("should not call unsubscribed listeners", () => {
const mockListener = jest.fn();
const mockListener = vi.fn();
const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener);
+2 -2
View File
@@ -1,5 +1,5 @@
// Mocking the obscenity library to control its behavior in tests.
jest.mock("obscenity", () => {
vi.mock("obscenity", () => {
return {
RegExpMatcher: class {
private dummy: string[] = ["foo", "bar", "leet", "code"];
@@ -26,7 +26,7 @@ jest.mock("obscenity", () => {
});
// Mocks the output of translation functions to return predictable values.
jest.mock("../src/client/Utils", () => ({
vi.mock("../src/client/Utils", () => ({
translateText: (key: string, vars?: any) =>
vars ? `${key}:${JSON.stringify(vars)}` : key,
}));
+4 -4
View File
@@ -112,7 +112,7 @@ describe("DeleteUnitExecution Security Tests", () => {
});
it("should prevent deleting units during spawn phase", () => {
jest.spyOn(game, "inSpawnPhase").mockReturnValue(true);
vi.spyOn(game, "inSpawnPhase").mockReturnValue(true);
const execution = new DeleteUnitExecution(player, unit.id());
execution.init(game, 0);
@@ -122,7 +122,7 @@ describe("DeleteUnitExecution Security Tests", () => {
});
it("should allow deleting units when all conditions are met", () => {
jest.spyOn(game, "inSpawnPhase").mockReturnValue(false);
vi.spyOn(game, "inSpawnPhase").mockReturnValue(false);
const execution = new DeleteUnitExecution(player, unit.id());
execution.init(game, 0);
@@ -131,7 +131,7 @@ describe("DeleteUnitExecution Security Tests", () => {
});
it("should delete after deletion delay", () => {
jest.spyOn(game, "inSpawnPhase").mockReturnValue(false);
vi.spyOn(game, "inSpawnPhase").mockReturnValue(false);
const execution = new DeleteUnitExecution(player, unit.id());
game.addExecution(execution);
@@ -144,7 +144,7 @@ describe("DeleteUnitExecution Security Tests", () => {
});
it("should reset deletion if captured", () => {
jest.spyOn(game, "inSpawnPhase").mockReturnValue(false);
vi.spyOn(game, "inSpawnPhase").mockReturnValue(false);
const execution = new DeleteUnitExecution(player, unit.id());
game.addExecution(execution);
+19 -22
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { AutoUpgradeEvent, InputHandler } from "../src/client/InputHandler";
import { EventBus } from "../src/core/EventBus";
@@ -18,7 +15,7 @@ class MockPointerEvent {
this.clientX = init.clientX;
this.clientY = init.clientY;
this.pointerId = init.pointerId;
this.preventDefault = jest.fn();
this.preventDefault = vi.fn();
}
}
@@ -45,7 +42,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Middle Mouse Button Handling", () => {
test("should emit AutoUpgradeEvent on middle mouse button press", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -65,7 +62,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should emit MouseDownEvent on left mouse button press instead of AutoUpgradeEvent", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 0,
@@ -89,7 +86,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should not emit AutoUpgradeEvent on right mouse button press", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 2,
@@ -109,7 +106,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle multiple middle mouse button presses", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
@@ -145,7 +142,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle middle mouse button press with zero coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -165,7 +162,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle middle mouse button press with negative coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -185,7 +182,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle middle mouse button press with decimal coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -207,7 +204,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Pointer Event Handling", () => {
test("should handle pointer events with different pointer IDs", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
@@ -229,7 +226,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle pointer events with same pointer ID", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
@@ -253,7 +250,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Edge Cases", () => {
test("should handle very large coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -273,7 +270,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle very small coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -293,7 +290,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle NaN coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -313,7 +310,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should handle Infinity coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit");
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
@@ -335,7 +332,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Integration with Event Bus", () => {
test("should allow event listeners to receive AutoUpgradeEvents", () => {
const mockListener = jest.fn();
const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener);
@@ -356,8 +353,8 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should allow multiple listeners for AutoUpgradeEvent", () => {
const mockListener1 = jest.fn();
const mockListener2 = jest.fn();
const mockListener1 = vi.fn();
const mockListener2 = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener1);
eventBus.on(AutoUpgradeEvent, mockListener2);
@@ -385,7 +382,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("should not call unsubscribed listeners", () => {
const mockListener = jest.fn();
const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener);
eventBus.off(AutoUpgradeEvent, mockListener);
@@ -444,7 +441,7 @@ describe("InputHandler AutoUpgrade", () => {
});
test("handles invalid JSON gracefully and warns", () => {
const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
localStorage.setItem("settings.keybinds", "not a json");
inputHandler.initialize();
+3 -2
View File
@@ -1,12 +1,13 @@
import { vi, type MockInstance } from "vitest";
import { getMessageTypeClasses, severityColors } from "../src/client/Utils";
import { MessageType } from "../src/core/game/Game";
describe("getMessageTypeClasses", () => {
// Spy on console.warn to track when the default case is hit
let consoleSpy: jest.SpyInstance;
let consoleSpy: MockInstance;
beforeEach(() => {
consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
+13 -15
View File
@@ -77,19 +77,17 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
}
});
jest.spyOn(player, "alliances").mockReturnValue(new Array(alliancesCount));
vi.spyOn(player, "alliances").mockReturnValue(new Array(alliancesCount));
const mockRequest = {
requestor: () => requestor,
recipient: () => player,
createdAt: () => 0 as unknown as Tick,
accept: jest.fn(),
reject: jest.fn(),
accept: vi.fn(),
reject: vi.fn(),
} as unknown as AllianceRequest;
jest
.spyOn(player, "incomingAllianceRequests")
.mockReturnValue([mockRequest]);
vi.spyOn(player, "incomingAllianceRequests").mockReturnValue([mockRequest]);
return mockRequest;
}
@@ -151,19 +149,19 @@ describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
let allianceBehavior: NationAllianceBehavior;
beforeEach(() => {
mockGame = { addExecution: jest.fn() };
mockHuman = { id: jest.fn(() => "human_id") };
mockGame = { addExecution: vi.fn() };
mockHuman = { id: vi.fn(() => "human_id") };
mockAlliance = {
onlyOneAgreedToExtend: jest.fn(() => true),
other: jest.fn(() => mockHuman),
onlyOneAgreedToExtend: vi.fn(() => true),
other: vi.fn(() => mockHuman),
};
mockRandom = { chance: jest.fn() };
mockRandom = { chance: vi.fn() };
mockPlayer = {
alliances: jest.fn(() => [mockAlliance]),
relation: jest.fn(),
id: jest.fn(() => "bot_id"),
type: jest.fn(() => PlayerType.Nation),
alliances: vi.fn(() => [mockAlliance]),
relation: vi.fn(),
id: vi.fn(() => "bot_id"),
type: vi.fn(() => PlayerType.Nation),
};
allianceBehavior = new NationAllianceBehavior(
+6 -9
View File
@@ -1,11 +1,8 @@
/**
* @jest-environment jsdom
*/
import { FluentSlider } from "../../../src/client/components/FluentSlider";
// Mock the translateText function
jest.mock("../../../src/client/Utils", () => ({
translateText: jest.fn((key: string) => key),
vi.mock("../../../src/client/Utils", () => ({
translateText: vi.fn((key: string) => key),
}));
describe("FluentSlider", () => {
@@ -84,7 +81,7 @@ describe("FluentSlider", () => {
describe("Value-Changed Event - CRITICAL FOR BUG FIX", () => {
it("should dispatch CustomEvent with detail.value (not event.target.value)", async () => {
const eventSpy = jest.fn();
const eventSpy = vi.fn();
slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.shadowRoot?.querySelector(
@@ -107,7 +104,7 @@ describe("FluentSlider", () => {
});
it("should not dispatch event on input, only on change", async () => {
const eventSpy = jest.fn();
const eventSpy = vi.fn();
slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.shadowRoot?.querySelector(
@@ -128,7 +125,7 @@ describe("FluentSlider", () => {
it("should work with the handler pattern used in HostLobbyModal", async () => {
// This simulates the actual handler code in HostLobbyModal.ts:656-660
const mockHandler = jest.fn((e: Event) => {
const mockHandler = vi.fn((e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
@@ -154,7 +151,7 @@ describe("FluentSlider", () => {
it("should work with the handler pattern used in SinglePlayerModal", async () => {
// This simulates the actual handler code in SinglePlayerModal.ts:444-451
const mockHandler = jest.fn((e: Event) => {
const mockHandler = vi.fn((e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
+4 -7
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { ProgressBar } from "../../../src/client/graphics/ProgressBar";
describe("ProgressBar", () => {
@@ -15,9 +12,9 @@ describe("ProgressBar", () => {
});
it("should initialize and draw the background", () => {
const spyClearRect = jest.spyOn(ctx, "clearRect");
const spyFillRect = jest.spyOn(ctx, "fillRect");
const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set");
const spyClearRect = vi.spyOn(ctx, "clearRect");
const spyFillRect = vi.spyOn(ctx, "fillRect");
const spyFillStyle = vi.spyOn(ctx, "fillStyle", "set");
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5);
expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12);
expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10);
@@ -28,7 +25,7 @@ describe("ProgressBar", () => {
it("should set progress and draw the progress bar", () => {
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10);
const spyFillRect = jest.spyOn(ctx, "fillRect");
const spyFillRect = vi.spyOn(ctx, "fillRect");
bar.setProgress(0.5);
expect(bar.getProgress()).toBe(0.5);
expect(spyFillRect).toHaveBeenCalledWith(
@@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { vi, type Mock } from "vitest";
import {
attackMenuElement,
buildMenuElement,
@@ -13,13 +11,15 @@ import { UnitType } from "../../../src/core/game/Game";
import { TileRef } from "../../../src/core/game/GameMap";
import { GameView, PlayerView } from "../../../src/core/game/GameView";
jest.mock("../../../src/client/Utils", () => ({
translateText: jest.fn((key: string) => key),
renderNumber: jest.fn((num: number) => num.toString()),
vi.mock("../../../src/client/Utils", () => ({
translateText: vi.fn((key: string) => key),
renderNumber: vi.fn((num: number) => num.toString()),
}));
jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
const { UnitType } = jest.requireActual("../../../src/core/game/Game");
vi.mock("../../../src/client/graphics/layers/BuildMenu", async () => {
const { UnitType } = await vi.importActual<
typeof import("../../../src/core/game/Game")
>("../../../src/core/game/Game");
return {
flattenedBuildTable: [
{
@@ -68,14 +68,14 @@ jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
};
});
jest.mock("nanoid", () => ({
customAlphabet: jest.fn(() => jest.fn(() => "mock-id")),
vi.mock("nanoid", () => ({
customAlphabet: vi.fn(() => vi.fn(() => "mock-id")),
}));
jest.mock("dompurify", () => ({
vi.mock("dompurify", () => ({
__esModule: true,
default: {
sanitize: jest.fn((str: string) => str),
sanitize: vi.fn((str: string) => str),
},
}));
@@ -90,29 +90,29 @@ describe("RadialMenuElements", () => {
beforeEach(() => {
mockPlayer = {
id: () => 1,
isAlliedWith: jest.fn(() => false),
isPlayer: jest.fn(() => true),
isAlliedWith: vi.fn(() => false),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockGame = {
inSpawnPhase: jest.fn(() => false),
owner: jest.fn(() => mockPlayer),
isLand: jest.fn(() => true),
config: jest.fn(() => ({
inSpawnPhase: vi.fn(() => false),
owner: vi.fn(() => mockPlayer),
isLand: vi.fn(() => true),
config: vi.fn(() => ({
theme: () => ({
territoryColor: () => ({
lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }),
}),
}),
isUnitDisabled: jest.fn(() => false),
isUnitDisabled: vi.fn(() => false),
})),
} as unknown as GameView;
mockBuildMenu = {
canBuildOrUpgrade: jest.fn(() => true),
cost: jest.fn(() => 100),
count: jest.fn(() => 5),
sendBuildOrUpgrade: jest.fn(),
canBuildOrUpgrade: vi.fn(() => true),
cost: vi.fn(() => 100),
count: vi.fn(() => 5),
sendBuildOrUpgrade: vi.fn(),
};
mockPlayerActions = {
@@ -148,7 +148,7 @@ describe("RadialMenuElements", () => {
playerPanel: {} as any,
chatIntegration: {} as any,
eventBus: {} as any,
closeMenu: jest.fn(),
closeMenu: vi.fn(),
};
});
@@ -161,19 +161,19 @@ describe("RadialMenuElements", () => {
});
it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true);
mockGame.inSpawnPhase = vi.fn(() => true);
expect(attackMenuElement.disabled(mockParams)).toBe(true);
});
it("should be enabled when not in spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => false);
mockGame.inSpawnPhase = vi.fn(() => false);
expect(attackMenuElement.disabled(mockParams)).toBe(false);
});
it("should return attack submenu with attack units only", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -203,7 +203,7 @@ describe("RadialMenuElements", () => {
it("should not include construction units in attack menu", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -237,12 +237,12 @@ describe("RadialMenuElements", () => {
});
it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true);
mockGame.inSpawnPhase = vi.fn(() => true);
expect(buildMenuElement.disabled(mockParams)).toBe(true);
});
it("should be enabled when not in spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => false);
mockGame.inSpawnPhase = vi.fn(() => false);
expect(buildMenuElement.disabled(mockParams)).toBe(false);
});
@@ -313,9 +313,9 @@ describe("RadialMenuElements", () => {
it("should show attack and boat menu on enemy territory", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockGame.owner = jest.fn(() => enemyPlayer);
mockGame.owner = vi.fn(() => enemyPlayer);
const subMenu = rootMenuElement.subMenu!(mockParams);
const buildMenu = subMenu.find((item) => item.id === Slot.Build);
@@ -337,8 +337,8 @@ describe("RadialMenuElements", () => {
it("should handle ally menu correctly", () => {
const allyPlayer = {
id: () => 2,
isAlliedWith: jest.fn(() => true),
isPlayer: jest.fn(() => true),
isAlliedWith: vi.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = allyPlayer;
@@ -367,7 +367,7 @@ describe("RadialMenuElements", () => {
it("should execute attack action correctly", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -389,7 +389,7 @@ describe("RadialMenuElements", () => {
it("should not execute action when buildable unit is not found", () => {
mockPlayerActions.buildableUnits = [];
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -420,7 +420,7 @@ describe("RadialMenuElements", () => {
it("should generate correct tooltip items for attack elements", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -452,7 +452,7 @@ describe("RadialMenuElements", () => {
it("should use correct colors for attack elements", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -465,7 +465,7 @@ describe("RadialMenuElements", () => {
});
it("should not set color when element is disabled", () => {
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -475,10 +475,12 @@ describe("RadialMenuElements", () => {
});
describe("Translation integration", () => {
it("should use translateText for tooltip items in build menu", () => {
const { translateText } = jest.requireMock("../../../src/client/Utils");
it("should use translateText for tooltip items in build menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
(translateText as jest.Mock).mockClear();
(translateText as Mock).mockClear();
buildMenuElement.subMenu!(mockParams);
@@ -488,14 +490,16 @@ describe("RadialMenuElements", () => {
expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc");
});
it("should use translateText for tooltip items in attack menu", () => {
const { translateText } = jest.requireMock("../../../src/client/Utils");
it("should use translateText for tooltip items in attack menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
(translateText as jest.Mock).mockClear();
(translateText as Mock).mockClear();
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
+2 -5
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { UILayer } from "../../../src/client/graphics/layers/UILayer";
import { UnitSelectionEvent } from "../../../src/client/InputHandler";
import { UnitView } from "../../../src/core/game/GameView";
@@ -28,7 +25,7 @@ describe("UILayer", () => {
ticks: () => 1,
updatesSinceLastTick: () => undefined,
};
eventBus = { on: jest.fn() };
eventBus = { on: vi.fn() };
transformHandler = {};
});
@@ -50,7 +47,7 @@ describe("UILayer", () => {
owner: () => ({}),
};
const event = { isSelected: true, unit };
ui.drawSelectionBox = jest.fn();
ui.drawSelectionBox = vi.fn();
ui["onUnitSelection"](event as UnitSelectionEvent);
expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit);
});
@@ -1,20 +1,20 @@
jest.mock("lit", () => ({
vi.mock("lit", () => ({
html: () => {},
LitElement: class {},
}));
jest.mock("lit/decorators.js", () => ({
vi.mock("lit/decorators.js", () => ({
customElement: () => (clazz: any) => clazz,
query: () => () => {},
state: () => () => {},
property: () => () => {},
}));
jest.mock("lit/directive.js", () => ({
vi.mock("lit/directive.js", () => ({
DirectiveResult: class {},
}));
jest.mock("lit/directives/unsafe-html.js", () => ({
vi.mock("lit/directives/unsafe-html.js", () => ({
unsafeHTML: () => {},
UnsafeHTMLDirective: class {},
}));
+4 -4
View File
@@ -28,11 +28,11 @@ describe("NukeExecution", () => {
],
);
(game.config() as TestConfig).nukeMagnitudes = jest.fn(() => ({
(game.config() as TestConfig).nukeMagnitudes = vi.fn(() => ({
inner: 10,
outer: 10,
}));
(game.config() as TestConfig).nukeAllianceBreakThreshold = jest.fn(() => 5);
(game.config() as TestConfig).nukeAllianceBreakThreshold = vi.fn(() => 5);
while (game.inSpawnPhase()) {
game.executeNextTick();
@@ -51,14 +51,14 @@ describe("NukeExecution", () => {
player.buildUnit(UnitType.MissileSilo, game.ref(1, 10), {});
// Build a SAM out of range
const sam = player.buildUnit(UnitType.SAMLauncher, game.ref(1, 11), {});
sam.touch = jest.fn();
sam.touch = vi.fn();
// Build a Defense post out of range AND out of redraw range
const defensePost = player.buildUnit(
UnitType.DefensePost,
game.ref(1, 27),
{},
);
defensePost.touch = jest.fn();
defensePost.touch = vi.fn();
// Add a nuke execution targeting the city
const nukeExec = new NukeExecution(
UnitType.AtomBomb,
@@ -20,70 +20,70 @@ describe("TradeShipExecution", () => {
infiniteGold: true,
instantBuild: true,
});
game.displayMessage = jest.fn();
game.displayMessage = vi.fn();
origOwner = {
canBuild: jest.fn(() => true),
buildUnit: jest.fn((type, spawn, opts) => tradeShip),
displayName: jest.fn(() => "Origin"),
addGold: jest.fn(),
units: jest.fn(() => [dstPort]),
unitCount: jest.fn(() => 1),
id: jest.fn(() => 1),
clientID: jest.fn(() => 1),
canTrade: jest.fn(() => true),
canBuild: vi.fn(() => true),
buildUnit: vi.fn((type, spawn, opts) => tradeShip),
displayName: vi.fn(() => "Origin"),
addGold: vi.fn(),
units: vi.fn(() => [dstPort]),
unitCount: vi.fn(() => 1),
id: vi.fn(() => 1),
clientID: vi.fn(() => 1),
canTrade: vi.fn(() => true),
} as any;
dstOwner = {
id: jest.fn(() => 2),
addGold: jest.fn(),
displayName: jest.fn(() => "Destination"),
units: jest.fn(() => [dstPort]),
unitCount: jest.fn(() => 1),
clientID: jest.fn(() => 2),
canTrade: jest.fn(() => true),
id: vi.fn(() => 2),
addGold: vi.fn(),
displayName: vi.fn(() => "Destination"),
units: vi.fn(() => [dstPort]),
unitCount: vi.fn(() => 1),
clientID: vi.fn(() => 2),
canTrade: vi.fn(() => true),
} as any;
pirate = {
id: jest.fn(() => 3),
addGold: jest.fn(),
displayName: jest.fn(() => "Destination"),
units: jest.fn(() => [piratePort]),
unitCount: jest.fn(() => 1),
canTrade: jest.fn(() => true),
id: vi.fn(() => 3),
addGold: vi.fn(),
displayName: vi.fn(() => "Destination"),
units: vi.fn(() => [piratePort]),
unitCount: vi.fn(() => 1),
canTrade: vi.fn(() => true),
} as any;
piratePort = {
tile: jest.fn(() => 40011),
owner: jest.fn(() => pirate),
isActive: jest.fn(() => true),
tile: vi.fn(() => 40011),
owner: vi.fn(() => pirate),
isActive: vi.fn(() => true),
} as any;
srcPort = {
tile: jest.fn(() => 20011),
owner: jest.fn(() => origOwner),
isActive: jest.fn(() => true),
tile: vi.fn(() => 20011),
owner: vi.fn(() => origOwner),
isActive: vi.fn(() => true),
} as any;
dstPort = {
tile: jest.fn(() => 30015), // 15x15
owner: jest.fn(() => dstOwner),
isActive: jest.fn(() => true),
tile: vi.fn(() => 30015), // 15x15
owner: vi.fn(() => dstOwner),
isActive: vi.fn(() => true),
} as any;
tradeShip = {
isActive: jest.fn(() => true),
owner: jest.fn(() => origOwner),
move: jest.fn(),
setTargetUnit: jest.fn(),
setSafeFromPirates: jest.fn(),
delete: jest.fn(),
tile: jest.fn(() => 2001),
isActive: vi.fn(() => true),
owner: vi.fn(() => origOwner),
move: vi.fn(),
setTargetUnit: vi.fn(),
setSafeFromPirates: vi.fn(),
delete: vi.fn(),
tile: vi.fn(() => 2001),
} as any;
tradeShipExecution = new TradeShipExecution(origOwner, srcPort, dstPort);
tradeShipExecution.init(game, 0);
tradeShipExecution["pathFinder"] = {
nextTile: jest.fn(() => ({ type: 0, node: 2001 })),
nextTile: vi.fn(() => ({ type: 0, node: 2001 })),
} as any;
tradeShipExecution["tradeShip"] = tradeShip;
});
@@ -94,27 +94,27 @@ describe("TradeShipExecution", () => {
});
it("should deactivate if tradeShip is not active", () => {
tradeShip.isActive = jest.fn(() => false);
tradeShip.isActive = vi.fn(() => false);
tradeShipExecution.tick(1);
expect(tradeShipExecution.isActive()).toBe(false);
});
it("should delete ship if port owner changes to current owner", () => {
dstPort.owner = jest.fn(() => origOwner);
dstPort.owner = vi.fn(() => origOwner);
tradeShipExecution.tick(1);
expect(tradeShip.delete).toHaveBeenCalledWith(false);
expect(tradeShipExecution.isActive()).toBe(false);
});
it("should pick another port if ship is captured", () => {
tradeShip.owner = jest.fn(() => pirate);
tradeShip.owner = vi.fn(() => pirate);
tradeShipExecution.tick(1);
expect(tradeShip.setTargetUnit).toHaveBeenCalledWith(piratePort);
});
it("should complete trade and award gold", () => {
tradeShipExecution["pathFinder"] = {
nextTile: jest.fn(() => ({ type: 2, node: 2001 })),
nextTile: vi.fn(() => ({ type: 2, node: 2001 })),
} as any;
tradeShipExecution.tick(1);
expect(tradeShip.delete).toHaveBeenCalledWith(false);
+18 -18
View File
@@ -13,52 +13,52 @@ describe("WinCheckExecution", () => {
maxTimerValue: 5,
instantBuild: true,
});
mg.setWinner = jest.fn();
mg.setWinner = vi.fn();
winCheck = new WinCheckExecution();
winCheck.init(mg, 0);
});
it("should call checkWinnerFFA in FFA mode", () => {
const spy = jest.spyOn(winCheck as any, "checkWinnerFFA");
const spy = vi.spyOn(winCheck as any, "checkWinnerFFA");
winCheck.tick(10);
expect(spy).toHaveBeenCalled();
});
it("should call checkWinnerTeam in non-FFA mode", () => {
mg.config = jest.fn(() => ({
gameConfig: jest.fn(() => ({
mg.config = vi.fn(() => ({
gameConfig: vi.fn(() => ({
maxTimerValue: 5,
gameMode: GameMode.Team,
})),
percentageTilesOwnedToWin: jest.fn(() => 50),
percentageTilesOwnedToWin: vi.fn(() => 50),
}));
winCheck.init(mg, 0);
const spy = jest.spyOn(winCheck as any, "checkWinnerTeam");
const spy = vi.spyOn(winCheck as any, "checkWinnerTeam");
winCheck.tick(10);
expect(spy).toHaveBeenCalled();
});
it("should set winner in FFA if percentage is reached", () => {
const player = {
numTilesOwned: jest.fn(() => 81),
name: jest.fn(() => "P1"),
numTilesOwned: vi.fn(() => 81),
name: vi.fn(() => "P1"),
};
mg.players = jest.fn(() => [player]);
mg.numLandTiles = jest.fn(() => 100);
mg.numTilesWithFallout = jest.fn(() => 0);
mg.players = vi.fn(() => [player]);
mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = vi.fn(() => 0);
winCheck.checkWinnerFFA();
expect(mg.setWinner).toHaveBeenCalledWith(player, expect.anything());
});
it("should set winner in FFA if timer is 0", () => {
const player = {
numTilesOwned: jest.fn(() => 10),
name: jest.fn(() => "P1"),
numTilesOwned: vi.fn(() => 10),
name: vi.fn(() => "P1"),
};
mg.players = jest.fn(() => [player]);
mg.numLandTiles = jest.fn(() => 100);
mg.numTilesWithFallout = jest.fn(() => 0);
mg.stats = jest.fn(() => ({ stats: () => ({ mocked: true }) }));
mg.players = vi.fn(() => [player]);
mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = vi.fn(() => 0);
mg.stats = vi.fn(() => ({ stats: () => ({ mocked: true }) }));
// Advance ticks until timeElapsed (in seconds) >= maxTimerValue * 60
// timeElapsed = (ticks - numSpawnPhaseTurns) / 10 =>
// ticks >= numSpawnPhaseTurns + maxTimerValue * 600
@@ -73,7 +73,7 @@ describe("WinCheckExecution", () => {
});
it("should not set winner if no players", () => {
mg.players = jest.fn(() => []);
mg.players = vi.fn(() => []);
winCheck.checkWinnerFFA();
expect(mg.setWinner).not.toHaveBeenCalled();
});
+7 -6
View File
@@ -1,18 +1,19 @@
import { vi, type Mocked } from "vitest";
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
const createMockStation = (id: string): jest.Mocked<TrainStation> => {
const createMockStation = (id: string): Mocked<TrainStation> => {
return {
id,
setCluster: jest.fn(),
getCluster: jest.fn(() => null),
setCluster: vi.fn(),
getCluster: vi.fn(() => null),
} as any;
};
describe("Cluster tests", () => {
let cluster: Cluster;
let stationA: jest.Mocked<TrainStation>;
let stationB: jest.Mocked<TrainStation>;
let stationC: jest.Mocked<TrainStation>;
let stationA: Mocked<TrainStation>;
let stationB: Mocked<TrainStation>;
let stationC: Mocked<TrainStation>;
beforeEach(() => {
cluster = new Cluster();
+2 -2
View File
@@ -67,7 +67,7 @@ describe("GameImpl", () => {
});
test("Don't become traitor when betraying inactive player", async () => {
jest.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
vi.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(attacker, defender.id()));
game.executeNextTick();
@@ -106,7 +106,7 @@ describe("GameImpl", () => {
});
test("Do become traitor when betraying active player", async () => {
jest.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
vi.spyOn(attacker, "canSendAllianceRequest").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(attacker, defender.id()));
game.executeNextTick();
+24 -24
View File
@@ -13,15 +13,15 @@ const createMockStation = (unitId: number): any => {
return {
unit: {
id: unitId,
setTrainStation: jest.fn(),
setTrainStation: vi.fn(),
},
tile: jest.fn(),
neighbors: jest.fn(() => []),
getCluster: jest.fn(() => cluster),
setCluster: jest.fn(),
addRailroad: jest.fn(),
getRailroads: jest.fn(() => railroads),
clearRailroads: jest.fn(),
tile: vi.fn(),
neighbors: vi.fn(() => []),
getCluster: vi.fn(() => cluster),
setCluster: vi.fn(),
addRailroad: vi.fn(),
getRailroads: vi.fn(() => railroads),
clearRailroads: vi.fn(),
};
};
@@ -54,18 +54,18 @@ describe("RailNetworkImpl", () => {
beforeEach(() => {
stationManager = {
addStation: jest.fn(),
removeStation: jest.fn(),
findStation: jest.fn(),
getAll: jest.fn(() => new Set()),
addStation: vi.fn(),
removeStation: vi.fn(),
findStation: vi.fn(),
getAll: vi.fn(() => new Set()),
};
pathService = {
findTilePath: jest.fn(() => [0]),
findStationsPath: jest.fn(() => [0]),
findTilePath: vi.fn(() => [0]),
findStationsPath: vi.fn(() => [0]),
};
game = {
nearbyUnits: jest.fn(() => []),
addExecution: jest.fn(),
nearbyUnits: vi.fn(() => []),
addExecution: vi.fn(),
config: () => ({
trainStationMaxRange: () => 80,
trainStationMinRange: () => 10,
@@ -86,7 +86,7 @@ describe("RailNetworkImpl", () => {
network.connectStation(stationA);
const cluster = stationB.getCluster();
cluster.addStation = jest.fn();
cluster.addStation = vi.fn();
expect(cluster.addStation).not.toHaveBeenCalled();
pathService.findTilePath.mockReturnValue(new Array(200));
@@ -95,9 +95,9 @@ describe("RailNetworkImpl", () => {
});
test("removeStation removes all neighbor links", () => {
const neighbor = { removeNeighboringRails: jest.fn() };
const neighbor = { removeNeighboringRails: vi.fn() };
const station = createMockStation(1);
station.neighbors = jest.fn(() => [neighbor]);
station.neighbors = vi.fn(() => [neighbor]);
stationManager.findStation.mockReturnValue(station);
network.removeStation(station);
expect(station.clearRailroads).toHaveBeenCalled();
@@ -119,9 +119,9 @@ describe("RailNetworkImpl", () => {
const cluster = new Cluster();
const neighbor = createMockStation(1);
const station = createMockStation(2);
station.getCluster = jest.fn(() => cluster);
station.neighbors = jest.fn(() => [neighbor]);
cluster.removeStation = jest.fn();
station.getCluster = vi.fn(() => cluster);
station.neighbors = vi.fn(() => [neighbor]);
cluster.removeStation = vi.fn();
stationManager.findStation.mockReturnValue(station);
@@ -150,8 +150,8 @@ describe("RailNetworkImpl", () => {
const neighborStation = createMockStation(2);
const cluster = new Cluster();
cluster.addStation(neighborStation);
neighborStation.getCluster = jest.fn(() => cluster);
cluster.has = jest.fn(() => false);
neighborStation.getCluster = vi.fn(() => cluster);
cluster.has = vi.fn(() => false);
const neighborUnit = { unit: neighborStation.unit, distSquared: 20 };
+24 -23
View File
@@ -1,47 +1,48 @@
import { vi, type Mocked } from "vitest";
import { TrainExecution } from "../../../src/core/execution/TrainExecution";
import { Game, Player, Unit, UnitType } from "../../../src/core/game/Game";
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
jest.mock("../../../src/core/game/Game");
jest.mock("../../../src/core/execution/TrainExecution");
jest.mock("../../../src/core/PseudoRandom");
vi.mock("../../../src/core/game/Game");
vi.mock("../../../src/core/execution/TrainExecution");
vi.mock("../../../src/core/PseudoRandom");
describe("TrainStation", () => {
let game: jest.Mocked<Game>;
let unit: jest.Mocked<Unit>;
let player: jest.Mocked<Player>;
let trainExecution: jest.Mocked<TrainExecution>;
let game: Mocked<Game>;
let unit: Mocked<Unit>;
let player: Mocked<Player>;
let trainExecution: Mocked<TrainExecution>;
beforeEach(() => {
game = {
ticks: jest.fn().mockReturnValue(123),
config: jest.fn().mockReturnValue({
ticks: vi.fn().mockReturnValue(123),
config: vi.fn().mockReturnValue({
trainGold: (isFriendly: boolean) =>
isFriendly ? BigInt(1000) : BigInt(500),
}),
addUpdate: jest.fn(),
addExecution: jest.fn(),
addUpdate: vi.fn(),
addExecution: vi.fn(),
} as any;
player = {
addGold: jest.fn(),
addGold: vi.fn(),
id: 1,
canTrade: jest.fn().mockReturnValue(true),
isFriendly: jest.fn().mockReturnValue(false),
canTrade: vi.fn().mockReturnValue(true),
isFriendly: vi.fn().mockReturnValue(false),
} as any;
unit = {
owner: jest.fn().mockReturnValue(player),
level: jest.fn().mockReturnValue(1),
tile: jest.fn().mockReturnValue({ x: 0, y: 0 }),
type: jest.fn(),
isActive: jest.fn().mockReturnValue(true),
owner: vi.fn().mockReturnValue(player),
level: vi.fn().mockReturnValue(1),
tile: vi.fn().mockReturnValue({ x: 0, y: 0 }),
type: vi.fn(),
isActive: vi.fn().mockReturnValue(true),
} as any;
trainExecution = {
loadCargo: jest.fn(),
owner: jest.fn().mockReturnValue(player),
level: jest.fn(),
loadCargo: vi.fn(),
owner: vi.fn().mockReturnValue(player),
level: vi.fn(),
} as any;
});
@@ -70,7 +71,7 @@ describe("TrainStation", () => {
it("checks trade availability (same owner)", () => {
const otherUnit = {
owner: jest.fn().mockReturnValue(unit.owner()),
owner: vi.fn().mockReturnValue(unit.owner()),
} as any;
const station = new TrainStation(game, unit);
-34
View File
@@ -1,34 +0,0 @@
declare module "*.png" {
const content: string;
export default content;
}
declare module "*.jpg" {
const value: string;
export default value;
}
declare module "*.webp" {
const value: string;
export default value;
}
declare module "*.jpeg" {
const value: string;
export default value;
}
declare module "*.svg" {
const value: string;
export default value;
}
declare module "*.bin" {
const value: string;
export default value;
}
declare module "*.txt" {
const value: string;
export default value;
}
declare module "*.html" {
const content: string;
export default content;
}
+15 -17
View File
@@ -1,15 +1,13 @@
import { vi } from "vitest";
// Mock BuildMenu to avoid importing lit and other ESM-heavy deps in this unit test
jest.mock(
"../src/client/graphics/layers/BuildMenu",
() => ({
BuildMenu: class {},
flattenedBuildTable: [],
}),
{ virtual: true },
);
vi.mock("../src/client/graphics/layers/BuildMenu", () => ({
BuildMenu: class {},
flattenedBuildTable: [],
}));
// Mock Utils to avoid touching DOM (document) during tests
jest.mock("../src/client/Utils", () => ({
vi.mock("../src/client/Utils", () => ({
translateText: (k: string) => k,
getSvgAspectRatio: async () => 1,
}));
@@ -57,20 +55,20 @@ const makeParams = (opts?: Partial<MenuElementParams>): MenuElementParams => {
} as any,
emojiTable: {} as any,
playerActionHandler: {
handleBreakAlliance: jest.fn(),
handleEmbargo: jest.fn(),
handleDonateGold: jest.fn(),
handleDonateTroops: jest.fn(),
handleTargetPlayer: jest.fn(),
handleBreakAlliance: vi.fn(),
handleEmbargo: vi.fn(),
handleDonateGold: vi.fn(),
handleDonateTroops: vi.fn(),
handleTargetPlayer: vi.fn(),
} as any,
playerPanel: {
show: jest.fn(),
show: vi.fn(),
} as any,
chatIntegration: {
createQuickChatMenu: jest.fn(() => []),
createQuickChatMenu: vi.fn(() => []),
} as any,
eventBus: {} as any,
closeMenu: jest.fn(),
closeMenu: vi.fn(),
};
};
+1
View File
@@ -0,0 +1 @@
// Add global mocks or configuration here if needed
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"experimentalDecorators": true,
"types": ["jest", "node"]
},
"include": ["tests/**/*", "src/**/*"]
}
+9 -5
View File
@@ -3,15 +3,16 @@
"compilerOptions": {
// Language and Environment
"target": "ES2020",
// Modules
"module": "ESNext",
"rootDir": ".",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"resources/*": ["resources/*"]
},
// Emit
"sourceMap": true,
// Type Checking
"allowSyntheticDefaultImports": true,
"allowUnusedLabels": false,
@@ -21,7 +22,9 @@
"resolveJsonModule": true,
"strictNullChecks": true,
"useDefineForClassFields": false,
"strictPropertyInitialization": false
"strictPropertyInitialization": false,
"skipLibCheck": true,
"types": ["vitest/globals", "node"]
},
"include": [
"src/**/*",
@@ -29,7 +32,8 @@
"proprietary/**/*",
"generated/**/*",
"tests/**/*",
"src/scripts"
"src/scripts",
"vite.config.ts"
],
"exclude": ["node_modules"]
}
+137
View File
@@ -0,0 +1,137 @@
import { execSync } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import { defineConfig, loadEnv } from "vite";
import { createHtmlPlugin } from "vite-plugin-html";
import { viteStaticCopy } from "vite-plugin-static-copy";
import tsconfigPaths from "vite-tsconfig-paths";
// Vite already handles these, but its good practice to define them explicitly
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let gitCommit = process.env.GIT_COMMIT;
if (!gitCommit) {
try {
gitCommit = execSync("git rev-parse HEAD").toString().trim();
} catch (error) {
if (process.env.NODE_ENV !== "production") {
console.warn("Unable to determine git commit:", error.message);
}
gitCommit = "unknown";
}
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const isProduction = mode === "production";
return {
test: {
globals: true,
environment: "jsdom",
setupFiles: "./tests/setup.ts",
},
root: "./",
base: "/",
publicDir: "resources", // Access static assets via import or explicit copy
resolve: {
alias: {
"protobufjs/minimal": path.resolve(
__dirname,
"node_modules/protobufjs/minimal.js",
),
resources: path.resolve(__dirname, "resources"),
},
},
plugins: [
tsconfigPaths(),
createHtmlPlugin({
minify: isProduction,
entry: "/src/client/Main.ts",
template: "index.html",
inject: {
data: {
// In case we need to inject variables into HTML
},
},
}),
viteStaticCopy({
targets: [
{
src: "proprietary/*",
dest: ".",
},
],
}),
],
define: {
"process.env.WEBSOCKET_URL": JSON.stringify(
isProduction ? "" : "localhost:3000",
),
"process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"),
"process.env.GIT_COMMIT": JSON.stringify(gitCommit),
"process.env.STRIPE_PUBLISHABLE_KEY": JSON.stringify(
env.STRIPE_PUBLISHABLE_KEY,
),
"process.env.API_DOMAIN": JSON.stringify(env.API_DOMAIN),
// Add other process.env variables if needed, OR migrate code to import.meta.env
},
build: {
outDir: "static", // Webpack outputs to 'static', assuming we want to keep this.
emptyOutDir: true,
assetsDir: "assets", // Sub-directory for assets
rollupOptions: {
output: {
manualChunks: {
vendor: ["pixi.js", "howler", "zod", "protobufjs"],
},
},
},
},
server: {
port: 9000,
proxy: {
"/socket": {
target: "ws://localhost:3000",
ws: true,
changeOrigin: true,
},
// Worker proxies
"/w0": {
target: "ws://localhost:3001",
ws: true,
secure: false,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/w0/, ""),
},
"/w1": {
target: "ws://localhost:3002",
ws: true,
secure: false,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/w1/, ""),
},
"/w2": {
target: "ws://localhost:3003",
ws: true,
secure: false,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/w2/, ""),
},
// API proxies
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
secure: false,
},
},
},
};
});
-263
View File
@@ -1,263 +0,0 @@
import { execSync } from "child_process";
import CopyPlugin from "copy-webpack-plugin";
import ESLintPlugin from "eslint-webpack-plugin";
import HtmlWebpackPlugin from "html-webpack-plugin";
import path from "path";
import { fileURLToPath } from "url";
import webpack from "webpack";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gitCommit =
process.env.GIT_COMMIT ?? execSync("git rev-parse HEAD").toString().trim();
export default async (env, argv) => {
const isProduction = argv.mode === "production";
return {
entry: "./src/client/Main.ts",
output: {
publicPath: "/",
filename: "js/[name].[contenthash].js", // Added content hash
path: path.resolve(__dirname, "static"),
clean: isProduction,
},
module: {
rules: [
{
test: /\.bin$/,
type: "asset/resource", // Changed from raw-loader
generator: {
filename: "binary/[name].[contenthash][ext]", // Added content hash
},
},
{
test: /\.txt$/,
type: "asset/source",
},
{
test: /\.md$/,
type: "asset/resource", // Changed from raw-loader
generator: {
filename: "text/[name].[contenthash][ext]", // Added content hash
},
},
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["tailwindcss", "autoprefixer"],
},
},
},
],
},
{
test: /\.(webp|png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name].[contenthash][ext]", // Added content hash
},
},
{
test: /\.html$/,
use: ["html-loader"],
},
{
test: /\.svg$/,
type: "asset/resource", // Changed from asset/inline for caching
generator: {
filename: "images/[name].[contenthash][ext]", // Added content hash
},
},
{
test: /\.(woff|woff2|eot|ttf|otf|xml)$/,
type: "asset/resource", // Changed from file-loader
generator: {
filename: "fonts/[name].[contenthash][ext]", // Added content hash and fixed path
},
},
{
test: /\.(mp3|wav|ogg)$/i,
type: "asset/resource",
generator: {
filename: "sounds/[name].[contenthash][ext]",
},
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
alias: {
"protobufjs/minimal": path.resolve(
__dirname,
"node_modules/protobufjs/minimal.js",
),
},
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/client/index.html",
filename: "index.html",
// Add optimization for HTML
minify: isProduction
? {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
}
: false,
}),
new webpack.DefinePlugin({
"process.env.WEBSOCKET_URL": JSON.stringify(
isProduction ? "" : "localhost:3000",
),
"process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"),
"process.env.GIT_COMMIT": JSON.stringify(gitCommit),
"process.env.STRIPE_PUBLISHABLE_KEY": JSON.stringify(
process.env.STRIPE_PUBLISHABLE_KEY,
),
"process.env.API_DOMAIN": JSON.stringify(process.env.API_DOMAIN),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "resources"),
to: path.resolve(__dirname, "static"),
noErrorOnMissing: true,
},
{
from: path.resolve(__dirname, "proprietary"),
to: path.resolve(__dirname, "static"),
noErrorOnMissing: true,
},
],
options: { concurrency: 100 },
}),
new ESLintPlugin({
context: __dirname,
}),
],
optimization: {
// Add optimization configuration for better caching
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
},
},
},
devServer: isProduction
? {}
: {
devMiddleware: { writeToDisk: true },
static: {
directory: path.join(__dirname, "static"),
},
historyApiFallback: true,
compress: true,
port: 9000,
proxy: [
// WebSocket proxies
{
context: ["/socket"],
target: "ws://localhost:3000",
ws: true,
changeOrigin: true,
logLevel: "debug",
},
// Worker WebSocket proxies - using direct paths without /socket suffix
{
context: ["/w0"],
target: "ws://localhost:3001",
ws: true,
secure: false,
changeOrigin: true,
logLevel: "debug",
},
{
context: ["/w1"],
target: "ws://localhost:3002",
ws: true,
secure: false,
changeOrigin: true,
logLevel: "debug",
},
{
context: ["/w2"],
target: "ws://localhost:3003",
ws: true,
secure: false,
changeOrigin: true,
logLevel: "debug",
},
// Worker proxies for HTTP requests
{
context: ["/w0"],
target: "http://localhost:3001",
pathRewrite: { "^/w0": "" },
secure: false,
changeOrigin: true,
logLevel: "debug",
},
{
context: ["/w1"],
target: "http://localhost:3002",
pathRewrite: { "^/w1": "" },
secure: false,
changeOrigin: true,
logLevel: "debug",
},
{
context: ["/w2"],
target: "http://localhost:3003",
pathRewrite: { "^/w2": "" },
secure: false,
changeOrigin: true,
logLevel: "debug",
},
// Original API endpoints
{
context: [
"/api/env",
"/api/game",
"/api/public_lobbies",
"/api/join_game",
"/api/start_game",
"/api/create_game",
"/api/archive_singleplayer_game",
"/api/auth/callback",
"/api/auth/discord",
"/api/kick_player",
],
target: "http://localhost:3000",
secure: false,
changeOrigin: true,
},
],
},
};
};