Merge branch 'main' into evan-add-map-gen

This commit is contained in:
evanpelle
2025-08-20 09:11:50 -07:00
committed by GitHub
242 changed files with 3061 additions and 1949 deletions
+2 -1
View File
@@ -50,7 +50,8 @@ RUN npm ci
# Final image
FROM base
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
+58 -19
View File
@@ -1,12 +1,13 @@
import eslintConfigPrettier from "eslint-config-prettier/flat";
import eslintPluginLocal from "./eslint-plugin-local/plugin.js";
import { fileURLToPath } from "node:url";
import globals from "globals";
import { includeIgnoreFile } from "@eslint/compat";
import jest from "eslint-plugin-jest";
import path from "node:path";
import pluginJs from "@eslint/js";
import stylisticTs from "@stylistic/eslint-plugin";
import stylistic from "@stylistic/eslint-plugin";
import tseslint from "typescript-eslint";
import { fileURLToPath } from "node:url";
import { includeIgnoreFile } from "@eslint/compat";
import eslintPluginLocal from "./eslint-plugin-local/plugin.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -43,22 +44,21 @@ export default [
{
rules: {
// Disable rules that would fail. The failures should be fixed, and the entries here removed.
"@typescript-eslint/no-explicit-any": "off", // https://github.com/openfrontio/OpenFrontIO/issues/1789
"@typescript-eslint/no-unused-expressions": "off", // https://github.com/openfrontio/OpenFrontIO/issues/1790
"no-case-declarations": "off", // https://github.com/openfrontio/OpenFrontIO/issues/1791
},
},
{
plugins: {
"@stylistic/ts": stylisticTs,
"@stylistic": stylistic,
},
rules: {
// Enable rules
// '@stylistic/ts/quotes': ['error', 'single'], TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1788
"@stylistic/ts/indent": ["error", 2],
"@stylistic/ts/semi": "error",
"@stylistic/ts/space-infix-ops": "error",
"@stylistic/ts/type-annotation-spacing": [
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
"@stylistic/indent": ["error", 2],
"@stylistic/semi": "error",
"@stylistic/space-infix-ops": "error",
"@stylistic/type-annotation-spacing": [
"error",
{
after: true,
@@ -70,11 +70,13 @@ export default [
},
},
],
"@stylistic/eol-last": "error",
"@typescript-eslint/consistent-type-definitions": [
"error",
"type",
],
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-mixed-enums": "error",
"@typescript-eslint/no-require-imports": "error",
@@ -84,12 +86,13 @@ export default [
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-literal-enum-member": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-readonly": "error",
"eqeqeq": "error",
"indent": "off", // @stylistic/ts/indent
"indent": "off", // @stylistic/indent
"sort-keys": "error",
// "@typescript-eslint/no-unsafe-argument": "error", // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1780
// "@typescript-eslint/no-unsafe-assignment": "error", // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1781
// "@typescript-eslint/no-unsafe-member-access": "error", // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1783
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
// "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1784
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/prefer-for-of": "error",
@@ -102,18 +105,25 @@ export default [
"func-call-spacing": ["error", "never"],
"function-call-argument-newline": ["error", "consistent"],
"max-depth": ["error", { max: 5 }],
// "max-len": ["error", { code: 120 }], // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1785
"max-lines": ["error", { max: 1065, skipBlankLines: true, skipComments: true }],
"max-len": ["error", { code: 120 }],
"max-lines": ["error", { max: 676, skipBlankLines: true, skipComments: true }],
"max-lines-per-function": ["error", { max: 561 }],
"no-loss-of-precision": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0 }],
"no-trailing-spaces": "error",
"object-curly-newline": ["error", { multiline: true, consistent: true }],
"object-curly-spacing": ["error", "always"],
"object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
// "no-undef": "error", // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1786
"object-shorthand": ["error", "always"],
"no-undef": "error",
"no-unused-vars": "off", // @typescript-eslint/no-unused-vars
"prefer-destructuring": ["error", {
array: false,
object: true,
}],
"quote-props": ["error", "consistent-as-needed"],
// 'sort-imports': 'error', // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1787
"sort-imports": "error",
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", {
anonymous: "always",
@@ -127,9 +137,38 @@ export default [
files: [
"**/*.config.{js,ts,jsx,tsx}",
"**/*.test.{js,ts,jsx,tsx}",
"tests/**/*.{js,ts,jsx,tsx}",
"eslint-plugin-local/**/*.{js,ts,jsx,tsx}",
],
rules: {
// Disabled rules for tests, configs
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"max-len": "off",
"sort-keys": "off",
},
},
{
languageOptions: {
globals: {
...globals.jest,
},
},
files: [
"**/*.test.{js,ts,jsx,tsx}",
"tests/**/*.{js,ts,jsx,tsx}",
],
plugins: ["jest"],
...jest.configs["flat/style"],
},
{
files: [
"src/client/**/*.{js,ts,jsx,tsx}",
],
rules: {
// Disabled rules for frontend
"sort-keys": "off",
},
},
+50 -19
View File
@@ -29,6 +29,7 @@
"js-yaml": "^4.1.0",
"nanoid": "^3.3.6",
"obscenity": "^0.4.3",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"uuid": "^11.1.0",
"winston": "^3.17.0",
@@ -43,6 +44,7 @@
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@stylistic/eslint-plugin": "^5.2.3",
"@swc/core": "^1.13.3",
"@swc/jest": "^0.2.39",
"@total-typescript/ts-reset": "^0.6.1",
"@types/benchmark": "^2.1.5",
@@ -57,6 +59,7 @@
"@types/msgpack5": "^3.4.6",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/seedrandom": "^3.0.8",
"@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
@@ -74,6 +77,7 @@
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2",
"eslint-plugin-jest": "^29.0.1",
"eslint-webpack-plugin": "^5.0.0",
"file-loader": "^6.2.0",
"globals": "^16.0.0",
@@ -3600,13 +3604,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
"integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.15.0",
"@eslint/core": "^0.15.2",
"levn": "^0.4.1"
},
"engines": {
@@ -3614,9 +3618,9 @@
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
"integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -6415,7 +6419,6 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.23"
@@ -6460,7 +6463,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6477,7 +6479,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6494,7 +6495,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6511,7 +6511,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6528,7 +6527,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6545,7 +6543,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6562,7 +6559,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6579,7 +6575,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6596,7 +6591,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6613,7 +6607,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=10"
}
@@ -6649,7 +6642,6 @@
"integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
@@ -7357,6 +7349,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/seedrandom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
@@ -11168,6 +11167,32 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/eslint-plugin-jest": {
"version": "29.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz",
"integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/utils": "^8.0.0"
},
"engines": {
"node": "^20.12.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0",
"eslint": "^8.57.0 || ^9.0.0",
"jest": "*"
},
"peerDependenciesMeta": {
"@typescript-eslint/eslint-plugin": {
"optional": true
},
"jest": {
"optional": true
}
}
},
"node_modules/eslint-scope": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
@@ -17969,6 +17994,12 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+4
View File
@@ -31,6 +31,7 @@
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@stylistic/eslint-plugin": "^5.2.3",
"@swc/core": "^1.13.3",
"@swc/jest": "^0.2.39",
"@total-typescript/ts-reset": "^0.6.1",
"@types/benchmark": "^2.1.5",
@@ -45,6 +46,7 @@
"@types/msgpack5": "^3.4.6",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/seedrandom": "^3.0.8",
"@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
@@ -62,6 +64,7 @@
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2",
"eslint-plugin-jest": "^29.0.1",
"eslint-webpack-plugin": "^5.0.0",
"file-loader": "^6.2.0",
"globals": "^16.0.0",
@@ -122,6 +125,7 @@
"js-yaml": "^4.1.0",
"nanoid": "^3.3.6",
"obscenity": "^0.4.3",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"uuid": "^11.1.0",
"winston": "^3.17.0",
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="834px" height="528px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="834px" height="528px" viewBox="0 0 834 528" preserveAspectRatio="xMidYMid meet" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:1" fill="#fdfdfd" d="M 135.5,-0.5 C 137.167,-0.5 138.833,-0.5 140.5,-0.5C 159.768,12.1427 179.434,24.3094 199.5,36C 201.584,37.5007 202.917,39.5007 203.5,42C 159.221,122.06 114.554,201.893 69.5,281.5C 67.9991,282.752 66.3325,283.752 64.5,284.5C 42.3037,271.908 20.637,258.575 -0.5,244.5C -0.5,242.833 -0.5,241.167 -0.5,239.5C 44.818,159.535 90.1513,79.5352 135.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#fdfdfd" d="M 692.5,-0.5 C 694.167,-0.5 695.833,-0.5 697.5,-0.5C 742.849,79.5352 788.182,159.535 833.5,239.5C 833.5,241.167 833.5,242.833 833.5,244.5C 812.581,258.234 791.248,271.401 769.5,284C 767.536,284.624 765.869,284.124 764.5,282.5C 718.86,202.722 673.86,122.555 629.5,42C 630.083,39.5007 631.416,37.5007 633.5,36C 653.566,24.3094 673.232,12.1427 692.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#fdfdfd" d="M 432.5,64.5 C 454.565,63.9178 476.565,64.7512 498.5,67C 526.867,72.5085 555.2,78.1752 583.5,84C 589.167,84.6667 594.833,84.6667 600.5,84C 610.347,82.4512 620.014,80.4512 629.5,78C 665.047,139.944 699.88,202.11 734,264.5C 723.137,275.697 711.971,286.53 700.5,297C 693.085,302.46 685.252,307.293 677,311.5C 597.446,244.776 511.279,187.609 418.5,140C 414.302,137.486 409.802,135.653 405,134.5C 390.991,138.461 377.158,142.961 363.5,148C 349.788,168.712 332.455,185.712 311.5,199C 297.958,206.51 283.458,210.677 268,211.5C 264.417,210.942 260.917,210.109 257.5,209C 255.418,206.591 254.418,203.757 254.5,200.5C 255.061,194.256 256.561,188.256 259,182.5C 280.319,148.023 303.486,114.856 328.5,83C 339.522,77.3271 351.188,73.6604 363.5,72C 386.537,68.431 409.537,65.931 432.5,64.5 Z"/></g>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Инструкции",
"how_to_play": "Как се играе",
"advertise": "Рекламиране",
"wiki": "Уики"
"wiki": "Уики",
"privacy_policy": "Поверителност",
"terms_of_service": "Условия за ползване"
},
"news": {
"full_changelog": "Вижте пълния лист с промени",
@@ -448,6 +450,7 @@
"gold": "Злато",
"ports": "Пристанища",
"cities": "Градове",
"factories": "Фабрики",
"missile_launchers": "Ракетни силози",
"sams": "Противоракетни установки земя-въздух SAM",
"warships": "Бойни кораби",
@@ -574,6 +577,9 @@
"grogu": "Грогу"
}
},
"spawn_ad": {
"loading": "Зарежда се реклама..."
},
"auth": {
"login_required": "За достъп до този уебсайт е необходим вход.",
"redirecting": "Пренасочваме ви...",
+8 -2
View File
@@ -21,7 +21,9 @@
"instructions": "Instruktioner",
"how_to_play": "Sådan spiller du",
"advertise": "Annoncér",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Privatlivspolitik",
"terms_of_service": "Servicevilkår"
},
"news": {
"full_changelog": "Vis hele ændringsloggen",
@@ -47,7 +49,7 @@
"ui_leaderboard_desc": "Viser de bedste spillere i spillet med deres navne, procentdel af ejet land, guld og tropper. Ved at vælge 'Vis alle' vises alle spillere i spillet. Hvis du ikke ønsker at se ranglisten, klik på 'Skjul'.",
"ui_control": "Betjeningspanel",
"ui_control_desc": "Kontrolpanelet indeholder følgende elementer:",
"ui_pop": "Befolk — Antal enheder, maksimal befolkning og tilvækst.",
"ui_pop": "Befolkning — Antal enheder, maksimal befolkning og tilvækst.",
"ui_gold": "Guld — Din beholdning og hvor hurtigt du tjener det.",
"ui_troops_workers": "Tropper og arbejdere — Antallet af tildelte tropper og arbejdere. Tropper bruges til at angribe eller forsvare mod angreb. Arbejdere bruges til at generere guld. Du kan justere antallet af tropper og arbejdere med skyderen.",
"ui_attack_ratio": "Angrebsforhold — Antallet af tropper, der bruges, når du angriber. Du kan justere angrebsforholdet med skyderen. Hvis du har flere angribende tropper end forsvarende, vil du tabe færre tropper under angrebet, mens færre angribende tropper øger skaden på dine tropper. Effekten gælder kun op til forholdet 2:1.",
@@ -448,6 +450,7 @@
"gold": "Guld",
"ports": "Havne",
"cities": "Byer",
"factories": "Fabrikker",
"missile_launchers": "Missilramper",
"sams": "SAMs",
"warships": "Krigsskibe",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Indlæser reklame..."
},
"auth": {
"login_required": "Login er påkrævet for at få adgang til denne hjemmeside.",
"redirecting": "Du bliver omdirigeret...",
+7 -1
View File
@@ -453,7 +453,13 @@
"team": "Team",
"owned": "Owned",
"gold": "Gold",
"troops": "Troops"
"troops": "Troops",
"launchers": "Launchers",
"sams": "SAMs",
"warships": "Warships",
"cities": "Cities",
"show_control": "Show Control",
"show_units": "Show Units"
},
"player_info_overlay": {
"type": "Type",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Instrukcioj",
"how_to_play": "Kiel ludi",
"advertise": "Reklami",
"wiki": "Vikio"
"wiki": "Vikio",
"privacy_policy": "Privateca politiko",
"terms_of_service": "Uzokondiĉoj"
},
"news": {
"full_changelog": "Vidi la plenan ŝanĝprotokolon",
@@ -448,6 +450,7 @@
"gold": "Oro",
"ports": "Havenoj",
"cities": "Urboj",
"factories": "Fabrikoj",
"missile_launchers": "Misillanĉiloj",
"sams": "SAM-oj",
"warships": "Militŝipoj",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Ŝarĝante reklamo..."
},
"auth": {
"login_required": "Ensaluto estas necesa por aliri ĉi tiun retejon.",
"redirecting": "Vi estas redirektata...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Ohjeet",
"how_to_play": "Kuinka pelata",
"advertise": "Mainosta",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Tietosuojaseloste",
"terms_of_service": "Käyttöehdot"
},
"news": {
"full_changelog": "Katso koko muutosloki",
@@ -448,6 +450,7 @@
"gold": "Kultaa",
"ports": "Satamia",
"cities": "Kaupunkeja",
"factories": "Tehtaita",
"missile_launchers": "Ohjussiiloja",
"sams": "Ilmatorjuntaohjusjärjestelmiä",
"warships": "Sota-aluksia",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Ladataan mainosta..."
},
"auth": {
"login_required": "Kirjautuminen vaaditaan tälle verkkosivustolle pääsemiseen.",
"redirecting": "Sinut uudelleenohjataan...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Instructions",
"how_to_play": "Comment jouer ?",
"advertise": "Faire de la publicité",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Politique de confidentialité",
"terms_of_service": "Conditions d'utilisation"
},
"news": {
"full_changelog": "Voir le journal des modifications complet",
@@ -448,6 +450,7 @@
"gold": "Or",
"ports": "Ports",
"cities": "Villes",
"factories": "Usines",
"missile_launchers": "Lance-missiles",
"sams": "SAMs",
"warships": "Navires de guerre",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Chargement de l'annonce..."
},
"auth": {
"login_required": "La connexion est nécessaire pour accéder à ce site Web.",
"redirecting": "Vous êtes en cours de redirection...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Instrucións",
"how_to_play": "Como xogar",
"advertise": "Promoción",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Política de privacidade",
"terms_of_service": "Termos de servizo"
},
"news": {
"full_changelog": "Ve o rexistro de actualizacións completo",
@@ -448,6 +450,7 @@
"gold": "Ouro",
"ports": "Portos",
"cities": "Cidades",
"factories": "Fábricas",
"missile_launchers": "Silos",
"sams": "Lanzadores SAM",
"warships": "Buques",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Cargando anuncio..."
},
"auth": {
"login_required": "Debes iniciar sesión para acceder a este sitio.",
"redirecting": "Estámosche a redirixir...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "説明書",
"how_to_play": "遊び方",
"advertise": "広告",
"wiki": "ウィキ"
"wiki": "ウィキ",
"privacy_policy": "プライバシーポリシー",
"terms_of_service": "利用規約"
},
"news": {
"full_changelog": "完全な変更ログを見る",
@@ -448,6 +450,7 @@
"gold": "資金",
"ports": "港",
"cities": "都市",
"factories": "工場",
"missile_launchers": "ミサイル格納庫",
"sams": "SAM",
"warships": "戦艦",
@@ -574,6 +577,9 @@
"grogu": "グローグー模様"
}
},
"spawn_ad": {
"loading": "広告を読み込み中…"
},
"auth": {
"login_required": "このサイトにアクセスするにはログインが必要です。",
"redirecting": "リダイレクト中…",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Instructies",
"how_to_play": "Hoe spelen?",
"advertise": "Adverteren",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Privacybeleid",
"terms_of_service": "Servicevoorwaarden"
},
"news": {
"full_changelog": "Bekijk het volledige changelog",
@@ -448,6 +450,7 @@
"gold": "Goud",
"ports": "Havens",
"cities": "Steden",
"factories": "Fabrieken",
"missile_launchers": "Raketsilo's",
"sams": "SAM-lanceerders",
"warships": "Oorlogsschepen",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Advertentie laden..."
},
"auth": {
"login_required": "Login is vereist voor toegang tot deze website.",
"redirecting": "Je wordt omgeleid...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Инструкции",
"how_to_play": "Как играть",
"advertise": "Рекламирование",
"wiki": "Вики"
"wiki": "Вики",
"privacy_policy": "Политика конфиденциальности",
"terms_of_service": "Пользовательское соглашение"
},
"news": {
"full_changelog": "Смотрите весь журнал изменений",
@@ -448,6 +450,7 @@
"gold": "Золото",
"ports": "Порты",
"cities": "Города",
"factories": "Фабрики",
"missile_launchers": "Ракетные установки",
"sams": "ЗРК",
"warships": "Военные корабли",
@@ -574,6 +577,9 @@
"grogu": "Грогу"
}
},
"spawn_ad": {
"loading": "Загрузка рекламы..."
},
"auth": {
"login_required": "Для доступа к этому сайту требуется войти в систему.",
"redirecting": "Вы будете перенаправлены...",
+589
View File
@@ -0,0 +1,589 @@
{
"lang": {
"en": "Slovak",
"native": "Slovenčina",
"svg": "sk",
"lang_code": "sk"
},
"common": {
"close": "Zavrieť"
},
"main": {
"title": "OpenFront (ALFA)",
"join_discord": "Pridaj sa do Discordu!",
"login_discord": "Prihlás sa cez Discord",
"checking_login": "Kontrolujem prihlásenie...",
"logged_in": "Prihlásený/á!",
"log_out": "Odhlásiť sa",
"create_lobby": "Vytvoriť miestnosť",
"join_lobby": "Pripojiť sa k miestnosti",
"single_player": "Hra jedného hráča",
"instructions": "Pokyny",
"how_to_play": "Ako hrať",
"advertise": "Inzerovať",
"wiki": "Wiki",
"privacy_policy": "Zásady ochrany osobných údajov",
"terms_of_service": "Podmienky používania"
},
"news": {
"full_changelog": "Pozri si celý zoznam zmien",
"github_link": "na GitHube",
"title": "Zoznam zmien"
},
"help_modal": {
"hotkeys": "Skratky",
"table_key": "Kľúč",
"table_action": "Akcia",
"action_alt_view": "Alternatívny pohľad (terén/krajiny)",
"action_attack_altclick": "Útok (keď je ľavé ťuknutie nastavené na otvorenie ponuky)",
"action_build": "Otvoriť stavebnú ponuku",
"action_emote": "Otvoriť ponuku emoji",
"action_center": "Sústrediť pohľad na hráča",
"action_zoom": "Priblížiť/oddialiť",
"action_move_camera": "Pohnúť pohľadom",
"action_ratio_change": "Znížiť/Zvýšiť pomer útoku",
"action_reset_gfx": "Obnoviť grafické nastavenia",
"ui_section": "Herné rozhranie",
"ui_leaderboard": "Rebríček",
"ui_your_team": "Tvoj tím:",
"ui_leaderboard_desc": "Ukáže vrchných hráčov a ich mená, % vlastneného územia, zlata a vojsk. Použitím „Ukázať všetkých“ ukáže všetkých hráčov v hre. Ak nechceš vidieť rebríček, ťukni „Skryť“.",
"ui_control": "Ovládací panel",
"ui_control_desc": "Ovládací panel obsahuje nasledovné prvky:",
"ui_pop": "Pop Počet jednotiek, ktoré máš, tvoja maximálna populácia, a rýchlosť, ktorou ich získavaš.",
"ui_gold": "Zlato Počet zlata, ktoré máš, a rýchlosť, ktorou ho získavaš.",
"ui_troops_workers": "Vojská a robotníci Počet pridelených vojsk a robotníkov. Vojská sa používajú na útočenie alebo obranu proti útokom. Robotníci sa používajú na tvorbu zlata. Môžeš upraviť pomer vojsk a robotníkov použitím posuvníka.",
"ui_attack_ratio": "Pomer útoku Počet vojsk, ktoré použiješ pri útoku. Pomer útoku môžeš upraviť použitím posuvníka. Čím viac útočiacich vojsk máš, tým menej ich pri útoku stratíš, zatiaľčo čím menej ich použiješ, tým rýchlejšie utrpia straty. Tento efekt neplatí nad pomer 2:1.",
"ui_events": "Panel udalostí",
"ui_events_desc": "Panel udalostí zobrazuje najnovšie udalosti, žiadosti a správy rýchleho chatu. Napríklad:",
"ui_events_alliance": "Spojenectvo Môžeš prijať alebo odmietnuť spojenectvo. Spojenci môžu zdieľať zdroje a vojská, ale nesmú na seba útočiť. Ťuknutím na „Sústrediť“ pohne pohľad na hráča, ktorý odoslal žiadosť.",
"ui_events_attack": "Útoky - Ukazujú sa tu prichádzajúce a odoslané útoky. Ťuknutím na správu zobrazíš útok, raketu, alebo loď. Môžeš zrušiť útok ťuknutím na červené X. Bude to stáť 25% útočiacich vojsk. Ak zrušíš útok lode, loď sa vráti na pôvodné miesto a zaútoči naň, ak bolo medzitým obsadené. Rakety nemôžu byť po vystrelení zrušené.",
"ui_events_quickchat": "Rýchly čet - Vidíš tu poslané a prijaté správy. Pošli správu ťuknutím na ikonu rýchleho četu v ich ponuke informácií.",
"ui_options": "Možnosti",
"ui_options_desc": "Nájdeš tu nasledujúce prvky:",
"ui_playeroverlay": "Rozhranie hráčskych informácií",
"ui_playeroverlay_desc": "Keď posunieš myšku nad krajinu, rozhranie hráčskych informácií sa zobrazí pod možnosťami. Ukazuje typ hráča: človek, národ (múdry bot), alebo bot; postoj národa ku tebe, od nepriateľského po priateľský; a brániace vojsko, zlato, a počet bojových lodí a rôznych budov, ktoré hráč má.",
"ui_wilderness": "Divočina",
"option_pause": "Zastaviť/Pokračovať hru Dostupné len v hre pre jedného hráča.",
"option_timer": "Časovač Čas uplynutý od začiatku hry.",
"option_exit": "Tlačidlo ukončenia.",
"option_settings": "Nastavenia Otvorí ponuku nastavení. V nej môžeš prepnúť alternatívny pohľad, emoji, tmavý režim, nindža (režim náhodných mien) a akciu pri ľavom stlačení.",
"radial_title": "Kruhová ponuka",
"radial_desc": "Pravým ťuknutím (alebo ťuknutím na mobile) otvorí kruhovú ponuku. Pravé ťuknutie mimo ho zatvorí. Z ponuky môžeš:",
"radial_build": "Otvoriť ponuku stavby.",
"radial_info": "Otvoriť ponuku informácií.",
"radial_boat": "Poslať (prepravnú) loď na zaútočenie na vybranú pozíciu. Dostupné len ak máš prístup k vode.",
"radial_close": "Zatvoriť ponuku.",
"info_title": "Ponuka informácií",
"info_enemy_desc": "Obsahuje informácie ako meno, zlato, a vojská vybraného hráča, či s tebou prestal obchodovať, počet rakiet vystrelených voči tebe, a či je hráč zradca. Prestať obchodovať znamená, že si nebudete navzájom posielať zlato cez obchodné lode. Dá sa to ručne („ak hráč ťukol skončiť obchodovať“, čo trvá, až obaja neťuknete „začať obchodovať“), alebo samovoľne (ak jeden z vás zradil vaše spojenectvo, čo trvá, kým sa znovu stanete spojenci alebo 5 minút). Zdradca sa ukazuje 30 sekúnd keď hráč zradil a zaútočil na hráča, s ktorým mal spojenectvo. Ikony nižšie predstavujú nasledovné interakcie:",
"info_chat": "Odošli rýchlu správu hráčovi. Vyber kategóriu, frázu, a ak fráza obsahuje [P1], vyber meno hráča, ktorým sa nahradí. Klikni na Odoslať.",
"info_target": "Umiestníš cieľ na hráča, označujúc ho pre všetkých spojencov, používané na koordináciu útoku.",
"info_alliance": "Pošleš žiadosť o spojenectvo hráčovi. Spojenci môžu zdieľať zdroje a vojská, ale nesmú na seba útočiť.",
"info_emoji": "Pošleš hráčovi emoji.",
"info_trade": "Použi „Prestať obchodovať“ na zastavenie vzájomného posielania zlata cez obchodné lode. Ak obaja ťuknete „Začať obchodovať“, znovu sa spustí.",
"info_ally_panel": "Tabuľa informácií spojenca",
"info_ally_desc": "Keď uzavrieš spojenectvo s hráčom, sprístupnia sa nasledujúce ikony:",
"ally_betray": "Zradíš svojho spojenca, čím ukončíš alianciu, zastavíš obchodovanie a oslabíš svoju obranu. Obchod medzi vami sa pozastaví na 5 minút (alebo kým sa znova nestanete spojencami) a ostatní môžu tiež prestať obchodovať. Ak druhý hráč tiež nebol zradca, budeš označený ako zradca na 30 sekúnd. Počas toho sa nad tvojím menom zobrazí ikona, a budeš mať 50% postih na obranu. Boti s tebou budú menej ochotní uzavrieť alianciu a ostatní hráči si to dvakrát rozmyslia.",
"ally_donate": "Daruj časť svojich vojsk spojencovi. Používa sa vtedy, keď má málo vojsk a je pod útokom, alebo vtedy, keď potrebuje silu navyše na zničenie nepriateľa.",
"ally_donate_gold": "Daruj časť svojho zlata svojmu spojencovi. Používa sa, keď má málo zlata a potrebuje ho na budovy, alebo keď si člen tvojho tímu šetrí na MIRV raketu.",
"build_menu_title": "Stavebná ponuka",
"build_menu_desc": "Postav ich alebo uviď, koľko z každej si už postavil:",
"build_name": "Názov",
"build_icon": "Ikona",
"build_desc": "Popis",
"build_city": "Mesto",
"build_city_desc": "Zvyšuje tvoju maximálnu populáciu. Užitočné, keď nemôžeš rozšíriť svoje územie, alebo keď si na limite populácie.",
"build_factory": "Továreň",
"build_factory_desc": "Automaticky vytvára železnice medzi blízkymi budovami a občas vytvorí vlaky.",
"build_defense": "Obranný bod",
"build_defense_desc": "Zvyšuje obranu okolo blízkych hraníc, ktoré sú zobrazené šachovnicovým vzorom. Útoky nepriateľov sú pomalšie a majú väčšie straty.",
"build_port": "Prístav",
"build_port_desc": "Dá sa postaviť len blízko vody. Umožňuje stavbu vojnových lodí. Automaticky posiela obchodné lode medzi prístavmi tvojej krajiny a iných krajín (okrem toho, keď je obchod zastavený), prinášajúc zlato obom stranám. Obchod sa automaticky zastaví, keď zaútočíš alebo si napadnutý daným hráčom. Obnoví sa po 5 minútach alebo ak sa stanete spojencami. Obchodovanie môžeš ručne prepnúť cez „Zastaviť obchodovanie“ alebo „Začať obchodovanie“.",
"build_warship": "Bojová loď",
"build_warship_desc": "Hliadkuje na území, ovláda obchodné lode, a ničí nepriateľské člny a bojové lode. Vypustí ju najbližší pristav a hliadkuje územie, ktoré ťukneš pri jej stavbe. Bojové lode môžeš ovládať ťuknutím na ne a potom ťuknutím na nové miesto, kam chceš, aby sa presunuli.",
"build_silo": "Raketové silo",
"build_silo_desc": "Umožňuje streľbu rakiet.",
"build_sam": "Systém zem-vzduch",
"build_sam_desc": "Dokáže zneškodniť nepriateľské rakety v okruhu 100 pixelov. 100% úspešnosť pre atómovú bombu, 80% pre vodíkovú bombu, a 50% pre jednotlivé hlavice MIRV. Systém zem-vzduch má 7,5 sekundový čas ochladnutia.",
"build_atom": "Atómová bomba",
"build_atom_desc": "Malá bomba, ktorá ničí územie, budovy, lode, a člny. Vypustí sa z najbližšieho raketového sila a pristane na mieste, kde klikneš na jej vybudovanie.",
"build_hydrogen": "Vodíková bomba",
"build_hydrogen_desc": "Veľká bomba. Vypustí sa z najbližšieho raketového sila a pristane na mieste, kde klikneš na jej vybudovanie.",
"build_mirv": "MIRV",
"build_mirv_desc": "Najsilnejšia bomba v hre. Rozdelí sa na menšie bomby, ktoré pokryjú obrovskú časť územia. Poškodzuje len nepriateľa, na ktorého si ťukol pri jej stavbe. Vypustí sa z najbližšieho raketového sila a pristane na mieste, kde klikneš na jej vybudovanie.",
"player_icons": "Ikony hráčov",
"icon_desc": "Príklady niektorých ikon, ktoré stretneš v hre, a čo znamenajú:",
"icon_crown": "Koruna Toto je hráč číslo 1 v rebríčku.",
"icon_traitor": "Zlomený štít Zradca. Tento hráč zaútočil na spojenca.",
"icon_ally": "Podanie ruky Spojenec. Tento hráč je tvoj spojenec.",
"icon_embargo": "Prekrížený dolár Embargo. Tento hráč prestal s tebou obchodovať.",
"icon_request": "Obálka Žiadosť o spojenectvo. Tento hráč ti poslal žiadosť o spojenectvo.",
"info_enemy_panel": "Tabuľa informácií nepriateľa",
"exit_confirmation": "Naozaj chceš opustiť hru?"
},
"single_modal": {
"title": "Hra jedného hráča",
"allow_alliances": "Povoliť spojenectvá",
"options_title": "Možnosti",
"bots": "Boti: ",
"bots_disabled": "Zakázané",
"disable_nations": "Zakázať krajiny",
"instant_build": "Okamžite postaviť",
"infinite_gold": "Nekonečné zlato",
"infinite_troops": "Nekonečné vojská",
"disable_nukes": "Zakázať jadrové bomby",
"enables_title": "Povoliť nastavenia",
"start": "Spustiť hru"
},
"map": {
"map": "Mapa",
"world": "Svet",
"giantworldmap": "Obrovská svetová mapa",
"europe": "Európa",
"mena": "Blízky východ a severná Afrika",
"northamerica": "Severná Amerika",
"oceania": "Oceánia",
"blacksea": "Čierne more",
"africa": "Afrika",
"asia": "Ázia",
"mars": "Mars",
"southamerica": "Južná Amerika",
"britannia": "Británia",
"gatewaytotheatlantic": "Brána do Atlantiku",
"australia": "Austrália",
"random": "Náhodné",
"iceland": "Island",
"pangaea": "Pangea",
"eastasia": "Východná Ázia",
"betweentwoseas": "Medzi dvomi morami",
"faroeislands": "Faerské ostrovy",
"deglaciatedantarctica": "Odľadovcovaná Antarktída",
"europeclassic": "Európa (klasická)",
"falklandislands": "Falklandské ostrovy",
"baikal": "Bajkal",
"halkidiki": "Chalkidiki",
"straitofgibraltar": "Gibraltársky prieliv",
"italia": "Itália"
},
"map_categories": {
"continental": "Kontinentálna",
"regional": "Regionálna",
"fantasy": "Ostatné"
},
"map_component": {
"loading": "Načítavanie..."
},
"private_lobby": {
"title": "Pripojiť sa ku súkromnej miestnosti",
"enter_id": "Zadaj ID miestnosti",
"player": "Hráč",
"players": "Hráči",
"join_lobby": "Pripojiť sa k miestnosti",
"checking": "Kontrola miestnosti...",
"not_found": "Miestnosť nenájdená. Prosím, skontroluj ID, a skús znovu.",
"error": "Nastala chyba. Prosím, skús to znova.",
"joined_waiting": "Úspešne pripojené! Čakanie na začiatok hry…"
},
"public_lobby": {
"join": "Pripojiť sa do ďalšej hry",
"waiting": "čakajúci hráči",
"teams_Duos": "Dvojice (tímy dvoch)",
"teams_Trios": "Trojice (tímy troch)",
"teams_Quads": "Štvorice (tímy štyroch)",
"teams": "{num} tímy"
},
"username": {
"enter_username": "Zadaj svoje používateľské meno",
"not_string": "Používateľské meno musí byť reťazec.",
"too_short": "Používateľské meno musí byť aspoň {min} znakov dlhé.",
"too_long": "Používateľské meno nesmie prekročiť {max} znakov.",
"invalid_chars": "Používateľské meno smie obsahovať iba písmená, čísla, medzery, podčiarniky, a [hranaté zátvorky]."
},
"host_modal": {
"title": "Súkromná miestnosť",
"mode": "Režim",
"team_count": "Počet tímov",
"options_title": "Možnosti",
"bots": "Boti: ",
"bots_disabled": "Vypnuté",
"disable_nations": "Vypnúť krajiny",
"instant_build": "Okamžite postaviť",
"infinite_gold": "Nekonečné zlato",
"infinite_troops": "Nekonečné vojská",
"enables_title": "Povoliť nastavenia",
"player": "Hráč",
"players": "Hráči",
"waiting": "Čakanie na hráčov…",
"start": "Spustiť hru"
},
"team_colors": {
"red": "Červená",
"blue": "Modrá",
"teal": "Tyrkysová",
"purple": "Fialová",
"yellow": "Žltá",
"orange": "Oranžová",
"green": "Zelená",
"bot": "Bot"
},
"game_starting_modal": {
"title": "Hra sa začína…",
"desc": "Miestnosť sa pripravuje na spustenie. Prosím, čakaj."
},
"difficulty": {
"difficulty": "Náročnosť",
"Relaxed": "Uvoľnená",
"Balanced": "Vyvážená",
"Intense": "Búrlivá",
"Impossible": "Nemožná"
},
"game_mode": {
"ffa": "Každý za seba",
"teams": "Tímy"
},
"select_lang": {
"title": "Vybrať jazyk"
},
"unit_type": {
"city": "Mesto",
"defense_post": "Obranný bod",
"port": "Prístav",
"warship": "Bojová loď",
"missile_silo": "Raketové silo",
"sam_launcher": "Systém zem-vzduch",
"atom_bomb": "Atómová bomba",
"hydrogen_bomb": "Vodíková bomba",
"mirv": "MIRV",
"factory": "Továreň"
},
"user_setting": {
"title": "Užívateľské nastavenia",
"tab_basic": "Základné nastavenia",
"tab_keybinds": "Skratky",
"dark_mode_label": "Tmavý režim",
"dark_mode_desc": "Prepne vzhľad stránky medzi svetlou a tmavou témou",
"dark_mode_enabled": "Tmavý režim spustený",
"light_mode_enabled": "Svetlý režim spustený",
"emojis_label": "Emoji",
"emojis_visible": "Emoji sú viditeľné",
"emojis_hidden": "Emoji sú skryté",
"emojis_desc": "Prepne, či sú v hre ukázané emoji",
"alert_frame_label": "Rámik upozornenia",
"alert_frame_desc": "Prepnúť rámik upozornenia. Pri povolení sa zobrazí rám, ak ťa niekto zradí.",
"special_effects_label": "Špeciálne efekty",
"special_effects_desc": "Prepni špeciálne efekty. Vypni pre zlepšenie výkonu",
"special_effects_enabled": "Špeciálne efekty zapnuté",
"special_effects_disabled": "Špeciálne efekty vypnuté",
"anonymous_names_label": "Skryté mená",
"anonymous_names_desc": "Skry ozajstné mená hráčov na obrazovke náhodnými.",
"anonymous_names_enabled": "Anonýmne mená povolené",
"real_names_shown": "Ukázané ozajstné mená",
"left_click_label": "Ťukni ľavým tlačidlom na otvorenie ponuky",
"left_click_desc": "Keď je toto zapnuté, ľavé ťuknutie otvorí ponuku a útočí sa cez tlačidlo mečov. Keď je toto vypnuté, ľavé ťuknutie útočí priamo.",
"left_click_menu": "Ponuka ľavého ťuknutia",
"left_click_opens_menu": "Ľavé ťuknutie otvorí ponuku",
"right_click_opens_menu": "Pravé ťuknutie otvorí ponuku",
"attack_ratio_label": "⚔️ Pomer útoku",
"attack_ratio_desc": "Percento tvojich vojsk na odoslanie do útoku (1 až 100%)",
"troop_ratio_label": "🪖🛠️ Pomer vojsk a robotníkov",
"troop_ratio_desc": "Upraví pomer medzi vojskami (na boj) a robotníkmi (na tvorbu zlata) (1 až 100%)",
"territory_patterns_label": "🏳️ Územné vzorce",
"territory_patterns_desc": "Vyber, či zobrazovať dizajny vzorcov území v hre",
"easter_writing_speed_label": "Znásobovač rýchlosti písania",
"easter_writing_speed_desc": "Upraví, ako rýchlo predstieraš, že kóduješ (x1 až x100)",
"easter_bug_count_label": "Počet chýb",
"easter_bug_count_desc": "S koľkými chybami si v pohode (0 až 1000, pocitovo)",
"view_options": "Možnosti zobrazenia",
"toggle_view": "Prepnúť zobrazenie",
"toggle_view_desc": "Alternatívny pohľad (terén/krajiny)",
"attack_ratio_controls": "Nastavenia pomeru útoku",
"attack_ratio_up": "Zýšiť pomer útoku",
"attack_ratio_up_desc": "Zýšiť pomer útoku o 10%",
"attack_ratio_down": "Znížiť pomer útoku",
"attack_ratio_down_desc": "Znížiť pomer útoku o 10%",
"attack_keybinds": "Klávesové skratky útoku",
"boat_attack": "Útok loďou",
"boat_attack_desc": "Pošli útok loďou na pixel pod tvojou myškou.",
"ground_attack": "Pozemný útok",
"ground_attack_desc": "Pošli pozemný útok na pixel pod tvojou myškou.",
"zoom_controls": "Tlačidlá priblíženia",
"zoom_out": "Oddialiť",
"zoom_out_desc": "Oddialiť mapu",
"zoom_in": "Priblížiť",
"zoom_in_desc": "Priblížiť mapu",
"camera_movement": "Pohyb pohľadu",
"center_camera": "Zaostriť pohľad na stred",
"center_camera_desc": "Sústrediť pohľad na hráča",
"move_up": "Pohnúť pohľad hore",
"move_up_desc": "Pohnúť pohľad smerom hore",
"move_left": "Pohnúť pohľad vľavo",
"move_left_desc": "Pohnúť pohľad smerom vľavo",
"move_down": "Pohnúť pohľad dolu",
"move_down_desc": "Pohnúť pohľad smerom dolu",
"move_right": "Pohnúť pohľad vpravo",
"move_right_desc": "Pohnúť pohľad smerom vpravo",
"reset": "Obnoviť",
"unbind": "Odnastaviť",
"on": "Zapnúť",
"off": "Vypnúť",
"toggle_terrain": "Prepnúť povrch",
"terrain_enabled": "Viditeľnosť povrchu zapnutá",
"terrain_disabled": "Viditeľnosť povrchu vypnutá",
"exit_game_label": "Opustiť hru",
"exit_game_info": "Vrátiť sa do hlavnej ponuky"
},
"chat": {
"title": "Rýchly čet",
"to": "Poslané {user}: {msg}",
"from": "Od {user}: {msg}",
"category": "Kategória",
"phrase": "Fráza",
"player": "Hráč",
"send": "Poslať",
"search": "Hľadať hráča…",
"build": "Napíš svoju správu…",
"cat": {
"help": "Nápoveda",
"attack": "Útok",
"defend": "Obrana",
"greet": "Pozdravujem",
"misc": "Ostatné",
"warnings": "Upozornenia"
},
"help": {
"troops": "Prosím, pošli mi vojská!",
"gold": "Prosím, pošli mi zlato!",
"no_attack": "Prosím, neútoč na mňa!",
"sorry_attack": "Prepáč, nechcel som zaútočiť.",
"alliance": "Spojenectvo?",
"help_defend": "Pomôž mi brániť sa pred [P1]!",
"team_up": "Zaútočme na [P1]!"
},
"attack": {
"attack": "Zaútoč na [P1]!",
"mirv": "Vystrel MIRV na [P1]!",
"focus": "Sústreď streľbu na [P1]!",
"finish": "Dokončime [P1]!"
},
"defend": {
"defend": "Obraňuj [P1]!",
"dont_attack": "Neútoč na [P1]!",
"ally": "[P1] je môj spojenec!"
},
"greet": {
"hello": "Ahoj!",
"good_luck": "Veľa šťastia!",
"have_fun": "Zabav sa!",
"gg": "GG!",
"nice_to_meet": "Rád ťa spoznávam!",
"well_played": "Dobrá hra!",
"hi_again": "Vitaj späť!",
"bye": "Dovidenia!",
"thanks": "Ďakujem!",
"oops": "Ups, zlé tlačidlo!",
"trust_me": "Môžeš mi veriť. Prisahám!",
"trust_broken": "Veril som ti…"
},
"misc": {
"go": "Poďme na to!",
"strategy": "Dobrá stratégia!",
"fun": "Táto hra bola zábavná!",
"pr": "Kedy sa moje PR konečne zlúči…?"
},
"warnings": {
"strong": "[P1] je silný/á.",
"weak": "[P1] je slabý/a.",
"mirv_soon": "[P1] môže čoskoro vystreliť MIRV!",
"number1_warning": "Hráč č. 1 môže čoskoro vyhrať, ak sa nespojíme!",
"stalemate": "Uzatvorme mier. Toto je pat, obaja prehráme.",
"has_allies": "[P1] má veľa spojencov.",
"no_allies": "[P1] nemá spojencov.",
"betrayed": "[P1] zradil/a svojho spojenca!",
"getting_big": "[P1] rastie prirýchlo!",
"danger_base": "[P1] je nechránený/á!",
"saving_for_mirv": "[P1] šetrí na vystrelenie MIRV.",
"mirv_ready": "[P1] má dosť zlata na vystrelenie MIRV!"
}
},
"build_menu": {
"desc": {
"atom_bomb": "Malý výbuch",
"hydrogen_bomb": "Veľký výbuch",
"mirv": "Veľký výbuch, útočí len na cieleného hráča",
"missile_silo": "Používa sa na streľbu rakíet",
"sam_launcher": "Bráni proti vystreleným raketám",
"warship": "Ovláda obchodné lode, ničí lode a člny",
"port": "Posiela obchodné lode na tvorbu zlata",
"defense_post": "Zvyšuje obranu okolitých hraníc",
"city": "Zvyšuje maximálnu populáciu",
"factory": "Tvorí železnice a vlaky"
},
"not_enough_money": "Nedostatok zlata"
},
"win_modal": {
"died": "Zomrel/a si",
"your_team": "Tvoj tím vyhral!",
"other_team": "Vyhral tím {team}!",
"you_won": "Vyhral/a si!",
"other_won": "Vyhral hráč {player}!",
"exit": "Opustiť hru",
"keep": "Hrať ďalej",
"wishlist": "Pridaj do zoznamu želaní na Steame!"
},
"leaderboard": {
"title": "Rebríček",
"hide": "Skryť",
"rank": "Poradie",
"player": "Hráč",
"team": "Tím",
"owned": "Vlastnené",
"gold": "Zlato",
"troops": "Vojská"
},
"player_info_overlay": {
"type": "Typ",
"bot": "Bot",
"nation": "Národ",
"player": "Hráč",
"team": "Tím",
"d_troops": "Brániace vojsko",
"a_troops": "Útočiace vojsko",
"gold": "Zlato",
"ports": "Prístavy",
"cities": "Mestá",
"factories": "Továrne",
"missile_launchers": "Raketomety",
"sams": "Systémy zem-vzduch",
"warships": "Bojové lode",
"health": "Život",
"attitude": "Postoj",
"levels": "Levely"
},
"events_display": {
"retreating": "ustupuje",
"boat": "Čln",
"alliance_request_status": "{name} {status} tvoju žiadosť o spojenectvo",
"alliance_accepted": "prijaté",
"alliance_rejected": "odmietnuté",
"duration_second": "1 sekunda",
"betrayal_description": "Prerušil/a si spojenectvo s {name}, kvôli čomu si ZRADCA ({malusPercent} % obranné oslabenie po dobu {durationText})",
"duration_seconds_plural": "{seconds} sekúnd",
"betrayed_you": "{name} prerušil/a s tebou spojenectvo",
"about_to_expire": "Tvoje spojenectvo s {name} čoskoro vyprší!",
"alliance_expired": "Tvoje spojenectvo s {name} vypršala",
"attack_request": "{name} žiada o napadnutie {target}",
"sent_emoji": "Poslané {name}: {emoji}",
"renew_alliance": "Požiadať o obnovenie",
"request_alliance": "{name} žiada o spojenectvo!",
"focus": "Zaostriť",
"accept_alliance": "Prijať",
"reject_alliance": "Odmietnuť",
"alliance_renewed": "Tvoje spojenectvo s {name} bola obnovená",
"ignore": "Ignorovať"
},
"unit_info_modal": {
"structure_info": "Informácie o štruktúre",
"unit_type_unknown": "Neznáme",
"close": "Zavrieť",
"cooldown": "Obnovenie",
"type": "Typ",
"upgrade": "Vylepšenie",
"level": "Úroveň"
},
"relation": {
"hostile": "Nepriateľský",
"distrustful": "Nedôverčivý",
"neutral": "Neutrálny",
"friendly": "Priateľský",
"default": "Základný"
},
"control_panel": {
"pop": "Populácia",
"gold": "Zlato",
"troops": "Vojská",
"workers": "Robotníci",
"attack_ratio": "Pomer útoku"
},
"player_panel": {
"gold": "Zlato",
"troops": "Vojská",
"betrayals": "Počet zrád",
"traitor": "Zradca",
"alliance_time_remaining": "Spojenectvo vyprší o",
"embargo": "Prestal s tebou obchodovať",
"nuke": "Rakety vystrelené voči tebe",
"start_trade": "Začať obchodovať",
"stop_trade": "Skončiť obchodovať",
"yes": "Áno",
"no": "Nie",
"none": "Žiadne",
"alliances": "Spojenectvá"
},
"replay_panel": {
"replay_speed": "Rýchlosť prehratia",
"game_speed": "Rýchlosť hry",
"fastest_game_speed": "max"
},
"error_modal": {
"crashed": "Hra sa pokazila!",
"connection_error": "Chyba pripojenia!",
"paste_discord": "Prosím, prilep nasledujúci obsah v tvojom hlásení chyby v Discorde:",
"copy_clipboard": "Kopírovať do schránky",
"copied": "Skopírované!",
"failed_copy": "Nepodarilo sa skopírovať",
"desync_notice": "Tvoja hra je desynchronizovaná od ostatných hráčov. To, čo vidíš, sa môže líšiť od nich."
},
"heads_up_message": {
"choose_spawn": "Vyber začiatočnú polohu"
},
"territory_patterns": {
"title": "Vybrať územný vzorec",
"purchase": "Kúpiť",
"blocked": {
"login": "Musíš byť prihlásená/ý na prístup k tomuto vzorcu.",
"purchase": "Kúp si tento vzorec na jeho odomknutie."
},
"pattern": {
"default": "Základné",
"custom": "Vlastné",
"stripes_v": "Zvislé",
"stripes_h": "Vodorovné",
"horizontal_stripes": "Vodorovné (alt)",
"vertical_bars": "Zvislé (alt)",
"checkerboard": "Šachovnica",
"choco": "Čoko",
"diagonal": "Priečne",
"cross": "Krížové",
"mini_cross": "Mini krížové",
"sword": "Meč",
"sparse_dots": "Riedko bodkované",
"evan": "Evan",
"diagonal_stripe": "Šikmé pásiky",
"mountain_ridge": "Horský hrebeň",
"scattered_dots": "Rozptýlené bodky",
"circuit_board": "Doska plošných spojov",
"shells": "Mušle",
"-w-": ".w.",
"white_rabbit": "Biely zajac",
"goat": "Koza",
"cats": "Mačky",
"cursor": "Kurzor",
"hand": "Ruka",
"radiation": "Radiácia",
"openfront_qr": "QR kód OpenFront.io",
"openfront": "OpenFront",
"t_rex": "T-rex",
"embelem": "Odznak",
"grogu_head": "Hlava Grogu",
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Načítavam reklamu..."
},
"auth": {
"login_required": "Na prístup k tejto stránke je potrebné prihlásenie.",
"redirecting": "Práve si presmerovaná/ý...",
"not_authorized": "Nemáš oprávnenie na prístup k tejto stránke.",
"contact_admin": "Ak si myslíš, že túto správu vidíš omylom, prosím kontaktuj administrátora webstránky."
}
}
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Navodila",
"how_to_play": "Kako igrati",
"advertise": "Oglaševanje",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Politika zasebnosti",
"terms_of_service": "Pogoji storitve"
},
"news": {
"full_changelog": "Poglej celotni seznam sprememb",
@@ -448,6 +450,7 @@
"gold": "Zlato",
"ports": "Luka",
"cities": "Mesta",
"factories": "Tovarne",
"missile_launchers": "Raketni silosi",
"sams": "SAM",
"warships": "Bojna ladja",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Nalaganje oglasa..."
},
"auth": {
"login_required": "Za dostop do te spletne strani je potrebna prijava.",
"redirecting": "Preusmerjeni ste...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "Instruktioner",
"how_to_play": "Hur man Spelar",
"advertise": "Annonsera",
"wiki": "Wiki"
"wiki": "Wiki",
"privacy_policy": "Sekretesspolicy",
"terms_of_service": "Användarvillkor"
},
"news": {
"full_changelog": "Se fullständig ändringslogg",
@@ -448,6 +450,7 @@
"gold": "Guld",
"ports": "Hamnar",
"cities": "Städer",
"factories": "Fabriker",
"missile_launchers": "Missilavfyringsramper",
"sams": "Luftvärn",
"warships": "Krigsskepp",
@@ -574,6 +577,9 @@
"grogu": "Grogu"
}
},
"spawn_ad": {
"loading": "Laddar annons..."
},
"auth": {
"login_required": "Inloggning krävs för att komma åt den här webbplatsen.",
"redirecting": "Du blir omdirigerad...",
+13 -7
View File
@@ -21,7 +21,9 @@
"instructions": "Інструкції",
"how_to_play": "Як грати",
"advertise": "Рекламування",
"wiki": "Вікі"
"wiki": "Вікі",
"privacy_policy": "Політика конфіденційності",
"terms_of_service": "Умови користування"
},
"news": {
"full_changelog": "Перегляньте повний журнал змін",
@@ -53,7 +55,7 @@
"ui_attack_ratio": "Коефіцієнт атаки — Кількість військ, що беруть участь в атаці. Ви можете налаштувати коефіцієнт атаки за допомогою повзунка. Якщо наступальних військ більше ніж оборонних, то буде зменшено втрати під час атаки, а якщо менше — буде збільшено шкоду, що буде завдано вашим наступальним військам. Ефективність не збільшується після коефіцієнту 2:1.",
"ui_events": "Панель подій",
"ui_events_desc": "На панелі подій показуються останні події, запити та повідомлення швидкого чату. Деякими прикладами є:",
"ui_events_alliance": "Союз — Запити на укладення союзів можуть бути прийняті або відхилені. Союзники можуть ділитися ресурсами та військами, але не можуть атакувати один одного. Клацання на кнопку «Оглянути» переміщає камеру на гравця, який надіслав запит.",
"ui_events_alliance": "Союз — Запрошення до союзів можуть бути прийняті або відхилені. Союзники можуть ділитися ресурсами та військами, але не можуть атакувати один одного. Клацання на кнопку «Оглянути» переміщає камеру на гравця, який надіслав запит.",
"ui_events_attack": "Атаки — Відображення вхідних та вихідних атак. Натисніть на повідомлення, щоб центрувати камеру на наступ, ракету або човен (транспортний корабель). Ви можете відкликати війська, натиснувши на червону кнопку «X». Це коштуватиме життя 25% ваших військ, що атакують. Якщо відкликати човен, то він повернеться до свого початкового розташування та атакуватиме, якщо територію було захоплено. Ракети неможливо відкликати після запуску.",
"ui_events_quickchat": "Швидкий чат — Тут ви можете переглядати надіслані та отримані повідомлення. Надішліть повідомлення гравцю клацанням на значок швидкого чату в його меню інформації.",
"ui_options": "Налаштування",
@@ -72,14 +74,14 @@
"radial_boat": "Відправити човен (транспортний корабель) атакувати вибране розташування. Доступно лише якщо ви маєте доступ до води.",
"radial_close": "Закрити меню.",
"info_title": "Меню інформації",
"info_enemy_desc": "Містить таку інформацію про вибраного гравця, як його ім'я, кількість золото, війська, стан торгувілі з вами, кількість запущених на вас ракет і мітку зрадника. Припинення торгівля означає, що ви не отримуватиме золото від гравця, і він не надсилатиме вам золото торговельними кораблями. Свідомо (якщо гравець натиснув «Припинити торгівлю», що триває, поки ви обидва не натиснете «Розпочати торгівлю») або автоматично (якщо ви зрадили союз, що триває, поки ви знову не станете союзниками або через 5 хвилин). Поле «Зрадник» показує стан «Так» протягом 30 секунд після того, як гравець зрадив й атакував гравця, який перебував у союзні з ним. Значки нижче позначають такі взаємодії:",
"info_enemy_desc": "Містить таку інформацію про вибраного гравця, як його ім'я, кількість золота, військ, стан торгувілі з вами, кількість запущених на вас ракет і мітку зрадника. Припинення торгівля означає, що ви не отримуватиме золото від гравця, а він не надсилатиме вам золото торговельними кораблями. Свідомо (якщо гравець натиснув «Припинити торгівлю», що триває, поки ви обидва не натиснете «Розпочати торгівлю») або автоматично (якщо ви зрадили союз, що триває, поки ви знову не станете союзниками або через 5 хвилин). Поле «Зрадник» показує стан «Так» протягом 30 секунд після того, як гравець зрадив й атакував гравця, який перебував у союзні з ним. Значки нижче позначають такі взаємодії:",
"info_chat": "Надсилає швидке повідомлення гравцю. Виберіть категорію, фразу та, якщо фраза містить слово «[P1]», оберіть ім'я гравця, котрим бажаєте заміни його. Тицьніть «Надіслати».",
"info_target": "Розмістити мітку цілі на гравці, позначивши його для всіх союзників. Використовується для координації атак.",
"info_alliance": "Надіслати гравцю запит на союз. Союзники можуть ділитися ресурсами та військами, але не можуть атакувати один одного.",
"info_emoji": "Надіслати емоджі гравцю.",
"info_trade": "Використайте «Припинити торгівлю», щоб припинити давати гравцеві золото та отримувати золото від нього через торгові кораблі. Якщо ви обидва натиснете «Розпочати торгівлю», вона розпочнеться знову.",
"info_ally_panel": "Панель інформації союзника",
"info_ally_desc": "Коли ви укладете альянс із гравцем, стануть доступними наступні значки:",
"info_ally_desc": "Коли ви укладете союз із гравцем, буде розблоковано наступні значки:",
"ally_betray": "Зрадьте свого союзника, розірвавши союз, припинивши торгівлю та послабивши свою оборону. Торгівля між вами призупиняється на 5 хвилин (або до відновлення союзу), і інші також можуть припинити торгівлю з вами. Якщо інший гравець сам не був зрадником, ви отримаєте мітку зрадника на 30 секунд. У цей час над вашим ім'ям з'явиться особливий значок, а ваша оборона знизиться на 50%. Боти рідше укладатимуть із вами союзи, а гравці двічі подумають, перш ніж мати з вами справу.",
"ally_donate": "Пожертвувати частину своїх військ союзнику. Використовується, коли в нього мало військ і його атакують, або коли йому необхідна додаткова сила для знищення ворога.",
"ally_donate_gold": "Пожертвувати частину свого золота союзнику. Використовуйте, коли в нього мало золота, яке він потребує для будівель, або коли член команди заощаджує на РГЧ ІН.",
@@ -95,7 +97,7 @@
"build_defense": "Пункт оборони",
"build_defense_desc": "Підсилює оборону навколо найближчих кордонів, що показано візерунком у клітинку. Атаки ворогів уповільнені та несуть більше жертв.",
"build_port": "Порт",
"build_port_desc": "Можуть бути збудовані лише біля води. Дозволяє будувати військові кораблі. Автоматично відправляє торгові кораблі між портами вашої та інших країн (крім випадків, коли торгівлю припинено), даючи золото обом сторонам. Торгівля автоматично припиняється коли ви атакуєте гравця або він атакує вас. Вона відновлюється через 5 хвилин або якщо ви укладаєте альянс. Ви можете вручну керувати торгівлею кнопками «Припинити торгівлю» та «Розпочати торгівлю».",
"build_port_desc": "Можуть бути збудовані лише біля води. Дозволяє будувати військові кораблі. Автоматично відправляє торгові кораблі між портами вашої та інших країн (крім випадків, коли торгівлю припинено), даючи золото обом сторонам. Торгівля автоматично припиняється, коли ви атакуєте гравця або він атакує вас. Її буде відновлено через 5 хвилин або при укладанні союзу. Можна керувати торгівлею вручну за допомогою кнопок «Припинити торгівлю» та «Розпочати торгівлю».",
"build_warship": "Військовий корабель",
"build_warship_desc": "Розвідує територію, захоплюючи ворожі торгові кораблі й знищуючи їхні човни (транспортні кораблі) та військові кораблі. З'являється з найближчого порту та розвідує ділянку, вибрану клацанням при створенні. Військовими кораблями можна керувати кнопкою атаки (див. дія «Атака» в розділі «Гарячі клавіші»): спочатку клацніть на корабель, а потім — на ділянку, до якої бажаєте його перемістити.",
"build_silo": "Ракетна шахта",
@@ -261,7 +263,7 @@
"dark_mode_desc": "Перемикання зовнішнього вигляду сайту між світлою та темною темою",
"dark_mode_enabled": "Увімкнено темний режим",
"light_mode_enabled": "Увімкнено світлий режим",
"emojis_label": "Емодзі",
"emojis_label": "Емоджі",
"emojis_visible": "Емоджі показані",
"emojis_hidden": "Емоджі приховані",
"emojis_desc": "Увімкнення/вимкнення видимості емоджі під час гри",
@@ -448,6 +450,7 @@
"gold": "Золото",
"ports": "Порти",
"cities": "Міста",
"factories": "Фабрики",
"missile_launchers": "Ракетні установки",
"sams": "ЗРК",
"warships": "Військові кораблі",
@@ -458,7 +461,7 @@
"events_display": {
"retreating": "відступає",
"boat": "Човен",
"alliance_request_status": "{name} {status} ваш запит альянсу",
"alliance_request_status": "{name} {status} запрошення до союзу",
"alliance_accepted": "прийняв",
"alliance_rejected": "відхилив",
"duration_second": "1 сек",
@@ -574,6 +577,9 @@
"grogu": "Ґроґу"
}
},
"spawn_ad": {
"loading": "Завантаження реклами..."
},
"auth": {
"login_required": "Для доступу до цього сайту потрібно ввійти в систему.",
"redirecting": "Вас буде переспрямовано...",
+7 -1
View File
@@ -21,7 +21,9 @@
"instructions": "操作说明",
"how_to_play": "如何游玩",
"advertise": "广告",
"wiki": "游戏百科"
"wiki": "游戏百科",
"privacy_policy": "隐私政策",
"terms_of_service": "服务条款"
},
"news": {
"full_changelog": "查看完整的更新日志",
@@ -448,6 +450,7 @@
"gold": "黄金",
"ports": "港口",
"cities": "城市",
"factories": "工厂",
"missile_launchers": "导弹发射井",
"sams": "防空塔",
"warships": "军舰",
@@ -574,6 +577,9 @@
"grogu": "格罗古"
}
},
"spawn_ad": {
"loading": "正在加载广告……"
},
"auth": {
"login_required": "需要登录才能访问此网站。",
"redirecting": "正在将您重新定向……",
+106 -4
View File
@@ -57,7 +57,7 @@
"coordinates": [637, 567],
"flag": "br",
"name": "Brazil",
"strength": 1
"strength": 2
},
{
"coordinates": [1280, 975],
@@ -201,7 +201,7 @@
"coordinates": [1178, 351],
"flag": "sa",
"name": "Saudi Arabia",
"strength": 1
"strength": 1.5
},
{
"coordinates": [1679, 657],
@@ -213,7 +213,7 @@
"coordinates": [1890, 775],
"flag": "nz",
"name": "New Zealand",
"strength": 0.5
"strength": 1
},
{
"coordinates": [918, 342],
@@ -261,7 +261,7 @@
"coordinates": [1075, 707],
"flag": "za",
"name": "South Africa",
"strength": 1
"strength": 1.5
},
{
"coordinates": [1194, 627],
@@ -274,6 +274,108 @@
"flag": "td",
"name": "Chad",
"strength": 1
},
{
"coordinates": [1030, 665],
"flag": "na",
"name": "Namibia",
"strength": 0.5
},
{
"coordinates": [1632, 465],
"flag": "ph",
"name": "Philippines",
"strength": 1
},
{
"coordinates": [1537, 426],
"flag": "th",
"name": "Thailand",
"strength": 1
},
{
"coordinates": [1610, 364],
"flag": "tw",
"name": "Taiwan",
"strength": 0.5
},
{
"coordinates": [1710, 290],
"flag": "jp",
"name": "Japan",
"strength": 1
},
{
"coordinates": [1869, 119],
"flag": "ru",
"name": "Siberia",
"strength": 1
},
{
"coordinates": [74, 117],
"flag": "polar_bears",
"name": "Polar Bears",
"strength": 2
},
{
"coordinates": [419, 975],
"flag": "aq",
"name": "West Antarctica",
"strength": 2
},
{
"coordinates": [542, 603],
"flag": "pe",
"name": "Peru",
"strength": 1
},
{
"coordinates": [1075, 615],
"flag": "zm",
"name": "Zambia",
"strength": 1
},
{
"coordinates": [1099, 165],
"flag": "lv",
"name": "Latvia",
"strength": 0.5
},
{
"coordinates": [1427, 336],
"flag": "bt",
"name": "Bhutan",
"strength": 0.5
},
{
"coordinates": [1511, 524],
"flag": "id",
"name": "Indonesia",
"strength": 1.5
},
{
"coordinates": [1809, 977],
"flag": "aq",
"name": "East Antarctica",
"strength": 2
},
{
"coordinates": [1255, 382],
"flag": "om",
"name": "Oman",
"strength": 0.75
},
{
"coordinates": [853, 373],
"flag": "ma",
"name": "Morocco",
"strength": 1
},
{
"coordinates": [656, 678],
"flag": "uy",
"name": "Uruguay",
"strength": 1
}
]
}
+42 -42
View File
@@ -1,30 +1,3 @@
import { translateText } from "../client/Utils";
import { EventBus } from "../core/EventBus";
import {
ClientID,
GameID,
GameRecord,
GameStartInfo,
PlayerRecord,
ServerMessage,
} from "../core/Schemas";
import { createGameRecord } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
import { PlayerActions, UnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
ErrorUpdate,
GameUpdateType,
GameUpdateViewData,
HashUpdate,
WinUpdate,
} from "../core/game/GameUpdates";
import { GameView, PlayerView } from "../core/game/GameView";
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
import { UserSettings } from "../core/game/UserSettings";
import { WorkerClient } from "../core/worker/WorkerClient";
import {
AutoUpgradeEvent,
DoBoatAttackEvent,
@@ -33,9 +6,24 @@ import {
MouseMoveEvent,
MouseUpEvent,
} from "./InputHandler";
import { endGame, startGame, startTime } from "./LocalPersistantStats";
import { getPersistentID } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import {
ClientID,
GameID,
GameRecord,
GameStartInfo,
PlayerRecord,
ServerMessage,
} from "../core/Schemas";
import {
ErrorUpdate,
GameUpdateType,
GameUpdateViewData,
HashUpdate,
WinUpdate,
} from "../core/game/GameUpdates";
import { GameRenderer, createRenderer } from "./graphics/GameRenderer";
import { GameView, PlayerView } from "../core/game/GameView";
import { PlayerActions, UnitType } from "../core/game/Game";
import {
SendAttackIntentEvent,
SendBoatAttackIntentEvent,
@@ -44,8 +32,20 @@ import {
SendUpgradeStructureIntentEvent,
Transport,
} from "./Transport";
import { TerrainMapData, loadTerrainMap } from "../core/game/TerrainMapLoader";
import { endGame, startGame, startTime } from "./LocalPersistantStats";
import { EventBus } from "../core/EventBus";
import { GameMapLoader } from "../core/game/GameMapLoader";
import { ServerConfig } from "../core/configuration/Config";
import { TileRef } from "../core/game/GameMap";
import { UserSettings } from "../core/game/UserSettings";
import { WorkerClient } from "../core/worker/WorkerClient";
import { createCanvas } from "./Utils";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { createGameRecord } from "../core/Util";
import { getConfig } from "../core/configuration/ConfigLoader";
import { getPersistentID } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { translateText } from "../client/Utils";
export type LobbyConfig = {
serverConfig: ServerConfig;
@@ -193,16 +193,16 @@ export class ClientGameRunner {
private lastMousePosition: { x: number; y: number } | null = null;
private lastMessageTime = 0;
private connectionCheckInterval: NodeJS.Timeout | null = null;
private connectionCheckInterval: ReturnType<typeof setTimeout> | null = null;
constructor(
private lobby: LobbyConfig,
private eventBus: EventBus,
private renderer: GameRenderer,
private input: InputHandler,
private transport: Transport,
private worker: WorkerClient,
private gameView: GameView,
private readonly lobby: LobbyConfig,
private readonly eventBus: EventBus,
private readonly renderer: GameRenderer,
private readonly input: InputHandler,
private readonly transport: Transport,
private readonly worker: WorkerClient,
private readonly gameView: GameView,
) {
this.lastMessageTime = Date.now();
}
@@ -288,7 +288,7 @@ export class ClientGameRunner {
this.saveGame(gu.updates[GameUpdateType.Win][0]);
}
});
const worker = this.worker;
const { worker } = this;
const keepWorkerAlive = () => {
if (this.isActive) {
worker.sendHeartbeat();
@@ -477,7 +477,7 @@ export class ClientGameRunner {
upgradeUnits.push({
unitId: bu.canUpgrade,
unitType: bu.type,
distance: distance,
distance,
});
}
}
@@ -563,7 +563,7 @@ export class ClientGameRunner {
(bu) => bu.type === UnitType.TransportShip,
);
if (bu === undefined) {
console.warn(`no transport ship buildable units`);
console.warn("no transport ship buildable units");
return false;
}
return (
+3 -3
View File
@@ -1,10 +1,10 @@
import { z } from "zod";
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
import {
StripeCreateCheckoutSessionResponseSchema,
UserMeResponse,
} from "../core/ApiSchemas";
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
import { getApiBase, getAuthHeader } from "./jwt";
import { z } from "zod";
export async function patterns(
userMe: UserMeResponse | null,
@@ -44,7 +44,7 @@ export async function handlePurchase(priceId: string) {
"authorization": getAuthHeader(),
},
body: JSON.stringify({
priceId: priceId,
priceId,
successUrl: `${window.location.origin}#purchase-completed=true`,
cancelUrl: `${window.location.origin}#purchase-completed=false`,
}),
+2 -2
View File
@@ -4,7 +4,7 @@ import { UserSettings } from "../core/game/UserSettings";
@customElement("dark-mode-button")
export class DarkModeButton extends LitElement {
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
@state() private darkMode: boolean = this.userSettings.darkMode();
createRenderRoot() {
@@ -21,7 +21,7 @@ export class DarkModeButton extends LitElement {
window.removeEventListener("dark-mode-changed", this.handleDarkModeChanged);
}
private handleDarkModeChanged = (e: Event) => {
private readonly handleDarkModeChanged = (e: Event) => {
const event = e as CustomEvent<{ darkMode: boolean }>;
this.darkMode = event.detail.darkMode;
};
+22 -3
View File
@@ -1,6 +1,8 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { FlagSchema } from "../core/Schemas";
import { renderPlayerFlag } from "../core/CustomFlag";
const flagKey = "flag";
@customElement("flag-input")
@@ -41,10 +43,24 @@ export class FlagInput extends LitElement {
);
}
private readonly updateFlag = (ev: Event) => {
const e = ev as CustomEvent<{ flag: string }>;
if (!FlagSchema.safeParse(e.detail.flag).success) return;
if (this.flag !== e.detail.flag) {
this.flag = e.detail.flag;
}
};
connectedCallback() {
super.connectedCallback();
this.flag = this.getStoredFlag();
this.dispatchFlagEvent();
window.addEventListener("flag-change", this.updateFlag as EventListener);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("flag-change", this.updateFlag as EventListener);
}
createRenderRoot() {
@@ -56,12 +72,15 @@ export class FlagInput extends LitElement {
<div class="flex relative">
<button
id="flag-input_"
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30
dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
title="Pick a flag!"
>
<span
id="flag-preview"
style="display:inline-block;width:48px;height:64px;vertical-align:middle;background:#333;border-radius:6px;overflow:hidden;"
style="display:inline-block; width:48px; height:64px;
vertical-align:middle; background:#333; border-radius:6px;
overflow:hidden;"
></span>
</button>
</div>
@@ -80,7 +99,7 @@ export class FlagInput extends LitElement {
renderPlayerFlag(this.flag, preview);
} else {
const img = document.createElement("img");
img.src = this.flag ? `/flags/${this.flag}.svg` : `/flags/xx.svg`;
img.src = this.flag ? `/flags/${this.flag}.svg` : "/flags/xx.svg";
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "contain";
+20 -8
View File
@@ -4,12 +4,13 @@ import Countries from "./data/countries.json";
@customElement("flag-input-modal")
export class FlagInputModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private search = "";
@state() private isModalOpen = false;
createRenderRoot() {
return this;
@@ -19,7 +20,11 @@ export class FlagInputModal extends LitElement {
return html`
<o-modal title="Flag Selector Modal" alwaysMaximized>
<input
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
class="h-[2rem] border-none border border-gray-300
rounded-xl shadow-sm text-2xl text-center focus:outline-none
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black
dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@@ -28,10 +33,8 @@ export class FlagInputModal extends LitElement {
<div
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden h-[90%]"
>
${Countries.filter(
(country) =>
country.name.toLowerCase().includes(this.search.toLowerCase()) ||
country.code.toLowerCase().includes(this.search.toLowerCase()),
${this.isModalOpen ? Countries.filter(
(country) => !country.restricted && this.includedInSearch(country),
).map(
(country) => html`
<button
@@ -58,12 +61,19 @@ export class FlagInputModal extends LitElement {
<span class="country-name">${country.name}</span>
</button>
`,
)}
) : html``}
</div>
</o-modal>
`;
}
private includedInSearch(country: { name: string; code: string }): boolean {
return (
country.name.toLowerCase().includes(this.search.toLowerCase()) ||
country.code.toLowerCase().includes(this.search.toLowerCase())
);
}
private handleSearch(event: Event) {
this.search = (event.target as HTMLInputElement).value;
}
@@ -80,9 +90,11 @@ export class FlagInputModal extends LitElement {
}
public open() {
this.isModalOpen = true;
this.modalEl?.open();
}
public close() {
this.isModalOpen = false;
this.modalEl?.close();
}
@@ -96,7 +108,7 @@ export class FlagInputModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
+4 -4
View File
@@ -1,12 +1,12 @@
import "./components/Difficulties";
import "./components/Maps";
import { LitElement, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import { getAltKey, getModifierKey, translateText } from "../client/Utils";
import "./components/Difficulties";
import "./components/Maps";
@customElement("help-modal")
export class HelpModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@@ -25,7 +25,7 @@ export class HelpModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
+42 -24
View File
@@ -1,8 +1,14 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
/* eslint-disable max-lines */
import "./components/Difficulties";
import "./components/Maps";
import "./components/baseComponents/Modal";
import {
ClientInfo,
GameConfig,
GameInfo,
GameInfoSchema,
TeamCountConfig,
} from "../core/Schemas";
import {
Difficulty,
Duos,
@@ -13,25 +19,20 @@ import {
UnitType,
mapCategories,
} from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import {
ClientInfo,
GameConfig,
GameInfo,
GameInfoSchema,
TeamCountConfig,
} from "../core/Schemas";
import { generateID } from "../core/Util";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import { JoinLobbyEvent } from "./Main";
import { UserSettings } from "../core/game/UserSettings";
import { generateID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import randomMap from "../../resources/images/RandomMap.webp";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import { translateText } from "../client/Utils";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@@ -54,10 +55,10 @@ export class HostLobbyModal extends LitElement {
@state() private lobbyCreatorClientID = "";
@state() private lobbyIdVisible = true;
private playersInterval: NodeJS.Timeout | null = null;
private playersInterval: ReturnType<typeof setTimeout> | null = null;
// Add a new timer for debouncing bot changes
private botsUpdateTimer: number | null = null;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
connectedCallback() {
super.connectedCallback();
@@ -69,7 +70,7 @@ export class HostLobbyModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
@@ -100,7 +101,13 @@ export class HostLobbyModal extends LitElement {
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 105c-101.8 0-188.4 62.7-224 151 35.6 88.3 122.2 151 224 151s188.4-62.7 224-151c-35.6-88.3-122.2-151-224-151zm0 251.7c-56 0-101.7-45.7-101.7-101.7S200 153.3 256 153.3 357.7 199 357.7 255 312 356.7 256 356.7zm0-161.1c-33 0-59.4 26.4-59.4 59.4s26.4 59.4 59.4 59.4 59.4-26.4 59.4-59.4-26.4-59.4-59.4-59.4z"
d="M256 105c-101.8 0-188.4 62.7-224 151 35.6 88.3 122.2
151 224 151s188.4-62.7
224-151c-35.6-88.3-122.2-151-224-151zm0 251.7c-56
0-101.7-45.7-101.7-101.7S200 153.3 256 153.3 357.7 199
357.7 255 312 356.7 256 356.7zm0-161.1c-33 0-59.4
26.4-59.4 59.4s26.4 59.4 59.4 59.4 59.4-26.4
59.4-59.4-26.4-59.4-59.4-59.4z"
></path>
</svg>`
: html`<svg
@@ -155,7 +162,16 @@ export class HostLobbyModal extends LitElement {
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5 32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
d="M296 48H176.5C154.4 48 136 65.4 136
87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1
18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4
39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296
48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4
8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1
3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5
32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4
0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5
8.5-7.5H264v128h128v167.5z"
></path>
</svg>
`
@@ -678,7 +694,9 @@ export class HostLobbyModal extends LitElement {
await this.putGameConfig();
console.log(
`Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
`Starting private game with map: ${
GameMapType[this.selectedMap as keyof typeof GameMapType]} ${
this.useRandomMap ? " (Randomly selected)" : ""}`,
);
this.close();
const config = await getServerConfigFromClient();
+9 -9
View File
@@ -1,8 +1,8 @@
import { EventBus, GameEvent } from "../core/EventBus";
import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
import { UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { UserSettings } from "../core/game/UserSettings";
import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
export class MouseUpEvent implements GameEvent {
constructor(
@@ -70,7 +70,7 @@ export class AlternateViewEvent implements GameEvent {
export class CloseViewEvent implements GameEvent {}
export class RefreshGraphicsEvent implements GameEvent {}
export class RedrawGraphicsEvent implements GameEvent {}
export class TogglePerformanceOverlayEvent implements GameEvent {}
@@ -121,7 +121,7 @@ export class InputHandler {
private lastPointerDownX = 0;
private lastPointerDownY = 0;
private pointers: Map<number, PointerEvent> = new Map();
private readonly pointers: Map<number, PointerEvent> = new Map();
private lastPinchDistance = 0;
@@ -129,18 +129,18 @@ export class InputHandler {
private alternateView = false;
private moveInterval: NodeJS.Timeout | null = null;
private activeKeys = new Set<string>();
private moveInterval: ReturnType<typeof setTimeout> | null = null;
private readonly activeKeys = new Set<string>();
private keybinds: Record<string, string> = {};
private readonly PAN_SPEED = 5;
private readonly ZOOM_SPEED = 10;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
constructor(
private canvas: HTMLCanvasElement,
private eventBus: EventBus,
private readonly canvas: HTMLCanvasElement,
private readonly eventBus: EventBus,
) {}
initialize() {
@@ -302,7 +302,7 @@ export class InputHandler {
if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey) {
e.preventDefault();
this.eventBus.emit(new RefreshGraphicsEvent());
this.eventBus.emit(new RedrawGraphicsEvent());
}
if (e.code === this.keybinds.boatAttack) {
+18 -12
View File
@@ -1,29 +1,29 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import { GameInfo, GameInfoSchema } from "../core/Schemas";
import { generateID } from "../core/Util";
import { LitElement, html } from "lit";
import {
WorkerApiArchivedGameLobbySchema,
WorkerApiGameIdExistsSchema,
} from "../core/WorkerSchemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { customElement, query, state } from "lit/decorators.js";
import { JoinLobbyEvent } from "./Main";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import { generateID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { translateText } from "../client/Utils";
@customElement("join-private-lobby-modal")
export class JoinPrivateLobbyModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
@query("#lobbyIdInput") private readonly lobbyIdInput!: HTMLInputElement;
@state() private message = "";
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval: NodeJS.Timeout | null = null;
private playersInterval: ReturnType<typeof setTimeout> | null = null;
connectedCallback() {
super.connectedCallback();
@@ -35,7 +35,7 @@ export class JoinPrivateLobbyModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
@@ -67,7 +67,13 @@ export class JoinPrivateLobbyModal extends LitElement {
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"
d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5
28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5
C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16
5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6
C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L
21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L
25 28 L 15 28 Z"
></path>
</svg>
</button>
+24 -3
View File
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import "./LanguageModal";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./LanguageModal";
import ar from "../../resources/lang/ar.json";
import bg from "../../resources/lang/bg.json";
@@ -24,6 +26,7 @@ import pl from "../../resources/lang/pl.json";
import pt_BR from "../../resources/lang/pt-BR.json";
import ru from "../../resources/lang/ru.json";
import sh from "../../resources/lang/sh.json";
import sk from "../../resources/lang/sk.json";
import sl from "../../resources/lang/sl.json";
import sv_SE from "../../resources/lang/sv-SE.json";
import tp from "../../resources/lang/tp.json";
@@ -36,13 +39,15 @@ export class LangSelector extends LitElement {
@state() public translations: Record<string, string> | undefined;
@state() public defaultTranslations: Record<string, string> | undefined;
@state() public currentLang = "en";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@state() private languageList: any[] = [];
@state() private showModal = false;
@state() private debugMode = false;
private debugKeyPressed = false;
private languageMap: Record<string, any> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly languageMap: Record<string, any> = {
ar,
bg,
bn,
@@ -71,6 +76,7 @@ export class LangSelector extends LitElement {
ko,
gl,
sl,
sk,
};
createRenderRoot() {
@@ -123,6 +129,7 @@ export class LangSelector extends LitElement {
private loadLanguage(lang: string): Record<string, string> {
const language = this.languageMap[lang] ?? {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const flat = flattenTranslations(language);
return flat;
}
@@ -130,6 +137,7 @@ export class LangSelector extends LitElement {
private async loadLanguageList() {
try {
const data = this.languageMap;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let list: any[] = [];
const browserLang = new Intl.Locale(navigator.language).language;
@@ -146,6 +154,7 @@ export class LangSelector extends LitElement {
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let debugLang: any = null;
if (this.debugKeyPressed) {
debugLang = {
@@ -177,10 +186,12 @@ export class LangSelector extends LitElement {
list.sort((a, b) => a.en.localeCompare(b.en));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const finalList: any[] = [];
if (currentLangEntry) finalList.push(currentLangEntry);
if (englishEntry) finalList.push(englishEntry);
if (browserLangEntry) finalList.push(browserLangEntry);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
finalList.push(...list);
if (debugLang) finalList.push(debugLang);
@@ -236,7 +247,9 @@ export class LangSelector extends LitElement {
components.forEach((tag) => {
document.querySelectorAll(tag).forEach((el) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (typeof (el as any).requestUpdate === "function") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(el as any).requestUpdate();
}
});
@@ -292,7 +305,12 @@ export class LangSelector extends LitElement {
<button
id="lang-selector"
@click=${this.openModal}
class="text-center appearance-none w-full bg-blue-100 dark:bg-gray-700 hover:bg-blue-200 dark:hover:bg-gray-600 text-blue-900 dark:text-gray-100 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300 flex items-center gap-2 justify-center"
class="text-center appearance-none w-full bg-blue-100
dark:bg-gray-700 hover:bg-blue-200 dark:hover:bg-gray-600
text-blue-900 dark:text-gray-100 p-3 sm:p-4 lg:p-5 font-medium
text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer
transition-colors duration-300 flex items-center gap-2
justify-center"
>
<img
id="lang-flag"
@@ -309,6 +327,7 @@ export class LangSelector extends LitElement {
.languageList=${this.languageList}
.currentLang=${this.currentLang}
@language-selected=${(e: CustomEvent) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.changeLanguage(e.detail.lang)}
@close-modal=${() => (this.showModal = false)}
></language-modal>
@@ -317,6 +336,7 @@ export class LangSelector extends LitElement {
}
function flattenTranslations(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj: Record<string, any>,
parentKey = "",
result: Record<string, string> = {},
@@ -328,6 +348,7 @@ function flattenTranslations(
if (typeof value === "string") {
result[fullKey] = value;
} else if (value && typeof value === "object" && !Array.isArray(value)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
flattenTranslations(value, fullKey, result);
} else {
console.warn("Unknown type", typeof value, value);
+13 -6
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { translateText } from "../client/Utils";
@@ -5,6 +6,7 @@ import { translateText } from "../client/Utils";
@customElement("language-modal")
export class LanguageModal extends LitElement {
@property({ type: Boolean }) visible = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@property({ type: Array }) languageList: any[] = [];
@property({ type: String }) currentLang = "en";
@@ -12,7 +14,7 @@ export class LanguageModal extends LitElement {
return this; // Use Light DOM for TailwindCSS classes
}
private close = () => {
private readonly close = () => {
this.dispatchEvent(
new CustomEvent("close-modal", {
bubbles: true,
@@ -42,14 +44,14 @@ export class LanguageModal extends LitElement {
document.body.style.overflow = "auto";
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
}
};
private selectLanguage = (lang: string) => {
private readonly selectLanguage = (lang: string) => {
this.dispatchEvent(
new CustomEvent("language-selected", {
detail: { lang },
@@ -93,19 +95,24 @@ export class LanguageModal extends LitElement {
if (isDebug) {
buttonClasses +=
" animate-pulse font-bold text-white border-2 border-dashed border-cyan-400 shadow-lg shadow-cyan-400/25 bg-gradient-to-r from-red-600 via-yellow-600 via-green-600 via-blue-600 to-purple-600";
" animate-pulse font-bold text-white border-2 border-dashed border-cyan-400 shadow-lg" +
" shadow-cyan-400/25 bg-gradient-to-r from-red-600 via-yellow-600 via-green-600 via-blue-600" +
" to-purple-600";
} else if (isActive) {
buttonClasses +=
" bg-gray-400 dark:bg-gray-500 border-gray-300 dark:border-gray-400 text-black dark:text-white";
} else {
buttonClasses +=
" bg-gray-600 dark:bg-gray-700 border-gray-500 dark:border-gray-600 text-white dark:text-gray-100 hover:bg-gray-500 dark:hover:bg-gray-600";
" bg-gray-600 dark:bg-gray-700 border-gray-500 dark:border-gray-600 text-white dark:text-gray-100" +
" hover:bg-gray-500 dark:hover:bg-gray-600";
}
return html`
<button
class="${buttonClasses}"
@click=${() => this.selectLanguage(lang.code)}
@click=${() =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.selectLanguage(lang.code)}
>
<img
src="/flags/${lang.svg}.svg"
+1 -1
View File
@@ -1,4 +1,3 @@
import { z } from "zod";
import {
GameConfig,
GameConfigSchema,
@@ -8,6 +7,7 @@ import {
ID,
} from "../core/Schemas";
import { replacer } from "../core/Util";
import { z } from "zod";
const LocalStatsDataSchema = z.record(
ID,
+14 -11
View File
@@ -1,5 +1,3 @@
import { z } from "zod";
import { EventBus } from "../core/EventBus";
import {
AllPlayersStats,
ClientMessage,
@@ -12,16 +10,18 @@ import {
Turn,
} from "../core/Schemas";
import { createGameRecord, decompressGameRecord, replacer } from "../core/Util";
import { EventBus } from "../core/EventBus";
import { LobbyConfig } from "./ClientGameRunner";
import { ReplaySpeedChangeEvent } from "./InputHandler";
import { getPersistentID } from "./Main";
import { defaultReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
import { getPersistentID } from "./Main";
import { z } from "zod";
export class LocalServer {
// All turns from the game record on replay.
private replayTurns: Turn[] = [];
private turns: Turn[] = [];
private readonly turns: Turn[] = [];
private intents: Intent[] = [];
private startedAt: number;
@@ -35,14 +35,14 @@ export class LocalServer {
private turnsExecuted = 0;
private turnStartTime = 0;
private turnCheckInterval: NodeJS.Timeout;
private turnCheckInterval: ReturnType<typeof setTimeout>;
constructor(
private lobbyConfig: LobbyConfig,
private clientConnect: () => void,
private clientMessage: (message: ServerMessage) => void,
private isReplay: boolean,
private eventBus: EventBus,
private readonly lobbyConfig: LobbyConfig,
private readonly clientConnect: () => void,
private readonly clientMessage: (message: ServerMessage) => void,
private readonly isReplay: boolean,
private readonly eventBus: EventBus,
) {}
start() {
@@ -117,7 +117,10 @@ export class LocalServer {
}
if (archivedHash !== clientMsg.hash) {
console.error(
`desync detected on turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}, server hash: ${archivedHash}`,
`desync detected on turn ${
clientMsg.turnNumber}, client hash: ${
clientMsg.hash}, server hash: ${
archivedHash}`,
);
this.clientMessage({
type: "desync",
+30 -33
View File
@@ -1,46 +1,42 @@
import version from "../../resources/version.txt";
import { UserMeResponse } from "../core/ApiSchemas";
import { EventBus } from "../core/EventBus";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
import { ServerConfig } from "../core/configuration/Config";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import { joinLobby } from "./ClientGameRunner";
import "./DarkModeButton";
import { DarkModeButton } from "./DarkModeButton";
import "./FlagInput";
import "./GoogleAdElement";
import "./LangSelector";
import "./PublicLobby";
import "./UsernameInput";
import "./components/NewsButton";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./styles.css";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt";
import { generateCryptoRandomUUID, incrementGamesPlayed, translateText } from "./Utils";
import { DarkModeButton } from "./DarkModeButton";
import { EventBus } from "../core/EventBus";
import { FlagInput } from "./FlagInput";
import { FlagInputModal } from "./FlagInputModal";
import { GameStartingModal } from "./GameStartingModal";
import "./GoogleAdElement";
import { GameType } from "../core/game/Game";
import { HelpModal } from "./HelpModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { HostLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import "./LangSelector";
import { LangSelector } from "./LangSelector";
import { LanguageModal } from "./LanguageModal";
import { NewsButton } from "./components/NewsButton";
import { NewsModal } from "./NewsModal";
import "./PublicLobby";
import { OButton } from "./components/baseComponents/Button";
import { PublicLobby } from "./PublicLobby";
import { SendKickPlayerIntentEvent } from "./Transport";
import { ServerConfig } from "../core/configuration/Config";
import { SinglePlayerModal } from "./SinglePlayerModal";
import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
import { SendKickPlayerIntentEvent } from "./Transport";
import { UserMeResponse } from "../core/ApiSchemas";
import { UserSettingModal } from "./UserSettingModal";
import "./UsernameInput";
import { UserSettings } from "../core/game/UserSettings";
import { UsernameInput } from "./UsernameInput";
import {
generateCryptoRandomUUID,
incrementGamesPlayed,
translateText,
} from "./Utils";
import "./components/NewsButton";
import { NewsButton } from "./components/NewsButton";
import "./components/baseComponents/Button";
import { OButton } from "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt";
import "./styles.css";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { joinLobby } from "./ClientGameRunner";
import version from "../../resources/version.txt";
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
@@ -56,6 +52,7 @@ declare global {
spaAddAds: (ads: Array<{ type: string; selectorId: string }>) => void;
destroyUnits: (adType: string) => void;
settings?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slots?: any;
};
spaNewPage: (url: string) => void;
@@ -86,7 +83,7 @@ export type KickPlayerEvent = {
class Client {
private gameStop: (() => void) | null = null;
private eventBus: EventBus = new EventBus();
private readonly eventBus: EventBus = new EventBus();
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
@@ -94,7 +91,7 @@ class Client {
private joinModal: JoinPrivateLobbyModal;
private publicLobby: PublicLobby;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
constructor() {}
@@ -359,8 +356,8 @@ class Client {
const hostModal = document.querySelector(
"host-lobby-modal",
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
) as HostLobbyModal;
hostModal instanceof HostLobbyModal;
const hostLobbyButton = document.getElementById("host-lobby-button");
if (hostLobbyButton === null) throw new Error("Missing host-lobby-button");
hostLobbyButton.addEventListener("click", () => {
@@ -616,7 +613,7 @@ function hasAllowedFlare(
const allowed = config.allowedFlares();
if (allowed === undefined) return true;
if (userMeResponse === false) return false;
const flares = userMeResponse.player.flares;
const { flares } = userMeResponse.player;
if (flares === undefined) return false;
return allowed.length === 0 || allowed.some((f) => flares.includes(f));
}
+7 -7
View File
@@ -1,14 +1,14 @@
import { LitElement, css, html } from "lit";
import { resolveMarkdown } from "lit-markdown";
import { customElement, property, query } from "lit/decorators.js";
import changelog from "../../resources/changelog.md";
import { translateText } from "../client/Utils";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import { LitElement, css, html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import changelog from "../../resources/changelog.md";
import { resolveMarkdown } from "lit-markdown";
import { translateText } from "../client/Utils";
@customElement("news-modal")
export class NewsModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@@ -23,7 +23,7 @@ export class NewsModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
+16 -15
View File
@@ -1,23 +1,23 @@
import { GameID, GameInfo } from "../core/Schemas";
import { GameMapType, GameMode } from "../core/game/Game";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { ApiPublicLobbiesResponseSchema } from "../core/ExpressSchemas";
import { GameMapType, GameMode } from "../core/game/Game";
import { GameID, GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
import { JoinLobbyEvent } from "./Main";
import { generateID } from "../core/Util";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { translateText } from "../client/Utils";
@customElement("public-lobby")
export class PublicLobby extends LitElement {
@state() private lobbies: GameInfo[] = [];
@state() public isLobbyHighlighted = false;
@state() private isButtonDebounced = false;
@state() private mapImages: Map<GameID, string> = new Map();
@state() private readonly mapImages: Map<GameID, string> = new Map();
private lobbiesInterval: number | null = null;
private currLobby: GameInfo | null = null;
private debounceDelay = 750;
private lobbyIDToStart = new Map<GameID, number>();
private readonly debounceDelay = 750;
private readonly lobbyIDToStart = new Map<GameID, number>();
createRenderRoot() {
return this;
@@ -75,7 +75,7 @@ export class PublicLobby extends LitElement {
async fetchLobbies(): Promise<GameInfo[]> {
try {
const response = await fetch(`/api/public_lobbies`);
const response = await fetch("/api/public_lobbies");
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const json = await response.json();
@@ -121,13 +121,14 @@ export class PublicLobby extends LitElement {
<button
@click=${() => this.lobbyClicked(lobby)}
?disabled=${this.isButtonDebounced}
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
.isLobbyHighlighted
? "bg-gradient-to-r from-green-600 to-green-500"
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
.isButtonDebounced
? "opacity-70 cursor-not-allowed"
: ""}"
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${
this.isLobbyHighlighted
? "bg-gradient-to-r from-green-600 to-green-500"
: "bg-gradient-to-r from-blue-600 to-blue-500"
} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${
this.isButtonDebounced
? "opacity-70 cursor-not-allowed"
: ""}"
>
${mapImageSrc
? html`<img
+22 -21
View File
@@ -1,7 +1,7 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import "./components/Difficulties";
import "./components/Maps";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import {
Difficulty,
Duos,
@@ -13,22 +13,22 @@ import {
UnitType,
mapCategories,
} from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import { TeamCountConfig } from "../core/Schemas";
import { generateID } from "../core/Util";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./components/Difficulties";
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main";
import { TeamCountConfig } from "../core/Schemas";
import { UserSettings } from "../core/game/UserSettings";
import { UsernameInput } from "./UsernameInput";
import { generateID } from "../core/Util";
import randomMap from "../../resources/images/RandomMap.webp";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
import { translateText } from "../client/Utils";
@customElement("single-player-modal")
export class SinglePlayerModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@@ -37,9 +37,9 @@ export class SinglePlayerModal extends LitElement {
@state() private disableNPCs = false;
@state() private bots = 400;
@state() private infiniteGold = false;
@state() private donateGold = false;
@state() private readonly donateGold = false;
@state() private infiniteTroops = false;
@state() private donateTroops = false;
@state() private readonly donateTroops = false;
@state() private instantBuild = false;
@state() private useRandomMap = false;
@state() private gameMode: GameMode = GameMode.FFA;
@@ -47,7 +47,7 @@ export class SinglePlayerModal extends LitElement {
@state() private disabledUnits: UnitType[] = [];
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
connectedCallback() {
super.connectedCallback();
@@ -59,7 +59,7 @@ export class SinglePlayerModal extends LitElement {
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
@@ -206,7 +206,7 @@ export class SinglePlayerModal extends LitElement {
<div class="option-card-title">
${typeof o === "string"
? translateText(`public_lobby.teams_${o}`)
: translateText(`public_lobby.teams`, { num: o })}
: translateText("public_lobby.teams", { num: o })}
</div>
</div>
`,
@@ -410,7 +410,8 @@ export class SinglePlayerModal extends LitElement {
}
console.log(
`Starting single player game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]}${this.useRandomMap ? " (Randomly selected)" : ""}`,
`Starting single player game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]
}${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
const clientID = generateID();
const gameID = generateID();
@@ -429,10 +430,10 @@ export class SinglePlayerModal extends LitElement {
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
clientID: clientID,
gameID: gameID,
clientID,
gameID,
gameStartInfo: {
gameID: gameID,
gameID,
players: [
{
clientID,
+2 -2
View File
@@ -1,4 +1,4 @@
import version from "../../resources/version.txt";
import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader";
import version from "../../resources/version.txt";
export const terrainMapFileLoader = new FetchGameMapLoader(`/maps`, version);
export const terrainMapFileLoader = new FetchGameMapLoader("/maps", version);
+14 -13
View File
@@ -1,19 +1,19 @@
import { base64url } from "jose";
import type { TemplateResult } from "lit";
import { html, LitElement, render } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { UserMeResponse } from "../core/ApiSchemas";
import { Pattern } from "../core/CosmeticSchemas";
import { UserSettings } from "../core/game/UserSettings";
import { PatternDecoder } from "../core/PatternDecoder";
import "./components/Difficulties";
import "./components/Maps";
import { LitElement, html, render } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { handlePurchase, patterns } from "./Cosmetics";
import { Pattern } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
import type { TemplateResult } from "lit";
import { UserMeResponse } from "../core/ApiSchemas";
import { UserSettings } from "../core/game/UserSettings";
import { base64url } from "jose";
import { translateText } from "./Utils";
@customElement("territory-patterns-modal")
export class TerritoryPatternsModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@@ -36,7 +36,7 @@ export class TerritoryPatternsModal extends LitElement {
public resizeObserver: ResizeObserver;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
private isActive = false;
@@ -71,7 +71,7 @@ export class TerritoryPatternsModal extends LitElement {
this.requestUpdate();
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
@@ -165,7 +165,8 @@ export class TerritoryPatternsModal extends LitElement {
${pattern.product !== null
? html`
<button
class="w-full mt-2 px-3 py-1 bg-green-500 hover:bg-green-600 text-white text-xs font-medium rounded transition-colors"
class="w-full mt-2 px-3 py-1 bg-green-500 hover:bg-green-600
text-white text-xs font-medium rounded transition-colors"
@click=${(e: Event) => {
e.stopPropagation();
handlePurchase(pattern.product!.priceId);
@@ -388,7 +389,7 @@ export function generatePreviewDataUrl(
// Create an image
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
const { data } = imageData;
let i = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
+11 -10
View File
@@ -1,5 +1,3 @@
import { z } from "zod";
import { EventBus, GameEvent } from "../core/EventBus";
import {
AllPlayers,
GameType,
@@ -9,8 +7,6 @@ import {
Tick,
UnitType,
} from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { PlayerView } from "../core/game/GameView";
import {
AllPlayersStats,
ClientHashMessage,
@@ -24,9 +20,13 @@ import {
ServerMessageSchema,
Winner,
} from "../core/Schemas";
import { replacer } from "../core/Util";
import { EventBus, GameEvent } from "../core/EventBus";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalServer } from "./LocalServer";
import { PlayerView } from "../core/game/GameView";
import { TileRef } from "../core/game/GameMap";
import { replacer } from "../core/Util";
import { z } from "zod";
export class PauseGameEvent implements GameEvent {
constructor(public readonly paused: boolean) {}
@@ -174,7 +174,7 @@ export class Transport {
private localServer: LocalServer;
private buffer: string[] = [];
private readonly buffer: string[] = [];
private onconnect: () => void;
private onmessage: (msg: ServerMessage) => void;
@@ -182,8 +182,8 @@ export class Transport {
private pingInterval: number | null = null;
public readonly isLocal: boolean;
constructor(
private lobbyConfig: LobbyConfig,
private eventBus: EventBus,
private readonly lobbyConfig: LobbyConfig,
private readonly eventBus: EventBus,
) {
// If gameRecord is not null, we are replaying an archived game.
// For multiplayer games, GameConfig is not known until game starts.
@@ -328,6 +328,7 @@ export class Transport {
};
this.socket.onmessage = (event: MessageEvent) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const parsed = JSON.parse(event.data);
const result = ServerMessageSchema.safeParse(parsed);
if (!result.success) {
@@ -545,7 +546,7 @@ export class Transport {
private onPauseGameEvent(event: PauseGameEvent) {
if (!this.isLocal) {
console.log(`cannot pause multiplayer games`);
console.log("cannot pause multiplayer games");
return;
}
if (event.paused) {
@@ -632,7 +633,7 @@ export class Transport {
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
const msg = {
type: "intent",
intent: intent,
intent,
} satisfies ClientIntentMessage;
this.sendMsg(msg);
} else {
+11 -21
View File
@@ -1,19 +1,19 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { z } from "zod";
import { translateText } from "../client/Utils";
import { UserSettings } from "../core/game/UserSettings";
import "./components/baseComponents/setting/SettingKeybind";
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
import "./components/baseComponents/setting/SettingNumber";
import "./components/baseComponents/setting/SettingSlider";
import "./components/baseComponents/setting/SettingToggle";
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
import { UserSettings } from "../core/game/UserSettings";
import { translateText } from "../client/Utils";
import { z } from "zod";
const KeybindSchema = z.record(z.string(), z.string());
@customElement("user-setting")
export class UserSettingModal extends LitElement {
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
@state() private settingsMode: "basic" | "keybinds" = "basic";
@state() private keybinds: Record<string, string> = {};
@@ -35,7 +35,7 @@ export class UserSettingModal extends LitElement {
}
}
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
isModalOpen: boolean;
@@ -51,7 +51,7 @@ export class UserSettingModal extends LitElement {
document.body.style.overflow = "auto";
}
private handleKeyDown = (e: KeyboardEvent) => {
private readonly handleKeyDown = (e: KeyboardEvent) => {
if (!this.modalEl?.isModalOpen || this.showEasterEggSettings) return;
if (e.code === "Escape") {
@@ -182,16 +182,6 @@ export class UserSettingModal extends LitElement {
}
}
private sliderTroopRatio(e: CustomEvent<{ value: number }>) {
const value = e.detail?.value;
if (typeof value === "number") {
const ratio = value / 100;
localStorage.setItem("settings.troopRatio", ratio.toString());
} else {
console.warn("Slider event missing detail.value", e);
}
}
private toggleTerritoryPatterns(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
if (typeof enabled !== "boolean") return;
@@ -389,7 +379,7 @@ export class UserSettingModal extends LitElement {
max="100"
value="40"
easter="true"
@change=${(e: CustomEvent) => {
@change=${(e: CustomEvent<{ value: unknown }>) => {
const value = e.detail?.value;
if (value !== undefined) {
console.log("Changed:", value);
@@ -408,7 +398,7 @@ export class UserSettingModal extends LitElement {
min="0"
max="1000"
easter="true"
@change=${(e: CustomEvent) => {
@change=${(e: CustomEvent<{ value: unknown }>) => {
const value = e.detail?.value;
if (value !== undefined) {
console.log("Changed:", value);
+12 -7
View File
@@ -1,12 +1,12 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { v4 as uuidv4 } from "uuid";
import { translateText } from "../client/Utils";
import { UserSettings } from "../core/game/UserSettings";
import {
MAX_USERNAME_LENGTH,
validateUsername,
} from "../core/validations/username";
import { customElement, property, state } from "lit/decorators.js";
import { UserSettings } from "../core/game/UserSettings";
import { translateText } from "../client/Utils";
import { v4 as uuidv4 } from "uuid";
const usernameKey = "username";
@@ -15,7 +15,7 @@ export class UsernameInput extends LitElement {
@state() private username = "";
@property({ type: String }) validationError = "";
private _isValid = true;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
// Remove static styles since we're using Tailwind
@@ -43,12 +43,17 @@ export class UsernameInput extends LitElement {
@change=${this.handleChange}
placeholder="${translateText("username.enter_username")}"
maxlength="${MAX_USERNAME_LENGTH}"
class="w-full px-4 py-2 border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
class="w-full px-4 py-2 border border-gray-300 rounded-xl shadow-sm
text-2xl text-center focus:outline-none focus:ring-2
focus:ring-blue-500 focus:border-blue-500 dark:border-gray-300/60
dark:bg-gray-700 dark:text-white"
/>
${this.validationError
? html`<div
id="username-validation-error"
class="absolute z-10 w-full mt-2 px-3 py-1 text-lg border rounded bg-white text-red-600 border-red-600 dark:bg-gray-700 dark:text-red-300 dark:border-red-300"
class="absolute z-10 w-full mt-2 px-3 py-1 text-lg border rounded
bg-white text-red-600 border-red-600 dark:bg-gray-700
dark:text-red-300 dark:border-red-300"
>
${this.validationError}
</div>`
+12 -2
View File
@@ -1,6 +1,6 @@
import IntlMessageFormat from "intl-messageformat";
import { MessageType } from "../core/game/Game";
import { LangSelector } from "./LangSelector";
import { MessageType } from "../core/game/Game";
export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
@@ -57,6 +57,7 @@ export function generateCryptoRandomUUID(): string {
// Fallback using crypto.getRandomValues
if (crypto !== undefined && "getRandomValues" in crypto) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
(c: number): string =>
@@ -83,8 +84,11 @@ export const translateText = (
key: string,
params: Record<string, string | number> = {},
): string => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const self = translateText as any;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
self.formatterCache ??= new Map();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
self.lastLang ??= null;
const langSelector = document.querySelector("lang-selector") as LangSelector;
@@ -100,15 +104,18 @@ export const translateText = (
return key;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (self.lastLang !== langSelector.currentLang) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
self.formatterCache.clear();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
self.lastLang = langSelector.currentLang;
}
let message = langSelector.translations[key];
if (!message && langSelector.defaultTranslations) {
const defaultTranslations = langSelector.defaultTranslations;
const { defaultTranslations } = langSelector;
if (defaultTranslations && defaultTranslations[key]) {
message = defaultTranslations[key];
}
@@ -122,13 +129,16 @@ export const translateText = (
? "en"
: langSelector.currentLang;
const cacheKey = `${key}:${locale}:${message}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
let formatter = self.formatterCache.get(cacheKey);
if (!formatter) {
formatter = new IntlMessageFormat(message, locale);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
self.formatterCache.set(cacheKey, formatter);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return formatter.format(params) as string;
} catch (e) {
console.warn("ICU format error", e);
+2
View File
@@ -67,6 +67,7 @@ export class DifficultyDisplay extends LitElement {
<circle cx="9" cy="12" r="1"></circle>
</svg>`;
/* eslint-disable max-len */
const burningSkull = html`<svg
stroke="currentColor"
fill="currentColor"
@@ -109,6 +110,7 @@ export class DifficultyDisplay extends LitElement {
d="M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15zM14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2z"
></path>
</svg>`;
/* eslint-enable max-len */
switch (difficultyKey) {
case "Easy":
+4 -3
View File
@@ -1,9 +1,9 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import megaphone from "../../../resources/images/Megaphone.svg";
import version from "../../../resources/version.txt";
import { NewsModal } from "../NewsModal";
import megaphone from "../../../resources/images/Megaphone.svg";
import { translateText } from "../Utils";
import version from "../../../resources/version.txt";
@customElement("news-button")
export class NewsButton extends LitElement {
@@ -44,7 +44,8 @@ export class NewsButton extends LitElement {
: ""}"
>
<button
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30
dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
@click=${this.handleClick}
>
<img
@@ -74,7 +74,7 @@ export class SettingKeybind extends LitElement {
if (!this.listening) return;
e.preventDefault();
const code = e.code;
const { code } = e;
this.value = code;
@@ -28,17 +28,6 @@ export class SettingSlider extends LitElement {
);
}
private handleSliderChange(e: Event) {
const detail = (e as CustomEvent)?.detail;
if (!detail || detail.value === undefined) {
console.warn("Invalid slider change event", e);
return;
}
const value = detail.value;
console.log("Slider changed to", value);
}
private updateSliderStyle(slider: HTMLInputElement) {
const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
slider.style.background = `linear-gradient(to right, #2196f3 ${percent}%, #444 ${percent}%)`;
+6 -3
View File
@@ -484,7 +484,8 @@
{
"code": "Confederate States",
"continent": "North America",
"name": "Confederate States"
"name": "Confederate States",
"restricted": true
},
{
"code": "1_Connacht",
@@ -702,7 +703,8 @@
{
"code": "Fascist Spain",
"continent": "Europe",
"name": "Fascist Spain"
"name": "Fascist Spain",
"restricted": true
},
{
"code": "fm",
@@ -805,7 +807,8 @@
{
"code": "German Empire",
"continent": "Europe",
"name": "German Empire"
"name": "German Empire",
"restricted": true
},
{
"code": "de",
+6 -6
View File
@@ -1,15 +1,15 @@
export class AnimatedSprite {
private frameHeight: number;
private readonly frameHeight: number;
private currentFrame = 0;
private elapsedTime = 0;
private active = true;
constructor(
private image: CanvasImageSource,
private frameWidth: number,
private frameCount: number,
private frameDuration: number, // in milliseconds
private looping = false,
private readonly image: CanvasImageSource,
private readonly frameWidth: number,
private readonly frameCount: number,
private readonly frameDuration: number, // in milliseconds
private readonly looping = false,
private originX: number,
private originY: number,
) {
+15 -15
View File
@@ -1,19 +1,19 @@
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
import conquestSword from "../../../resources/sprites/conquestSword.png";
import dust from "../../../resources/sprites/dust.png";
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
import miniFire from "../../../resources/sprites/minifire.png";
import nuke from "../../../resources/sprites/nukeExplosion.png";
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
import miniSmoke from "../../../resources/sprites/smoke.png";
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
import { Theme } from "../../core/configuration/Config";
import { PlayerView } from "../../core/game/GameView";
import { AnimatedSprite } from "./AnimatedSprite";
import { FxType } from "./fx/Fx";
import { PlayerView } from "../../core/game/GameView";
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
import { Theme } from "../../core/configuration/Config";
import { colorizeCanvas } from "./SpriteLoader";
import conquestSword from "../../../resources/sprites/conquestSword.png";
import dust from "../../../resources/sprites/dust.png";
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
import miniFire from "../../../resources/sprites/minifire.png";
import miniSmoke from "../../../resources/sprites/smoke.png";
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
import nuke from "../../../resources/sprites/nukeExplosion.png";
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
type AnimatedSpriteConfig = {
url: string;
@@ -128,9 +128,9 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
};
export class AnimatedSpriteLoader {
private animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
private readonly animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
// Do not color the same sprite twice
private coloredAnimatedSpriteCache: Map<string, HTMLCanvasElement> =
private readonly coloredAnimatedSpriteCache: Map<string, HTMLCanvasElement> =
new Map();
public async loadAllAnimatedSpriteImages(): Promise<void> {
+13 -13
View File
@@ -1,21 +1,17 @@
import { EventBus } from "../../core/EventBus";
import { GameView } from "../../core/game/GameView";
import { UserSettings } from "../../core/game/UserSettings";
import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
import { AlertFrame } from "./layers/AlertFrame";
import { BuildMenu } from "./layers/BuildMenu";
import { ChatDisplay } from "./layers/ChatDisplay";
import { ChatModal } from "./layers/ChatModal";
import { ControlPanel } from "./layers/ControlPanel";
import { EmojiTable } from "./layers/EmojiTable";
import { EventBus } from "../../core/EventBus";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FPSDisplay } from "./layers/FPSDisplay";
import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { GameStartingModal } from "../GameStartingModal";
import { GameView } from "../../core/game/GameView";
import { GutterAdModal } from "./layers/GutterAdModal";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { Layer } from "./layers/Layer";
@@ -26,6 +22,7 @@ import { NameLayer } from "./layers/NameLayer";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer";
import { RedrawGraphicsEvent } from "../InputHandler";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SettingsModal } from "./layers/SettingsModal";
import { SpawnAd } from "./layers/SpawnAd";
@@ -35,9 +32,12 @@ import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { TransformHandler } from "./TransformHandler";
import { UILayer } from "./layers/UILayer";
import { UIState } from "./UIState";
import { UnitDisplay } from "./layers/UnitDisplay";
import { UnitLayer } from "./layers/UnitLayer";
import { UserSettings } from "../../core/game/UserSettings";
import { WinModal } from "./layers/WinModal";
export function createRenderer(
@@ -283,16 +283,16 @@ export function createRenderer(
}
export class GameRenderer {
private context: CanvasRenderingContext2D;
private readonly context: CanvasRenderingContext2D;
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly canvas: HTMLCanvasElement,
public transformHandler: TransformHandler,
public uiState: UIState,
private layers: Layer[],
private fpsDisplay: FPSDisplay,
private readonly layers: Layer[],
private readonly fpsDisplay: FPSDisplay,
) {
const context = canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
+11 -9
View File
@@ -81,17 +81,19 @@ export function createGrid(
const width = scaledBoundingBox.max.x - scaledBoundingBox.min.x + 1;
const height = scaledBoundingBox.max.y - scaledBoundingBox.min.y + 1;
const grid: boolean[][] = Array(width)
.fill(null)
.map(() => Array(height).fill(false));
const grid: boolean[][] = Array<Array<boolean>>(width)
.fill(null as unknown as boolean[])
.map(() => Array<boolean>(height).fill(false));
for (let x = scaledBoundingBox.min.x; x <= scaledBoundingBox.max.x; x++) {
for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) {
for (let { x } = scaledBoundingBox.min; x <= scaledBoundingBox.max.x; x++) {
for (let { y } = scaledBoundingBox.min; y <= scaledBoundingBox.max.y; y++) {
const cell = new Cell(x * scalingFactor, y * scalingFactor);
if (game.isOnMap(cell)) {
const tile = game.ref(cell.x, cell.y);
grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] =
game.isLake(tile) || game.owner(tile) === player; // TODO: okay if lake
game.isLake(tile) ||
game.owner(tile) === player ||
game.hasFallout(tile);
}
}
}
@@ -102,7 +104,7 @@ export function createGrid(
export function findLargestInscribedRectangle(grid: boolean[][]): Rectangle {
const rows = grid[0].length;
const cols = grid.length;
const heights: number[] = new Array(cols).fill(0);
const heights: number[] = new Array<number>(cols).fill(0);
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
for (let row = 0; row < rows; row++) {
@@ -149,8 +151,8 @@ export function largestRectangleInHistogram(widths: number[]): Rectangle {
largestRect = {
x: stack.length === 0 ? 0 : stack[stack.length - 1] + 1,
y: 0,
width: width,
height: height,
width,
height,
};
}
}
+6 -6
View File
@@ -1,12 +1,12 @@
export class ProgressBar {
private static readonly CLEAR_PADDING = 2;
constructor(
private colors: string[] = [],
private ctx: CanvasRenderingContext2D,
private x: number,
private y: number,
private w: number,
private h: number,
private readonly colors: string[] = [],
private readonly ctx: CanvasRenderingContext2D,
private readonly x: number,
private readonly y: number,
private readonly w: number,
private readonly h: number,
private progress = 0, // Progress from 0 to 1
) {
this.setProgress(progress);
+5 -5
View File
@@ -1,17 +1,17 @@
import { TrainType, UnitType } from "../../core/game/Game";
import { Colord } from "colord";
import { Theme } from "../../core/configuration/Config";
import { UnitView } from "../../core/game/GameView";
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 trainLoadedCarriageSprite from "../../../resources/sprites/trainCarriageLoaded.png";
import transportShipSprite from "../../../resources/sprites/transportship.png";
import warshipSprite from "../../../resources/sprites/warship.png";
import { Theme } from "../../core/configuration/Config";
import { TrainType, UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
// Can't reuse TrainType because "loaded" is not a type, just an attribute
const TrainTypeSprite = {
@@ -124,7 +124,7 @@ export const colorizeCanvas = (
ctx.drawImage(source, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const { data } = imageData;
const colorARgb = colorA.toRgb();
const colorBRgb = colorB.toRgb();
+7 -7
View File
@@ -1,12 +1,12 @@
import { EventBus } from "../../core/EventBus";
import { Cell } from "../../core/game/Game";
import { GameView } from "../../core/game/GameView";
import { CenterCameraEvent, DragEvent, ZoomEvent } from "../InputHandler";
import {
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./layers/Leaderboard";
import { Cell } from "../../core/game/Game";
import { EventBus } from "../../core/EventBus";
import { GameView } from "../../core/game/GameView";
export const GOTO_INTERVAL_MS = 16;
export const CAMERA_MAX_SPEED = 15;
@@ -20,13 +20,13 @@ export class TransformHandler {
private lastGoToCallTime: number | null = null;
private target: Cell | null;
private intervalID: NodeJS.Timeout | null = null;
private intervalID: ReturnType<typeof setTimeout> | null = null;
private changed = false;
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly canvas: HTMLCanvasElement,
) {
this._boundingRect = this.canvas.getBoundingClientRect();
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
+6 -6
View File
@@ -1,10 +1,10 @@
import { FadeFx, SpriteFx } from "./SpriteFx";
import { Fx, FxType } from "./Fx";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { ConquestUpdate } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { renderNumber } from "../../Utils";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { FadeFx, SpriteFx } from "./SpriteFx";
import { TextFx } from "./TextFx";
import { renderNumber } from "../../Utils";
/**
* Conquest FX:
@@ -18,8 +18,8 @@ export function conquestFxFactory(
): Fx[] {
const conquestFx: Fx[] = [];
const conquered = game.player(conquest.conqueredId);
const x = conquered.nameLocation().x;
const y = conquered.nameLocation().y;
const { x } = conquered.nameLocation();
const { y } = conquered.nameLocation();
const swordAnimation = new SpriteFx(
animatedSpriteLoader,
+7 -7
View File
@@ -1,7 +1,7 @@
import { GameView } from "../../../core/game/GameView";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { FadeFx, SpriteFx } from "./SpriteFx";
import { Fx, FxType } from "./Fx";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { GameView } from "../../../core/game/GameView";
/**
* Shockwave effect: draw a growing 1px white circle
@@ -9,10 +9,10 @@ import { FadeFx, SpriteFx } from "./SpriteFx";
export class ShockwaveFx implements Fx {
private lifeTime = 0;
constructor(
private x: number,
private y: number,
private duration: number,
private maxRadius: number,
private readonly x: number,
private readonly y: number,
private readonly duration: number,
private readonly maxRadius: number,
) {}
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
+6 -6
View File
@@ -1,8 +1,8 @@
import { Theme } from "../../../core/configuration/Config";
import { PlayerView } from "../../../core/game/GameView";
import { Fx, FxType } from "./Fx";
import { AnimatedSprite } from "../AnimatedSprite";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { PlayerView } from "../../../core/game/GameView";
import { Theme } from "../../../core/configuration/Config";
function fadeInOut(t: number, fadeIn = 0.3, fadeOut = 0.7): number {
if (t < fadeIn) {
@@ -20,9 +20,9 @@ function fadeInOut(t: number, fadeIn = 0.3, fadeOut = 0.7): number {
*/
export class FadeFx implements Fx {
constructor(
private fxToFade: SpriteFx,
private fadeIn: number,
private fadeOut: number,
private readonly fxToFade: SpriteFx,
private readonly fadeIn: number,
private readonly fadeOut: number,
) {}
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
+7 -7
View File
@@ -4,13 +4,13 @@ export class TextFx implements Fx {
private lifeTime = 0;
constructor(
private text: string,
private x: number,
private y: number,
private duration: number,
private riseDistance = 30,
private font = "11px sans-serif",
private color: { r: number; g: number; b: number } = {
private readonly text: string,
private readonly x: number,
private readonly y: number,
private readonly duration: number,
private readonly riseDistance = 30,
private readonly font = "11px sans-serif",
private readonly color: { r: number; g: number; b: number } = {
r: 255,
g: 255,
b: 255,
+1 -1
View File
@@ -8,7 +8,7 @@ type TimedTask = {
* Basic timeline to chain actions
*/
export class Timeline {
private tasks: TimedTask[] = [];
private readonly tasks: TimedTask[] = [];
private timeElapsed = 0;
add(delay: number, action: () => void): Timeline {
+6 -6
View File
@@ -1,6 +1,6 @@
import { GameView } from "../../../core/game/GameView";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Fx, FxType } from "./Fx";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { GameView } from "../../../core/game/GameView";
import { SpriteFx } from "./SpriteFx";
import { Timeline } from "./Timeline";
@@ -8,13 +8,13 @@ import { Timeline } from "./Timeline";
* Explosion Effect: a few timed explosions
*/
export class UnitExplosionFx implements Fx {
private timeline = new Timeline();
private explosions: Fx[] = [];
private readonly timeline = new Timeline();
private readonly explosions: Fx[] = [];
constructor(
animatedSpriteLoader: AnimatedSpriteLoader,
private x: number,
private y: number,
private readonly x: number,
private readonly y: number,
game: GameView,
) {
const config = [
+4 -4
View File
@@ -1,12 +1,12 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import {
BrokeAllianceUpdate,
GameUpdateType,
} from "../../../core/game/GameUpdates";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { Layer } from "./Layer";
import { UserSettings } from "../../../core/game/UserSettings";
// Parameters for the alert animation
const ALERT_SPEED = 1.6;
@@ -15,7 +15,7 @@ const ALERT_COUNT = 2;
@customElement("alert-frame")
export class AlertFrame extends LitElement implements Layer {
public game: GameView;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
@state()
private isActive = false;
+23 -23
View File
@@ -1,39 +1,39 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import missileSiloIcon from "../../../../resources/non-commercial/svg/MissileSiloIconWhite.svg";
import samlauncherIcon from "../../../../resources/non-commercial/svg/SamLauncherIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
BuildUnitIntentEvent,
SendUpgradeStructureIntentEvent,
} from "../../Transport";
import {
BuildableUnit,
Gold,
PlayerActions,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import {
CloseViewEvent,
MouseDownEvent,
ShowBuildMenuEvent,
ShowEmojiMenuEvent,
} from "../../InputHandler";
import {
BuildUnitIntentEvent,
SendUpgradeStructureIntentEvent,
} from "../../Transport";
import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { TileRef } from "../../../core/game/GameMap";
import { TransformHandler } from "../TransformHandler";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import missileSiloIcon from "../../../../resources/non-commercial/svg/MissileSiloIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import { renderNumber } from "../../Utils";
import samlauncherIcon from "../../../../resources/non-commercial/svg/SamLauncherIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { translateText } from "../../../client/Utils";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
export type BuildItemDisplay = {
unitType: UnitType;
+12 -11
View File
@@ -1,16 +1,16 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js";
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
import { EventBus } from "../../../core/EventBus";
import { MessageType } from "../../../core/game/Game";
import {
DisplayMessageUpdate,
GameUpdateType,
} from "../../../core/game/GameUpdates";
import { LitElement, html } from "lit";
import { UnsafeHTMLDirective, unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { onlyImages } from "../../../core/Util";
import { Layer } from "./Layer";
import { MessageType } from "../../../core/game/Game";
import { onlyImages } from "../../../core/Util";
type ChatEvent = {
description: string;
@@ -24,7 +24,7 @@ export class ChatDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
private active = false;
private readonly active = false;
@state() private _hidden = false;
@state() private newEvents = 0;
@@ -125,9 +125,10 @@ export class ChatDisplay extends LitElement implements Layer {
}
return html`
<div
class="${this._hidden
? "w-fit px-[10px] py-[5px]"
: ""} rounded-md bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw] lg:w-full lg:w-auto"
class="${this._hidden ? "w-fit px-[10px] py-[5px]" : ""} rounded-md
bg-black bg-opacity-60 relative max-h-[30vh] flex flex-col-reverse
overflow-y-auto w-full lg:bottom-2.5 lg:right-2.5 z-50 lg:max-w-[30vw]
lg:w-full lg:w-auto"
style="pointer-events: auto"
>
<div>
@@ -1,16 +1,16 @@
import { EventBus } from "../../../core/EventBus";
import { COLORS, MenuElement, MenuElementParams } from "./RadialMenuElements";
import { ChatModal, QuickChatPhrase, quickChatPhrases } from "./ChatModal";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { EventBus } from "../../../core/EventBus";
import { SendQuickChatEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { ChatModal, QuickChatPhrase, quickChatPhrases } from "./ChatModal";
import { COLORS, MenuElement, MenuElementParams } from "./RadialMenuElements";
export class ChatIntegration {
private ctModal: ChatModal;
private readonly ctModal: ChatModal;
constructor(
private game: GameView,
private eventBus: EventBus,
private readonly game: GameView,
private readonly eventBus: EventBus,
) {
this.ctModal = document.querySelector("chat-modal") as ChatModal;
+5 -7
View File
@@ -1,13 +1,11 @@
import { GameView, PlayerView } from "../../../core/game/GameView";
import { LitElement, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import { PlayerType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import quickChatData from "../../../../resources/QuickChat.json";
import { EventBus } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { EventBus } from "../../../core/EventBus";
import { PlayerType } from "../../../core/game/Game";
import { SendQuickChatEvent } from "../../Transport";
import quickChatData from "../../../../resources/QuickChat.json";
import { translateText } from "../../Utils";
export type QuickChatPhrase = {
@@ -21,7 +19,7 @@ export const quickChatPhrases: QuickChatPhrases = quickChatData;
@customElement("chat-modal")
export class ChatModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
@query("o-modal") private readonly modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
+9 -8
View File
@@ -1,14 +1,14 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Gold } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import { AttackRatioEvent } from "../../InputHandler";
import { ClientID } from "../../../core/Schemas";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { Gold } from "../../../core/game/Game";
import { Layer } from "./Layer";
import { UIState } from "../UIState";
import { translateText } from "../../../client/Utils";
@customElement("control-panel")
export class ControlPanel extends LitElement implements Layer {
@@ -162,7 +162,8 @@ export class ControlPanel extends LitElement implements Layer {
</style>
<div
class="${this._isVisible
? "w-full sm:max-w-[320px] text-sm sm:text-base bg-gray-800/70 p-2 pr-3 sm:p-4 shadow-lg sm:rounded-lg backdrop-blur"
? "w-full sm:max-w-[320px] text-sm sm:text-base bg-gray-800/70 p-2 " +
"pr-3 sm:p-4 shadow-lg sm:rounded-lg backdrop-blur"
: "hidden"}"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
+19 -14
View File
@@ -1,12 +1,12 @@
import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { AllPlayers } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl";
import { emojiTable, flattenedEmojiTable } from "../../../core/Util";
import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler";
import { AllPlayers } from "../../../core/game/Game";
import { EventBus } from "../../../core/EventBus";
import { SendEmojiIntentEvent } from "../../Transport";
import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl";
import { TransformHandler } from "../TransformHandler";
@customElement("emoji-table")
@@ -64,16 +64,17 @@ export class EmojiTable extends LitElement {
return html`
<div
class="bg-slate-800 max-w-[95vw] max-h-[95vh] pt-[15px] pb-[15px] fixed flex flex-col -translate-x-1/2 -translate-y-1/2
items-center rounded-[10px] z-[9999] top-[50%] left-[50%] justify-center"
class="bg-slate-800 max-w-[95vw] max-h-[95vh] pt-[15px] pb-[15px] fixed
flex flex-col -translate-x-1/2 -translate-y-1/2 items-center
rounded-[10px] z-[9999] top-[50%] left-[50%] justify-center"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
@wheel=${(e: WheelEvent) => e.stopPropagation()}
>
<!-- Close button -->
<button
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
bg-red-500 hover:bg-red-900 text-white rounded-full
text-sm font-bold transition-colors"
class="absolute -top-2 -right-2 w-6 h-6 flex items-center
justify-center bg-red-500 hover:bg-red-900 text-white rounded-full
text-sm font-bold transition-colors"
@click=${this.hideTable}
>
@@ -88,10 +89,14 @@ export class EmojiTable extends LitElement {
${row.map(
(emoji) => html`
<button
class="flex transition-transform duration-300 ease justify-center items-center cursor-pointer
border border-solid border-slate-500 rounded-[12px] bg-slate-700 hover:bg-slate-600 active:bg-slate-500
md:m-[8px] md:text-[60px] md:w-[80px] md:h-[80px] hover:scale-[1.1] active:scale-[0.95]
sm:w-[60px] sm:h-[60px] sm:text-[32px] sm:m-[5px] text-[28px] w-[50px] h-[50px] m-[3px]"
class="flex transition-transform duration-300 ease
justify-center items-center cursor-pointer border
border-solid border-slate-500 rounded-[12px]
bg-slate-700 hover:bg-slate-600 active:bg-slate-500
md:m-[8px] md:text-[60px] md:w-[80px] md:h-[80px]
hover:scale-[1.1] active:scale-[0.95] sm:w-[60px]
sm:h-[60px] sm:text-[32px] sm:m-[5px] text-[28px]
w-[50px] h-[50px] m-[3px]"
@click=${() => this.onEmojiClicked(emoji)}
>
${emoji}
+31 -26
View File
@@ -1,20 +1,12 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js";
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
/* eslint-disable max-lines */
import {
AllPlayers,
getMessageCategory,
MessageCategory,
MessageType,
PlayerType,
Tick,
UnitType,
getMessageCategory,
} from "../../../core/game/Game";
import {
AllianceExpiredUpdate,
@@ -35,18 +27,25 @@ import {
SendAllianceExtensionIntentEvent,
SendAllianceReplyIntentEvent,
} from "../../Transport";
import { Layer } from "./Layer";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { onlyImages } from "../../../core/Util";
import { renderNumber, renderTroops } from "../../Utils";
import {
GoToPlayerEvent,
GoToPositionEvent,
GoToUnitEvent,
} from "./Leaderboard";
import { LitElement, TemplateResult, html } from "lit";
import { UnsafeHTMLDirective, unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, state } from "lit/decorators.js";
import { getMessageTypeClasses, translateText } from "../../Utils";
import { renderNumber, renderTroops } from "../../Utils";
import { DirectiveResult } from "lit/directive.js";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import { onlyImages } from "../../../core/Util";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
type GameEvent = {
description: string;
@@ -77,7 +76,7 @@ export class EventsDisplay extends LitElement implements Layer {
private events: GameEvent[] = [];
// allianceID -> last checked at tick
private alliancesCheckedAt = new Map<number, Tick>();
private readonly alliancesCheckedAt = new Map<number, Tick>();
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
@state() private outgoingLandAttacks: AttackUpdate[] = [];
@@ -88,7 +87,7 @@ export class EventsDisplay extends LitElement implements Layer {
@state() private latestGoldAmount: bigint | null = null;
@state() private goldAmountAnimating = false;
private goldAmountTimeoutId: ReturnType<typeof setTimeout> | null = null;
@state() private eventsFilters: Map<MessageCategory, boolean> = new Map([
@state() private readonly eventsFilters: Map<MessageCategory, boolean> = new Map([
[MessageCategory.ATTACK, false],
[MessageCategory.TRADE, false],
[MessageCategory.ALLIANCE, false],
@@ -96,7 +95,7 @@ export class EventsDisplay extends LitElement implements Layer {
]);
private renderButton(options: {
content: any; // Can be string, TemplateResult, or other renderable content
content: string | TemplateResult | DirectiveResult<typeof UnsafeHTMLDirective>;
onClick?: () => void;
className?: string;
disabled?: boolean;
@@ -142,7 +141,7 @@ export class EventsDisplay extends LitElement implements Layer {
this.requestUpdate();
}
private updateMap = [
private readonly updateMap = [
[GameUpdateType.DisplayEvent, this.onDisplayMessageEvent.bind(this)],
[GameUpdateType.DisplayChatEvent, this.onDisplayChatEvent.bind(this)],
[GameUpdateType.AllianceRequest, this.onAllianceRequestEvent.bind(this)],
@@ -359,7 +358,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
this.addEvent({
description: description,
description,
createdAt: this.game.ticks(),
highlight: true,
type: event.messageType,
@@ -535,8 +534,8 @@ export class EventsDisplay extends LitElement implements Layer {
this.addEvent({
description: translateText("events_display.betrayal_description", {
name: betrayed.name(),
malusPercent: malusPercent,
durationText: durationText,
malusPercent,
durationText,
}),
type: MessageType.ALLIANCE_BROKEN,
highlight: true,
@@ -686,7 +685,7 @@ export class EventsDisplay extends LitElement implements Layer {
unsafeDescription: false,
highlight: true,
createdAt: this.game.ticks(),
unitView: unitView,
unitView,
});
}
@@ -918,7 +917,8 @@ export class EventsDisplay extends LitElement implements Layer {
`,
onClick: this.toggleHidden,
className:
"text-white cursor-pointer pointer-events-auto w-fit p-2 lg:p-3 rounded-md bg-gray-800/70 backdrop-blur",
"text-white cursor-pointer pointer-events-auto w-fit p-2 " +
"lg:p-3 rounded-md bg-gray-800/70 backdrop-blur",
})}
</div>
`
@@ -1015,7 +1015,9 @@ export class EventsDisplay extends LitElement implements Layer {
<!-- Content Area -->
<div
class="rounded-b-none md:rounded-b-md bg-gray-800/70 max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full h-full"
class="rounded-b-none md:rounded-b-md bg-gray-800/70
max-h-[30vh] flex flex-col-reverse overflow-y-auto w-full
h-full"
>
<div>
<table
@@ -1059,7 +1061,10 @@ export class EventsDisplay extends LitElement implements Layer {
${event.buttons.map(
(btn) => html`
<button
class="inline-block px-3 py-1 text-white rounded text-md md:text-sm cursor-pointer transition-colors duration-300
class="inline-block px-3 py-1
text-white rounded text-md
md:text-sm cursor-pointer
transition-colors duration-300
${btn.className.includes("btn-info")
? "bg-blue-500 hover:bg-blue-600"
: btn.className.includes(
+7 -7
View File
@@ -1,9 +1,9 @@
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { TogglePerformanceOverlayEvent } from "../../InputHandler";
import { Layer } from "./Layer";
import { TogglePerformanceOverlayEvent } from "../../InputHandler";
import { UserSettings } from "../../../core/game/UserSettings";
@customElement("fps-display")
export class FPSDisplay extends LitElement implements Layer {
@@ -33,8 +33,8 @@ export class FPSDisplay extends LitElement implements Layer {
private frameCount = 0;
private lastTime = 0;
private frameTimes: number[] = [];
private fpsHistory: number[] = [];
private readonly frameTimes: number[] = [];
private readonly fpsHistory: number[] = [];
private lastSecondTime = 0;
private framesThisSecond = 0;
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
@@ -118,7 +118,7 @@ export class FPSDisplay extends LitElement implements Layer {
this.userSettings.togglePerformanceOverlay();
}
private handleMouseDown = (e: MouseEvent) => {
private readonly handleMouseDown = (e: MouseEvent) => {
// Don't start dragging if clicking on close button
if ((e.target as HTMLElement).classList.contains("close-button")) {
return;
@@ -135,7 +135,7 @@ export class FPSDisplay extends LitElement implements Layer {
e.preventDefault();
};
private handleMouseMove = (e: MouseEvent) => {
private readonly handleMouseMove = (e: MouseEvent) => {
if (!this.isDragging) return;
const newX = e.clientX - this.dragStart.x;
@@ -153,7 +153,7 @@ export class FPSDisplay extends LitElement implements Layer {
this.requestUpdate();
};
private handleMouseUp = () => {
private readonly handleMouseUp = () => {
this.isDragging = false;
document.removeEventListener("mousemove", this.handleMouseMove);
document.removeEventListener("mouseup", this.handleMouseUp);
+17 -16
View File
@@ -1,34 +1,35 @@
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import {
BonusEventUpdate,
ConquestUpdate,
GameUpdateType,
RailroadUpdate,
} from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { renderNumber } from "../../Utils";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { conquestFxFactory } from "../fx/ConquestFx";
import { Fx, FxType } from "../fx/Fx";
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
import { GameView, UnitView } from "../../../core/game/GameView";
import { ShockwaveFx, nukeFxFactory } from "../fx/NukeFx";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { Layer } from "./Layer";
import { SpriteFx } from "../fx/SpriteFx";
import { TextFx } from "../fx/TextFx";
import { Theme } from "../../../core/configuration/Config";
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
import { Layer } from "./Layer";
import { UnitType } from "../../../core/game/Game";
import { conquestFxFactory } from "../fx/ConquestFx";
import { renderNumber } from "../../Utils";
export class FxLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private lastRefresh = 0;
private refreshRate = 10;
private theme: Theme;
private animatedSpriteLoader: AnimatedSpriteLoader =
private readonly refreshRate = 10;
private readonly theme: Theme;
private readonly animatedSpriteLoader: AnimatedSpriteLoader =
new AnimatedSpriteLoader();
private allFx: Fx[] = [];
constructor(private game: GameView) {
constructor(private readonly game: GameView) {
this.theme = this.game.config().theme();
}
@@ -70,11 +71,11 @@ export class FxLayer implements Layer {
// Only display text fx for the current player
return;
}
const tile = bonus.tile;
const { tile } = bonus;
const x = this.game.x(tile);
let y = this.game.y(tile);
const gold = bonus.gold;
const troops = bonus.troops;
const { gold } = bonus;
const { troops } = bonus;
if (gold > 0) {
const shortened = renderNumber(gold, 0);
@@ -148,7 +149,7 @@ export class FxLayer implements Layer {
}
onRailroadEvent(railroad: RailroadUpdate) {
const railTiles = railroad.railTiles;
const { railTiles } = railroad;
for (const rail of railTiles) {
// No need for pseudorandom, this is fx
const chanceFx = Math.floor(Math.random() * 3);
+10 -8
View File
@@ -1,14 +1,14 @@
import { Colord } from "colord";
import { html, LitElement } from "lit";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Colord } from "colord";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import leaderboardRegularIcon from "../../../../resources/images/LeaderboardIconRegularWhite.svg";
import leaderboardSolidIcon from "../../../../resources/images/LeaderboardIconSolidWhite.svg";
import teamRegularIcon from "../../../../resources/images/TeamIconRegularWhite.svg";
import teamSolidIcon from "../../../../resources/images/TeamIconSolidWhite.svg";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
@customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Layer {
@@ -87,9 +87,11 @@ export class GameLeftSidebar extends LitElement implements Layer {
render() {
return html`
<aside
class=${`fixed top-[20px] left-0 z-[1000] flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40 backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg transition-transform duration-300 ease-out transform ${
this.isVisible ? "translate-x-0" : "-translate-x-full"
}`}
class=${`fixed top-[20px] left-0 z-[1000] flex flex-col
max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-slate-800/40
backdrop-blur-sm shadow-xs rounded-tr-lg rounded-br-lg
transition-transform duration-300 ease-out transform
${this.isVisible ? "translate-x-0" : "-translate-x-full"}`}
>
${this.isPlayerTeamLabelVisible
? html`
+17 -14
View File
@@ -1,20 +1,20 @@
import { html, LitElement } from "lit";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { PauseGameEvent } from "../../Transport";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
import pauseIcon from "../../../../resources/images/PauseIconWhite.svg";
import playIcon from "../../../../resources/images/PlayIconWhite.svg";
import replayRegularIcon from "../../../../resources/images/ReplayRegularIconWhite.svg";
import replaySolidIcon from "../../../../resources/images/ReplaySolidIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { PauseGameEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
@@ -64,7 +64,7 @@ export class GameRightSidebar extends LitElement implements Layer {
}
}
private secondsToHms = (d: number): string => {
private readonly secondsToHms = (d: number): string => {
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60);
@@ -109,9 +109,10 @@ export class GameRightSidebar extends LitElement implements Layer {
return html`
<aside
class=${`flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg transition-transform duration-300 ease-out transform ${
this._isVisible ? "translate-x-0" : "translate-x-full"
}`}
class=${`flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2
bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg
transition-transform duration-300 ease-out transform
${this._isVisible ? "translate-x-0" : "translate-x-full"}`}
@contextmenu=${(e: Event) => e.preventDefault()}
>
<div
@@ -139,7 +140,9 @@ export class GameRightSidebar extends LitElement implements Layer {
<!-- Timer display below buttons -->
<div class="flex justify-center items-center mt-2">
<div
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5 text-xs md:text-sm lg:text-base flex items-center justify-center text-white px-1"
class="w-[70px] h-8 lg:w-24 lg:h-10 border border-slate-400 p-0.5
text-xs md:text-sm lg:text-base flex items-center justify-center
text-white px-1"
>
${this.secondsToHms(this.timer)}
</div>
+13 -9
View File
@@ -1,8 +1,8 @@
import { EventBus, GameEvent } from "../../../core/EventBus";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { getGamesPlayed } from "../../Utils";
import { Layer } from "./Layer";
import { getGamesPlayed } from "../../Utils";
export class GutterAdModalEvent implements GameEvent {
constructor(public readonly isVisible: boolean) {}
@@ -18,11 +18,11 @@ export class GutterAdModal extends LitElement implements Layer {
@state()
private adLoaded = false;
private leftAdType = "left_rail";
private rightAdType = "right_rail";
private leftContainerId = "gutter-ad-container-left";
private rightContainerId = "gutter-ad-container-right";
private margin = "10px";
private readonly leftAdType = "left_rail";
private readonly rightAdType = "right_rail";
private readonly leftContainerId = "gutter-ad-container-left";
private readonly rightContainerId = "gutter-ad-container-right";
private readonly margin = "10px";
// Override createRenderRoot to disable shadow DOM
createRenderRoot() {
@@ -136,7 +136,9 @@ export class GutterAdModal extends LitElement implements Layer {
return html`
<!-- Left Gutter Ad -->
<div
class="hidden xl:flex fixed left-0 top-1/2 transform -translate-y-1/2 w-[160px] min-h-[600px] z-[10] pointer-events-auto items-center justify-center"
class="hidden xl:flex fixed left-0 top-1/2 transform -translate-y-1/2
w-[160px] min-h-[600px] z-[10] pointer-events-auto items-center
justify-center"
style="margin-left: ${this.margin};"
>
<div
@@ -147,7 +149,9 @@ export class GutterAdModal extends LitElement implements Layer {
<!-- Right Gutter Ad -->
<div
class="hidden xl:flex fixed right-0 top-1/2 transform -translate-y-1/2 w-[160px] min-h-[600px] z-[10] pointer-events-auto items-center justify-center"
class="hidden xl:flex fixed right-0 top-1/2 transform -translate-y-1/2
w-[160px] min-h-[600px] z-[10] pointer-events-auto items-center
justify-center"
style="margin-right: ${this.margin};"
>
<div
+1 -1
View File
@@ -1,8 +1,8 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { translateText } from "../../Utils";
@customElement("heads-up-message")
export class HeadsUpMessage extends LitElement implements Layer {
+9 -7
View File
@@ -1,11 +1,11 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { renderNumber } from "../../Utils";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Layer } from "./Layer";
import { renderNumber } from "../../Utils";
import { repeat } from "lit/directives/repeat.js";
import { translateText } from "../../../client/Utils";
type Entry = {
name: string;
@@ -115,7 +115,7 @@ export class Leaderboard extends LitElement implements Layer {
gold: renderNumber(player.gold()),
troops: renderNumber(troops),
isMyPlayer: player === myPlayer,
player: player,
player,
};
});
@@ -255,7 +255,9 @@ export class Leaderboard extends LitElement implements Layer {
</div>
<button
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto block"
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs
lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto
block"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
+27 -28
View File
@@ -1,45 +1,44 @@
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { PlayerActions } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { BuildMenu } from "./BuildMenu";
import { ChatIntegration } from "./ChatIntegration";
import { EmojiTable } from "./EmojiTable";
import { Layer } from "./Layer";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel";
import { RadialMenu, RadialMenuConfig } from "./RadialMenu";
import {
centerButtonElement,
COLORS,
MenuElementParams,
centerButtonElement,
rootMenuElement,
} from "./RadialMenuElements";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { RadialMenu, RadialMenuConfig } from "./RadialMenu";
import { BuildMenu } from "./BuildMenu";
import { ChatIntegration } from "./ChatIntegration";
import { ContextMenuEvent } from "../../InputHandler";
import { EmojiTable } from "./EmojiTable";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import { LitElement } from "lit";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerActions } from "../../../core/game/Game";
import { PlayerPanel } from "./PlayerPanel";
import { TileRef } from "../../../core/game/GameMap";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { customElement } from "lit/decorators.js";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
@customElement("main-radial-menu")
export class MainRadialMenu extends LitElement implements Layer {
private radialMenu: RadialMenu;
private readonly radialMenu: RadialMenu;
private playerActionHandler: PlayerActionHandler;
private chatIntegration: ChatIntegration;
private readonly playerActionHandler: PlayerActionHandler;
private readonly chatIntegration: ChatIntegration;
private clickedTile: TileRef | null = null;
constructor(
private eventBus: EventBus,
private game: GameView,
private transformHandler: TransformHandler,
private emojiTable: EmojiTable,
private buildMenu: BuildMenu,
private uiState: UIState,
private playerPanel: PlayerPanel,
private readonly eventBus: EventBus,
private readonly game: GameView,
private readonly transformHandler: TransformHandler,
private readonly emojiTable: EmojiTable,
private readonly buildMenu: BuildMenu,
private readonly uiState: UIState,
private readonly playerPanel: PlayerPanel,
) {
super();
+3 -2
View File
@@ -3,9 +3,9 @@ import { customElement, property, state } from "lit/decorators.js";
import { GameEnv } from "../../../core/configuration/Config";
import { GameType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { MultiTabDetector } from "../../MultiTabDetector";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
@customElement("multi-tab-modal")
export class MultiTabModal extends LitElement implements Layer {
@@ -121,7 +121,8 @@ export class MultiTabModal extends LitElement implements Layer {
class="fixed inset-0 z-50 overflow-auto bg-red-500/20 flex items-center justify-center"
>
<div
class="relative p-6 bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full m-4 transition-all transform"
class="relative p-6 bg-white dark:bg-gray-800 rounded-xl shadow-xl
max-w-md w-full m-4 transition-all transform"
>
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl font-bold text-red-600 dark:text-red-400">
+38 -39
View File
@@ -1,3 +1,13 @@
import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
import { AlternateViewEvent } from "../../InputHandler";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
import { UserSettings } from "../../../core/game/UserSettings";
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
import allianceRequestBlackIcon from "../../../../resources/images/AllianceRequestBlackIcon.svg";
import allianceRequestWhiteIcon from "../../../../resources/images/AllianceRequestWhiteIcon.svg";
@@ -7,20 +17,10 @@ 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 { renderPlayerFlag } from "../../../core/CustomFlag";
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
import targetIcon from "../../../../resources/images/TargetIcon.svg";
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent } from "../../InputHandler";
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
class RenderInfo {
public icons: Map<string, HTMLImageElement> = new Map(); // Track icon elements
@@ -38,33 +38,33 @@ class RenderInfo {
export class NameLayer implements Layer {
private canvas: HTMLCanvasElement;
private lastChecked = 0;
private renderCheckRate = 100;
private renderRefreshRate = 500;
private rand = new PseudoRandom(10);
private readonly renderCheckRate = 100;
private readonly renderRefreshRate = 500;
private readonly rand = new PseudoRandom(10);
private renders: RenderInfo[] = [];
private seenPlayers: Set<PlayerView> = new Set();
private traitorIconImage: HTMLImageElement;
private disconnectedIconImage: HTMLImageElement;
private allianceRequestBlackIconImage: HTMLImageElement;
private allianceRequestWhiteIconImage: HTMLImageElement;
private allianceIconImage: HTMLImageElement;
private targetIconImage: HTMLImageElement;
private crownIconImage: HTMLImageElement;
private embargoBlackIconImage: HTMLImageElement;
private embargoWhiteIconImage: HTMLImageElement;
private nukeWhiteIconImage: HTMLImageElement;
private nukeRedIconImage: HTMLImageElement;
private shieldIconImage: HTMLImageElement;
private readonly seenPlayers: Set<PlayerView> = new Set();
private readonly traitorIconImage: HTMLImageElement;
private readonly disconnectedIconImage: HTMLImageElement;
private readonly allianceRequestBlackIconImage: HTMLImageElement;
private readonly allianceRequestWhiteIconImage: HTMLImageElement;
private readonly allianceIconImage: HTMLImageElement;
private readonly targetIconImage: HTMLImageElement;
private readonly crownIconImage: HTMLImageElement;
private readonly embargoBlackIconImage: HTMLImageElement;
private readonly embargoWhiteIconImage: HTMLImageElement;
private readonly nukeWhiteIconImage: HTMLImageElement;
private readonly nukeRedIconImage: HTMLImageElement;
private readonly shieldIconImage: HTMLImageElement;
private container: HTMLDivElement;
private firstPlace: PlayerView | null = null;
private theme: Theme = this.game.config().theme();
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
private isVisible = true;
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private eventBus: EventBus,
private readonly game: GameView,
private readonly transformHandler: TransformHandler,
private readonly eventBus: EventBus,
) {
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
@@ -185,7 +185,9 @@ export class NameLayer implements Layer {
screenPosOld.x - window.innerWidth / 2,
screenPosOld.y - window.innerHeight / 2,
);
this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`;
this.container.style.transform =
`translate(${screenPos.x}px, ${screenPos.y}px) ` +
`scale(${this.transformHandler.scale})`;
const now = Date.now();
if (now > this.lastChecked + this.renderCheckRate) {
@@ -231,7 +233,7 @@ export class NameLayer implements Layer {
};
if (player.cosmetics.flag) {
const flag = player.cosmetics.flag;
const { flag } = player.cosmetics;
if (flag !== undefined && flag !== null && flag.startsWith("!")) {
const flagWrapper = document.createElement("div");
applyFlagStyles(flagWrapper);
@@ -531,17 +533,13 @@ export class NameLayer implements Layer {
// Embargo icon
let existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
const hasEmbargo =
myPlayer &&
(render.player.hasEmbargoAgainst(myPlayer) ||
myPlayer.hasEmbargoAgainst(render.player));
const isThemeEmbargoIcon =
existingEmbargo?.getAttribute("dark-mode") === isDarkMode.toString();
const embargoIconImageSrc = isDarkMode
? this.embargoWhiteIconImage.src
: this.embargoBlackIconImage.src;
if (myPlayer && hasEmbargo) {
if (myPlayer?.hasEmbargo(render.player)) {
// Create new icon to match theme
if (existingEmbargo && !isThemeEmbargoIcon) {
existingEmbargo.remove();
@@ -609,7 +607,8 @@ export class NameLayer implements Layer {
// Position element with scale
if (render.location && render.location !== oldLocation) {
const scale = Math.min(baseSize * 0.25, 3);
render.element.style.transform = `translate(${render.location.x}px, ${render.location.y}px) translate(-50%, -50%) scale(${scale})`;
render.element.style.transform =
`translate(${render.location.x}px, ${render.location.y}px) translate(-50%, -50%) scale(${scale})`;
}
}
+10 -8
View File
@@ -1,14 +1,14 @@
import { html, LitElement } from "lit";
import { AlternateViewEvent, RedrawGraphicsEvent } from "../../InputHandler";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { PauseGameEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { PauseGameEvent } from "../../Transport";
import { UserSettings } from "../../../core/game/UserSettings";
import { translateText } from "../../Utils";
const button = ({
classes = "",
@@ -45,7 +45,7 @@ const secondsToHms = (d: number): string => {
export class OptionsMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private userSettings: UserSettings = new UserSettings();
private readonly userSettings: UserSettings = new UserSettings();
@state()
private showPauseButton = true;
@@ -116,7 +116,7 @@ export class OptionsMenu extends LitElement implements Layer {
private onToggleDarkModeButtonClick() {
this.userSettings.toggleDarkMode();
this.requestUpdate();
this.eventBus.emit(new RefreshGraphicsEvent());
this.eventBus.emit(new RedrawGraphicsEvent());
}
private onToggleRandomNameModeButtonClick() {
@@ -205,7 +205,9 @@ export class OptionsMenu extends LitElement implements Layer {
</div>
<div
class="options-menu flex flex-col justify-around gap-y-3 mt-2 bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-lg backdrop-blur-md ${!this
class="options-menu flex flex-col justify-around gap-y-3 mt-2
bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-lg backdrop-blur-md
${!this
.showSettings
? "hidden"
: ""}"
@@ -1,7 +1,4 @@
import { EventBus } from "../../../core/EventBus";
import { PlayerActions, PlayerID } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { PlayerView } from "../../../core/game/GameView";
import {
SendAllianceRequestIntentEvent,
SendAttackIntentEvent,
@@ -16,12 +13,15 @@ import {
SendSpawnIntentEvent,
SendTargetPlayerIntentEvent,
} from "../../Transport";
import { EventBus } from "../../../core/EventBus";
import { PlayerView } from "../../../core/game/GameView";
import { TileRef } from "../../../core/game/GameMap";
import { UIState } from "../UIState";
export class PlayerActionHandler {
constructor(
private eventBus: EventBus,
private uiState: UIState,
private readonly eventBus: EventBus,
private readonly uiState: UIState,
) {}
async getPlayerActions(
@@ -97,8 +97,8 @@ export class PlayerActionHandler {
this.eventBus.emit(new SendEmojiIntentEvent(targetPlayer, emojiIndex));
}
handleQuickChat(recipient: PlayerView, chatKey: string, params: any = {}) {
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, params));
handleQuickChat(recipient: PlayerView, chatKey: string, target?: PlayerID) {
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, target));
}
handleDeleteUnit(unitId: number) {
+13 -11
View File
@@ -1,9 +1,6 @@
import { ContextMenuEvent, MouseMoveEvent } from "../../InputHandler";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
PlayerProfile,
PlayerType,
@@ -11,13 +8,16 @@ import {
Unit,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ContextMenuEvent, MouseMoveEvent } from "../../InputHandler";
import { customElement, property, state } from "lit/decorators.js";
import { renderNumber, renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import { TileRef } from "../../../core/game/GameMap";
import { TransformHandler } from "../TransformHandler";
import { ref } from "lit-html/directives/ref.js";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { translateText } from "../../../client/Utils";
function euclideanDistWorld(
coord: { x: number; y: number },
@@ -356,7 +356,9 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<div
class="bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg shadow-lg transition-all duration-300 text-white text-lg md:text-base ${containerClasses}"
class="bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-lg shadow-lg
transition-all duration-300 text-white text-lg md:text-base
${containerClasses}"
>
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
+15 -15
View File
@@ -1,19 +1,7 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { AllPlayers, PlayerActions } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { flattenedEmojiTable } from "../../../core/Util";
import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { LitElement, html } from "lit";
import {
SendAllianceRequestIntentEvent,
SendBreakAllianceIntentEvent,
@@ -23,11 +11,23 @@ import {
SendEmojiIntentEvent,
SendTargetPlayerIntentEvent,
} from "../../Transport";
import { customElement, state } from "lit/decorators.js";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import { ChatModal } from "./ChatModal";
import { EmojiTable } from "./EmojiTable";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import { TileRef } from "../../../core/game/GameMap";
import { UIState } from "../UIState";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import { flattenedEmojiTable } from "../../../core/Util";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import { translateText } from "../../../client/Utils";
@customElement("player-panel")
export class PlayerPanel extends LitElement implements Layer {
+40 -33
View File
@@ -1,15 +1,16 @@
/* eslint-disable max-lines */
import * as d3 from "d3";
import backIcon from "../../../../resources/images/BackIconWhite.svg";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import {
CenterButtonElement,
MenuElement,
MenuElementParams,
TooltipKey,
} from "./RadialMenuElements";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { Layer } from "./Layer";
import backIcon from "../../../../resources/images/BackIconWhite.svg";
import { translateText } from "../../Utils";
export class CloseRadialMenuEvent implements GameEvent {
constructor() {}
@@ -55,20 +56,20 @@ export class RadialMenu implements Layer {
private isTransitioning = false;
private lastHideTime = 0;
private reopenCooldownMs = 300;
private readonly reopenCooldownMs = 300;
private anchorX = 0;
private anchorY = 0;
private menuGroups: Map<
private readonly menuGroups: Map<
number,
d3.Selection<SVGGElement, unknown, null, undefined>
> = new Map();
private menuPaths: Map<
private readonly menuPaths: Map<
string,
d3.Selection<SVGPathElement, unknown, null, undefined>
> = new Map();
private menuIcons: Map<
private readonly menuIcons: Map<
string,
d3.Selection<SVGImageElement, unknown, null, undefined>
> = new Map();
@@ -77,14 +78,14 @@ export class RadialMenu implements Layer {
private submenuHoverTimeout: number | null = null;
private backButtonHoverTimeout: number | null = null;
private navigationInProgress = false;
private originalCenterButtonIcon = "";
private readonly originalCenterButtonIcon: string = "";
private params: MenuElementParams | null = null;
constructor(
private eventBus: EventBus,
private rootMenu: MenuElement,
private centerButtonElement: CenterButtonElement,
private readonly eventBus: EventBus,
private readonly rootMenu: MenuElement,
private readonly centerButtonElement: CenterButtonElement,
config: RadialMenuConfig = {},
) {
this.config = {
@@ -131,7 +132,7 @@ export class RadialMenu implements Layer {
this.hideRadialMenu();
this.eventBus.emit(new CloseRadialMenuEvent());
})
.on("contextmenu", (e) => {
.on("contextmenu", (e: Event) => {
e.preventDefault();
this.hideRadialMenu();
this.eventBus.emit(new CloseRadialMenuEvent());
@@ -149,7 +150,9 @@ export class RadialMenu implements Layer {
.style("position", "absolute")
.style("top", "50%")
.style("left", "50%")
.style("transition", `top ${this.config.menuTransitionDuration}ms ease, left ${this.config.menuTransitionDuration}ms ease`)
.style("transition", `top ${
this.config.menuTransitionDuration}ms ease, left ${
this.config.menuTransitionDuration}ms ease`)
.style("transform", "translate(-50%, -50%)")
.style("pointer-events", "all")
.on("click", (event) => this.hideRadialMenu());
@@ -178,7 +181,7 @@ export class RadialMenu implements Layer {
.attr("r", this.config.centerButtonSize)
.attr("fill", "transparent")
.style("cursor", "pointer")
.on("click", (event) => {
.on("click", (event: Event) => {
event.stopPropagation();
this.handleCenterButtonClick();
})
@@ -266,7 +269,7 @@ export class RadialMenu implements Layer {
menuGroup.style("opacity", 0).style("transform", "scale(0.5)");
}
this.menuGroups.set(level, menuGroup as any);
this.menuGroups.set(level, menuGroup);
const offset = -Math.PI / items.length;
@@ -307,7 +310,7 @@ export class RadialMenu implements Layer {
SVGGElement,
unknown
>,
arc: d3.Arc<any, d3.PieArcDatum<MenuElement>>,
arc: d3.Arc<unknown, d3.PieArcDatum<MenuElement>>,
level: number,
) {
arcs
@@ -325,7 +328,7 @@ export class RadialMenu implements Layer {
return color;
}
return d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color;
return d3.color(color)?.copy({ opacity })?.toString() ?? color;
})
.attr("stroke", "#ffffff")
.attr("stroke-width", "2")
@@ -348,7 +351,7 @@ export class RadialMenu implements Layer {
arcs.each((d) => {
const pathId = d.data.id;
const path = d3.select(`path[data-id="${pathId}"]`);
this.menuPaths.set(pathId, path as any);
this.menuPaths.set(pathId, path as never);
if (
pathId === this.selectedItemId &&
@@ -388,7 +391,9 @@ export class RadialMenu implements Layer {
>,
level: number,
) {
const onHover = (d: d3.PieArcDatum<MenuElement>, path: any) => {
const onHover = (d: d3.PieArcDatum<MenuElement>, path: d3.Selection<
d3.BaseType, unknown, HTMLElement, unknown
>) => {
const disabled = this.params === null || d.data.disabled(this.params);
if (d.data.tooltipItems && d.data.tooltipItems.length > 0) {
this.showTooltip(d.data.tooltipItems);
@@ -407,7 +412,9 @@ export class RadialMenu implements Layer {
path.attr("stroke-width", "3");
};
const onMouseOut = (d: d3.PieArcDatum<MenuElement>, path: any) => {
const onMouseOut = (d: d3.PieArcDatum<MenuElement>, path: d3.Selection<
d3.BaseType, unknown, HTMLElement, unknown
>) => {
const disabled = this.params === null || d.data.disabled(this.params);
if (this.submenuHoverTimeout !== null) {
window.clearTimeout(this.submenuHoverTimeout);
@@ -431,7 +438,7 @@ export class RadialMenu implements Layer {
const opacity = disabled ? 0.5 : 0.7;
path.attr(
"fill",
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
d3.color(color)?.copy({ opacity })?.toString() ?? color,
);
};
@@ -487,15 +494,15 @@ export class RadialMenu implements Layer {
onMouseOut(d, path);
});
path.on("mousemove", function (event) {
handleMouseMove(event as MouseEvent);
path.on("mousemove", function (event: MouseEvent) {
handleMouseMove(event);
});
path.on("click", function (event) {
path.on("click", function (event: Event) {
onClick(d, event);
});
path.on("touchstart", function (event) {
path.on("touchstart", function (event: Event) {
event.preventDefault();
event.stopPropagation();
onClick(d, event);
@@ -518,7 +525,7 @@ export class RadialMenu implements Layer {
SVGGElement,
unknown
>,
arc: d3.Arc<any, d3.PieArcDatum<MenuElement>>,
arc: d3.Arc<unknown, d3.PieArcDatum<MenuElement>>,
) {
arcs
.append("g")
@@ -553,7 +560,7 @@ export class RadialMenu implements Layer {
.attr("opacity", disabled ? 0.5 : 1);
}
this.menuIcons.set(contentId, content as any);
this.menuIcons.set(contentId, content as never);
});
}
@@ -735,8 +742,8 @@ export class RadialMenu implements Layer {
});
}
private animateExistingMenu(
previousMenu: d3.Selection<any, unknown, null, undefined>,
private animateExistingMenu<T extends d3.BaseType>(
previousMenu: d3.Selection<T, unknown, null, undefined>,
) {
previousMenu
.transition()
@@ -976,7 +983,7 @@ export class RadialMenu implements Layer {
// Update path appearance
path.attr(
"fill",
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
d3.color(color)?.copy({ opacity })?.toString() ?? color,
);
path.style("opacity", disabled ? 0.5 : 1);
path.style("cursor", disabled ? "not-allowed" : "pointer");
@@ -1062,7 +1069,7 @@ export class RadialMenu implements Layer {
.style("left", `${clampedX}px`);
}
private handleResize = () => {
private readonly handleResize = () => {
if (this.isVisible) this.clampAndSetMenuPositionForLevel(this.currentLevel);
};
}
@@ -1,16 +1,15 @@
import { Config } from "../../../core/configuration/Config";
import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { flattenedEmojiTable } from "../../../core/Util";
import { renderNumber, translateText } from "../../Utils";
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderNumber, translateText } from "../../Utils";
import { ChatIntegration } from "./ChatIntegration";
import { Config } from "../../../core/configuration/Config";
import { EmojiTable } from "./EmojiTable";
import { EventBus } from "../../../core/EventBus";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel";
import { TileRef } from "../../../core/game/GameMap";
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";
@@ -18,12 +17,12 @@ 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 { flattenedEmojiTable } from "../../../core/Util";
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";
export type MenuElementParams = {
myPlayer: PlayerView;
+8 -8
View File
@@ -1,15 +1,15 @@
import { Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { PlayerID } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import {
GameUpdateType,
RailroadUpdate,
RailTile,
RailType,
RailroadUpdate,
} from "../../../core/game/GameUpdates";
import { Colord } from "colord";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { PlayerID } from "../../../core/game/Game";
import { Theme } from "../../../core/configuration/Config";
import { TileRef } from "../../../core/game/GameMap";
import { getRailroadRects } from "./RailroadSprites";
type RailRef = {
@@ -21,13 +21,13 @@ type RailRef = {
export class RailroadLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private theme: Theme;
private readonly theme: Theme;
// Save the number of railroads per tiles. Delete when it reaches 0
private existingRailroads = new Map<TileRef, RailRef>();
private readonly existingRailroads = new Map<TileRef, RailRef>();
private nextRailIndexToCheck = 0;
private railTileList: TileRef[] = [];
constructor(private game: GameView) {
constructor(private readonly game: GameView) {
this.theme = game.config().theme();
}
+7 -7
View File
@@ -1,14 +1,14 @@
import { html, LitElement } from "lit";
import { LitElement, html } from "lit";
import {
ReplaySpeedMultiplier,
defaultReplaySpeedMultiplier,
} from "../../utilities/ReplaySpeedMultiplier";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { ReplaySpeedChangeEvent } from "../../InputHandler";
import {
defaultReplaySpeedMultiplier,
ReplaySpeedMultiplier,
} from "../../utilities/ReplaySpeedMultiplier";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { ReplaySpeedChangeEvent } from "../../InputHandler";
import { translateText } from "../../Utils";
export class ShowReplayPanelEvent {
constructor(
+30 -21
View File
@@ -1,6 +1,10 @@
import { html, LitElement } from "lit";
import { AlternateViewEvent, RedrawGraphicsEvent } from "../../InputHandler";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
import { PauseGameEvent } from "../../Transport";
import { UserSettings } from "../../../core/game/UserSettings";
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
@@ -8,13 +12,9 @@ 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 treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { PauseGameEvent } from "../../Transport";
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
export class ShowSettingsModalEvent {
constructor(
@@ -36,7 +36,7 @@ export class SettingsModal extends LitElement implements Layer {
private alternateView = false;
@query(".modal-overlay")
private modalOverlay!: HTMLElement;
private readonly modalOverlay!: HTMLElement;
@property({ type: Boolean })
shouldPause = false;
@@ -69,7 +69,7 @@ export class SettingsModal extends LitElement implements Layer {
super.disconnectedCallback();
}
private handleOutsideClick = (event: MouseEvent) => {
private readonly handleOutsideClick = (event: MouseEvent) => {
if (
this.isVisible &&
this.modalOverlay &&
@@ -79,7 +79,7 @@ export class SettingsModal extends LitElement implements Layer {
}
};
private handleKeyDown = (event: KeyboardEvent) => {
private readonly handleKeyDown = (event: KeyboardEvent) => {
if (this.isVisible && event.key === "Escape") {
this.closeModal();
}
@@ -126,7 +126,7 @@ export class SettingsModal extends LitElement implements Layer {
private onToggleDarkModeButtonClick() {
this.userSettings.toggleDarkMode();
this.eventBus.emit(new RefreshGraphicsEvent());
this.eventBus.emit(new RedrawGraphicsEvent());
this.requestUpdate();
}
@@ -188,7 +188,8 @@ export class SettingsModal extends LitElement implements Layer {
<div class="p-4 space-y-3">
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onTerrainButtonClick}"
>
<img src=${treeIcon} alt="treeIcon" width="20" height="20" />
@@ -210,7 +211,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleEmojisButtonClick}"
>
<img src=${emojiIcon} alt="emojiIcon" width="20" height="20" />
@@ -232,7 +234,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleDarkModeButtonClick}"
>
<img
@@ -259,7 +262,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleSpecialEffectsButtonClick}"
>
<img
@@ -286,7 +290,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleStructureSpritesButtonClick}"
>
<img
@@ -313,7 +318,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleRandomNameModeButtonClick}"
>
<img src=${ninjaIcon} alt="ninjaIcon" width="20" height="20" />
@@ -335,7 +341,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleLeftClickOpensMenu}"
>
<img src=${mouseIcon} alt="mouseIcon" width="20" height="20" />
@@ -357,7 +364,8 @@ export class SettingsModal extends LitElement implements Layer {
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onTogglePerformanceOverlayButtonClick}"
>
<img
@@ -387,7 +395,8 @@ export class SettingsModal extends LitElement implements Layer {
<div class="border-t border-slate-600 pt-3 mt-4">
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-red-600/20 rounded text-red-400 transition-colors"
class="flex gap-3 items-center w-full text-left p-3
hover:bg-red-600/20 rounded text-red-400 transition-colors"
@click="${this.onExitButtonClick}"
>
<img src=${exitIcon} alt="exitIcon" width="20" height="20" />
+4 -3
View File
@@ -1,9 +1,9 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { GameView } from "../../../core/game/GameView";
import { getGamesPlayed } from "../../Utils";
import { Layer } from "./Layer";
import { getGamesPlayed } from "../../Utils";
import { translateText } from "../../../client/Utils";
const AD_TYPE = "bottom_rail";
const AD_CONTAINER_ID = "bottom-rail-ad-container";
@@ -117,7 +117,8 @@ export class SpawnAd extends LitElement implements Layer {
return html`
<div
class="fixed bottom-0 left-0 w-full min-h-[100px] bg-gray-900 border border-gray-600 flex items-center justify-center z-50"
class="fixed bottom-0 left-0 w-full min-h-[100px] bg-gray-900 border
border-gray-600 flex items-center justify-center z-50"
>
<div
id="${AD_CONTAINER_ID}"
+3 -3
View File
@@ -1,15 +1,15 @@
import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { TransformHandler } from "../TransformHandler";
export class SpawnTimer implements Layer {
private ratios = [0];
private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"];
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly transformHandler: TransformHandler,
) {}
init() {}
@@ -1,20 +1,20 @@
import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
import { Cell, PlayerID, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { Layer } from "./Layer";
import { OutlineFilter } from "pixi-filters";
import SAMMissileIcon from "../../../../resources/images/SamLauncherUnit.png";
import { Theme } from "../../../core/configuration/Config";
import { ToggleStructureEvent } from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "../../../../resources/images/AnchorIcon.png";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
import cityIcon from "../../../../resources/images/CityIcon.png";
import factoryIcon from "../../../../resources/images/FactoryUnit.png";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import SAMMissileIcon from "../../../../resources/images/SamLauncherUnit.png";
import shieldIcon from "../../../../resources/images/ShieldIcon.png";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerID, UnitType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ToggleStructureEvent } from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
type ShapeType = "triangle" | "square" | "pentagon" | "octagon" | "circle";
@@ -59,12 +59,12 @@ export class StructureIconsLayer implements Layer {
private levelsStage: PIXI.Container;
private dotsStage: PIXI.Container;
private shouldRedraw = true;
private textureCache: Map<string, PIXI.Texture> = new Map();
private theme: Theme;
private readonly textureCache: Map<string, PIXI.Texture> = new Map();
private readonly theme: Theme;
private renderer: PIXI.Renderer;
private renders: StructureRenderInfo[] = [];
private seenUnits: Set<UnitView> = new Set();
private structures: Map<
private readonly seenUnits: Set<UnitView> = new Set();
private readonly structures: Map<
UnitType,
{ visible: boolean; iconPath: string; image: HTMLImageElement | null }
> = new Map([
@@ -87,9 +87,9 @@ export class StructureIconsLayer implements Layer {
private renderSprites = true;
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
this.structures.forEach((u, unitType) => this.loadIcon(u, unitType));
@@ -532,7 +532,7 @@ export class StructureIconsLayer implements Layer {
const screenPos = this.transformHandler.worldToScreenCoordinates(worldPos);
const { type, stage } = options;
const scale = this.transformHandler.scale;
const { scale } = this.transformHandler;
const spritesEnabled = this.game
.config()
.userSettings()
@@ -612,7 +612,7 @@ export class StructureIconsLayer implements Layer {
const screenPos = this.transformHandler.worldToScreenCoordinates(worldPos);
screenPos.x = Math.round(screenPos.x);
const scale = this.transformHandler.scale;
const { scale } = this.transformHandler;
screenPos.y = Math.round(
scale >= ZOOM_THRESHOLD &&
this.game.config().userSettings()?.structureSprites()
+17 -18
View File
@@ -1,19 +1,18 @@
import { colord, Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { Cell, UnitType } from "../../../core/game/Game";
import { Colord, colord } from "colord";
import { GameView, UnitView } from "../../../core/game/GameView";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { EventBus } from "../../../core/EventBus";
import { TransformHandler } from "../TransformHandler";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { Layer } from "./Layer";
import SAMMissileIcon from "../../../../resources/non-commercial/images/buildings/silo4.png";
import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "../../../../resources/non-commercial/images/buildings/port1.png";
import cityIcon from "../../../../resources/non-commercial/images/buildings/cityAlt1.png";
import factoryIcon from "../../../../resources/non-commercial/images/buildings/factoryAlt1.png";
import shieldIcon from "../../../../resources/non-commercial/images/buildings/fortAlt3.png";
import anchorIcon from "../../../../resources/non-commercial/images/buildings/port1.png";
import missileSiloIcon from "../../../../resources/non-commercial/images/buildings/silo1.png";
import SAMMissileIcon from "../../../../resources/non-commercial/images/buildings/silo4.png";
import { Cell, UnitType } from "../../../core/game/Game";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import shieldIcon from "../../../../resources/non-commercial/images/buildings/fortAlt3.png";
const underConstructionColor = colord({ r: 150, g: 150, b: 150 });
@@ -32,10 +31,10 @@ type UnitRenderConfig = {
export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private unitIcons: Map<string, HTMLImageElement> = new Map();
private theme: Theme;
private tempCanvas: HTMLCanvasElement;
private tempContext: CanvasRenderingContext2D;
private readonly unitIcons: Map<string, HTMLImageElement> = new Map();
private readonly theme: Theme;
private readonly tempCanvas: HTMLCanvasElement;
private readonly tempContext: CanvasRenderingContext2D;
// Configuration for supported unit types only
private readonly unitConfigs: Partial<Record<UnitType, UnitRenderConfig>> = {
@@ -72,9 +71,9 @@ export class StructureLayer implements Layer {
};
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
this.tempCanvas = document.createElement("canvas");
+106 -57
View File
@@ -1,9 +1,9 @@
import { GameMode, Team, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameMode, Team } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderNumber, translateText } from "../../Utils";
import { EventBus } from "../../../core/EventBus";
import { Layer } from "./Layer";
type TeamEntry = {
@@ -11,6 +11,10 @@ type TeamEntry = {
totalScoreStr: string;
totalGold: string;
totalTroops: string;
totalSAMs: string;
totalLaunchers: string;
totalWarShips: string;
totalCities: string;
totalScoreSort: number;
players: PlayerView[];
};
@@ -23,6 +27,7 @@ export class TeamStats extends LitElement implements Layer {
@property({ type: Boolean }) visible = false;
teams: TeamEntry[] = [];
private _shownOnInit = false;
private showUnits = false;
createRenderRoot() {
return this; // use light DOM for Tailwind
@@ -61,12 +66,20 @@ export class TeamStats extends LitElement implements Layer {
let totalGold = 0n;
let totalTroops = 0;
let totalScoreSort = 0;
let totalSAMs = 0;
let totalLaunchers = 0;
let totalWarShips = 0;
let totalCities = 0;
for (const p of teamPlayers) {
if (p.isAlive()) {
totalTroops += p.troops();
totalGold += p.gold();
totalScoreSort += p.numTilesOwned();
totalLaunchers += p.totalUnitLevels(UnitType.MissileSilo);
totalSAMs += p.totalUnitLevels(UnitType.SAMLauncher);
totalWarShips += p.totalUnitLevels(UnitType.Warship);
totalCities += p.totalUnitLevels(UnitType.City);
}
}
@@ -79,6 +92,11 @@ export class TeamStats extends LitElement implements Layer {
totalGold: renderNumber(totalGold),
totalTroops: renderNumber(totalTroops / 10),
players: teamPlayers,
totalLaunchers: renderNumber(totalLaunchers),
totalSAMs: renderNumber(totalSAMs),
totalWarShips: renderNumber(totalWarShips),
totalCities: renderNumber(totalCities),
};
})
.sort((a, b) => b.totalScoreSort - a.totalScoreSort);
@@ -93,73 +111,104 @@ export class TeamStats extends LitElement implements Layer {
}
render() {
if (!this.visible) {
return html``;
}
if (!this.visible) return html``;
return html`
<div
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm ${this
.visible
? ""
: "hidden"}"
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<div
class="grid w-full"
style="grid-template-columns: 1fr 1fr 1fr 1fr;"
style="grid-template-columns: repeat(${this.showUnits ? 5 : 4}, 1fr);"
>
<!-- Header row -->
<!-- Header -->
<div class="contents font-bold bg-slate-700/50">
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
<div class="py-1.5 md:py-2.5 text-center border-b border-slate-500">
${translateText("leaderboard.team")}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
${translateText("leaderboard.owned")}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
${translateText("leaderboard.gold")}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500 cursor-pointer"
>
${translateText("leaderboard.troops")}
</div>
${this.showUnits
? html`
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.launchers")}
</div>
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.sams")}
</div>
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.warships")}
</div>
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.cities")}
</div>
`
: html`
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.owned")}
</div>
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.gold")}
</div>
<div class="py-1.5 text-center border-b border-slate-500">
${translateText("leaderboard.troops")}
</div>
`}
</div>
${this.teams.map(
(team) => html`
<div
class="contents hover:bg-slate-600/60 text-center cursor-pointer"
>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.teamName}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalScoreStr}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalGold}
</div>
<div
class="py-1.5 md:py-2.5 text-center border-b border-slate-500"
>
${team.totalTroops}
</div>
</div>
`,
<!-- Data rows -->
${this.teams.map((team) =>
this.showUnits
? html`
<div
class="contents hover:bg-slate-600/60 text-center cursor-pointer"
>
<div class="py-1.5 border-b border-slate-500">
${team.teamName}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalLaunchers}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalSAMs}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalWarShips}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalCities}
</div>
</div>
`
: html`
<div
class="contents hover:bg-slate-600/60 text-center cursor-pointer"
>
<div class="py-1.5 border-b border-slate-500">
${team.teamName}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalScoreStr}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalGold}
</div>
<div class="py-1.5 border-b border-slate-500">
${team.totalTroops}
</div>
</div>
`,
)}
</div>
<button
class="team-stats-button"
aria-pressed=${String(this.showUnits)}
@click=${() => {
this.showUnits = !this.showUnits;
this.requestUpdate();
}}
>
${this.showUnits ? translateText("leaderboard.show_control") : translateText("leaderboard.show_units")}
</button>
</div>
`;
}
+4 -4
View File
@@ -1,7 +1,7 @@
import { Theme } from "../../../core/configuration/Config";
import { GameView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
export class TerrainLayer implements Layer {
private canvas: HTMLCanvasElement;
@@ -10,8 +10,8 @@ export class TerrainLayer implements Layer {
private theme: Theme;
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly transformHandler: TransformHandler,
) {}
shouldTransform(): boolean {
return true;
+82 -63
View File
@@ -1,24 +1,24 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { Colord } from "colord";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { PseudoRandom } from "../../../core/PseudoRandom";
import {
AlternateViewEvent,
DragEvent,
MouseOverEvent,
RefreshGraphicsEvent,
RedrawGraphicsEvent,
} from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TileRef, euclDistFN } from "../../../core/game/GameMap";
import { Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { Layer } from "./Layer";
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
import { UserSettings } from "../../../core/game/UserSettings";
export class TerritoryLayer implements Layer {
private userSettings: UserSettings;
private readonly userSettings: UserSettings;
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private imageData: ImageData;
@@ -26,14 +26,14 @@ export class TerritoryLayer implements Layer {
private cachedTerritoryPatternsEnabled: boolean | undefined;
private tileToRenderQueue: PriorityQueue<{
private readonly tileToRenderQueue: PriorityQueue<{
tile: TileRef;
lastUpdate: number;
}> = new PriorityQueue((a, b) => {
return a.lastUpdate - b.lastUpdate;
});
private random = new PseudoRandom(123);
private theme: Theme;
private readonly random = new PseudoRandom(123);
private readonly theme: Theme;
// Used for spawn highlighting
private highlightCanvas: HTMLCanvasElement;
@@ -42,19 +42,19 @@ export class TerritoryLayer implements Layer {
private highlightedTerritory: PlayerView | null = null;
private alternativeView = false;
private lastDragTime = 0;
private nodrawDragDuration = 200;
private readonly lastDragTime = 0;
private readonly nodrawDragDuration = 200;
private lastMousePosition: { x: number; y: number } | null = null;
private refreshRate = 10; //refresh every 10ms
private readonly refreshRate = 10; //refresh every 10ms
private lastRefresh = 0;
private lastFocusedPlayer: PlayerView | null = null;
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly transformHandler: TransformHandler,
userSettings: UserSettings,
) {
this.userSettings = userSettings;
@@ -77,7 +77,7 @@ export class TerritoryLayer implements Layer {
const prev = this.cachedTerritoryPatternsEnabled;
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
if (prev !== undefined && prev !== this.cachedTerritoryPatternsEnabled) {
this.eventBus.emit(new RefreshGraphicsEvent());
this.eventBus.emit(new RedrawGraphicsEvent());
}
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
const updates = this.game.updatesSinceLastTick();
@@ -104,10 +104,8 @@ export class TerritoryLayer implements Layer {
if (myPlayer) {
updates?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
const territory = this.game.playerBySmallID(update.betrayedID);
console.log("betrayedID", update.betrayedID);
console.log("territory", territory);
if (territory && territory instanceof PlayerView) {
this.redrawTerritory(territory);
this.redrawBorder(territory);
}
});
@@ -123,10 +121,23 @@ export class TerritoryLayer implements Layer {
: update.request.requestorID;
const territory = this.game.playerBySmallID(territoryId);
if (territory && territory instanceof PlayerView) {
this.redrawTerritory(territory);
this.redrawBorder(territory);
}
}
});
updates?.[GameUpdateType.EmbargoEvent]?.forEach((update) => {
const player = this.game.playerBySmallID(update.playerID) as PlayerView;
const embargoed = this.game.playerBySmallID(
update.embargoedID,
) as PlayerView;
if (
player.id() === myPlayer?.id() ||
embargoed.id() === myPlayer?.id()
) {
this.redrawBorder(player, embargoed);
}
});
}
const focusedPlayer = this.game.focusedPlayer();
@@ -237,7 +248,7 @@ export class TerritoryLayer implements Layer {
if (this.highlightedTerritory) {
territories.push(this.highlightedTerritory);
}
this.redrawTerritory(territories);
this.redrawBorder(...territories);
}
}
@@ -298,16 +309,15 @@ export class TerritoryLayer implements Layer {
});
}
redrawTerritory(territory: PlayerView | PlayerView[]) {
const territories = Array.isArray(territory) ? territory : [territory];
const territorySet = new Set(territories);
this.game.forEachTile((t) => {
const owner = this.game.owner(t) as PlayerView;
if (territorySet.has(owner)) {
this.paintTerritory(t);
}
});
redrawBorder(...players: PlayerView[]) {
return Promise.all(
players.map(async (player) => {
const tiles = await player.borderTiles();
tiles.borderTiles.forEach((tile: TileRef) => {
this.paintTerritory(tile, true);
});
}),
);
}
initImageData() {
@@ -383,7 +393,7 @@ export class TerritoryLayer implements Layer {
break;
}
const tile = entry.tile;
const { tile } = entry;
this.paintTerritory(tile);
for (const neighbor of this.game.neighbors(tile)) {
this.paintTerritory(neighbor, true);
@@ -419,12 +429,7 @@ export class TerritoryLayer implements Layer {
if (this.game.isBorder(tile)) {
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
if (myPlayer) {
let alternativeColor = owner.isFriendly(myPlayer)
? this.theme.allyColor()
: this.theme.enemyColor();
if (owner.smallID() === myPlayer.smallID()) {
alternativeColor = this.theme.selfColor();
}
const alternativeColor = this.alternateViewColor(owner);
this.paintTile(this.alternativeImageData, tile, alternativeColor, 255);
}
if (
@@ -449,25 +454,12 @@ export class TerritoryLayer implements Layer {
this.paintTile(this.imageData, tile, useBorderColor, 255);
}
} else {
const pattern = owner.cosmetics.pattern;
// Interior tiles
const { pattern } = owner.cosmetics;
const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false;
if (myPlayer) {
let alternativeColor = owner.isFriendly(myPlayer)
? this.theme.allyColor()
: this.theme.enemyColor();
// If the current player is the owner
if (owner.smallID() === myPlayer.smallID()) {
alternativeColor = this.theme.selfColor();
}
// If the tile is on a ally territory, use the ally color
this.paintTile(
this.alternativeImageData,
tile,
alternativeColor,
isHighlighted ? 150 : 60,
);
}
// Alternative view only shows borders.
this.clearAlternativeTile(tile);
if (pattern === undefined || patternsEnabled === false) {
this.paintTile(
@@ -490,6 +482,28 @@ export class TerritoryLayer implements Layer {
}
}
alternateViewColor(other: PlayerView): Colord {
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
return this.theme.neutralColor();
}
if (other.smallID() === myPlayer.smallID()) {
return this.theme.selfColor();
}
if (other.isFriendly(myPlayer)) {
return this.theme.allyColor();
}
if (!other.hasEmbargo(myPlayer)) {
return this.theme.neutralColor();
}
return this.theme.enemyColor();
}
paintAlternateViewTile(tile: TileRef, other: PlayerView) {
const color = this.alternateViewColor(other);
this.paintTile(this.alternativeImageData, tile, color, 255);
}
paintTile(imageData: ImageData, tile: TileRef, color: Colord, alpha: number) {
const offset = tile * 4;
imageData.data[offset] = color.rgba.r;
@@ -504,9 +518,14 @@ export class TerritoryLayer implements Layer {
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
clearAlternativeTile(tile: TileRef) {
const offset = tile * 4;
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
enqueueTile(tile: TileRef) {
this.tileToRenderQueue.push({
tile: tile,
tile,
lastUpdate: this.game.ticks() + this.random.nextFloat(0, 0.5),
});
}
+15 -15
View File
@@ -1,14 +1,14 @@
import { GameView, UnitView } from "../../../core/game/GameView";
import { Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { UnitSelectionEvent } from "../../InputHandler";
import { ProgressBar } from "../ProgressBar";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { ProgressBar } from "../ProgressBar";
import { Theme } from "../../../core/configuration/Config";
import { TransformHandler } from "../TransformHandler";
import { UnitSelectionEvent } from "../../InputHandler";
import { UnitType } from "../../../core/game/Game";
import { UserSettings } from "../../../core/game/UserSettings";
const COLOR_PROGRESSION = [
"rgb(232, 25, 25)",
@@ -27,14 +27,14 @@ const PROGRESSBAR_HEIGHT = 3; // Height of a bar
export class UILayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D | null;
private theme: Theme | null = null;
private userSettings: UserSettings = new UserSettings();
private readonly theme: Theme | null = null;
private readonly userSettings: UserSettings = new UserSettings();
private selectionAnimTime = 0;
private allProgressBars: Map<
private readonly allProgressBars: Map<
number,
{ unit: UnitView; progressBar: ProgressBar }
> = new Map();
private allHealthBars: Map<number, ProgressBar> = new Map();
private readonly allHealthBars: Map<number, ProgressBar> = new Map();
// Keep track of currently selected unit
private selectedUnit: UnitView | null = null;
@@ -49,9 +49,9 @@ export class UILayer implements Layer {
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
private readonly game: GameView,
private readonly eventBus: EventBus,
private readonly transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
}
@@ -263,7 +263,7 @@ export class UILayer implements Layer {
* Draw health bar for a unit
*/
public drawHealthBar(unit: UnitView) {
const maxHealth = this.game.unitInfo(unit.type()).maxHealth;
const { maxHealth } = this.game.unitInfo(unit.type());
if (maxHealth === undefined || this.context === null) {
return;
}
+14 -12
View File
@@ -1,23 +1,23 @@
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import portIcon from "../../../../resources/images/AnchorIcon.png";
import { LitElement, html } from "lit";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { ToggleStructureEvent } from "../../InputHandler";
import { UnitType } from "../../../core/game/Game";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import { customElement } from "lit/decorators.js";
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import defensePostIcon from "../../../../resources/images/ShieldIconWhite.svg";
import samLauncherIcon from "../../../../resources/non-commercial/svg/SamLauncherIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { UnitType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ToggleStructureEvent } from "../../InputHandler";
import portIcon from "../../../../resources/images/AnchorIcon.png";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
import samLauncherIcon from "../../../../resources/non-commercial/svg/SamLauncherIconWhite.svg";
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private _selectedStructure: UnitType | null = null;
private readonly _selectedStructure: UnitType | null = null;
private _cities = 0;
private _factories = 0;
private _missileSilo = 0;
@@ -104,7 +104,9 @@ export class UnitDisplay extends LitElement implements Layer {
return html`
<div
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100] bg-gray-800/70 backdrop-blur-sm border border-slate-400 rounded-lg p-2 hidden lg:block"
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-[1100]
bg-gray-800/70 backdrop-blur-sm border border-slate-400 rounded-lg p-2
hidden lg:block"
>
<div class="grid grid-rows-1 auto-cols-max grid-flow-col gap-1">
${this.renderUnitItem(cityIcon, this._cities, UnitType.City, "city")}
+17 -18
View File
@@ -1,25 +1,24 @@
import { colord, Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, UnitView } from "../../../core/game/GameView";
import { BezenhamLine } from "../../../core/utilities/Line";
import {
AlternateViewEvent,
MouseUpEvent,
UnitSelectionEvent,
} from "../../InputHandler";
import { MoveWarshipIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { Colord, colord } from "colord";
import { GameView, UnitView } from "../../../core/game/GameView";
import {
getColoredSprite,
isSpriteReady,
loadAllSprites,
} from "../SpriteLoader";
import { BezenhamLine } from "../../../core/utilities/Line";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { Layer } from "./Layer";
import { MoveWarshipIntentEvent } from "../../Transport";
import { Theme } from "../../../core/configuration/Config";
import { TileRef } from "../../../core/game/GameMap";
import { TransformHandler } from "../TransformHandler";
import { UnitType } from "../../../core/game/Game";
enum Relationship {
Self,
@@ -33,15 +32,15 @@ export class UnitLayer implements Layer {
private transportShipTrailCanvas: HTMLCanvasElement;
private unitTrailContext: CanvasRenderingContext2D;
private unitToTrail = new Map<UnitView, TileRef[]>();
private readonly unitToTrail = new Map<UnitView, TileRef[]>();
private theme: Theme;
private readonly theme: Theme;
private alternateView = false;
private oldShellTile = new Map<UnitView, TileRef>();
private readonly oldShellTile = new Map<UnitView, TileRef>();
private transformHandler: TransformHandler;
private readonly transformHandler: TransformHandler;
// Selected unit property as suggested in the review comment
private selectedUnit: UnitView | null = null;
@@ -50,8 +49,8 @@ export class UnitLayer implements Layer {
private readonly WARSHIP_SELECTION_RADIUS = 10; // Radius in game cells for warship selection hit zone
constructor(
private game: GameView,
private eventBus: EventBus,
private readonly game: GameView,
private readonly eventBus: EventBus,
transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
+2 -2
View File
@@ -1,12 +1,12 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { SendWinnerEvent } from "../../Transport";
import { GutterAdModalEvent } from "./GutterAdModal";
import { Layer } from "./Layer";
import { SendWinnerEvent } from "../../Transport";
import { translateText } from "../../../client/Utils";
@customElement("win-modal")
export class WinModal extends LitElement implements Layer {

Some files were not shown because too many files have changed in this diff Show More