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/ .clinic/
CLAUDE.md CLAUDE.md
.idea/ .idea/
# this is autogenerated by script
src/assets/
Executable → Regular
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" # Deprecated with husky v9
#. "$(dirname -- "$0")/_/husky.sh"
# Add PATH setup to ensure npx is found # 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" export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
+1 -2
View File
@@ -12,8 +12,7 @@ RUN --mount=type=cache,target=/root/.npm \
# Copy only what's needed for build # Copy only what's needed for build
COPY tsconfig.json ./ COPY tsconfig.json ./
COPY tsconfig.jest.json ./ COPY vite.config.ts ./
COPY webpack.config.js ./
COPY tailwind.config.js ./ COPY tailwind.config.js ./
COPY postcss.config.js ./ COPY postcss.config.js ./
COPY eslint.config.js ./ COPY eslint.config.js ./
+1 -2
View File
@@ -26,10 +26,9 @@ export default [
allowDefaultProject: [ allowDefaultProject: [
"__mocks__/fileMock.js", "__mocks__/fileMock.js",
"eslint.config.js", "eslint.config.js",
"jest.config.ts",
"postcss.config.js", "postcss.config.js",
"tailwind.config.js", "tailwind.config.js",
"webpack.config.js", "scripts/sync-assets.mjs",
], ],
}, },
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
+6 -8
View File
@@ -7,12 +7,8 @@
content="width=device-width, initial-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, user-scalable=no"
/> />
<title>OpenFront (ALPHA)</title> <title>OpenFront (ALPHA)</title>
<link rel="manifest" href="../../resources/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link <link rel="icon" type="image/x-icon" href="/images/Favicon.svg" />
rel="icon"
type="image/x-icon"
href="../../resources/images/Favicon.svg"
/>
<!-- SEO --> <!-- SEO -->
<link rel="canonical" href="https://openfront.io/" /> <link rel="canonical" href="https://openfront.io/" />
@@ -119,6 +115,7 @@
from { from {
transform: translateY(-100%) rotate(0deg); /* Start off-screen */ transform: translateY(-100%) rotate(0deg); /* Start off-screen */
} }
to { to {
transform: translateY(105vh) rotate(360deg); /* Fall completely out of view */ transform: translateY(105vh) rotate(360deg); /* Fall completely out of view */
} }
@@ -338,7 +335,7 @@
style="width: 80px; height: 80px; background-color: var(--primaryColor)" style="width: 80px; height: 80px; background-color: var(--primaryColor)"
> >
<img <img
src="../../resources/images/SettingIconWhite.svg" src="/images/SettingIconWhite.svg"
alt="Settings" alt="Settings"
style="width: 72px; height: 72px" style="width: 72px; height: 72px"
/> />
@@ -416,7 +413,7 @@
> >
© OpenFront™ and Contributors © OpenFront™ and Contributors
<img <img
src="../../resources/icons/github-mark-white.svg" src="/icons/github-mark-white.svg"
alt="GitHub" alt="GitHub"
width="20" width="20"
height="20" height="20"
@@ -511,5 +508,6 @@
src="https://static.cloudflareinsights.com/beacon.min.js" src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}' data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'
></script> ></script>
<script type="module" src="/src/client/Main.ts"></script>
</body> </body>
</html> </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", "name": "openfront-client",
"scripts": { "scripts": {
"build-dev": "webpack --config webpack.config.js --mode development", "build-dev": "concurrently \"tsc --noEmit\" \"vite build --mode development\"",
"build-prod": "webpack --config webpack.config.js --mode production", "build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
"start:client": "webpack serve --open --node-env development", "start:client": "vite",
"start:server": "node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts", "start:server": "tsx 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", "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": "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: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\"", "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", "docs:map-generator": "cd map-generator && go doc -cmd -u -all",
"tunnel": "npm run build-prod && npm run start:server", "tunnel": "npm run build-prod && npm run start:server",
"test": "jest", "test": "vitest run",
"perf": "npx tsx tests/perf/*.ts", "perf": "npx tsx tests/perf/*.ts",
"test:coverage": "jest --coverage", "test:coverage": "vitest run --coverage",
"format": "prettier --ignore-unknown --write .", "format": "prettier --ignore-unknown --write .",
"format:map-generator": "cd map-generator && go fmt .", "format:map-generator": "cd map-generator && go fmt .",
"lint": "eslint", "lint": "eslint",
"lint:fix": "eslint --fix", "lint:fix": "eslint --fix",
"prepare": "husky", "prepare": "husky",
"gen-maps": "cd map-generator && go run . && npm run format", "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": { "lint-staged": {
"**/*": [ "**/*": [
@@ -29,13 +36,9 @@
] ]
}, },
"devDependencies": { "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", "@datastructures-js/priority-queue": "^6.3.3",
"@eslint/compat": "^1.2.7", "@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",
"@swc/jest": "^0.2.39",
"@types/benchmark": "^2.1.5", "@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17", "@types/chai": "^4.3.17",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
@@ -43,7 +46,6 @@
"@types/google-protobuf": "^3.15.12", "@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46", "@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
"@types/jest": "^30.0.0",
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/msgpack5": "^3.4.6", "@types/msgpack5": "^3.4.6",
@@ -53,29 +55,21 @@
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1", "@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11", "@types/ws": "^8.5.11",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"benchmark": "^2.1.4", "benchmark": "^2.1.4",
"binary-base64-loader": "^1.0.0",
"binary-loader": "^0.0.1",
"canvas": "^3.1.0", "canvas": "^3.1.0",
"chai": "^5.1.1", "chai": "^5.1.1",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"d3": "^7.9.0", "d3": "^7.9.0",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2", "eslint-formatter-gha": "^1.5.2",
"eslint-webpack-plugin": "^5.0.0",
"file-loader": "^6.2.0",
"globals": "^16.0.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", "husky": "^9.1.7",
"jest": "^30.0.0", "jsdom": "^27.4.0",
"jest-environment-jsdom": "^30.0.0",
"lint-staged": "^16.1.2", "lint-staged": "^16.1.2",
"lit": "^3.3.1", "lit": "^3.3.1",
"lit-markdown": "^1.3.2", "lit-markdown": "^1.3.2",
@@ -83,25 +77,22 @@
"pixi-filters": "^6.1.4", "pixi-filters": "^6.1.4",
"pixi.js": "^8.11.0", "pixi.js": "^8.11.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"postcss-loader": "^8.1.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-sh": "^0.17.4", "prettier-plugin-sh": "^0.17.4",
"protobufjs": "^7.5.3", "protobufjs": "^7.5.3",
"raw-loader": "^4.0.2",
"sinon": "^21.0.0", "sinon": "^21.0.0",
"sinon-chai": "^4.0.0", "sinon-chai": "^4.0.0",
"style-loader": "^4.0.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-loader": "^9.5.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0", "tsx": "^4.17.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.26.0", "typescript-eslint": "^8.26.0",
"webpack": "^5.100.2", "vite": "^7.3.0",
"webpack-cli": "^6.0.1", "vite-plugin-html": "^3.2.2",
"webpack-dev-server": "^5.2.2", "vite-plugin-static-copy": "^3.1.4",
"worker-loader": "^3.0.8" "vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@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 { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js"; 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"; import { translateText } from "./Utils";
@customElement("flag-input-modal") @customElement("flag-input-modal")
+1 -1
View File
@@ -1,6 +1,5 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js"; import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils"; import { translateText } from "../client/Utils";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { import {
@@ -32,6 +31,7 @@ import { crazyGamesSDK } from "./CrazyGamesSDK";
import { JoinLobbyEvent } from "./Main"; import { JoinLobbyEvent } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import randomMap from "/images/RandomMap.webp?url";
@customElement("host-lobby-modal") @customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement { export class HostLobbyModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & { @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 { customElement, state } from "lit/decorators.js";
import "./LanguageModal"; import "./LanguageModal";
import ar from "../../resources/lang/ar.json"; import ar from "../assets/lang/ar.json";
import bg from "../../resources/lang/bg.json"; import bg from "../assets/lang/bg.json";
import bn from "../../resources/lang/bn.json"; import bn from "../assets/lang/bn.json";
import cs from "../../resources/lang/cs.json"; import cs from "../assets/lang/cs.json";
import da from "../../resources/lang/da.json"; import da from "../assets/lang/da.json";
import de from "../../resources/lang/de.json"; import de from "../assets/lang/de.json";
import el from "../../resources/lang/el.json"; import el from "../assets/lang/el.json";
import en from "../../resources/lang/en.json"; import en from "../assets/lang/en.json";
import eo from "../../resources/lang/eo.json"; import eo from "../assets/lang/eo.json";
import es from "../../resources/lang/es.json"; import es from "../assets/lang/es.json";
import fa from "../../resources/lang/fa.json"; import fa from "../assets/lang/fa.json";
import fi from "../../resources/lang/fi.json"; import fi from "../assets/lang/fi.json";
import fr from "../../resources/lang/fr.json"; import fr from "../assets/lang/fr.json";
import gl from "../../resources/lang/gl.json"; import gl from "../assets/lang/gl.json";
import he from "../../resources/lang/he.json"; import he from "../assets/lang/he.json";
import hi from "../../resources/lang/hi.json"; import hi from "../assets/lang/hi.json";
import hu from "../../resources/lang/hu.json"; import hu from "../assets/lang/hu.json";
import it from "../../resources/lang/it.json"; import it from "../assets/lang/it.json";
import ja from "../../resources/lang/ja.json"; import ja from "../assets/lang/ja.json";
import ko from "../../resources/lang/ko.json"; import ko from "../assets/lang/ko.json";
import mk from "../../resources/lang/mk.json"; import mk from "../assets/lang/mk.json";
import nl from "../../resources/lang/nl.json"; import nl from "../assets/lang/nl.json";
import pl from "../../resources/lang/pl.json"; import pl from "../assets/lang/pl.json";
import pt_BR from "../../resources/lang/pt-BR.json"; import pt_BR from "../assets/lang/pt-BR.json";
import pt_PT from "../../resources/lang/pt-PT.json"; import pt_PT from "../assets/lang/pt-PT.json";
import ru from "../../resources/lang/ru.json"; import ru from "../assets/lang/ru.json";
import sh from "../../resources/lang/sh.json"; import sh from "../assets/lang/sh.json";
import sk from "../../resources/lang/sk.json"; import sk from "../assets/lang/sk.json";
import sl from "../../resources/lang/sl.json"; import sl from "../assets/lang/sl.json";
import sv_SE from "../../resources/lang/sv-SE.json"; import sv_SE from "../assets/lang/sv-SE.json";
import tp from "../../resources/lang/tp.json"; import tp from "../assets/lang/tp.json";
import tr from "../../resources/lang/tr.json"; import tr from "../assets/lang/tr.json";
import uk from "../../resources/lang/uk.json"; import uk from "../assets/lang/uk.json";
import zh_CN from "../../resources/lang/zh-CN.json"; import zh_CN from "../assets/lang/zh-CN.json";
@customElement("lang-selector") @customElement("lang-selector")
export class LangSelector extends LitElement { export class LangSelector extends LitElement {
+12 -3
View File
@@ -1,5 +1,4 @@
import Snowflake3Png from "../../resources/images/Snowflake.webp"; import version from "../assets/data/version.txt?raw";
import version from "../../resources/version.txt";
import { UserMeResponse } from "../core/ApiSchemas"; import { UserMeResponse } from "../core/ApiSchemas";
import { EventBus } from "../core/EventBus"; import { EventBus } from "../core/EventBus";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas"; import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
@@ -46,7 +45,17 @@ import "./components/baseComponents/Button";
import "./components/baseComponents/Modal"; import "./components/baseComponents/Modal";
import "./snow.css"; import "./snow.css";
import "./styles.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 { declare global {
interface Window { interface Window {
turnstile: any; turnstile: any;
+4 -4
View File
@@ -1,13 +1,13 @@
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { resolveMarkdown } from "lit-markdown"; import { resolveMarkdown } from "lit-markdown";
import { customElement, property, query } from "lit/decorators.js"; import { customElement, property, query } from "lit/decorators.js";
import changelog from "../../resources/changelog.md"; import version from "../assets/data/version.txt?raw";
import megaphone from "../../resources/images/Megaphone.svg";
import santaHatIcon from "../../resources/images/SantaHat.webp";
import version from "../../resources/version.txt";
import { translateText } from "../client/Utils"; import { translateText } from "../client/Utils";
import "./components/baseComponents/Button"; import "./components/baseComponents/Button";
import "./components/baseComponents/Modal"; 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") @customElement("news-modal")
export class NewsModal extends LitElement { export class NewsModal extends LitElement {
+1 -1
View File
@@ -1,6 +1,5 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js"; import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils"; import { translateText } from "../client/Utils";
import { import {
Difficulty, Difficulty,
@@ -28,6 +27,7 @@ import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main"; import { JoinLobbyEvent } from "./Main";
import { UsernameInput } from "./UsernameInput"; import { UsernameInput } from "./UsernameInput";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import randomMap from "/images/RandomMap.webp?url";
@customElement("single-player-modal") @customElement("single-player-modal")
export class SinglePlayerModal extends LitElement { 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"; import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader";
export const terrainMapFileLoader = new FetchGameMapLoader(`/maps`, version); 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 { Theme } from "../../core/configuration/Config";
import { PlayerView } from "../../core/game/GameView"; import { PlayerView } from "../../core/game/GameView";
import { AnimatedSprite } from "./AnimatedSprite"; import { AnimatedSprite } from "./AnimatedSprite";
import { FxType } from "./fx/Fx"; import { FxType } from "./fx/Fx";
import { colorizeCanvas } from "./SpriteLoader"; 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 = { type AnimatedSpriteConfig = {
url: string; 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 { AllPlayers, nukeTypes } from "../../core/game/Game";
import { GameView, PlayerView } from "../../core/game/GameView"; 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 = export type PlayerIconId =
| "crown" | "crown"
+10 -10
View File
@@ -1,17 +1,17 @@
import { Colord } from "colord"; 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 { Theme } from "../../core/configuration/Config";
import { TrainType, UnitType } from "../../core/game/Game"; import { TrainType, UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView"; 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 // Can't reuse TrainType because "loaded" is not a type, just an attribute
const TrainTypeSprite = { const TrainTypeSprite = {
+11 -11
View File
@@ -1,16 +1,5 @@
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js"; 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 { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { import {
@@ -34,6 +23,17 @@ import {
import { renderNumber } from "../../Utils"; import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer"; 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 { export interface BuildItemDisplay {
unitType: UnitType; unitType: UnitType;
+1 -1
View File
@@ -4,7 +4,7 @@ import { customElement, query } from "lit/decorators.js";
import { PlayerType } from "../../../core/game/Game"; import { PlayerType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView"; 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 { EventBus } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler"; import { CloseViewEvent } from "../../InputHandler";
import { SendQuickChatEvent } from "../../Transport"; 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 { customElement, query, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js"; import { DirectiveResult } from "lit/directive.js";
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.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 { EventBus } from "../../../core/EventBus";
import { import {
AllPlayers, AllPlayers,
@@ -50,6 +45,11 @@ import {
import { getMessageTypeClasses, translateText } from "../../Utils"; import { getMessageTypeClasses, translateText } from "../../Utils";
import { UIState } from "../UIState"; 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 { interface GameEvent {
description: string; description: string;
@@ -1,14 +1,14 @@
import { Colord } from "colord"; import { Colord } from "colord";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js"; 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 { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView"; import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils"; import { translateText } from "../../Utils";
import { Layer } from "./Layer"; 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") @customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Layer { export class GameLeftSidebar extends LitElement implements Layer {
@@ -1,10 +1,5 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js"; 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 { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game"; import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameUpdateType } from "../../../core/game/GameUpdates";
@@ -15,6 +10,11 @@ import { translateText } from "../../Utils";
import { Layer } from "./Layer"; import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel"; import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal"; 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") @customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer { export class GameRightSidebar extends LitElement implements Layer {
+2 -2
View File
@@ -19,9 +19,9 @@ import {
MenuElementParams, MenuElementParams,
rootMenuElement, rootMenuElement,
} from "./RadialMenuElements"; } 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"; import { ContextMenuEvent } from "../../InputHandler";
@customElement("main-radial-menu") @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 { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom"; import { PseudoRandom } from "../../../core/PseudoRandom";
@@ -17,6 +16,7 @@ import {
} from "../PlayerIcons"; } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer"; import { Layer } from "./Layer";
import shieldIcon from "/images/ShieldIconBlack.svg?url";
class RenderInfo { class RenderInfo {
public icons: Map<PlayerIconId, HTMLElement> = new Map(); // Track icon elements public icons: Map<PlayerIconId, HTMLElement> = new Map(); // Track icon elements
@@ -1,14 +1,6 @@
import { LitElement, TemplateResult, html } from "lit"; import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js"; import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.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 { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { import {
@@ -32,6 +24,14 @@ import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer"; import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu"; 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( function euclideanDistWorld(
coord: { x: number; y: number }, coord: { x: number; y: number },
+12 -12
View File
@@ -1,15 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg"; import Countries from "../../../assets/data/countries.json" with { type: "json" };
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 { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { import {
AllPlayers, AllPlayers,
@@ -23,7 +14,6 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
import { Emoji, flattenedEmojiTable } from "../../../core/Util"; import { Emoji, flattenedEmojiTable } from "../../../core/Util";
import { actionButton } from "../../components/ui/ActionButton"; import { actionButton } from "../../components/ui/ActionButton";
import "../../components/ui/Divider"; import "../../components/ui/Divider";
import Countries from "../../data/countries.json";
import { CloseViewEvent, MouseUpEvent } from "../../InputHandler"; import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
import { import {
SendAllianceRequestIntentEvent, SendAllianceRequestIntentEvent,
@@ -44,6 +34,16 @@ import { ChatModal } from "./ChatModal";
import { EmojiTable } from "./EmojiTable"; import { EmojiTable } from "./EmojiTable";
import { Layer } from "./Layer"; import { Layer } from "./Layer";
import "./SendResourceModal"; 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") @customElement("player-panel")
export class PlayerPanel extends LitElement implements Layer { export class PlayerPanel extends LitElement implements Layer {
@@ -445,7 +445,7 @@ export class PlayerPanel extends LitElement implements Layer {
${country && typeof flagCode === "string" ${country && typeof flagCode === "string"
? html`<img ? html`<img
src="/flags/${encodeURIComponent(flagCode)}.svg" src="/flags/${encodeURIComponent(flagCode)}.svg"
alt=${country?.name || "Flag"} alt=${country?.name ?? "Flag"}
class="h-10 w-10 rounded-full object-cover" class="h-10 w-10 rounded-full object-cover"
@error=${(e: Event) => { @error=${(e: Event) => {
(e.target as HTMLImageElement).style.display = "none"; (e.target as HTMLImageElement).style.display = "none";
+1 -1
View File
@@ -1,5 +1,4 @@
import * as d3 from "d3"; import * as d3 from "d3";
import backIcon from "../../../../resources/images/BackIconWhite.svg";
import { EventBus, GameEvent } from "../../../core/EventBus"; import { EventBus, GameEvent } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler"; import { CloseViewEvent } from "../../InputHandler";
import { getSvgAspectRatio, translateText } from "../../Utils"; import { getSvgAspectRatio, translateText } from "../../Utils";
@@ -10,6 +9,7 @@ import {
MenuElementParams, MenuElementParams,
TooltipKey, TooltipKey,
} from "./RadialMenuElements"; } from "./RadialMenuElements";
import backIcon from "/images/BackIconWhite.svg?url";
export class CloseRadialMenuEvent implements GameEvent { export class CloseRadialMenuEvent implements GameEvent {
constructor() {} constructor() {}
@@ -12,19 +12,19 @@ import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel"; import { PlayerPanel } from "./PlayerPanel";
import { TooltipItem } from "./RadialMenu"; 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 { 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 { export interface MenuElementParams {
myPlayer: PlayerView; myPlayer: PlayerView;
+12 -12
View File
@@ -1,17 +1,5 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; 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 { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings"; import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler"; import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
@@ -19,6 +7,18 @@ import { PauseGameIntentEvent } from "../../Transport";
import { translateText } from "../../Utils"; import { translateText } from "../../Utils";
import SoundManager from "../../sound/SoundManager"; import SoundManager from "../../sound/SoundManager";
import { Layer } from "./Layer"; 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 { export class ShowSettingsModalEvent {
constructor( constructor(
@@ -3,13 +3,12 @@ import { Theme } from "../../../core/configuration/Config";
import { Cell, UnitType } from "../../../core/game/Game"; import { Cell, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import anchorIcon from "/images/AnchorIcon.png?url";
import anchorIcon from "../../../../resources/images/AnchorIcon.png"; import cityIcon from "/images/CityIcon.png?url";
import cityIcon from "../../../../resources/images/CityIcon.png"; import factoryIcon from "/images/FactoryUnit.png?url";
import factoryIcon from "../../../../resources/images/FactoryUnit.png"; import missileSiloIcon from "/images/MissileSiloUnit.png?url";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png"; import SAMMissileIcon from "/images/SamLauncherUnit.png?url";
import SAMMissileIcon from "../../../../resources/images/SamLauncherUnit.png"; import shieldIcon from "/images/ShieldIcon.png?url";
import shieldIcon from "../../../../resources/images/ShieldIcon.png";
export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = { export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
[UnitType.City]: "circle", [UnitType.City]: "circle",
@@ -2,7 +2,6 @@ import { extend } from "colord";
import a11yPlugin from "colord/plugins/a11y"; import a11yPlugin from "colord/plugins/a11y";
import { OutlineFilter } from "pixi-filters"; import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js"; import * as PIXI from "pixi.js";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
import { Theme } from "../../../core/configuration/Config"; import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { import {
@@ -40,6 +39,7 @@ import {
STRUCTURE_SHAPES, STRUCTURE_SHAPES,
ZOOM_THRESHOLD, ZOOM_THRESHOLD,
} from "./StructureDrawingUtils"; } from "./StructureDrawingUtils";
import bitmapFont from "/fonts/round_6x6_modified.xml?url";
extend([a11yPlugin]); extend([a11yPlugin]);
@@ -75,7 +75,8 @@ export class StructureIconsLayer implements Layer {
public playerActions: PlayerActions | null = null; public playerActions: PlayerActions | null = null;
private dotsStage: PIXI.Container; private dotsStage: PIXI.Container;
private readonly theme: Theme; private readonly theme: Theme;
private renderer: PIXI.Renderer; private renderer: PIXI.Renderer | null = null;
private rendererInitialized: boolean = false;
private renders: StructureRenderInfo[] = []; private renders: StructureRenderInfo[] = [];
private readonly seenUnits: Set<UnitView> = new Set(); private readonly seenUnits: Set<UnitView> = new Set();
private readonly mousePos = { x: 0, y: 0 }; private readonly mousePos = { x: 0, y: 0 };
@@ -113,7 +114,7 @@ export class StructureIconsLayer implements Layer {
} catch (error) { } catch (error) {
console.error("Failed to load bitmap font:", 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 = document.createElement("canvas");
this.pixicanvas.width = window.innerWidth; this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight; this.pixicanvas.height = window.innerHeight;
@@ -143,7 +144,7 @@ export class StructureIconsLayer implements Layer {
this.rootStage.position.set(0, 0); this.rootStage.position.set(0, 0);
this.rootStage.setSize(this.pixicanvas.width, this.pixicanvas.height); this.rootStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
await this.renderer.init({ await renderer.init({
canvas: this.pixicanvas, canvas: this.pixicanvas,
resolution: 1, resolution: 1,
width: this.pixicanvas.width, width: this.pixicanvas.width,
@@ -153,6 +154,9 @@ export class StructureIconsLayer implements Layer {
backgroundAlpha: 0, backgroundAlpha: 0,
backgroundColor: 0x00000000, backgroundColor: 0x00000000,
}); });
this.renderer = renderer;
this.rendererInitialized = true;
} }
shouldTransform(): boolean { shouldTransform(): boolean {
@@ -202,7 +206,7 @@ export class StructureIconsLayer implements Layer {
} }
renderLayer(mainContext: CanvasRenderingContext2D) { renderLayer(mainContext: CanvasRenderingContext2D) {
if (!this.renderer) { if (!this.renderer || !this.rendererInitialized) {
return; return;
} }
+6 -6
View File
@@ -4,16 +4,16 @@ import { EventBus } from "../../../core/EventBus";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer"; 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 { Cell, UnitType } from "../../../core/game/Game";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap"; import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView"; 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)"); const underConstructionColor = colord("rgb(150,150,150)");
+10 -10
View File
@@ -1,15 +1,5 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js"; 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 { EventBus } from "../../../core/EventBus";
import { Gold, PlayerActions, UnitType } from "../../../core/game/Game"; import { Gold, PlayerActions, UnitType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView"; import { GameView } from "../../../core/game/GameView";
@@ -20,6 +10,16 @@ import {
import { renderNumber, translateText } from "../../Utils"; import { renderNumber, translateText } from "../../Utils";
import { UIState } from "../UIState"; import { UIState } from "../UIState";
import { Layer } from "./Layer"; 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") @customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer { 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 of4 from "../../../proprietary/sounds/music/of4.mp3";
import openfront from "../../../proprietary/sounds/music/openfront.mp3"; import openfront from "../../../proprietary/sounds/music/openfront.mp3";
import war from "../../../proprietary/sounds/music/war.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 { export enum SoundEffect {
KaChing = "ka-ching", KaChing = "ka-ching",
+1 -10
View File
@@ -1,16 +1,7 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @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; -webkit-box-sizing: border-box;
-moz-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 { z } from "zod";
import quickChatData from "../../resources/QuickChat.json" with { type: "json" }; import countries from "../assets/data/countries.json" with { type: "json" };
import countries from "../client/data/countries.json" with { type: "json" }; import quickChatData from "../assets/data/QuickChat.json" with { type: "json" };
import { import {
ColorPaletteSchema, ColorPaletteSchema,
PatternDataSchema, PatternDataSchema,
+3 -2
View File
@@ -3,6 +3,7 @@ import { GameConfig } from "../Schemas";
import { Config, GameEnv, ServerConfig } from "./Config"; import { Config, GameEnv, ServerConfig } from "./Config";
import { DefaultConfig } from "./DefaultConfig"; import { DefaultConfig } from "./DefaultConfig";
import { DevConfig, DevServerConfig } from "./DevConfig"; import { DevConfig, DevServerConfig } from "./DevConfig";
import { Env } from "./Env";
import { preprodConfig } from "./PreprodConfig"; import { preprodConfig } from "./PreprodConfig";
import { prodConfig } from "./ProdConfig"; import { prodConfig } from "./ProdConfig";
@@ -22,7 +23,7 @@ export async function getConfig(
console.log("using prod config"); console.log("using prod config");
return new DefaultConfig(sc, gameConfig, userSettings, isReplay); return new DefaultConfig(sc, gameConfig, userSettings, isReplay);
default: default:
throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`); throw Error(`unsupported server configuration: ${Env.GAME_ENV}`);
} }
} }
export async function getServerConfigFromClient(): Promise<ServerConfig> { export async function getServerConfigFromClient(): Promise<ServerConfig> {
@@ -44,7 +45,7 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
return cachedSC; return cachedSC;
} }
export function getServerConfigFromServer(): ServerConfig { export function getServerConfigFromServer(): ServerConfig {
const gameEnv = process.env.GAME_ENV ?? "dev"; const gameEnv = Env.GAME_ENV;
return getServerConfig(gameEnv); return getServerConfig(gameEnv);
} }
export function getServerConfig(gameEnv: string) { export function getServerConfig(gameEnv: string) {
+11 -10
View File
@@ -27,6 +27,7 @@ import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas"; import { NukeType } from "../StatsSchemas";
import { assertNever, sigmoid, simpleHash, within } from "../Util"; import { assertNever, sigmoid, simpleHash, within } from "../Util";
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config"; import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
import { Env } from "./Env";
import { PastelTheme } from "./PastelTheme"; import { PastelTheme } from "./PastelTheme";
import { PastelThemeDark } from "./PastelThemeDark"; import { PastelThemeDark } from "./PastelThemeDark";
@@ -88,20 +89,20 @@ const numPlayersConfig = {
export abstract class DefaultServerConfig implements ServerConfig { export abstract class DefaultServerConfig implements ServerConfig {
turnstileSecretKey(): string { turnstileSecretKey(): string {
return process.env.TURNSTILE_SECRET_KEY ?? ""; return Env.TURNSTILE_SECRET_KEY ?? "";
} }
abstract turnstileSiteKey(): string; abstract turnstileSiteKey(): string;
allowedFlares(): string[] | undefined { allowedFlares(): string[] | undefined {
return; return;
} }
stripePublishableKey(): string { stripePublishableKey(): string {
return process.env.STRIPE_PUBLISHABLE_KEY ?? ""; return Env.STRIPE_PUBLISHABLE_KEY ?? "";
} }
domain(): string { domain(): string {
return process.env.DOMAIN ?? ""; return Env.DOMAIN ?? "";
} }
subdomain(): string { subdomain(): string {
return process.env.SUBDOMAIN ?? ""; return Env.SUBDOMAIN ?? "";
} }
private publicKey: JWK; private publicKey: JWK;
@@ -134,24 +135,24 @@ export abstract class DefaultServerConfig implements ServerConfig {
); );
} }
otelEndpoint(): string { otelEndpoint(): string {
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? ""; return Env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
} }
otelAuthHeader(): string { otelAuthHeader(): string {
return process.env.OTEL_AUTH_HEADER ?? ""; return Env.OTEL_AUTH_HEADER ?? "";
} }
gitCommit(): string { gitCommit(): string {
return process.env.GIT_COMMIT ?? ""; return Env.GIT_COMMIT ?? "";
} }
apiKey(): string { apiKey(): string {
return process.env.API_KEY ?? ""; return Env.API_KEY ?? "";
} }
adminHeader(): string { adminHeader(): string {
return "x-admin-key"; return "x-admin-key";
} }
adminToken(): string { adminToken(): string {
const token = process.env.ADMIN_TOKEN; const token = Env.ADMIN_TOKEN;
if (!token) { if (!token) {
throw new Error("ADMIN_TOKEN not set"); throw new Error("ADMIN_TOKEN not set");
} }
@@ -225,7 +226,7 @@ export class DefaultConfig implements Config {
) {} ) {}
stripePublishableKey(): string { stripePublishableKey(): string {
return process.env.STRIPE_PUBLISHABLE_KEY ?? ""; return Env.STRIPE_PUBLISHABLE_KEY ?? "";
} }
isReplay(): boolean { 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 { GameMapLoader, MapData } from "./GameMapLoader";
import { MapManifest } from "./TerrainMapLoader"; import { MapManifest } from "./TerrainMapLoader";
export interface BinModule {
default: string;
}
interface NationMapModule {
default: MapManifest;
}
export class BinaryLoaderGameMapLoader implements GameMapLoader { export class BinaryLoaderGameMapLoader implements GameMapLoader {
private maps: Map<GameMapType, MapData>; private maps: Map<GameMapType, MapData>;
@@ -36,59 +28,38 @@ export class BinaryLoaderGameMapLoader implements GameMapLoader {
); );
const fileName = key?.toLowerCase(); 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 = { const mapData = {
mapBin: this.createLazyLoader(() => mapBin: this.createLazyLoader(() => loadBinary(`${mapBasePath}/map.bin`)),
(
import(
`!!binary-loader!../../../resources/maps/${fileName}/map.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
),
map4xBin: this.createLazyLoader(() => map4xBin: this.createLazyLoader(() =>
( loadBinary(`${mapBasePath}/map4x.bin`),
import(
`!!binary-loader!../../../resources/maps/${fileName}/map4x.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
), ),
map16xBin: this.createLazyLoader(() => map16xBin: this.createLazyLoader(() =>
( loadBinary(`${mapBasePath}/map16x.bin`),
import(
`!!binary-loader!../../../resources/maps/${fileName}/map16x.bin`
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
), ),
manifest: this.createLazyLoader(() => manifest: this.createLazyLoader(() =>
( fetch(`${mapBasePath}/manifest.json`).then((res) => {
import( if (!res.ok) {
`../../../resources/maps/${fileName}/manifest.json` throw new Error(`Failed to load ${mapBasePath}/manifest.json`);
) as Promise<NationMapModule> }
).then((m) => m.default), return res.json() as Promise<MapManifest>;
}),
), ),
webpPath: this.createLazyLoader(() => webpPath: this.createLazyLoader(() =>
( Promise.resolve(`${mapBasePath}/thumbnail.webp`),
import(
`../../../resources/maps/${fileName}/thumbnail.webp`
) as Promise<{ default: string }>
).then((m) => m.default),
), ),
} satisfies MapData; } satisfies MapData;
this.maps.set(map, mapData); this.maps.set(map, mapData);
return 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}`; let url = `${this.prefix}/${map}/${path}`;
if (this.cacheBuster) { if (this.cacheBuster) {
url += `${url.includes("?") ? "&" : "?"}v=${this.cacheBuster}`; url += `${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(
this.cacheBuster.trim(),
)}`;
} }
return url; 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 { createGameRunner, GameRunner } from "../GameRunner";
import { FetchGameMapLoader } from "../game/FetchGameMapLoader"; import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
+3 -1
View File
@@ -23,7 +23,9 @@ export class WorkerClient {
private gameStartInfo: GameStartInfo, private gameStartInfo: GameStartInfo,
private clientID: ClientID, 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(); this.messageHandlers = new Map();
// Set up global message handler // 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 // tailwind.config.js
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./src/**/*.{html,ts,js}"], content: ["./index.html", "./src/**/*.{html,ts,js}"],
theme: { theme: {
extend: {}, extend: {},
}, },
+12 -12
View File
@@ -35,9 +35,9 @@ describe("AllianceExtensionExecution", () => {
}); });
test("Successfully extends existing alliance between Humans", () => { test("Successfully extends existing alliance between Humans", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true); vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player2, "isAlive").mockReturnValue(true); vi.spyOn(player2, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true); vi.spyOn(player1, "isAlive").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(player1, player2.id())); game.addExecution(new AllianceRequestExecution(player1, player2.id()));
game.executeNextTick(); game.executeNextTick();
@@ -53,7 +53,7 @@ describe("AllianceExtensionExecution", () => {
expect(player2.allianceWith(player1)).toBeTruthy(); expect(player2.allianceWith(player1)).toBeTruthy();
const allianceBefore = player1.allianceWith(player2)!; const allianceBefore = player1.allianceWith(player2)!;
const allianceSpy = jest.spyOn(allianceBefore, "extend"); const allianceSpy = vi.spyOn(allianceBefore, "extend");
const expirationBefore = allianceBefore.expiresAt(); const expirationBefore = allianceBefore.expiresAt();
@@ -82,9 +82,9 @@ describe("AllianceExtensionExecution", () => {
}); });
test("Successfully extends existing alliance between Human and non-Human", () => { test("Successfully extends existing alliance between Human and non-Human", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true); vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player3, "isAlive").mockReturnValue(true); vi.spyOn(player3, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true); vi.spyOn(player1, "isAlive").mockReturnValue(true);
game.addExecution(new AllianceRequestExecution(player1, player3.id())); game.addExecution(new AllianceRequestExecution(player1, player3.id()));
game.executeNextTick(); game.executeNextTick();
@@ -100,7 +100,7 @@ describe("AllianceExtensionExecution", () => {
expect(player3.allianceWith(player1)).toBeTruthy(); expect(player3.allianceWith(player1)).toBeTruthy();
const allianceBefore = player1.allianceWith(player3)!; const allianceBefore = player1.allianceWith(player3)!;
const allianceSpy = jest.spyOn(allianceBefore, "extend"); const allianceSpy = vi.spyOn(allianceBefore, "extend");
const expirationBefore = allianceBefore.expiresAt(); const expirationBefore = allianceBefore.expiresAt();
game.addExecution(new AllianceExtensionExecution(player1, player3.id())); game.addExecution(new AllianceExtensionExecution(player1, player3.id()));
@@ -120,9 +120,9 @@ describe("AllianceExtensionExecution", () => {
}); });
test("Sends message to other player when one player requests renewal", () => { test("Sends message to other player when one player requests renewal", () => {
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true); vi.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
jest.spyOn(player2, "isAlive").mockReturnValue(true); vi.spyOn(player2, "isAlive").mockReturnValue(true);
jest.spyOn(player1, "isAlive").mockReturnValue(true); vi.spyOn(player1, "isAlive").mockReturnValue(true);
// Create alliance between player1 and player2 // Create alliance between player1 and player2
game.addExecution(new AllianceRequestExecution(player1, player2.id())); game.addExecution(new AllianceRequestExecution(player1, player2.id()));
@@ -139,7 +139,7 @@ describe("AllianceExtensionExecution", () => {
expect(player2.allianceWith(player1)).toBeTruthy(); expect(player2.allianceWith(player1)).toBeTruthy();
// Spy on displayMessage to verify it's called // Spy on displayMessage to verify it's called
const displayMessageSpy = jest.spyOn(game, "displayMessage"); const displayMessageSpy = vi.spyOn(game, "displayMessage");
// Player1 requests renewal // Player1 requests renewal
game.addExecution(new AllianceExtensionExecution(player1, player2.id())); 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 { AutoUpgradeEvent } from "../src/client/InputHandler";
import { EventBus } from "../src/core/EventBus"; import { EventBus } from "../src/core/EventBus";
@@ -19,7 +16,7 @@ describe("AutoUpgrade Feature", () => {
}); });
test("should emit AutoUpgradeEvent when created", () => { test("should emit AutoUpgradeEvent when created", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const event = new AutoUpgradeEvent(150, 250); const event = new AutoUpgradeEvent(150, 250);
eventBus.emit(event); eventBus.emit(event);
@@ -36,7 +33,7 @@ describe("AutoUpgrade Feature", () => {
describe("AutoUpgradeEvent Integration", () => { describe("AutoUpgradeEvent Integration", () => {
test("should handle multiple AutoUpgradeEvents", () => { test("should handle multiple AutoUpgradeEvents", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const event1 = new AutoUpgradeEvent(100, 200); const event1 = new AutoUpgradeEvent(100, 200);
const event2 = new AutoUpgradeEvent(300, 400); const event2 = new AutoUpgradeEvent(300, 400);
@@ -70,7 +67,7 @@ describe("AutoUpgrade Feature", () => {
describe("AutoUpgradeEvent Event Bus Integration", () => { describe("AutoUpgradeEvent Event Bus Integration", () => {
test("should allow event listeners to subscribe to AutoUpgradeEvent", () => { test("should allow event listeners to subscribe to AutoUpgradeEvent", () => {
const mockListener = jest.fn(); const mockListener = vi.fn();
const event = new AutoUpgradeEvent(100, 200); const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener); eventBus.on(AutoUpgradeEvent, mockListener);
@@ -80,8 +77,8 @@ describe("AutoUpgrade Feature", () => {
}); });
test("should allow multiple listeners for AutoUpgradeEvent", () => { test("should allow multiple listeners for AutoUpgradeEvent", () => {
const mockListener1 = jest.fn(); const mockListener1 = vi.fn();
const mockListener2 = jest.fn(); const mockListener2 = vi.fn();
const event = new AutoUpgradeEvent(100, 200); const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener1); eventBus.on(AutoUpgradeEvent, mockListener1);
@@ -93,7 +90,7 @@ describe("AutoUpgrade Feature", () => {
}); });
test("should not call unsubscribed listeners", () => { test("should not call unsubscribed listeners", () => {
const mockListener = jest.fn(); const mockListener = vi.fn();
const event = new AutoUpgradeEvent(100, 200); const event = new AutoUpgradeEvent(100, 200);
eventBus.on(AutoUpgradeEvent, mockListener); eventBus.on(AutoUpgradeEvent, mockListener);
+2 -2
View File
@@ -1,5 +1,5 @@
// Mocking the obscenity library to control its behavior in tests. // Mocking the obscenity library to control its behavior in tests.
jest.mock("obscenity", () => { vi.mock("obscenity", () => {
return { return {
RegExpMatcher: class { RegExpMatcher: class {
private dummy: string[] = ["foo", "bar", "leet", "code"]; private dummy: string[] = ["foo", "bar", "leet", "code"];
@@ -26,7 +26,7 @@ jest.mock("obscenity", () => {
}); });
// Mocks the output of translation functions to return predictable values. // 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) => translateText: (key: string, vars?: any) =>
vars ? `${key}:${JSON.stringify(vars)}` : key, 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", () => { 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()); const execution = new DeleteUnitExecution(player, unit.id());
execution.init(game, 0); execution.init(game, 0);
@@ -122,7 +122,7 @@ describe("DeleteUnitExecution Security Tests", () => {
}); });
it("should allow deleting units when all conditions are met", () => { 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()); const execution = new DeleteUnitExecution(player, unit.id());
execution.init(game, 0); execution.init(game, 0);
@@ -131,7 +131,7 @@ describe("DeleteUnitExecution Security Tests", () => {
}); });
it("should delete after deletion delay", () => { 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()); const execution = new DeleteUnitExecution(player, unit.id());
game.addExecution(execution); game.addExecution(execution);
@@ -144,7 +144,7 @@ describe("DeleteUnitExecution Security Tests", () => {
}); });
it("should reset deletion if captured", () => { 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()); const execution = new DeleteUnitExecution(player, unit.id());
game.addExecution(execution); game.addExecution(execution);
+19 -22
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { AutoUpgradeEvent, InputHandler } from "../src/client/InputHandler"; import { AutoUpgradeEvent, InputHandler } from "../src/client/InputHandler";
import { EventBus } from "../src/core/EventBus"; import { EventBus } from "../src/core/EventBus";
@@ -18,7 +15,7 @@ class MockPointerEvent {
this.clientX = init.clientX; this.clientX = init.clientX;
this.clientY = init.clientY; this.clientY = init.clientY;
this.pointerId = init.pointerId; this.pointerId = init.pointerId;
this.preventDefault = jest.fn(); this.preventDefault = vi.fn();
} }
} }
@@ -45,7 +42,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Middle Mouse Button Handling", () => { describe("Middle Mouse Button Handling", () => {
test("should emit AutoUpgradeEvent on middle mouse button press", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -65,7 +62,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should emit MouseDownEvent on left mouse button press instead of AutoUpgradeEvent", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 0, button: 0,
@@ -89,7 +86,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should not emit AutoUpgradeEvent on right mouse button press", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 2, button: 2,
@@ -109,7 +106,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle multiple middle mouse button presses", () => { 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", { const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -145,7 +142,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle middle mouse button press with zero coordinates", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -165,7 +162,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle middle mouse button press with negative coordinates", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -185,7 +182,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle middle mouse button press with decimal coordinates", () => { 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", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -207,7 +204,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Pointer Event Handling", () => { describe("Pointer Event Handling", () => {
test("should handle pointer events with different pointer IDs", () => { 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", { const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -229,7 +226,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle pointer events with same pointer ID", () => { 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", { const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -253,7 +250,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Edge Cases", () => { describe("Edge Cases", () => {
test("should handle very large coordinates", () => { test("should handle very large coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -273,7 +270,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle very small coordinates", () => { test("should handle very small coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -293,7 +290,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle NaN coordinates", () => { test("should handle NaN coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -313,7 +310,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should handle Infinity coordinates", () => { test("should handle Infinity coordinates", () => {
const mockEmit = jest.spyOn(eventBus, "emit"); const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", { const pointerEvent = new PointerEvent("pointerdown", {
button: 1, button: 1,
@@ -335,7 +332,7 @@ describe("InputHandler AutoUpgrade", () => {
describe("Integration with Event Bus", () => { describe("Integration with Event Bus", () => {
test("should allow event listeners to receive AutoUpgradeEvents", () => { test("should allow event listeners to receive AutoUpgradeEvents", () => {
const mockListener = jest.fn(); const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener); eventBus.on(AutoUpgradeEvent, mockListener);
@@ -356,8 +353,8 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should allow multiple listeners for AutoUpgradeEvent", () => { test("should allow multiple listeners for AutoUpgradeEvent", () => {
const mockListener1 = jest.fn(); const mockListener1 = vi.fn();
const mockListener2 = jest.fn(); const mockListener2 = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener1); eventBus.on(AutoUpgradeEvent, mockListener1);
eventBus.on(AutoUpgradeEvent, mockListener2); eventBus.on(AutoUpgradeEvent, mockListener2);
@@ -385,7 +382,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("should not call unsubscribed listeners", () => { test("should not call unsubscribed listeners", () => {
const mockListener = jest.fn(); const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener); eventBus.on(AutoUpgradeEvent, mockListener);
eventBus.off(AutoUpgradeEvent, mockListener); eventBus.off(AutoUpgradeEvent, mockListener);
@@ -444,7 +441,7 @@ describe("InputHandler AutoUpgrade", () => {
}); });
test("handles invalid JSON gracefully and warns", () => { 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"); localStorage.setItem("settings.keybinds", "not a json");
inputHandler.initialize(); inputHandler.initialize();
+3 -2
View File
@@ -1,12 +1,13 @@
import { vi, type MockInstance } from "vitest";
import { getMessageTypeClasses, severityColors } from "../src/client/Utils"; import { getMessageTypeClasses, severityColors } from "../src/client/Utils";
import { MessageType } from "../src/core/game/Game"; import { MessageType } from "../src/core/game/Game";
describe("getMessageTypeClasses", () => { describe("getMessageTypeClasses", () => {
// Spy on console.warn to track when the default case is hit // Spy on console.warn to track when the default case is hit
let consoleSpy: jest.SpyInstance; let consoleSpy: MockInstance;
beforeEach(() => { beforeEach(() => {
consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
}); });
afterEach(() => { 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 = { const mockRequest = {
requestor: () => requestor, requestor: () => requestor,
recipient: () => player, recipient: () => player,
createdAt: () => 0 as unknown as Tick, createdAt: () => 0 as unknown as Tick,
accept: jest.fn(), accept: vi.fn(),
reject: jest.fn(), reject: vi.fn(),
} as unknown as AllianceRequest; } as unknown as AllianceRequest;
jest vi.spyOn(player, "incomingAllianceRequests").mockReturnValue([mockRequest]);
.spyOn(player, "incomingAllianceRequests")
.mockReturnValue([mockRequest]);
return mockRequest; return mockRequest;
} }
@@ -151,19 +149,19 @@ describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
let allianceBehavior: NationAllianceBehavior; let allianceBehavior: NationAllianceBehavior;
beforeEach(() => { beforeEach(() => {
mockGame = { addExecution: jest.fn() }; mockGame = { addExecution: vi.fn() };
mockHuman = { id: jest.fn(() => "human_id") }; mockHuman = { id: vi.fn(() => "human_id") };
mockAlliance = { mockAlliance = {
onlyOneAgreedToExtend: jest.fn(() => true), onlyOneAgreedToExtend: vi.fn(() => true),
other: jest.fn(() => mockHuman), other: vi.fn(() => mockHuman),
}; };
mockRandom = { chance: jest.fn() }; mockRandom = { chance: vi.fn() };
mockPlayer = { mockPlayer = {
alliances: jest.fn(() => [mockAlliance]), alliances: vi.fn(() => [mockAlliance]),
relation: jest.fn(), relation: vi.fn(),
id: jest.fn(() => "bot_id"), id: vi.fn(() => "bot_id"),
type: jest.fn(() => PlayerType.Nation), type: vi.fn(() => PlayerType.Nation),
}; };
allianceBehavior = new NationAllianceBehavior( allianceBehavior = new NationAllianceBehavior(
+6 -9
View File
@@ -1,11 +1,8 @@
/**
* @jest-environment jsdom
*/
import { FluentSlider } from "../../../src/client/components/FluentSlider"; import { FluentSlider } from "../../../src/client/components/FluentSlider";
// Mock the translateText function // Mock the translateText function
jest.mock("../../../src/client/Utils", () => ({ vi.mock("../../../src/client/Utils", () => ({
translateText: jest.fn((key: string) => key), translateText: vi.fn((key: string) => key),
})); }));
describe("FluentSlider", () => { describe("FluentSlider", () => {
@@ -84,7 +81,7 @@ describe("FluentSlider", () => {
describe("Value-Changed Event - CRITICAL FOR BUG FIX", () => { describe("Value-Changed Event - CRITICAL FOR BUG FIX", () => {
it("should dispatch CustomEvent with detail.value (not event.target.value)", async () => { 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); slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.shadowRoot?.querySelector( const rangeInput = slider.shadowRoot?.querySelector(
@@ -107,7 +104,7 @@ describe("FluentSlider", () => {
}); });
it("should not dispatch event on input, only on change", async () => { it("should not dispatch event on input, only on change", async () => {
const eventSpy = jest.fn(); const eventSpy = vi.fn();
slider.addEventListener("value-changed", eventSpy); slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.shadowRoot?.querySelector( const rangeInput = slider.shadowRoot?.querySelector(
@@ -128,7 +125,7 @@ describe("FluentSlider", () => {
it("should work with the handler pattern used in HostLobbyModal", async () => { it("should work with the handler pattern used in HostLobbyModal", async () => {
// This simulates the actual handler code in HostLobbyModal.ts:656-660 // 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 customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value; const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) { if (isNaN(value) || value < 0 || value > 400) {
@@ -154,7 +151,7 @@ describe("FluentSlider", () => {
it("should work with the handler pattern used in SinglePlayerModal", async () => { it("should work with the handler pattern used in SinglePlayerModal", async () => {
// This simulates the actual handler code in SinglePlayerModal.ts:444-451 // 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 customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value; const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) { 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"; import { ProgressBar } from "../../../src/client/graphics/ProgressBar";
describe("ProgressBar", () => { describe("ProgressBar", () => {
@@ -15,9 +12,9 @@ describe("ProgressBar", () => {
}); });
it("should initialize and draw the background", () => { it("should initialize and draw the background", () => {
const spyClearRect = jest.spyOn(ctx, "clearRect"); const spyClearRect = vi.spyOn(ctx, "clearRect");
const spyFillRect = jest.spyOn(ctx, "fillRect"); const spyFillRect = vi.spyOn(ctx, "fillRect");
const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set"); const spyFillStyle = vi.spyOn(ctx, "fillStyle", "set");
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5); const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5);
expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12); expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12);
expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10); expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10);
@@ -28,7 +25,7 @@ describe("ProgressBar", () => {
it("should set progress and draw the progress bar", () => { it("should set progress and draw the progress bar", () => {
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10); 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); bar.setProgress(0.5);
expect(bar.getProgress()).toBe(0.5); expect(bar.getProgress()).toBe(0.5);
expect(spyFillRect).toHaveBeenCalledWith( expect(spyFillRect).toHaveBeenCalledWith(
@@ -1,6 +1,4 @@
/** import { vi, type Mock } from "vitest";
* @jest-environment jsdom
*/
import { import {
attackMenuElement, attackMenuElement,
buildMenuElement, buildMenuElement,
@@ -13,13 +11,15 @@ import { UnitType } from "../../../src/core/game/Game";
import { TileRef } from "../../../src/core/game/GameMap"; import { TileRef } from "../../../src/core/game/GameMap";
import { GameView, PlayerView } from "../../../src/core/game/GameView"; import { GameView, PlayerView } from "../../../src/core/game/GameView";
jest.mock("../../../src/client/Utils", () => ({ vi.mock("../../../src/client/Utils", () => ({
translateText: jest.fn((key: string) => key), translateText: vi.fn((key: string) => key),
renderNumber: jest.fn((num: number) => num.toString()), renderNumber: vi.fn((num: number) => num.toString()),
})); }));
jest.mock("../../../src/client/graphics/layers/BuildMenu", () => { vi.mock("../../../src/client/graphics/layers/BuildMenu", async () => {
const { UnitType } = jest.requireActual("../../../src/core/game/Game"); const { UnitType } = await vi.importActual<
typeof import("../../../src/core/game/Game")
>("../../../src/core/game/Game");
return { return {
flattenedBuildTable: [ flattenedBuildTable: [
{ {
@@ -68,14 +68,14 @@ jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
}; };
}); });
jest.mock("nanoid", () => ({ vi.mock("nanoid", () => ({
customAlphabet: jest.fn(() => jest.fn(() => "mock-id")), customAlphabet: vi.fn(() => vi.fn(() => "mock-id")),
})); }));
jest.mock("dompurify", () => ({ vi.mock("dompurify", () => ({
__esModule: true, __esModule: true,
default: { default: {
sanitize: jest.fn((str: string) => str), sanitize: vi.fn((str: string) => str),
}, },
})); }));
@@ -90,29 +90,29 @@ describe("RadialMenuElements", () => {
beforeEach(() => { beforeEach(() => {
mockPlayer = { mockPlayer = {
id: () => 1, id: () => 1,
isAlliedWith: jest.fn(() => false), isAlliedWith: vi.fn(() => false),
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockGame = { mockGame = {
inSpawnPhase: jest.fn(() => false), inSpawnPhase: vi.fn(() => false),
owner: jest.fn(() => mockPlayer), owner: vi.fn(() => mockPlayer),
isLand: jest.fn(() => true), isLand: vi.fn(() => true),
config: jest.fn(() => ({ config: vi.fn(() => ({
theme: () => ({ theme: () => ({
territoryColor: () => ({ territoryColor: () => ({
lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }), lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }),
}), }),
}), }),
isUnitDisabled: jest.fn(() => false), isUnitDisabled: vi.fn(() => false),
})), })),
} as unknown as GameView; } as unknown as GameView;
mockBuildMenu = { mockBuildMenu = {
canBuildOrUpgrade: jest.fn(() => true), canBuildOrUpgrade: vi.fn(() => true),
cost: jest.fn(() => 100), cost: vi.fn(() => 100),
count: jest.fn(() => 5), count: vi.fn(() => 5),
sendBuildOrUpgrade: jest.fn(), sendBuildOrUpgrade: vi.fn(),
}; };
mockPlayerActions = { mockPlayerActions = {
@@ -148,7 +148,7 @@ describe("RadialMenuElements", () => {
playerPanel: {} as any, playerPanel: {} as any,
chatIntegration: {} as any, chatIntegration: {} as any,
eventBus: {} as any, eventBus: {} as any,
closeMenu: jest.fn(), closeMenu: vi.fn(),
}; };
}); });
@@ -161,19 +161,19 @@ describe("RadialMenuElements", () => {
}); });
it("should be disabled during spawn phase", () => { it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true); mockGame.inSpawnPhase = vi.fn(() => true);
expect(attackMenuElement.disabled(mockParams)).toBe(true); expect(attackMenuElement.disabled(mockParams)).toBe(true);
}); });
it("should be enabled when not in spawn phase", () => { 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); expect(attackMenuElement.disabled(mockParams)).toBe(false);
}); });
it("should return attack submenu with attack units only", () => { it("should return attack submenu with attack units only", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
@@ -203,7 +203,7 @@ describe("RadialMenuElements", () => {
it("should not include construction units in attack menu", () => { it("should not include construction units in attack menu", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
@@ -237,12 +237,12 @@ describe("RadialMenuElements", () => {
}); });
it("should be disabled during spawn phase", () => { it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true); mockGame.inSpawnPhase = vi.fn(() => true);
expect(buildMenuElement.disabled(mockParams)).toBe(true); expect(buildMenuElement.disabled(mockParams)).toBe(true);
}); });
it("should be enabled when not in spawn phase", () => { 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); expect(buildMenuElement.disabled(mockParams)).toBe(false);
}); });
@@ -313,9 +313,9 @@ describe("RadialMenuElements", () => {
it("should show attack and boat menu on enemy territory", () => { it("should show attack and boat menu on enemy territory", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockGame.owner = jest.fn(() => enemyPlayer); mockGame.owner = vi.fn(() => enemyPlayer);
const subMenu = rootMenuElement.subMenu!(mockParams); const subMenu = rootMenuElement.subMenu!(mockParams);
const buildMenu = subMenu.find((item) => item.id === Slot.Build); const buildMenu = subMenu.find((item) => item.id === Slot.Build);
@@ -337,8 +337,8 @@ describe("RadialMenuElements", () => {
it("should handle ally menu correctly", () => { it("should handle ally menu correctly", () => {
const allyPlayer = { const allyPlayer = {
id: () => 2, id: () => 2,
isAlliedWith: jest.fn(() => true), isAlliedWith: vi.fn(() => true),
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = allyPlayer; mockParams.selected = allyPlayer;
@@ -367,7 +367,7 @@ describe("RadialMenuElements", () => {
it("should execute attack action correctly", () => { it("should execute attack action correctly", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
@@ -389,7 +389,7 @@ describe("RadialMenuElements", () => {
it("should not execute action when buildable unit is not found", () => { it("should not execute action when buildable unit is not found", () => {
mockPlayerActions.buildableUnits = []; mockPlayerActions.buildableUnits = [];
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false); mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams); const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City"); const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -420,7 +420,7 @@ describe("RadialMenuElements", () => {
it("should generate correct tooltip items for attack elements", () => { it("should generate correct tooltip items for attack elements", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
@@ -452,7 +452,7 @@ describe("RadialMenuElements", () => {
it("should use correct colors for attack elements", () => { it("should use correct colors for attack elements", () => {
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
@@ -465,7 +465,7 @@ describe("RadialMenuElements", () => {
}); });
it("should not set color when element is disabled", () => { 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 subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City"); const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -475,10 +475,12 @@ describe("RadialMenuElements", () => {
}); });
describe("Translation integration", () => { describe("Translation integration", () => {
it("should use translateText for tooltip items in build menu", () => { it("should use translateText for tooltip items in build menu", async () => {
const { translateText } = jest.requireMock("../../../src/client/Utils"); 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); buildMenuElement.subMenu!(mockParams);
@@ -488,14 +490,16 @@ describe("RadialMenuElements", () => {
expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc"); expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc");
}); });
it("should use translateText for tooltip items in attack menu", () => { it("should use translateText for tooltip items in attack menu", async () => {
const { translateText } = jest.requireMock("../../../src/client/Utils"); const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
(translateText as jest.Mock).mockClear(); (translateText as Mock).mockClear();
const enemyPlayer = { const enemyPlayer = {
id: () => 2, id: () => 2,
isPlayer: jest.fn(() => true), isPlayer: vi.fn(() => true),
} as unknown as PlayerView; } as unknown as PlayerView;
mockParams.selected = enemyPlayer; mockParams.selected = enemyPlayer;
+2 -5
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { UILayer } from "../../../src/client/graphics/layers/UILayer"; import { UILayer } from "../../../src/client/graphics/layers/UILayer";
import { UnitSelectionEvent } from "../../../src/client/InputHandler"; import { UnitSelectionEvent } from "../../../src/client/InputHandler";
import { UnitView } from "../../../src/core/game/GameView"; import { UnitView } from "../../../src/core/game/GameView";
@@ -28,7 +25,7 @@ describe("UILayer", () => {
ticks: () => 1, ticks: () => 1,
updatesSinceLastTick: () => undefined, updatesSinceLastTick: () => undefined,
}; };
eventBus = { on: jest.fn() }; eventBus = { on: vi.fn() };
transformHandler = {}; transformHandler = {};
}); });
@@ -50,7 +47,7 @@ describe("UILayer", () => {
owner: () => ({}), owner: () => ({}),
}; };
const event = { isSelected: true, unit }; const event = { isSelected: true, unit };
ui.drawSelectionBox = jest.fn(); ui.drawSelectionBox = vi.fn();
ui["onUnitSelection"](event as UnitSelectionEvent); ui["onUnitSelection"](event as UnitSelectionEvent);
expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit); expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit);
}); });
@@ -1,20 +1,20 @@
jest.mock("lit", () => ({ vi.mock("lit", () => ({
html: () => {}, html: () => {},
LitElement: class {}, LitElement: class {},
})); }));
jest.mock("lit/decorators.js", () => ({ vi.mock("lit/decorators.js", () => ({
customElement: () => (clazz: any) => clazz, customElement: () => (clazz: any) => clazz,
query: () => () => {}, query: () => () => {},
state: () => () => {}, state: () => () => {},
property: () => () => {}, property: () => () => {},
})); }));
jest.mock("lit/directive.js", () => ({ vi.mock("lit/directive.js", () => ({
DirectiveResult: class {}, DirectiveResult: class {},
})); }));
jest.mock("lit/directives/unsafe-html.js", () => ({ vi.mock("lit/directives/unsafe-html.js", () => ({
unsafeHTML: () => {}, unsafeHTML: () => {},
UnsafeHTMLDirective: class {}, 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, inner: 10,
outer: 10, outer: 10,
})); }));
(game.config() as TestConfig).nukeAllianceBreakThreshold = jest.fn(() => 5); (game.config() as TestConfig).nukeAllianceBreakThreshold = vi.fn(() => 5);
while (game.inSpawnPhase()) { while (game.inSpawnPhase()) {
game.executeNextTick(); game.executeNextTick();
@@ -51,14 +51,14 @@ describe("NukeExecution", () => {
player.buildUnit(UnitType.MissileSilo, game.ref(1, 10), {}); player.buildUnit(UnitType.MissileSilo, game.ref(1, 10), {});
// Build a SAM out of range // Build a SAM out of range
const sam = player.buildUnit(UnitType.SAMLauncher, game.ref(1, 11), {}); 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 // Build a Defense post out of range AND out of redraw range
const defensePost = player.buildUnit( const defensePost = player.buildUnit(
UnitType.DefensePost, UnitType.DefensePost,
game.ref(1, 27), game.ref(1, 27),
{}, {},
); );
defensePost.touch = jest.fn(); defensePost.touch = vi.fn();
// Add a nuke execution targeting the city // Add a nuke execution targeting the city
const nukeExec = new NukeExecution( const nukeExec = new NukeExecution(
UnitType.AtomBomb, UnitType.AtomBomb,
@@ -20,70 +20,70 @@ describe("TradeShipExecution", () => {
infiniteGold: true, infiniteGold: true,
instantBuild: true, instantBuild: true,
}); });
game.displayMessage = jest.fn(); game.displayMessage = vi.fn();
origOwner = { origOwner = {
canBuild: jest.fn(() => true), canBuild: vi.fn(() => true),
buildUnit: jest.fn((type, spawn, opts) => tradeShip), buildUnit: vi.fn((type, spawn, opts) => tradeShip),
displayName: jest.fn(() => "Origin"), displayName: vi.fn(() => "Origin"),
addGold: jest.fn(), addGold: vi.fn(),
units: jest.fn(() => [dstPort]), units: vi.fn(() => [dstPort]),
unitCount: jest.fn(() => 1), unitCount: vi.fn(() => 1),
id: jest.fn(() => 1), id: vi.fn(() => 1),
clientID: jest.fn(() => 1), clientID: vi.fn(() => 1),
canTrade: jest.fn(() => true), canTrade: vi.fn(() => true),
} as any; } as any;
dstOwner = { dstOwner = {
id: jest.fn(() => 2), id: vi.fn(() => 2),
addGold: jest.fn(), addGold: vi.fn(),
displayName: jest.fn(() => "Destination"), displayName: vi.fn(() => "Destination"),
units: jest.fn(() => [dstPort]), units: vi.fn(() => [dstPort]),
unitCount: jest.fn(() => 1), unitCount: vi.fn(() => 1),
clientID: jest.fn(() => 2), clientID: vi.fn(() => 2),
canTrade: jest.fn(() => true), canTrade: vi.fn(() => true),
} as any; } as any;
pirate = { pirate = {
id: jest.fn(() => 3), id: vi.fn(() => 3),
addGold: jest.fn(), addGold: vi.fn(),
displayName: jest.fn(() => "Destination"), displayName: vi.fn(() => "Destination"),
units: jest.fn(() => [piratePort]), units: vi.fn(() => [piratePort]),
unitCount: jest.fn(() => 1), unitCount: vi.fn(() => 1),
canTrade: jest.fn(() => true), canTrade: vi.fn(() => true),
} as any; } as any;
piratePort = { piratePort = {
tile: jest.fn(() => 40011), tile: vi.fn(() => 40011),
owner: jest.fn(() => pirate), owner: vi.fn(() => pirate),
isActive: jest.fn(() => true), isActive: vi.fn(() => true),
} as any; } as any;
srcPort = { srcPort = {
tile: jest.fn(() => 20011), tile: vi.fn(() => 20011),
owner: jest.fn(() => origOwner), owner: vi.fn(() => origOwner),
isActive: jest.fn(() => true), isActive: vi.fn(() => true),
} as any; } as any;
dstPort = { dstPort = {
tile: jest.fn(() => 30015), // 15x15 tile: vi.fn(() => 30015), // 15x15
owner: jest.fn(() => dstOwner), owner: vi.fn(() => dstOwner),
isActive: jest.fn(() => true), isActive: vi.fn(() => true),
} as any; } as any;
tradeShip = { tradeShip = {
isActive: jest.fn(() => true), isActive: vi.fn(() => true),
owner: jest.fn(() => origOwner), owner: vi.fn(() => origOwner),
move: jest.fn(), move: vi.fn(),
setTargetUnit: jest.fn(), setTargetUnit: vi.fn(),
setSafeFromPirates: jest.fn(), setSafeFromPirates: vi.fn(),
delete: jest.fn(), delete: vi.fn(),
tile: jest.fn(() => 2001), tile: vi.fn(() => 2001),
} as any; } as any;
tradeShipExecution = new TradeShipExecution(origOwner, srcPort, dstPort); tradeShipExecution = new TradeShipExecution(origOwner, srcPort, dstPort);
tradeShipExecution.init(game, 0); tradeShipExecution.init(game, 0);
tradeShipExecution["pathFinder"] = { tradeShipExecution["pathFinder"] = {
nextTile: jest.fn(() => ({ type: 0, node: 2001 })), nextTile: vi.fn(() => ({ type: 0, node: 2001 })),
} as any; } as any;
tradeShipExecution["tradeShip"] = tradeShip; tradeShipExecution["tradeShip"] = tradeShip;
}); });
@@ -94,27 +94,27 @@ describe("TradeShipExecution", () => {
}); });
it("should deactivate if tradeShip is not active", () => { it("should deactivate if tradeShip is not active", () => {
tradeShip.isActive = jest.fn(() => false); tradeShip.isActive = vi.fn(() => false);
tradeShipExecution.tick(1); tradeShipExecution.tick(1);
expect(tradeShipExecution.isActive()).toBe(false); expect(tradeShipExecution.isActive()).toBe(false);
}); });
it("should delete ship if port owner changes to current owner", () => { it("should delete ship if port owner changes to current owner", () => {
dstPort.owner = jest.fn(() => origOwner); dstPort.owner = vi.fn(() => origOwner);
tradeShipExecution.tick(1); tradeShipExecution.tick(1);
expect(tradeShip.delete).toHaveBeenCalledWith(false); expect(tradeShip.delete).toHaveBeenCalledWith(false);
expect(tradeShipExecution.isActive()).toBe(false); expect(tradeShipExecution.isActive()).toBe(false);
}); });
it("should pick another port if ship is captured", () => { it("should pick another port if ship is captured", () => {
tradeShip.owner = jest.fn(() => pirate); tradeShip.owner = vi.fn(() => pirate);
tradeShipExecution.tick(1); tradeShipExecution.tick(1);
expect(tradeShip.setTargetUnit).toHaveBeenCalledWith(piratePort); expect(tradeShip.setTargetUnit).toHaveBeenCalledWith(piratePort);
}); });
it("should complete trade and award gold", () => { it("should complete trade and award gold", () => {
tradeShipExecution["pathFinder"] = { tradeShipExecution["pathFinder"] = {
nextTile: jest.fn(() => ({ type: 2, node: 2001 })), nextTile: vi.fn(() => ({ type: 2, node: 2001 })),
} as any; } as any;
tradeShipExecution.tick(1); tradeShipExecution.tick(1);
expect(tradeShip.delete).toHaveBeenCalledWith(false); expect(tradeShip.delete).toHaveBeenCalledWith(false);
+18 -18
View File
@@ -13,52 +13,52 @@ describe("WinCheckExecution", () => {
maxTimerValue: 5, maxTimerValue: 5,
instantBuild: true, instantBuild: true,
}); });
mg.setWinner = jest.fn(); mg.setWinner = vi.fn();
winCheck = new WinCheckExecution(); winCheck = new WinCheckExecution();
winCheck.init(mg, 0); winCheck.init(mg, 0);
}); });
it("should call checkWinnerFFA in FFA mode", () => { 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); winCheck.tick(10);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it("should call checkWinnerTeam in non-FFA mode", () => { it("should call checkWinnerTeam in non-FFA mode", () => {
mg.config = jest.fn(() => ({ mg.config = vi.fn(() => ({
gameConfig: jest.fn(() => ({ gameConfig: vi.fn(() => ({
maxTimerValue: 5, maxTimerValue: 5,
gameMode: GameMode.Team, gameMode: GameMode.Team,
})), })),
percentageTilesOwnedToWin: jest.fn(() => 50), percentageTilesOwnedToWin: vi.fn(() => 50),
})); }));
winCheck.init(mg, 0); winCheck.init(mg, 0);
const spy = jest.spyOn(winCheck as any, "checkWinnerTeam"); const spy = vi.spyOn(winCheck as any, "checkWinnerTeam");
winCheck.tick(10); winCheck.tick(10);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it("should set winner in FFA if percentage is reached", () => { it("should set winner in FFA if percentage is reached", () => {
const player = { const player = {
numTilesOwned: jest.fn(() => 81), numTilesOwned: vi.fn(() => 81),
name: jest.fn(() => "P1"), name: vi.fn(() => "P1"),
}; };
mg.players = jest.fn(() => [player]); mg.players = vi.fn(() => [player]);
mg.numLandTiles = jest.fn(() => 100); mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = jest.fn(() => 0); mg.numTilesWithFallout = vi.fn(() => 0);
winCheck.checkWinnerFFA(); winCheck.checkWinnerFFA();
expect(mg.setWinner).toHaveBeenCalledWith(player, expect.anything()); expect(mg.setWinner).toHaveBeenCalledWith(player, expect.anything());
}); });
it("should set winner in FFA if timer is 0", () => { it("should set winner in FFA if timer is 0", () => {
const player = { const player = {
numTilesOwned: jest.fn(() => 10), numTilesOwned: vi.fn(() => 10),
name: jest.fn(() => "P1"), name: vi.fn(() => "P1"),
}; };
mg.players = jest.fn(() => [player]); mg.players = vi.fn(() => [player]);
mg.numLandTiles = jest.fn(() => 100); mg.numLandTiles = vi.fn(() => 100);
mg.numTilesWithFallout = jest.fn(() => 0); mg.numTilesWithFallout = vi.fn(() => 0);
mg.stats = jest.fn(() => ({ stats: () => ({ mocked: true }) })); mg.stats = vi.fn(() => ({ stats: () => ({ mocked: true }) }));
// Advance ticks until timeElapsed (in seconds) >= maxTimerValue * 60 // Advance ticks until timeElapsed (in seconds) >= maxTimerValue * 60
// timeElapsed = (ticks - numSpawnPhaseTurns) / 10 => // timeElapsed = (ticks - numSpawnPhaseTurns) / 10 =>
// ticks >= numSpawnPhaseTurns + maxTimerValue * 600 // ticks >= numSpawnPhaseTurns + maxTimerValue * 600
@@ -73,7 +73,7 @@ describe("WinCheckExecution", () => {
}); });
it("should not set winner if no players", () => { it("should not set winner if no players", () => {
mg.players = jest.fn(() => []); mg.players = vi.fn(() => []);
winCheck.checkWinnerFFA(); winCheck.checkWinnerFFA();
expect(mg.setWinner).not.toHaveBeenCalled(); 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"; import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
const createMockStation = (id: string): jest.Mocked<TrainStation> => { const createMockStation = (id: string): Mocked<TrainStation> => {
return { return {
id, id,
setCluster: jest.fn(), setCluster: vi.fn(),
getCluster: jest.fn(() => null), getCluster: vi.fn(() => null),
} as any; } as any;
}; };
describe("Cluster tests", () => { describe("Cluster tests", () => {
let cluster: Cluster; let cluster: Cluster;
let stationA: jest.Mocked<TrainStation>; let stationA: Mocked<TrainStation>;
let stationB: jest.Mocked<TrainStation>; let stationB: Mocked<TrainStation>;
let stationC: jest.Mocked<TrainStation>; let stationC: Mocked<TrainStation>;
beforeEach(() => { beforeEach(() => {
cluster = new Cluster(); cluster = new Cluster();
+2 -2
View File
@@ -67,7 +67,7 @@ describe("GameImpl", () => {
}); });
test("Don't become traitor when betraying inactive player", async () => { 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.addExecution(new AllianceRequestExecution(attacker, defender.id()));
game.executeNextTick(); game.executeNextTick();
@@ -106,7 +106,7 @@ describe("GameImpl", () => {
}); });
test("Do become traitor when betraying active player", async () => { 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.addExecution(new AllianceRequestExecution(attacker, defender.id()));
game.executeNextTick(); game.executeNextTick();
+24 -24
View File
@@ -13,15 +13,15 @@ const createMockStation = (unitId: number): any => {
return { return {
unit: { unit: {
id: unitId, id: unitId,
setTrainStation: jest.fn(), setTrainStation: vi.fn(),
}, },
tile: jest.fn(), tile: vi.fn(),
neighbors: jest.fn(() => []), neighbors: vi.fn(() => []),
getCluster: jest.fn(() => cluster), getCluster: vi.fn(() => cluster),
setCluster: jest.fn(), setCluster: vi.fn(),
addRailroad: jest.fn(), addRailroad: vi.fn(),
getRailroads: jest.fn(() => railroads), getRailroads: vi.fn(() => railroads),
clearRailroads: jest.fn(), clearRailroads: vi.fn(),
}; };
}; };
@@ -54,18 +54,18 @@ describe("RailNetworkImpl", () => {
beforeEach(() => { beforeEach(() => {
stationManager = { stationManager = {
addStation: jest.fn(), addStation: vi.fn(),
removeStation: jest.fn(), removeStation: vi.fn(),
findStation: jest.fn(), findStation: vi.fn(),
getAll: jest.fn(() => new Set()), getAll: vi.fn(() => new Set()),
}; };
pathService = { pathService = {
findTilePath: jest.fn(() => [0]), findTilePath: vi.fn(() => [0]),
findStationsPath: jest.fn(() => [0]), findStationsPath: vi.fn(() => [0]),
}; };
game = { game = {
nearbyUnits: jest.fn(() => []), nearbyUnits: vi.fn(() => []),
addExecution: jest.fn(), addExecution: vi.fn(),
config: () => ({ config: () => ({
trainStationMaxRange: () => 80, trainStationMaxRange: () => 80,
trainStationMinRange: () => 10, trainStationMinRange: () => 10,
@@ -86,7 +86,7 @@ describe("RailNetworkImpl", () => {
network.connectStation(stationA); network.connectStation(stationA);
const cluster = stationB.getCluster(); const cluster = stationB.getCluster();
cluster.addStation = jest.fn(); cluster.addStation = vi.fn();
expect(cluster.addStation).not.toHaveBeenCalled(); expect(cluster.addStation).not.toHaveBeenCalled();
pathService.findTilePath.mockReturnValue(new Array(200)); pathService.findTilePath.mockReturnValue(new Array(200));
@@ -95,9 +95,9 @@ describe("RailNetworkImpl", () => {
}); });
test("removeStation removes all neighbor links", () => { test("removeStation removes all neighbor links", () => {
const neighbor = { removeNeighboringRails: jest.fn() }; const neighbor = { removeNeighboringRails: vi.fn() };
const station = createMockStation(1); const station = createMockStation(1);
station.neighbors = jest.fn(() => [neighbor]); station.neighbors = vi.fn(() => [neighbor]);
stationManager.findStation.mockReturnValue(station); stationManager.findStation.mockReturnValue(station);
network.removeStation(station); network.removeStation(station);
expect(station.clearRailroads).toHaveBeenCalled(); expect(station.clearRailroads).toHaveBeenCalled();
@@ -119,9 +119,9 @@ describe("RailNetworkImpl", () => {
const cluster = new Cluster(); const cluster = new Cluster();
const neighbor = createMockStation(1); const neighbor = createMockStation(1);
const station = createMockStation(2); const station = createMockStation(2);
station.getCluster = jest.fn(() => cluster); station.getCluster = vi.fn(() => cluster);
station.neighbors = jest.fn(() => [neighbor]); station.neighbors = vi.fn(() => [neighbor]);
cluster.removeStation = jest.fn(); cluster.removeStation = vi.fn();
stationManager.findStation.mockReturnValue(station); stationManager.findStation.mockReturnValue(station);
@@ -150,8 +150,8 @@ describe("RailNetworkImpl", () => {
const neighborStation = createMockStation(2); const neighborStation = createMockStation(2);
const cluster = new Cluster(); const cluster = new Cluster();
cluster.addStation(neighborStation); cluster.addStation(neighborStation);
neighborStation.getCluster = jest.fn(() => cluster); neighborStation.getCluster = vi.fn(() => cluster);
cluster.has = jest.fn(() => false); cluster.has = vi.fn(() => false);
const neighborUnit = { unit: neighborStation.unit, distSquared: 20 }; 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 { TrainExecution } from "../../../src/core/execution/TrainExecution";
import { Game, Player, Unit, UnitType } from "../../../src/core/game/Game"; import { Game, Player, Unit, UnitType } from "../../../src/core/game/Game";
import { Cluster, TrainStation } from "../../../src/core/game/TrainStation"; import { Cluster, TrainStation } from "../../../src/core/game/TrainStation";
jest.mock("../../../src/core/game/Game"); vi.mock("../../../src/core/game/Game");
jest.mock("../../../src/core/execution/TrainExecution"); vi.mock("../../../src/core/execution/TrainExecution");
jest.mock("../../../src/core/PseudoRandom"); vi.mock("../../../src/core/PseudoRandom");
describe("TrainStation", () => { describe("TrainStation", () => {
let game: jest.Mocked<Game>; let game: Mocked<Game>;
let unit: jest.Mocked<Unit>; let unit: Mocked<Unit>;
let player: jest.Mocked<Player>; let player: Mocked<Player>;
let trainExecution: jest.Mocked<TrainExecution>; let trainExecution: Mocked<TrainExecution>;
beforeEach(() => { beforeEach(() => {
game = { game = {
ticks: jest.fn().mockReturnValue(123), ticks: vi.fn().mockReturnValue(123),
config: jest.fn().mockReturnValue({ config: vi.fn().mockReturnValue({
trainGold: (isFriendly: boolean) => trainGold: (isFriendly: boolean) =>
isFriendly ? BigInt(1000) : BigInt(500), isFriendly ? BigInt(1000) : BigInt(500),
}), }),
addUpdate: jest.fn(), addUpdate: vi.fn(),
addExecution: jest.fn(), addExecution: vi.fn(),
} as any; } as any;
player = { player = {
addGold: jest.fn(), addGold: vi.fn(),
id: 1, id: 1,
canTrade: jest.fn().mockReturnValue(true), canTrade: vi.fn().mockReturnValue(true),
isFriendly: jest.fn().mockReturnValue(false), isFriendly: vi.fn().mockReturnValue(false),
} as any; } as any;
unit = { unit = {
owner: jest.fn().mockReturnValue(player), owner: vi.fn().mockReturnValue(player),
level: jest.fn().mockReturnValue(1), level: vi.fn().mockReturnValue(1),
tile: jest.fn().mockReturnValue({ x: 0, y: 0 }), tile: vi.fn().mockReturnValue({ x: 0, y: 0 }),
type: jest.fn(), type: vi.fn(),
isActive: jest.fn().mockReturnValue(true), isActive: vi.fn().mockReturnValue(true),
} as any; } as any;
trainExecution = { trainExecution = {
loadCargo: jest.fn(), loadCargo: vi.fn(),
owner: jest.fn().mockReturnValue(player), owner: vi.fn().mockReturnValue(player),
level: jest.fn(), level: vi.fn(),
} as any; } as any;
}); });
@@ -70,7 +71,7 @@ describe("TrainStation", () => {
it("checks trade availability (same owner)", () => { it("checks trade availability (same owner)", () => {
const otherUnit = { const otherUnit = {
owner: jest.fn().mockReturnValue(unit.owner()), owner: vi.fn().mockReturnValue(unit.owner()),
} as any; } as any;
const station = new TrainStation(game, unit); 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 // Mock BuildMenu to avoid importing lit and other ESM-heavy deps in this unit test
jest.mock( vi.mock("../src/client/graphics/layers/BuildMenu", () => ({
"../src/client/graphics/layers/BuildMenu", BuildMenu: class {},
() => ({ flattenedBuildTable: [],
BuildMenu: class {}, }));
flattenedBuildTable: [],
}),
{ virtual: true },
);
// Mock Utils to avoid touching DOM (document) during tests // Mock Utils to avoid touching DOM (document) during tests
jest.mock("../src/client/Utils", () => ({ vi.mock("../src/client/Utils", () => ({
translateText: (k: string) => k, translateText: (k: string) => k,
getSvgAspectRatio: async () => 1, getSvgAspectRatio: async () => 1,
})); }));
@@ -57,20 +55,20 @@ const makeParams = (opts?: Partial<MenuElementParams>): MenuElementParams => {
} as any, } as any,
emojiTable: {} as any, emojiTable: {} as any,
playerActionHandler: { playerActionHandler: {
handleBreakAlliance: jest.fn(), handleBreakAlliance: vi.fn(),
handleEmbargo: jest.fn(), handleEmbargo: vi.fn(),
handleDonateGold: jest.fn(), handleDonateGold: vi.fn(),
handleDonateTroops: jest.fn(), handleDonateTroops: vi.fn(),
handleTargetPlayer: jest.fn(), handleTargetPlayer: vi.fn(),
} as any, } as any,
playerPanel: { playerPanel: {
show: jest.fn(), show: vi.fn(),
} as any, } as any,
chatIntegration: { chatIntegration: {
createQuickChatMenu: jest.fn(() => []), createQuickChatMenu: vi.fn(() => []),
} as any, } as any,
eventBus: {} 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": { "compilerOptions": {
// Language and Environment // Language and Environment
"target": "ES2020", "target": "ES2020",
// Modules // Modules
"module": "ESNext", "module": "ESNext",
"rootDir": ".", "rootDir": ".",
"moduleResolution": "node", "moduleResolution": "node",
"baseUrl": ".",
"paths": {
"resources/*": ["resources/*"]
},
// Emit // Emit
"sourceMap": true, "sourceMap": true,
// Type Checking // Type Checking
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"allowUnusedLabels": false, "allowUnusedLabels": false,
@@ -21,7 +22,9 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"strictNullChecks": true, "strictNullChecks": true,
"useDefineForClassFields": false, "useDefineForClassFields": false,
"strictPropertyInitialization": false "strictPropertyInitialization": false,
"skipLibCheck": true,
"types": ["vitest/globals", "node"]
}, },
"include": [ "include": [
"src/**/*", "src/**/*",
@@ -29,7 +32,8 @@
"proprietary/**/*", "proprietary/**/*",
"generated/**/*", "generated/**/*",
"tests/**/*", "tests/**/*",
"src/scripts" "src/scripts",
"vite.config.ts"
], ],
"exclude": ["node_modules"] "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,
},
],
},
};
};