Merge branch 'main' into Sound&Music
@@ -8,7 +8,6 @@ Describe the PR.
|
||||
- [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file
|
||||
- [ ] I have added relevant tests to the test directory
|
||||
- [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
|
||||
- [ ] I have read and accepted the CLA agreement (only required once).
|
||||
|
||||
## Please put your Discord username so you can be contacted if a bug or regression is found:
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: 🧪 CI
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -41,7 +44,7 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run test:coverage
|
||||
|
||||
eslint:
|
||||
name: 🔍 ESLint
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: 🧼 PR
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
pull_request:
|
||||
types:
|
||||
- demilestoned
|
||||
@@ -20,6 +23,11 @@ jobs:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'merge_group') {
|
||||
// Ignore merge_group events
|
||||
return;
|
||||
}
|
||||
|
||||
const body = context.payload.pull_request.body || '';
|
||||
|
||||
const errors = [];
|
||||
@@ -36,7 +44,6 @@ jobs:
|
||||
/- \[x\] I process any text displayed to the user through translateText\(\) and I\'ve added it to the en\.json file/i,
|
||||
/- \[x\] I have added relevant tests to the test directory/i,
|
||||
/- \[x\] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced/i,
|
||||
/- \[x\] I have read and accepted the CLA agreement \(only required once\)\./i
|
||||
];
|
||||
|
||||
for (const box of requiredBoxes) {
|
||||
@@ -60,10 +67,16 @@ jobs:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Get the pull request data
|
||||
const milestone = context.payload.pull_request.milestone;
|
||||
if (!milestone) {
|
||||
core.setFailed('❌ Pull request must have a milestone assigned before merging.');
|
||||
return;
|
||||
if (context.eventName === 'merge_group') {
|
||||
// Ignore merge_group events
|
||||
} else if (context.eventName === 'pull_request') {
|
||||
// Get the pull request data
|
||||
const milestone = context.payload.pull_request.milestone;
|
||||
if (!milestone) {
|
||||
core.setFailed('❌ Pull request must have a milestone assigned before merging.');
|
||||
return;
|
||||
}
|
||||
console.log(`✅ Milestone found: ${milestone.title}`);
|
||||
} else {
|
||||
core.setFailed(`❌ Unknown event type ${context.eventName}.`);
|
||||
}
|
||||
console.log(`✅ Milestone found: ${milestone.title}`);
|
||||
|
||||
@@ -9,3 +9,4 @@ resources/.DS_Store
|
||||
.env*
|
||||
.DS_Store
|
||||
.clinic/
|
||||
CLAUDE.md
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.[tj]s
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022"
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,5 @@
|
||||
* @openfrontio/review-approver
|
||||
resources/lang @openfrontio/translation-approver
|
||||
resources/lang/en.json @openfrontio/review-approver
|
||||
src/client @openfrontio/frontend-approver
|
||||
src/server @openfrontio/backend-approver
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:24-slim AS base
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Create dependency layer
|
||||
FROM base AS dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
supervisor \
|
||||
git \
|
||||
curl \
|
||||
jq \
|
||||
@@ -13,46 +14,69 @@ RUN apt-get update && apt-get install -y \
|
||||
apache2-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
|
||||
&& dpkg -i cloudflared.deb \
|
||||
&& rm cloudflared.deb
|
||||
|
||||
# Final image
|
||||
FROM base
|
||||
|
||||
# Copy installed packages from dependencies stage
|
||||
COPY --from=dependencies / /
|
||||
# Update worker_connections in the existing nginx.conf
|
||||
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
|
||||
|
||||
FROM dependencies AS build
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT="$GIT_COMMIT"
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Disable Husky hooks
|
||||
ENV HUSKY=0
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies while bypassing Husky hooks
|
||||
ENV HUSKY=0
|
||||
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
|
||||
RUN mkdir -p .git && npm ci
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Build the client-side application
|
||||
RUN npm run build-prod
|
||||
|
||||
# So we can see which commit was used to build the container
|
||||
# https://openfront.io/commit.txt
|
||||
RUN echo "$GIT_COMMIT" > static/commit.txt
|
||||
|
||||
# Update worker_connections in the existing nginx.conf
|
||||
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
|
||||
# Remove maps data from final image
|
||||
FROM base AS prod-files
|
||||
COPY . .
|
||||
RUN rm -rf resources/maps
|
||||
|
||||
FROM dependencies AS npm-dependencies
|
||||
# Disable Husky hooks
|
||||
ENV HUSKY=0
|
||||
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
# Install dependencies
|
||||
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 \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy installed packages from dependencies stage
|
||||
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
|
||||
&& dpkg -i cloudflared.deb \
|
||||
&& rm cloudflared.deb
|
||||
|
||||
# Copy Nginx configuration and ensure it's used instead of the default
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -f /etc/nginx/sites-enabled/default
|
||||
COPY --from=dependencies /etc/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy npm dependencies
|
||||
COPY --from=npm-dependencies /usr/src/app/node_modules node_modules
|
||||
COPY package.json .
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY --from=prod-files /usr/src/app/ /usr/src/app/
|
||||
|
||||
# Copy frontend
|
||||
COPY --from=build /usr/src/app/static static
|
||||
|
||||
# Setup supervisor configuration
|
||||
RUN mkdir -p /var/log/supervisor
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import noZArray from "./rules/no-z-array.js";
|
||||
|
||||
export default {
|
||||
rules: {
|
||||
"no-z-array": noZArray,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === "MemberExpression" &&
|
||||
node.callee.object.name === "z" &&
|
||||
node.callee.property.name === "array" &&
|
||||
node.arguments.length === 1
|
||||
) {
|
||||
const argSource = context.sourceCode.getText(node.arguments[0]);
|
||||
context.report({
|
||||
data: { type: argSource },
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, `${argSource}.array()`);
|
||||
},
|
||||
messageId: "noZArray",
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Disallow z.array(type) in favor of type.array()",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
noZArray: "Use `{{type}}.array()` instead of `z.array({{type}})`.",
|
||||
},
|
||||
schema: [],
|
||||
type: "suggestion",
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import pluginJs from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
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 stylistic from "@stylistic/eslint-plugin";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -25,6 +28,8 @@ export default [
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
"__mocks__/fileMock.js",
|
||||
"eslint-plugin-local/plugin.js",
|
||||
"eslint-plugin-local/rules/no-z-array.js",
|
||||
"eslint.config.js",
|
||||
"jest.config.ts",
|
||||
"postcss.config.js",
|
||||
@@ -39,17 +44,140 @@ export default [
|
||||
{
|
||||
rules: {
|
||||
// Disable rules that would fail. The failures should be fixed, and the entries here removed.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-case-declarations": "off",
|
||||
"@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": stylistic,
|
||||
},
|
||||
rules: {
|
||||
// Enable rules
|
||||
"@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,
|
||||
before: true,
|
||||
overrides: {
|
||||
colon: {
|
||||
before: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"@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",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-literal-enum-member": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
eqeqeq: "error",
|
||||
"@typescript-eslint/prefer-readonly": "error",
|
||||
"eqeqeq": "error",
|
||||
"indent": "off", // @stylistic/indent
|
||||
"sort-keys": "error",
|
||||
"@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",
|
||||
"array-bracket-newline": ["error", "consistent"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"array-element-newline": ["error", "consistent"],
|
||||
"arrow-parens": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": ["error", { before: false, after: true }],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"function-call-argument-newline": ["error", "consistent"],
|
||||
"max-depth": ["error", { max: 5 }],
|
||||
"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 }],
|
||||
"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",
|
||||
"space-before-blocks": ["error", "always"],
|
||||
"space-before-function-paren": ["error", {
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
}],
|
||||
"space-infix-ops": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
local: eslintPluginLocal,
|
||||
},
|
||||
rules: {
|
||||
"local/no-z-array": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,29 +10,16 @@ export default {
|
||||
"\\.(css|less)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
tsconfig: {
|
||||
target: "ES2020",
|
||||
module: "es2022",
|
||||
moduleResolution: "node",
|
||||
experimentalDecorators: true,
|
||||
types: ["jest", "node"],
|
||||
},
|
||||
},
|
||||
],
|
||||
"^.+\\.tsx?$": ["@swc/jest"],
|
||||
},
|
||||
transformIgnorePatterns: ["node_modules/(?!(node:)/)"],
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 0,
|
||||
functions: 0,
|
||||
lines: 0,
|
||||
statements: 0,
|
||||
statements: 21.5,
|
||||
branches: 17.0,
|
||||
lines: 22.0,
|
||||
functions: 20.5,
|
||||
},
|
||||
},
|
||||
coverageReporters: ["text", "lcov", "html"],
|
||||
|
||||
@@ -26,120 +26,110 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@datastructures-js/priority-queue": "^6.3.3",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@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",
|
||||
"@types/chai": "^4.3.17",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jquery": "^3.5.31",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@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",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"benchmark": "^2.1.4",
|
||||
"binary-base64-loader": "^1.0.0",
|
||||
"binary-loader": "^0.0.1",
|
||||
"canvas": "^3.1.0",
|
||||
"chai": "^5.1.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"d3": "^7.9.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-formatter-gha": "^1.5.2",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-webpack-plugin": "^5.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"html-inline-script-webpack-plugin": "^3.2.1",
|
||||
"html-loader": "^5.1.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lit": "^3.3.1",
|
||||
"lit-markdown": "^1.3.2",
|
||||
"mrmime": "^2.0.0",
|
||||
"pixi-filters": "^6.1.4",
|
||||
"pixi.js": "^8.11.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-sh": "^0.17.4",
|
||||
"protobufjs": "^7.5.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sinon": "^21.0.0",
|
||||
"sinon-chai": "^4.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-jest": "^29.2.4",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.26.0",
|
||||
"webpack": "^5.100.2",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-redshift-data": "^3.758.0",
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/secret-manager": "^5.6.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.200.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.58.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/host-metrics": "^0.36.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-logs": "^0.200.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.200.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.32.0",
|
||||
"@opentelemetry/winston-transport": "^0.11.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"@types/hammerjs": "^2.0.45",
|
||||
"@types/msgpack5": "^3.4.6",
|
||||
"binary-loader": "^0.0.1",
|
||||
"colord": "^2.9.3",
|
||||
"colorjs.io": "^0.5.2",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"fastpriorityqueue": "^0.7.5",
|
||||
"google-auth-library": "^9.14.0",
|
||||
"googleapis": "^143.0.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"jimp": "^0.22.12",
|
||||
"jose": "^6.0.10",
|
||||
"lit": "^3.2.1",
|
||||
"lit-markdown": "^1.3.2",
|
||||
"msgpack5": "^6.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nanoid": "^3.3.6",
|
||||
"obscenity": "^0.4.3",
|
||||
"pg": "^8.13.3",
|
||||
"pixi-filters": "^6.1.3",
|
||||
"pixi.js": "^8.10.1",
|
||||
"prom-client": "^15.1.3",
|
||||
"protobufjs": "^7.3.2",
|
||||
"pureimage": "^0.4.13",
|
||||
"sharp": "^0.34.2",
|
||||
"systeminformation": "^5.25.11",
|
||||
"twemoji": "^14.0.2",
|
||||
"seedrandom": "^3.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"uuid": "^11.1.0",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-transport": "^4.9.0",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.28"
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,493 @@
|
||||
# header
|
||||
- This is a sample changelog based off of v0.24.0.
|
||||
- This file will be replaced with real release notes during the release build process.
|
||||
- Indented bullets look like this
|
||||
|
||||
changelog here
|
||||
📦 **OpenFront v24 Changelog**
|
||||
|
||||
⚖️ **Balance Changes**
|
||||
|
||||
- Trade ships are now capped at 150 (Evan)
|
||||
→ Each port you own now increases the gold per trade, counterbalancing the cap.
|
||||
- MIRVs have been nerfed
|
||||
→ Expect less devastating multi-warhead nukes. Land in-between the fallout can be more quickly conquered.
|
||||
- Warships prioritize enemy transport ships over warships. Reload instantly after shooting a transport ship. (Evan)
|
||||
- Building discounts can only be used one time.
|
||||
- AI nukes now avoid SAM launchers
|
||||
|
||||
🚅 **Major Features**
|
||||
|
||||
- Trains added for new movement mechanics (experimental for private lobbies and single player) (DevelopingTom)
|
||||
- Factories spawn trains and railroads (choose Factory as unit in private lobby or for single player, to use trains)
|
||||
- Railroads can form loops
|
||||
- Added Trios and Quads. Add them to public lobby rotation together with Duos. (FakeNeo)
|
||||
- Upgradable structures: Cities, Ports, SAMs, and Silos can now be improved
|
||||
- Multi-level radial menu with dynamic build options
|
||||
- Creative Commons License added to non-commercial resources
|
||||
- Factories added for private lobbies and single player games
|
||||
- Hash-based routing implemented
|
||||
- Flares system implemented
|
||||
- GitHub Releases with release notes are now supported (click the What's New button/megaphone icon)
|
||||
|
||||
🔧 **Game Improvements**
|
||||
|
||||
- Improved territory drawing performance
|
||||
- SAMs now only target nukes threatening nearby areas
|
||||
- Nukes are now faster (speed increased from 4 → 6)
|
||||
- Better color mixing for small player counts (Ble4Ch)
|
||||
- Unique player colors to avoid confusion (Ble4Ch)
|
||||
- Better and optimized bot behaviour and spawn logic (tryout33 & FakeNeo)
|
||||
- Boat build discounts now scale with unit ownership
|
||||
- Improved username censoring and management
|
||||
- Updated East Asia map (formerly "Japan and Neighbors")
|
||||
- Reworked and optimized leaderboard UI
|
||||
- Improved visual clarity for alliances and stacked buildings
|
||||
|
||||
🔧 **Game Improvements (continued)**
|
||||
|
||||
- Better handling for betrayal alerts and radial menu behavior
|
||||
- Red alert frame when betrayed (devalnor)
|
||||
- Attack hotkeys added (Engla)
|
||||
- Boat hotkey added
|
||||
- Nations can spawn cities without a port
|
||||
- Team sizes now equalized
|
||||
- MIRV warhead intercepted stats are now recorded
|
||||
- Text FX added
|
||||
- Terrain manipulation for attack advantage
|
||||
- New logo added
|
||||
- Fix Duo partner (Nation) always same in Single player (tryout33)
|
||||
- Rename Replay Speed to Game Speed for Single player (tryout33)
|
||||
- Fix Nations building more than allowed (tryout33)
|
||||
|
||||
🧪 **UI & Quality of Life**
|
||||
|
||||
- Fixed text overflow in UI (Diessel)
|
||||
- Fixed websocket and join bugs
|
||||
- Fixed boat-on-land issues
|
||||
- Fixed modal errors and null pointer warnings
|
||||
- Fixed input handler edge cases on Mac (proper modifier and emoji key detection) (Ble4Ch)
|
||||
- Fixed scrollbar appearing unnecessarily in small boxes on Chromium browsers
|
||||
- Fixed giant world map key
|
||||
- Leaderboards, alerts, and modals now support translation & dark mode
|
||||
- New custom flag support and pattern icons
|
||||
- Various patterns available (Sword, Shells, White Rabbit, Goat, Cats, Hand, Radiation, Cursor, QR)
|
||||
- Patterned territory support
|
||||
- More responsive scrollbar and player info panels
|
||||
- Top bar redesign (Diessel)
|
||||
- More responsive design for in-game elements
|
||||
- New icon layer/sprites for structures
|
||||
- Building/loading/HP bars improved
|
||||
- Proper alliance timer naming
|
||||
- Logout button added
|
||||
- Handle not spawned player fixes
|
||||
- Multiple patterns support
|
||||
- Fix: anonymized name isn't displayed in chat message (tryout33)
|
||||
- Fix Leaderboard: show 0% instead of NaN when all terrain is nuked (tryout33)
|
||||
- Some fixes to the new Radial menu (tryout33)
|
||||
- Fix bug/performance improvements for trade ships (tryout33)
|
||||
- News Notification Badge for new release notes (floriankilian)
|
||||
- Translation improvements
|
||||
|
||||
🛠️ **Backend & Technical**
|
||||
|
||||
- Stats endpoints are now available
|
||||
- Added CORS origin headers
|
||||
- Added support for mobile apps native login
|
||||
- Discord user and guild member caching
|
||||
- Improved session error handling
|
||||
- Changed server logging
|
||||
- Improved data loading and fixed various bugs
|
||||
|
||||
🔒 **Security & Bug Fixes**
|
||||
|
||||
- Fixed naval attack spam exploit
|
||||
- Fixed gold donation validation exploit
|
||||
- Fixed pot issue
|
||||
- Various stability improvements and bug fixes
|
||||
|
||||
🌐 **Translations**
|
||||
|
||||
- Bulgarian🇧🇬: Nikola123 & NewHappyRabbit
|
||||
- Japanese🇯🇵: Aotumuri, daimyo_panda2 & gafunuko
|
||||
- French🇫🇷: cldprv, gx21 & r3ms
|
||||
- Dutch🇳🇱: cldprv & tryout33
|
||||
- German🇩🇪: Pilkey, jacks0n, floriankilian, Fibig & Texxter
|
||||
- Spanish🇪🇸: 6uzm4n
|
||||
- Russian🇷🇺: Rulfam
|
||||
- Ukrainian🇺🇦: Rulfam
|
||||
- Polish🇵🇱: zibi, RinkyDinky & Rulfam
|
||||
- Serbo-Croatian🇷🇸🇭🇷🇧🇦🇲🇪: Vekser
|
||||
- Italian🇮🇹: frappa10 & Lollosean
|
||||
- Brazilian Portuguese🇧🇷: theskeleton4393 & juliosilvaqwerty5
|
||||
- Turkish🇹🇷: Toyatak
|
||||
- Arabic🇸🇦: N0ur, Moha & SyntaxPM
|
||||
- Swedish🇸🇪: Moha, theangel2 & Keevee
|
||||
- Hindi🇮🇳: sheikh
|
||||
- Bengali🇧🇩: sheikh
|
||||
- Esperanto: r3ms
|
||||
- Toki Pona: Makonede
|
||||
- Slovak🇸🇰: extraextra
|
||||
- Czech🇨🇿: Xaelor & erinthegirl
|
||||
- Hebrew🇮🇱: Goblinon
|
||||
- Finnish🇫🇮: Tanepro193
|
||||
- Korean🇰🇷: Jinyoon
|
||||
- Danish🇩🇰: NiclasWK
|
||||
- Chinese Simplified🇨🇳: Moki
|
||||
- Galician: toldinsound
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Bugfix: don't allow other players to move warships by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/879
|
||||
- Proper alliance timer naming by @tnhnblgl in https://github.com/openfrontio/OpenFrontIO/pull/886
|
||||
- Add naval combat animations by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/858
|
||||
- Use array index access instead of .at by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/889
|
||||
- mls by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/888
|
||||
- Revert "add addinplay ads" by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/897
|
||||
- Fix Toki Pona by @Duwibi in https://github.com/openfrontio/OpenFrontIO/pull/898
|
||||
- remove player id from Schemas, fix archive bug by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/907
|
||||
- Unit menu by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/867
|
||||
- Convert stats to bigints by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/909
|
||||
- Flag fixes for Europe map and for Brittany in flag menu and Gateway To the Atlantic map by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/910
|
||||
- Add deploy concurrency configuration by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/911
|
||||
- Add Github Logo on footer by @LucasLion in https://github.com/openfrontio/OpenFrontIO/pull/875
|
||||
- Revert "Population visualization (#842)" by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/908
|
||||
- floor by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/913
|
||||
- remove known world by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/914
|
||||
- Main menu UI cleanup by @Demonessica in https://github.com/openfrontio/OpenFrontIO/pull/857
|
||||
- Improve territory drawing performances by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/696
|
||||
- bug: Clicking out of bounds throws uncaught exception by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/920
|
||||
- Removes CSS rule causing performance issues by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/925
|
||||
- Always delete tradeship on pathfinding fail by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/921
|
||||
- Fix bigint serialization error by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/916
|
||||
- Revert tradeship path caching by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/927
|
||||
- Meta Adjustments from [UN] clan test by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/932
|
||||
- fix alternate view regression by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/937
|
||||
- fix warship targetting range by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/938
|
||||
- Add instructional overlay message during spawn phase by @spicydll in https://github.com/openfrontio/OpenFrontIO/pull/934
|
||||
- Add test coverage script by @aqw42 in https://github.com/openfrontio/OpenFrontIO/pull/929
|
||||
- Added two checkboxes to the default pull request template by @aqw42 in https://github.com/openfrontio/OpenFrontIO/pull/930
|
||||
- Fix slow singleplayer timer by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/943
|
||||
- improved perfomance of PseudoRandom by @falcolnic in https://github.com/openfrontio/OpenFrontIO/pull/933
|
||||
- Change deploy concurrency group by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/944
|
||||
- Set singleplayer gitCommit in the client by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/945
|
||||
- Simplify bots retaliation logic by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/946
|
||||
- Add close label by @drillskibo in https://github.com/openfrontio/OpenFrontIO/pull/949
|
||||
- Remove ClientID from GameRenderer by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/878
|
||||
- Resolve code scanning warning about HTML injection by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/953
|
||||
- Fix invalid username popup being behind public game button by @Demonessica in https://github.com/openfrontio/OpenFrontIO/pull/951
|
||||
- Server role lookup by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/954
|
||||
- Flag fixes in several maps by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/957
|
||||
- Fix map jsons by @Duwibi in https://github.com/openfrontio/OpenFrontIO/pull/960
|
||||
- change defaults to reflect meta by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/942
|
||||
- Even more flag flair by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/959
|
||||
- Only load tiles when viewed by player by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/887
|
||||
- Hide login button by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/965
|
||||
- Fix discord user schema by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/969
|
||||
- Balance Adjustment for Attack Mechanism by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/973
|
||||
- Prevent Attack Spam by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/977
|
||||
- Update HeadsUpMessage.ts to support translations by @spicydll in https://github.com/openfrontio/OpenFrontIO/pull/981
|
||||
- Cap lobby sizes at 150 by @Duwibi in https://github.com/openfrontio/OpenFrontIO/pull/984
|
||||
- Fix Translations showing as untranslated keys by @Duwibi in https://github.com/openfrontio/OpenFrontIO/pull/983
|
||||
- Another Balance Change by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/987
|
||||
- make bots weaker by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/985
|
||||
- Remove shield icon from bots by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/986
|
||||
- Balance Update by @1brucben in https://github.com/openfrontio/OpenFrontIO/pull/996
|
||||
- Revert meta by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1002
|
||||
- Fix text overflow in instructions for longer translations by @ERHash in https://github.com/openfrontio/OpenFrontIO/pull/971
|
||||
- Add dynamic sorting to leaderboard by tiles, gold, and troops by @ERHash in https://github.com/openfrontio/OpenFrontIO/pull/961
|
||||
- Fix Player Name Monospaced Text Overflow on PlayerInfo by @ERHash in https://github.com/openfrontio/OpenFrontIO/pull/975
|
||||
- Scroll bar Behavior on Chromium Browsers, c-modal_content by @andrewNiziolek in https://github.com/openfrontio/OpenFrontIO/pull/976
|
||||
- Synced the single player and host files together, and fix issue withc… by @shaan150 in https://github.com/openfrontio/OpenFrontIO/pull/991
|
||||
- Equalize team sizes by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/992
|
||||
- Added support for dark mode icons for Alliance Request Icon and Embargo Icon by @Vermylion in https://github.com/openfrontio/OpenFrontIO/pull/993
|
||||
- Use bigint for gold by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1000
|
||||
- Fix : Donation when max pop already reached by @aqw42 in https://github.com/openfrontio/OpenFrontIO/pull/904
|
||||
- Validate incoming API data with zod by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/891
|
||||
- this is a fix for the "possibly null" error. dosent seem to cause runtime issues but does cause the compiler to throw an error by @Jerryslang in https://github.com/openfrontio/OpenFrontIO/pull/1005
|
||||
- Fixnukeboatbug by @rldtech in https://github.com/openfrontio/OpenFrontIO/pull/1011
|
||||
- added ratio controls by @falcolnic in https://github.com/openfrontio/OpenFrontIO/pull/963
|
||||
- Add a status check for the milestone field by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1029
|
||||
- Fix discord login issue by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1028
|
||||
- Changed consolex to console logging by @falcolnic in https://github.com/openfrontio/OpenFrontIO/pull/1036
|
||||
- Center map on start by @Demonessica in https://github.com/openfrontio/OpenFrontIO/pull/1013
|
||||
- Rev: Update "Japan and Neighbors" map to "East Asia" by @andrewNiziolek in https://github.com/openfrontio/OpenFrontIO/pull/1007
|
||||
- Close socket on ClientMessageSchema, improve zod error by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1003
|
||||
- SAMs should target only nukes aimed at nearby targets by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1038
|
||||
- AI nukes avoid SAM launchers by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1045
|
||||
- Show alliances on the PlayerPanel by @Maaxion in https://github.com/openfrontio/OpenFrontIO/pull/1053
|
||||
- Improve readability of alliance acceptation logic for bots and add tests by @Nephty in https://github.com/openfrontio/OpenFrontIO/pull/1049
|
||||
- [Cleanup] Pass Player into execution constructor instead of PlayerID by @LJoyL in https://github.com/openfrontio/OpenFrontIO/pull/1022
|
||||
- Monitoring client connections by @aqw42 in https://github.com/openfrontio/OpenFrontIO/pull/941
|
||||
- have master create tunnels for all workers #780 by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1042
|
||||
- Add Boat hotkey by @tnhnblgl in https://github.com/openfrontio/OpenFrontIO/pull/1060
|
||||
- bug: logout by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1073
|
||||
- fix cloudflare tunnels by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1076
|
||||
- Duo partner SP always same: randomize players before team assignment by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1051
|
||||
- Multi-level radial menu by @oleksandr-shysh in https://github.com/openfrontio/OpenFrontIO/pull/1018
|
||||
- Fix broken flag images by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1078
|
||||
- kick existing client when duplicate persistent id is found by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1077
|
||||
- Update PlayerImpl.ts by @E-EE-E in https://github.com/openfrontio/OpenFrontIO/pull/1079
|
||||
- Add back #646 - trade ship gold by travelled distance by @Maaxion in https://github.com/openfrontio/OpenFrontIO/pull/1085
|
||||
- #1086 prevent clicking on other structures than your own by @Maaxion in https://github.com/openfrontio/OpenFrontIO/pull/1087
|
||||
- rename Event interface -> GameEvent by @Maaxion in https://github.com/openfrontio/OpenFrontIO/pull/1094
|
||||
- refactor radial, fix boat on terra nullius not working fixes by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1095
|
||||
- Disable donations public ffa matches by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1097
|
||||
- Nations can spawn cities without a port by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1072
|
||||
- Ci coverage by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1099
|
||||
- Revert "Ci coverage" by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1101
|
||||
- Add filters tabs to EvensDisplay to let users filter events by @Maaxion in https://github.com/openfrontio/OpenFrontIO/pull/1080
|
||||
- Fix bug in FakeHumanExecution by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1102
|
||||
- Fix: Hide username validation error in-game by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1110
|
||||
- cloudflare fixed tunnel name by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1096
|
||||
- Remove duplicate gold accumulation in team stats calculation by @rldtech in https://github.com/openfrontio/OpenFrontIO/pull/1010
|
||||
- Optimizations for botbehaviour by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1114
|
||||
- fix: correct mac modifier and emoji key detection in input handler by @Ble4Ch in https://github.com/openfrontio/OpenFrontIO/pull/1118
|
||||
- fix duplicate websocket handler by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1124
|
||||
- Adding unit info modal translation support. by @its-sii in https://github.com/openfrontio/OpenFrontIO/pull/1122
|
||||
- increase nuke speed from 4 to 6 by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1125
|
||||
- Avoid using as to cast values by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1115
|
||||
- Fix Māori flag name by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1133
|
||||
- use newer attack, delete existing attack by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1134
|
||||
- counter attack doesn't cancel out attack by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1132
|
||||
- Move version and changelog to files by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1109
|
||||
- Fix non valid SafeString flag codes by @ghisloufou in https://github.com/openfrontio/OpenFrontIO/pull/1135
|
||||
- Add a Replay speed control feature by @ghisloufou in https://github.com/openfrontio/OpenFrontIO/pull/1106
|
||||
- Add progress bars to show loading time and healthbars by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1107
|
||||
- feat: assign unique colors for players by @Ble4Ch in https://github.com/openfrontio/OpenFrontIO/pull/1063
|
||||
- lazy loading and current data var by @falcolnic in https://github.com/openfrontio/OpenFrontIO/pull/988
|
||||
- fix(client): use the right language-modal selector by @ghisloufou in https://github.com/openfrontio/OpenFrontIO/pull/1136
|
||||
- Simple Upgradable Structures (Cities, Ports, SAMs and Silos) by @Egraveline in https://github.com/openfrontio/OpenFrontIO/pull/1012
|
||||
- Rename Replay speed to Game speed in Singleplayer by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1145
|
||||
- discriminatedUnion by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1130
|
||||
- Fixed bad translation string bug for unit info modal. by @its-sii in https://github.com/openfrontio/OpenFrontIO/pull/1143
|
||||
- fix timer overflow by @DiesselOne in https://github.com/openfrontio/OpenFrontIO/pull/1148
|
||||
- optimize leaderboard by @DiesselOne in https://github.com/openfrontio/OpenFrontIO/pull/1151
|
||||
- Fix regression cooldown bars by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1154
|
||||
- favor transport ships, no reload penalty by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1153
|
||||
- dynamic radial menu build options by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1152
|
||||
- Update building images and adjust border/territory radii for unit configuration by @rldtech in https://github.com/openfrontio/OpenFrontIO/pull/1037
|
||||
- Fixed quick chat text injection by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1144
|
||||
- Rework leaderboard and team stats by @DiesselOne in https://github.com/openfrontio/OpenFrontIO/pull/1164
|
||||
- Extend token lifetime to 3 days by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1172
|
||||
- Redraw stacked buildings sprites by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1170
|
||||
- Fix Nations building more than allowed by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1176
|
||||
- Set a targetable status for nukes by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1174
|
||||
- fixed giantworldmap key by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1188
|
||||
- Fix Leaderboard: convert NaN into 0% by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1190
|
||||
- Update pr-description regex by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1181
|
||||
- discriminatedUnion by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1193
|
||||
- UsernameSchema, FlagSchema by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1185
|
||||
- feat: colors are better mixed up when players count is low by @Ble4Ch in https://github.com/openfrontio/OpenFrontIO/pull/1149
|
||||
- Improve handling of HTTP 401 by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1194
|
||||
- increase worker connections by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1179
|
||||
- Fix: Handle not spawned player focus by @tnhnblgl in https://github.com/openfrontio/OpenFrontIO/pull/1186
|
||||
- Fix Radial menu undefined params error during spawn phase by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1192
|
||||
- Better handling of bad tokens by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1180
|
||||
- Hash-based routing by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1198
|
||||
- cache busting: Import version, changelog by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1201
|
||||
- REV - Improved Username Censoring by @andrewNiziolek in https://github.com/openfrontio/OpenFrontIO/pull/1119
|
||||
- Jest v30 by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1206
|
||||
- Release workflow by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1202
|
||||
- Fix unnecessary join check by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1209
|
||||
- fix websocket error by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1208
|
||||
- add playwire ads by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1128
|
||||
- Update webpack-dev-server to 5.2.2 by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1207
|
||||
- Add a 30 minute timeout to actions by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1210
|
||||
- Update release workflow by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1212
|
||||
- update leaderboard align by @DiesselOne in https://github.com/openfrontio/OpenFrontIO/pull/1189
|
||||
- Fix gutter ads, move in-game add to bottom right corner. by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1214
|
||||
- have worker send error back to client by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1178
|
||||
- Fix build menu on water tile by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1216
|
||||
- Update default version number by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1218
|
||||
- Schema cleanup by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1219
|
||||
- ads on death screen by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1223
|
||||
- Delay win modal by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1224
|
||||
- Dependency removals and updates by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1215
|
||||
- add spawn ads by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1228
|
||||
- upgrade to zod 4 by @omrih4 in https://github.com/openfrontio/OpenFrontIO/pull/1161
|
||||
- Record MIRV warhead intercepted stats, perf improvements by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1220
|
||||
- Simplfiy LangSelector by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1226
|
||||
- Pot issue fix by @tnhnblgl in https://github.com/openfrontio/OpenFrontIO/pull/1233
|
||||
- Logout Button Fix by @tnhnblgl in https://github.com/openfrontio/OpenFrontIO/pull/1234
|
||||
- fix bad tile crash by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1237
|
||||
- fix is valid ref by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1240
|
||||
- Remove babel-jest from devDependencies by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1247
|
||||
- Refactor radial menu by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1246
|
||||
- Simplify ClientMessage handling by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1235
|
||||
- Add trains by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1159
|
||||
- Add back the trade ship send stat by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1253
|
||||
- Remove maxTokenAge by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1255
|
||||
- Patterned territory by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/786
|
||||
- Discounts can only be used one time by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/892
|
||||
- Fix singleplayer check by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1260
|
||||
- Move maps generation out of repo, new map structure by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1256
|
||||
- Show a red alert frame when the player is betrayed by @devalnor in https://github.com/openfrontio/OpenFrontIO/pull/1195
|
||||
- Allow boat discount based on number of units owned by @devalnor in https://github.com/openfrontio/OpenFrontIO/pull/1261
|
||||
- Move map metadata to map manifest by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1262
|
||||
- Refactor cosmetics.json by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1263
|
||||
- bug: StatsSchema zod validation error by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1267
|
||||
- White Rabbit pattern by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1264
|
||||
- Cleanup log spam in TerritoryPatternsModal by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1269
|
||||
- Fix pattern locking logic by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1270
|
||||
- Keybind Ground Attack by @dengh in https://github.com/openfrontio/OpenFrontIO/pull/1258
|
||||
- UrlEncode patterns in cosmetics.json by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1273
|
||||
- improve astar perf by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1268
|
||||
- Log public id by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1278
|
||||
- clarify license by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1277
|
||||
- Fix sam targetting everything by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1280
|
||||
- Add Creative Commons License to resources/non-commercial by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1284
|
||||
- Sword pattern by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1283
|
||||
- Display OFM25 ad in WinModal by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1281
|
||||
- QR code pattern by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1288
|
||||
- custom flag (1) by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1257
|
||||
- Allow railroad loops by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1274
|
||||
- patterns by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1290
|
||||
- Split build & deploy scripts by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1239
|
||||
- New icons by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1287
|
||||
- Add GitHub deployment support by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1291
|
||||
- bug: Fix version number and changelog by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1293
|
||||
- Revert "counter attack doesn't cancel out attack (#1132)" by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1301
|
||||
- Graceful handling of ping before join by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1295
|
||||
- refactor cosmetics out of PlayerInfo by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1299
|
||||
- Remove unused MON\_\* credentials by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1304
|
||||
- Add new patterns by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1294
|
||||
- Fix error-modal filling up the whole screen by @fraxxio in https://github.com/openfrontio/OpenFrontIO/pull/1298
|
||||
- Reapply "enable otel logs and metrics for staging environments" by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1310
|
||||
- Separate prod release environments by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1311
|
||||
- Change news title to release notes by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1312
|
||||
- Add localization support for leaderboard and team-related UI elements by @TomaszOleszko in https://github.com/openfrontio/OpenFrontIO/pull/1308
|
||||
- Better In Game UI by @DiesselOne in https://github.com/openfrontio/OpenFrontIO/pull/1243
|
||||
- w-320 by @PilkeySEK in https://github.com/openfrontio/OpenFrontIO/pull/1316
|
||||
- Patterns by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1318
|
||||
- Show structure levels by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1305
|
||||
- fix alliance expired message by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1323
|
||||
- Mark train stations and factories as experimental by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1309
|
||||
- allow alliance extension Fixes #491 by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1314
|
||||
- Additional patterns and subclass creation by @Sgt-lewis in https://github.com/openfrontio/OpenFrontIO/pull/1327
|
||||
- fix healthbars not being removed by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1329
|
||||
- lighten pattern by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1326
|
||||
- custom flag (2) by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1303
|
||||
- Make patterns puchasable with stripe by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1313
|
||||
- Improve icons readability by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1321
|
||||
- remove select on hover by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1330
|
||||
- Fix role lookup by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1335
|
||||
- Extend winner schema by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1333
|
||||
- mls 4.0 by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1336
|
||||
- upgrade unit when building a unit of same type by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1328
|
||||
- remove unit menu by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1338
|
||||
- unit upgrade minor improvements by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1337
|
||||
- Add gold fx when a tradeship lands by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1322
|
||||
- validate coords in construction execution by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1339
|
||||
- fix pattern and role bugs by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1343
|
||||
- Disable trains in public games by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1342
|
||||
- Add levels on structure sprites by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1346
|
||||
- Automatic train stations by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1353
|
||||
- Quads by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1347
|
||||
- Quads fix by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1356
|
||||
- Revert "enable otel logs and metrics for staging environments" by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1358
|
||||
- alliance renewal: fix request to renew when ally is dead, fix translation keys by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1359
|
||||
- Add new icon shapes and filter for filtering icons on the layer by @jrouillard in https://github.com/openfrontio/OpenFrontIO/pull/1348
|
||||
- upgrades not counting towards building discount bugfix by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1361
|
||||
- Add strait of Gibraltar and Italia maps by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1363
|
||||
- Radial menu: remove player info sub-radial by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1362
|
||||
- move unit display to bottom of screen by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1365
|
||||
- Move settings to it's own modal by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1366
|
||||
- update ui by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1368
|
||||
- Add localization support for game events, settings, and UI text elements by @TomaszOleszko in https://github.com/openfrontio/OpenFrontIO/pull/1372
|
||||
- Validate incoming parameters by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1371
|
||||
- Add domain, subdomain to GameRecord by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1370
|
||||
- bugfix: Crash during replay by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1375
|
||||
- fix top bar small screens by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1377
|
||||
- add domain and subdomain for dev env by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1379
|
||||
- fix pop delta number in TopBar by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1373
|
||||
- Add expand ratio to bot behavior class by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1376
|
||||
- bugfix: Crash by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1381
|
||||
- Don't erase patterns on page load by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1383
|
||||
- Require login to connect to staging by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1360
|
||||
- feat(news-button): highlight button when new version is available by @floriankilian in https://github.com/openfrontio/OpenFrontIO/pull/1385
|
||||
- Fix local development by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1388
|
||||
- fixed Custom Flags via Path Traversal by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1384
|
||||
- fix odd dimension maps by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1389
|
||||
- Improve unit updates & reloading by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1394
|
||||
- update meta by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1397
|
||||
- port execution bugfixes by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1400
|
||||
- Internationalization: Add i18n support for login/auth messages in main by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1406
|
||||
- Update README.md by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1407
|
||||
- Redraw existing railroads when redrawing the complete layer by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1410
|
||||
- Unit count by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1402
|
||||
- fix color allocator not selecting distinct colors by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1404
|
||||
- mls (v4.1) by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1357
|
||||
- remove levels player overview panel by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1414
|
||||
- Remove top bar & revert control panel by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1415
|
||||
- move player overview higher up by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1418
|
||||
- have mirv attack enemy units by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1419
|
||||
- fix team bar by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1422
|
||||
- fix team bar by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1425
|
||||
- Leaderboard improvements by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1424
|
||||
- radial menu attack self bugfix by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1426
|
||||
- remove radial animation, fix back button by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1427
|
||||
- Factory spawns trains by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1408
|
||||
- Followup: news-button: blue-glow; simpler localStorage by @floriankilian in https://github.com/openfrontio/OpenFrontIO/pull/1431
|
||||
- fix unit upgrade not considering cost by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1434
|
||||
- Enable @typescript eslint/prefer nullish coalescing eslint rule by @g-santos-m in https://github.com/openfrontio/OpenFrontIO/pull/1420
|
||||
- Eslint by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/998
|
||||
- Restore nation AI by @scottanderson in https://github.com/openfrontio/OpenFrontIO/pull/1440
|
||||
- fix number of land tiles fixes #1409 by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1445
|
||||
- Have radial menu refresh when open by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1437
|
||||
- make radial menu thicker by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1446
|
||||
- Fix: anonymized name isn't used in chat message by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1265
|
||||
- Revert MIRV attacks enemy units by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1452
|
||||
- Tradeship performance by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1448
|
||||
- Fix: "Game speed" not "Replay speed" during Single player game by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1457
|
||||
- Update asset license by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1458
|
||||
- Fix: attack on ally even with greyed out button by @VariableVince in https://github.com/openfrontio/OpenFrontIO/pull/1460
|
||||
- Create CLA.md by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1462
|
||||
- update pr template to have CLA checkbox. by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1465
|
||||
- Increase trade ship spawn rate by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1455
|
||||
- Increase traitor punishment by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1456
|
||||
- fix team leaderboard margin by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1469
|
||||
- leaderboard bugfix: show by default for medium to large screens. by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1470
|
||||
- fix control panel & events display scaling on mobile by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1471
|
||||
- alert on ws 1002 error by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1472
|
||||
- Fix Regex to allow Umlaute "üÜ" in username by @floriankilian in https://github.com/openfrontio/OpenFrontIO/pull/1466
|
||||
- Have port destination likelihood scale with level by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1473
|
||||
- remove spawn ad by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1482
|
||||
- fix squad allocator color palette by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1483
|
||||
- bug fix?: Hide UnitDisplay frame when all unit types are disabled by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1392
|
||||
- fix pop & gold not showing up on mobile UI by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1484
|
||||
- meta: reduce port gold multiplier & trade ship frequency by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1486
|
||||
- Fix language code mismatch during language switching by @Aotumuri in https://github.com/openfrontio/OpenFrontIO/pull/1416
|
||||
- Add close button to emoji table by @DevelopingTom in https://github.com/openfrontio/OpenFrontIO/pull/1479
|
||||
- increase MIRV to 35M by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1487
|
||||
- increase player panel z index so it is on top of spawn timer by @evanpelle in https://github.com/openfrontio/OpenFrontIO/pull/1488
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @LucasLion made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/875
|
||||
- @spicydll made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/934
|
||||
- @falcolnic made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/933
|
||||
- @drillskibo made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/949
|
||||
- @ERHash made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/971
|
||||
- @andrewNiziolek made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/976
|
||||
- @shaan150 made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/991
|
||||
- @Vermylion made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/993
|
||||
- @Jerryslang made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1005
|
||||
- @rldtech made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1011
|
||||
- @Maaxion made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1053
|
||||
- @Nephty made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1049
|
||||
- @LJoyL made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1022
|
||||
- @oleksandr-shysh made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1018
|
||||
- @E-EE-E made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1079
|
||||
- @Ble4Ch made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1118
|
||||
- @its-sii made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1122
|
||||
- @ghisloufou made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1135
|
||||
- @Egraveline made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1012
|
||||
- @omrih4 made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1161
|
||||
- @devalnor made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1195
|
||||
- @dengh made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1258
|
||||
- @fraxxio made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1298
|
||||
- @TomaszOleszko made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1308
|
||||
- @Sgt-lewis made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1327
|
||||
- @floriankilian made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1385
|
||||
- @g-santos-m made their first contribution in https://github.com/openfrontio/OpenFrontIO/pull/1420
|
||||
|
||||
**Full Changelog**: https://github.com/openfrontio/OpenFrontIO/compare/v0.23.19...v0.24.0
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
"name": "embelem",
|
||||
"role_group": "donor"
|
||||
},
|
||||
"AMo0AAAAAAAAAAAAAIAAJAACEAIIQCAgAAGCAAQgCBCAgEAAAggBCIAEIAAAAAAAAAAAAAAA": {
|
||||
"name": "contributor"
|
||||
},
|
||||
"AMlNAAAAAAAAAAAAAPAfAACAHwDwgAcAAMQf4ADgAAAgwM8BAAwACEPABwDA__8xEACIAAAACAMOgCQAAGEwwAAoAPCAAQGMCCBhAAYIQPwAnwEYgADSB_QEQAAIkD_wJwABgAH8gx8ABAAYwE99ABgAAAcAACBwAADgBDCA-AAAADwQoH8AAAAA_v8DAAAAAAAMAAAAAAAAAAAAAAA": {
|
||||
"name": "grogu_head",
|
||||
"role_group": "donor"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
Admin 1286738076386856991
|
||||
OG 1286743849707769936
|
||||
Creator 1286745100411473930
|
||||
Bots 1286910984702791711
|
||||
Challenger 1292157381496799264
|
||||
OG100 1314802550314237952
|
||||
Contributor 1314972008362020957
|
||||
Ping 1316444187276738612
|
||||
Server Booster 1319387513206345770
|
||||
Content Creator 1320961080750637076
|
||||
Beta Tester 1327125593791397929
|
||||
Early Access Supporter 1330243292306341969
|
||||
Mod 1338654590043820148
|
||||
Support Staff 1343759662545244296
|
||||
DevChatAccess 1345831753528377425
|
||||
Member 1347621713852235808
|
||||
Active Contributor 1354828445489692692
|
||||
Retired Staff 1355753028099117147
|
||||
Head Mod 1357747869742010661
|
||||
Money Haters 1359441841371480176
|
||||
Translator 1367345579272831128
|
||||
Head Translator 1367345660852174930
|
||||
Development Stream Ping 1369340951109304340
|
||||
Core Contributor 1370238576868200488
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
|
||||
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADICAYAAACZBDirAAAAAXNSR0IArs4c6QAACp1JREFUeF7t3U2InWcVB/D/NPRjIxYqtBSloaR+ZNMPCraCdEAhWSnSjSBUjTR1pYgNzaZoEbGlinanKYmlGxWsS0lAoaELg4JEkFZsGhoUMdBCFiLWmo55U0dCdfHc4cy9c3h/A6GQPnPe8/zOy5+bmfs+dy3/+3VfkgeSrCe59f/8f39FgACBTgJnL+XZ85fy7NkkJ69sfO0duziS5MFOO9MrAQIEFhB4OsnBzfVXBuDxJPsWKGQpAQIEOgqcSLJ/anwzAL3y6zhGPRMgsFWBy68EpwCcfuY3/fvYFwECBOYksD4F4NEkB+a0a3slQIBAkmNTAL7it71uBgIEZihwdgrAjRlu3JYJECBw+ZcgAtCNQIDALAUE4CzHbtMECEwCWw7Ae+5IPn9/8qE9yd49yQ3XAyVAgMByBF6/kLx45u0/zzyXnDq9tesuHIDXXZs89uXkqweSXbu2dlHfRYAAgSqBixeTbx9NHv1u8ua/Fqu6UADe/sHkJ08lH/CE8GLKVhMgsO0Cv/tD8tlDyfTf0a+FAvDxQ8kj/32KbvQS1hEgQGA5Ak8cSQ4/OX6t4QD8yF3JCz9KrrpqvLiVBAgQWKbAW28ld31y/FXgcAB+/xvJQ59e5lZciwABAosL/ODHyRcfHfu+4QA89dPkw7ePFbWKAAECqxI4+etk/TNjVx8OwAu/Td79rrGiVhEgQGBVAudfS266d+zqwwG48fJYQasIECCwaoG128Y6EIBjTlYRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIQAA2GpZWCRCoFRCAtZ6qESDQSEAANhqWVgkQqBUQgLWeqhEg0EhAADYallYJEKgVEIC1nqoRINBIoD4AzzXavVYJEJi1wNotY9tfS7IxsvSfv7x6ZJk1BAgQWLnANR97c6iH4QD8y8dvHCpoEQECBFYtcPMvzg+1MByAr+5+31BBiwgQILBqgd2v/mmoheEAPPf+9w4VtIgAAQKrFrjlj38eakEADjFZRIBAJwEB2GlaeiVAoFRAAJZyKkaAQCcBAdhpWnolQKBUQACWcipGgEAnAQHYaVp6JUCgVEAAlnIqRoBAJ4HyAPRG6E7j1yuBeQuUvxHao3DzvqHsnkAngfJH4RyG0Gn8eiUwb4HywxA2HIc17zvK7gk0Eig/Dmvj5Ua71yoBArMWqD8QVQDO+oayeQKdBARgp2nplQCBUgEBWMqpGAECnQQEYKdp6ZUAgVIBAVjKqRgBAp0EBGCnaemVAIFSAQFYyqkYAQKdBOoD0BuhO81frwRmLVD+RmiPws36frJ5Aq0Eyh+FcxhCq/lrlsCsBcoPQ3Ac1qzvJ5sn0Eqg/Dgsnwvcav6aJTBrgfIDUQXgrO8nmyfQSkAAthqXZgkQqBQQgJWaahEg0EpAALYal2YJEKgUEICVmmoRINBKQAC2GpdmCRCoFBCAlZpqESDQSqA8AL0RutX8NUtg1gLlb4T2KNys7yebJ9BKoPxROIchtJq/ZgnMWqD8MASfCzzr+8nmCbQSKD8Oy+cCt5q/ZgnMWqD+QFSfCzzrG8rmCXQSEICdpqVXAgRKBQRgKadiBAh0EhCAnaalVwIESgUEYCmnYgQIdBIQgJ2mpVcCBEoFBGApp2IECHQSqA9Anwvcaf56JTBrgfI3QnsUbtb3k80TaCVQ/iicwxBazV+zBGYtUH4YguOwZn0/2TyBVgLlx2H5WMxW89csgVkLlB+IKgBnfT/ZPIFWAgKw1bg0S4BApYAArNRUiwCBVgICsNW4NEuAQKWAAKzUVIsAgVYCArDVuDRLgEClgACs1FSLAIFWAuUB6I3QreavWQKzFih/I7RH4WZ9P9k8gVYC5Y/COQyh1fw1S2DWAuWHIfhc4FnfTzZPoJVA+XFYPhe41fw1S2DWAvUHovpc4FnfUDZPoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJOAAOw0Lb0SIFAqIABLORUjQKCTgADsNC29EiBQKiAASzkVI0Cgk4AA7DQtvRIgUCogAEs5FSNAoJNAeQD+9VfJje/pRKBXAgTmKHD+teSme8d2vpZkY2Tp8WPJvo+OrLSGAAECqxM48UKy/8DY9YcD8PFDySMHx4paRYAAgVUJPHEkOfzk2NWHA/C23cnvf55cc/VYYasIECCwbIG//T258xPJmXNjVx4OwKnc4YeSbz08VtgqAgQILFvgK99MvvfM+FUXCsBdu5Lf/Cy5c+/4BawkQIDAMgROv5Tc/ank4sXxqy0UgFPZ665Nvv6l5OEvJFMg+iJAgMAqBabA+86x5GtPJf94Y7FOFg7AzfL33JF87v5k7563/9xw/WIXtpoAAQJbFXj9QvLimeSlM8kPn0tOnd5apS0H4NYu57sIECCwcwQE4M6ZhU4IEFiygABcMrjLESCwcwSmAHwlya07pyWdECBAYCkCZ6cAPJpk8MGRpTTlIgQIEFiGwLEpAO9L8vwyruYaBAgQ2EEC61MATl9Hkjy4gxrTCgECBLZT4OkkBzcDcLrQ8ST7tvOKahMgQGAHCJxIsn/q48oA9EpwB0xGCwQIbKvA5Vd+m1d4ZwBOfz/9TPCBSz8XXPfb4W0dhOIECCxH4Ox/fs/xbJKTV17y3+OTpZ+KLXH6AAAAAElFTkSuQmCC" x="7.500" y="32.813" width="135.000" height="84.375" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 198 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 291 B |
|
After Width: | Height: | Size: 812 KiB |
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 224 B After Width: | Height: | Size: 235 B |
@@ -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": "Пренасочваме ви...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"terms_of_service": "Terms of Service"
|
||||
},
|
||||
"news": {
|
||||
"full_changelog": "See the complete change log",
|
||||
"see_all_releases": "See all releases",
|
||||
"github_link": "on GitHub",
|
||||
"title": "Release Notes"
|
||||
},
|
||||
@@ -43,6 +43,7 @@
|
||||
"action_move_camera": "Move camera",
|
||||
"action_ratio_change": "Decrease/Increase attack ratio",
|
||||
"action_reset_gfx": "Reset graphics",
|
||||
"action_auto_upgrade": "Auto-upgrade nearest building",
|
||||
"ui_section": "Game UI",
|
||||
"ui_leaderboard": "Leaderboard",
|
||||
"ui_your_team": "Your team:",
|
||||
@@ -51,7 +52,6 @@
|
||||
"ui_control_desc": "The control panel contains the following elements:",
|
||||
"ui_pop": "Pop - The amount of units you have, your max population and the rate at which you gain them.",
|
||||
"ui_gold": "Gold - The amount of gold you have and the rate at which you gain it.",
|
||||
"ui_troops_workers": "Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.",
|
||||
"ui_attack_ratio": "Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider. Having more attacking troops than defending troops will make you lose fewer troops in the attack, while having less will increase the damage dealt to your attacking troops. The effect doesn't go beyond ratios of 2:1.",
|
||||
"ui_events": "Event panel",
|
||||
"ui_events_desc": "The Event panel displays the latest events, requests and Quick Chat messages. Some examples are:",
|
||||
@@ -130,7 +130,9 @@
|
||||
"disable_nations": "Disable Nations",
|
||||
"instant_build": "Instant build",
|
||||
"infinite_gold": "Infinite gold",
|
||||
"donate_gold": "Donate gold",
|
||||
"infinite_troops": "Infinite troops",
|
||||
"donate_troops": "Donate troops",
|
||||
"disable_nukes": "Disable Nukes",
|
||||
"enables_title": "Enable Settings",
|
||||
"start": "Start Game"
|
||||
@@ -147,6 +149,7 @@
|
||||
"africa": "Africa",
|
||||
"asia": "Asia",
|
||||
"mars": "Mars",
|
||||
"marsrevised": "Mars (Revised)",
|
||||
"southamerica": "South America",
|
||||
"britannia": "Britannia",
|
||||
"gatewaytotheatlantic": "Gateway to the Atlantic",
|
||||
@@ -163,7 +166,9 @@
|
||||
"baikal": "Baikal",
|
||||
"halkidiki": "Halkidiki",
|
||||
"straitofgibraltar": "Strait of Gibraltar",
|
||||
"italia": "Italia"
|
||||
"italia": "Italia",
|
||||
"yenisei": "Yenisei",
|
||||
"pluto": "Pluto"
|
||||
},
|
||||
"map_categories": {
|
||||
"continental": "Continental",
|
||||
@@ -209,12 +214,15 @@
|
||||
"disable_nations": "Disable Nations",
|
||||
"instant_build": "Instant build",
|
||||
"infinite_gold": "Infinite gold",
|
||||
"donate_gold": "Donate gold",
|
||||
"infinite_troops": "Infinite troops",
|
||||
"donate_troops": "Donate troops",
|
||||
"enables_title": "Enable Settings",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
"waiting": "Waiting for players...",
|
||||
"start": "Start Game"
|
||||
"start": "Start Game",
|
||||
"host_badge": "Host"
|
||||
},
|
||||
"team_colors": {
|
||||
"red": "Red",
|
||||
@@ -274,9 +282,15 @@
|
||||
"special_effects_desc": "Toggle special effects. Deactivate to improve performances",
|
||||
"special_effects_enabled": "Special effects enabled",
|
||||
"special_effects_disabled": "Special effects disabled",
|
||||
"structure_sprites_label": "Structure Sprites",
|
||||
"structure_sprites_desc": "Toggle structure sprites",
|
||||
"structure_sprites_enabled": "Structure Sprites enabled",
|
||||
"structure_sprites_disabled": "Structure Sprites disabled",
|
||||
"anonymous_names_label": "Hidden Names",
|
||||
"anonymous_names_desc": "Hide real player names with random ones on your screen.",
|
||||
"anonymous_names_enabled": "Anonymous names enabled",
|
||||
"lobby_id_visibility_label": "Hidden Lobby IDs",
|
||||
"lobby_id_visibility_desc": "Hide Lobby ID in private lobby creation",
|
||||
"real_names_shown": "Real names shown",
|
||||
"left_click_label": "Left Click to Open Menu",
|
||||
"left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.",
|
||||
@@ -285,10 +299,11 @@
|
||||
"right_click_opens_menu": "Right click opens menu",
|
||||
"attack_ratio_label": "⚔️ Attack Ratio",
|
||||
"attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)",
|
||||
"troop_ratio_label": "🪖🛠️ Troops and Workers Ratio",
|
||||
"troop_ratio_desc": "Adjust the balance between troops (for combat) and workers (for gold production) (1–100%)",
|
||||
"territory_patterns_label": "🏳️ Territory Patterns",
|
||||
"territory_patterns_desc": "Choose whether to display territory pattern designs in game",
|
||||
"performance_overlay_label": "Performance Overlay",
|
||||
"performance_overlay_desc": "Toggle the performance overlay. When enabled, the performance overlay will be displayed. Press shift-D during game to toggle.",
|
||||
"easter_writing_speed_label": "Writing Speed Multiplier",
|
||||
"easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)",
|
||||
"easter_bug_count_label": "Bug Count",
|
||||
@@ -438,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",
|
||||
@@ -479,7 +500,8 @@
|
||||
"accept_alliance": "Accept",
|
||||
"reject_alliance": "Reject",
|
||||
"alliance_renewed": "Your alliance with {name} has been renewed",
|
||||
"ignore": "Ignore"
|
||||
"ignore": "Ignore",
|
||||
"unit_voluntarily_deleted": "Unit voluntarily deleted"
|
||||
},
|
||||
"unit_info_modal": {
|
||||
"structure_info": "Structure Info",
|
||||
@@ -498,10 +520,8 @@
|
||||
"default": "Default"
|
||||
},
|
||||
"control_panel": {
|
||||
"pop": "Pop",
|
||||
"gold": "Gold",
|
||||
"troops": "Troops",
|
||||
"workers": "Workers",
|
||||
"attack_ratio": "Attack Ratio"
|
||||
},
|
||||
"player_panel": {
|
||||
@@ -574,6 +594,7 @@
|
||||
"openfront": "OpenFront",
|
||||
"t_rex": "T-Rex",
|
||||
"embelem": "Emblem",
|
||||
"contributor": "Contributor",
|
||||
"grogu_head": "Grogu Head",
|
||||
"grogu": "Grogu"
|
||||
}
|
||||
@@ -586,5 +607,9 @@
|
||||
"redirecting": "You are being redirected...",
|
||||
"not_authorized": "You are not authorized to access this website.",
|
||||
"contact_admin": "If you believe you are seeing this message in error, please contact the website administrator."
|
||||
},
|
||||
"radial_menu": {
|
||||
"delete_unit_title": "Delete Unit",
|
||||
"delete_unit_description": "Click to delete the nearest unit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "リダイレクト中…",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Вы будете перенаправлены...",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Вас буде переспрямовано...",
|
||||
|
||||
@@ -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": "正在将您重新定向……",
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
"flag": "tm",
|
||||
"name": "Turkmenistan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [851, 589],
|
||||
"flag": "Circassia",
|
||||
"name": "Circassia",
|
||||
"strength": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@
|
||||
"flag": "am",
|
||||
"name": "Armenia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1115, 555],
|
||||
"flag": "Circassia",
|
||||
"name": "Circassia",
|
||||
"strength": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,267 +12,585 @@
|
||||
"name": "Giant_World_Map",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [777, 540],
|
||||
"flag": "us",
|
||||
"name": "United States",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [770.784, 287.776],
|
||||
"flag": "ca",
|
||||
"name": "Canada",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [777, 780],
|
||||
"flag": "mx",
|
||||
"name": "Mexico",
|
||||
"coordinates": [2309, 535],
|
||||
"flag": "tr",
|
||||
"name": "Türkiye",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1025, 744],
|
||||
"flag": "cu",
|
||||
"name": "Cuba",
|
||||
"coordinates": [2030, 409],
|
||||
"flag": "west_germany",
|
||||
"name": "West Germany",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1085.728, 990],
|
||||
"flag": "co",
|
||||
"name": "Colombia",
|
||||
"coordinates": [2074, 382],
|
||||
"flag": "east_germany",
|
||||
"name": "East Germany",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1228.6960000000001, 990],
|
||||
"flag": "ve",
|
||||
"name": "Venezuela",
|
||||
"coordinates": [1966, 442],
|
||||
"flag": "fr",
|
||||
"name": "France",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1220, 1485],
|
||||
"flag": "ar",
|
||||
"name": "Argentina",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1330, 1190],
|
||||
"flag": "br",
|
||||
"name": "Brazil",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2650, 1897],
|
||||
"flag": "aq",
|
||||
"name": "Antarctica",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1469.048, 120.61200000000001],
|
||||
"flag": "gl",
|
||||
"name": "Greenland",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1721.832, 236.99200000000002],
|
||||
"flag": "is",
|
||||
"name": "Iceland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1916.6000000000001, 393.576],
|
||||
"flag": "gb",
|
||||
"name": "United Kingdom",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1837.864, 387.228],
|
||||
"flag": "ie",
|
||||
"name": "Ireland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1885, 550],
|
||||
"flag": "es",
|
||||
"coordinates": [1872, 528],
|
||||
"flag": "Fascist Spain",
|
||||
"name": "Spain",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2080.288, 529],
|
||||
"coordinates": [2074, 498],
|
||||
"flag": "it",
|
||||
"name": "Italy",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1980, 455],
|
||||
"flag": "fr",
|
||||
"name": "France",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [2060, 425],
|
||||
"flag": "de",
|
||||
"name": "Germany",
|
||||
"coordinates": [1912, 379],
|
||||
"flag": "gb",
|
||||
"name": "United Kingdom",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2111, 277],
|
||||
"flag": "se",
|
||||
"name": "Sweden",
|
||||
"coordinates": [1841, 373],
|
||||
"flag": "ie",
|
||||
"name": "Ireland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2165, 400],
|
||||
"coordinates": [2153, 378],
|
||||
"flag": "pl",
|
||||
"name": "Poland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2205, 397.808],
|
||||
"flag": "by",
|
||||
"name": "Belarus",
|
||||
"coordinates": [2178, 539],
|
||||
"flag": "gr",
|
||||
"name": "Greece",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2223.256, 514.188],
|
||||
"flag": "ro",
|
||||
"coordinates": [2222, 493],
|
||||
"flag": "bg",
|
||||
"name": "Bulgaria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2135, 481],
|
||||
"flag": "yugoslavia",
|
||||
"name": "Yugoslavia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2242, 461],
|
||||
"flag": "Communist Romania",
|
||||
"name": "Romania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2405.592, 579.784],
|
||||
"flag": "tr",
|
||||
"name": "Turkey",
|
||||
"coordinates": [2163, 441],
|
||||
"flag": "hu",
|
||||
"name": "Hungary",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2007.768, 281.428],
|
||||
"coordinates": [2272, 418],
|
||||
"flag": "Ukrainian SSR",
|
||||
"name": "Ukrainian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2093, 297],
|
||||
"flag": "se",
|
||||
"name": "Sweden",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2027, 282],
|
||||
"flag": "no",
|
||||
"name": "Norway",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2200.464, 281.428],
|
||||
"coordinates": [2191, 194],
|
||||
"flag": "Sami flag",
|
||||
"name": "Sapmi",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2206, 262],
|
||||
"flag": "fi",
|
||||
"name": "Finland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2277.128, 443],
|
||||
"flag": "ua",
|
||||
"name": "Ukraine",
|
||||
"coordinates": [2376, 363],
|
||||
"flag": "Russian SSR",
|
||||
"name": "Russian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2480, 311],
|
||||
"flag": "ru",
|
||||
"name": "Russia",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [3175, 400],
|
||||
"flag": "mn",
|
||||
"name": "Mongolia",
|
||||
"coordinates": [2222, 371],
|
||||
"flag": "Byelorussian SSR",
|
||||
"name": "Byelorussian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3170, 680],
|
||||
"flag": "cn",
|
||||
"name": "China",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [2834.496, 789.268],
|
||||
"flag": "in",
|
||||
"name": "India",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [2643.8720000000003, 505.72400000000005],
|
||||
"flag": "kz",
|
||||
"name": "Kazakhstan",
|
||||
"coordinates": [2441, 507],
|
||||
"flag": "Georgian SSR",
|
||||
"name": "Georgian SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2565.136, 653.844],
|
||||
"flag": "ir",
|
||||
"name": "Islamic Republic Of Iran",
|
||||
"coordinates": [2402, 580],
|
||||
"flag": "Second Republic of Iraq",
|
||||
"name": "Iraq",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2440.8160000000003, 742.716],
|
||||
"coordinates": [2353, 595],
|
||||
"flag": "sy",
|
||||
"name": "Syria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2414, 679],
|
||||
"flag": "sa",
|
||||
"name": "Saudi Arabia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3478, 1370],
|
||||
"coordinates": [2434, 815],
|
||||
"flag": "North yemen",
|
||||
"name": "North Yemen",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2479, 824],
|
||||
"flag": "south yemen",
|
||||
"name": "South Yemen",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2554, 724],
|
||||
"flag": "ae",
|
||||
"name": "United Arab Emirates",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2532, 609],
|
||||
"flag": "Pahlavi Iran",
|
||||
"name": "Iran",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2683, 650],
|
||||
"flag": "pk",
|
||||
"name": "Pakistan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2654, 580],
|
||||
"flag": "af",
|
||||
"name": "Afghanistan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2727, 416],
|
||||
"flag": "Kazakh SSR",
|
||||
"name": "Kazakh SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2556, 544],
|
||||
"flag": "Turkmen SSR",
|
||||
"name": "Turkmen SSR",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2947, 362],
|
||||
"flag": "Zheleznogorsk",
|
||||
"name": "Zheleznogorsk",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3252, 229],
|
||||
"flag": "Siberia",
|
||||
"name": "Siberia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2810, 744],
|
||||
"flag": "in",
|
||||
"name": "India",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1717, 237],
|
||||
"flag": "is",
|
||||
"name": "Iceland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2944, 709],
|
||||
"flag": "bd",
|
||||
"name": "Bangladesh",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2868, 635],
|
||||
"flag": "np",
|
||||
"name": "Nepal",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3254, 672],
|
||||
"flag": "cn",
|
||||
"name": "China",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3373, 521],
|
||||
"flag": "kp",
|
||||
"name": "North Korea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3389, 573],
|
||||
"flag": "kr",
|
||||
"name": "South Korea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3515, 571],
|
||||
"flag": "jp",
|
||||
"name": "Japan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3026, 457],
|
||||
"flag": "mn",
|
||||
"name": "Mongolia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3229, 995],
|
||||
"flag": "id",
|
||||
"name": "Indonesia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3121, 755],
|
||||
"flag": "vn",
|
||||
"name": "North Vietnam",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3153, 833],
|
||||
"flag": "South Vietnam",
|
||||
"name": "South Vietnam",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3013, 722],
|
||||
"flag": "Burma2",
|
||||
"name": "Burma",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3095, 822],
|
||||
"flag": "kh",
|
||||
"name": "Cambodia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3538, 1067],
|
||||
"flag": "pg",
|
||||
"name": "Papua New Guinea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3542, 1356],
|
||||
"flag": "au",
|
||||
"name": "Australia",
|
||||
"strength": 2
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3880, 1516],
|
||||
"coordinates": [3422, 1203],
|
||||
"flag": "Australian Aboriginal Flag",
|
||||
"name": "Nawan-mirri",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [3880, 1521],
|
||||
"flag": "nz",
|
||||
"name": "New Zealand",
|
||||
"strength": 0.5
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1902.096, 700],
|
||||
"coordinates": [2632, 1893],
|
||||
"flag": "aq",
|
||||
"name": "Antarctica",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2038, 590],
|
||||
"flag": "tn",
|
||||
"name": "Tunisia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2116, 653],
|
||||
"flag": "ly",
|
||||
"name": "Libya",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2281, 653],
|
||||
"flag": "United Arab Republic",
|
||||
"name": "United Arab Republic",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1859, 613],
|
||||
"flag": "ma",
|
||||
"name": "Morocco",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1943, 615],
|
||||
"flag": "dz",
|
||||
"name": "Algeria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2134.16, 680],
|
||||
"flag": "ly",
|
||||
"name": "Libyan Arab Jamahiriya",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2262.6240000000003, 708.86],
|
||||
"flag": "eg",
|
||||
"name": "Egypt",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1995.336, 867.5600000000001],
|
||||
"flag": "ne",
|
||||
"name": "Niger",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2304.064, 859.096],
|
||||
"coordinates": [2317, 754],
|
||||
"flag": "sd",
|
||||
"name": "Sudan",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2225.328, 1074.928],
|
||||
"flag": "cd",
|
||||
"name": "The Democratic Republic of the Congo",
|
||||
"coordinates": [2466, 918],
|
||||
"flag": "so",
|
||||
"name": "Somalia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2391.088, 937.388],
|
||||
"flag": "et",
|
||||
"coordinates": [2352, 895],
|
||||
"flag": "Imperial Ethiopia",
|
||||
"name": "Ethiopia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2188, 1374.0120000000002],
|
||||
"flag": "za",
|
||||
"coordinates": [1790, 729],
|
||||
"flag": "Mauritania",
|
||||
"name": "Mauritania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2154, 764],
|
||||
"flag": "td",
|
||||
"name": "Chad",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2051, 745],
|
||||
"flag": "ne",
|
||||
"name": "Niger",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2040, 930],
|
||||
"flag": "ng",
|
||||
"name": "Nigeria",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1805, 907],
|
||||
"flag": "lr",
|
||||
"name": "Liberia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2195, 918],
|
||||
"flag": "cf",
|
||||
"name": "Central African Republic",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2197, 1070],
|
||||
"flag": "Zaire",
|
||||
"name": "Zaire",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2189, 1372],
|
||||
"flag": "Apartheid South Africa",
|
||||
"name": "South Africa",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2459, 1230],
|
||||
"coordinates": [2452, 1247],
|
||||
"flag": "mg",
|
||||
"name": "Madagascar",
|
||||
"strength": 0.5
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2170, 880],
|
||||
"flag": "td",
|
||||
"name": "Chad",
|
||||
"coordinates": [2356, 1165],
|
||||
"flag": "mz",
|
||||
"name": "Mozambique",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2368, 1032],
|
||||
"flag": "tz",
|
||||
"name": "Tanzania",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1934, 762],
|
||||
"flag": "ml",
|
||||
"name": "Mali",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2128, 1292],
|
||||
"flag": "Apartheid South Africa",
|
||||
"name": "South West Africa",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2099, 1178],
|
||||
"flag": "ao",
|
||||
"name": "Angola",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1375, 1121],
|
||||
"flag": "br",
|
||||
"name": "Brazil",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1203, 1059],
|
||||
"flag": "amazonas",
|
||||
"name": "Amazonas",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1210, 1395],
|
||||
"flag": "ar",
|
||||
"name": "Argentina",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1107, 1419],
|
||||
"flag": "cl",
|
||||
"name": "Chile",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1064, 1114],
|
||||
"flag": "pe",
|
||||
"name": "Peru",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1065, 938],
|
||||
"flag": "co",
|
||||
"name": "Colombia",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1192, 938],
|
||||
"flag": "ve",
|
||||
"name": "Venezuela",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [913, 833],
|
||||
"flag": "ni",
|
||||
"name": "Nicaragua",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [788, 744],
|
||||
"flag": "mx",
|
||||
"name": "Mexico",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1011, 555],
|
||||
"flag": "us",
|
||||
"name": "USA",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [800, 624],
|
||||
"flag": "Texas",
|
||||
"name": "Texas",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [551, 564],
|
||||
"flag": "California",
|
||||
"name": "California",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [703, 483],
|
||||
"flag": "Utah",
|
||||
"name": "Utah",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1077, 444],
|
||||
"flag": "Quebec",
|
||||
"name": "Quebec",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1231, 395],
|
||||
"flag": "Newfoundland",
|
||||
"name": "Newfoundland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [967, 418],
|
||||
"flag": "ca",
|
||||
"name": "Canada",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [170, 244],
|
||||
"flag": "Alaska",
|
||||
"name": "Alaska",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [741, 234],
|
||||
"flag": "Nunavut",
|
||||
"name": "Nunavut",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [484, 256],
|
||||
"flag": "Yukon",
|
||||
"name": "Yukon",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1434, 223],
|
||||
"flag": "gl",
|
||||
"name": "Greenland",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2247, 1229],
|
||||
"flag": "Rhodesia",
|
||||
"name": "Rhodesia",
|
||||
"strength": 1
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 1150,
|
||||
"num_land_tiles": 1021856,
|
||||
"width": 2500
|
||||
},
|
||||
"mini_map": {
|
||||
"height": 575,
|
||||
"num_land_tiles": 248400,
|
||||
"width": 1250
|
||||
},
|
||||
"name": "Mars",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [2030, 868],
|
||||
"flag": "us",
|
||||
"name": "United States Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1710, 340],
|
||||
"flag": "ru",
|
||||
"name": "Russian Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [192, 410],
|
||||
"flag": "",
|
||||
"name": "European Space Agency",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1460, 488],
|
||||
"flag": "gb",
|
||||
"name": "British Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [700, 600],
|
||||
"flag": "cn",
|
||||
"name": "Chinese Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1200, 276],
|
||||
"flag": "kp",
|
||||
"name": "North Korean Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1760, 840],
|
||||
"flag": "",
|
||||
"name": "Olympus Mons Tribe",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [837, 200],
|
||||
"flag": "African union",
|
||||
"name": "African Space Agency",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1140, 890],
|
||||
"flag": "",
|
||||
"name": "Elysium Mons Tribe",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [430, 740],
|
||||
"flag": "in",
|
||||
"name": "Indian Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1350, 100],
|
||||
"flag": "un",
|
||||
"name": "United Nations Base",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [460, 110],
|
||||
"flag": "ca",
|
||||
"name": "Canadian Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1070, 575],
|
||||
"flag": "jp",
|
||||
"name": "Japanese Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1960, 555],
|
||||
"flag": "de",
|
||||
"name": "German Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [2100, 400],
|
||||
"flag": "fr",
|
||||
"name": "French Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [2200, 515],
|
||||
"flag": "tr",
|
||||
"name": "Turkish Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1924, 1015],
|
||||
"flag": "za",
|
||||
"name": "South African Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1727, 510],
|
||||
"flag": "es",
|
||||
"name": "Spanish Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [295, 565],
|
||||
"flag": "ch",
|
||||
"name": "Swiss Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [94, 82],
|
||||
"flag": "br",
|
||||
"name": "Brazilian Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1070, 69],
|
||||
"flag": "sa",
|
||||
"name": "Saudi Arabian Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [470, 240],
|
||||
"flag": "is",
|
||||
"name": "Iceland Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [106, 312],
|
||||
"flag": "ie",
|
||||
"name": "Irish Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1900, 380],
|
||||
"flag": "ro",
|
||||
"name": "Romanian Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1518, 40],
|
||||
"flag": "se",
|
||||
"name": "Swedish Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1870, 200],
|
||||
"flag": "mn",
|
||||
"name": "Mongolian Colony",
|
||||
"strength": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -23,12 +23,6 @@
|
||||
"name": "New Zealand",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [686, 407],
|
||||
"flag": "pg",
|
||||
"name": "Papua New Guinea",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [436, 407],
|
||||
"flag": "tl",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 1300,
|
||||
"num_land_tiles": 1986092,
|
||||
"width": 2100
|
||||
},
|
||||
"mini_map": {
|
||||
"height": 650,
|
||||
"num_land_tiles": 489193,
|
||||
"width": 1050
|
||||
},
|
||||
"name": "Pluto",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [396, 364],
|
||||
"flag": "us",
|
||||
"name": "United States Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [285, 970],
|
||||
"flag": "ru",
|
||||
"name": "Russian Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1700, 400],
|
||||
"flag": "",
|
||||
"name": "European Space Agency",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1190, 1210],
|
||||
"flag": "gb",
|
||||
"name": "United Kingdom Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1700, 1000],
|
||||
"flag": "cn",
|
||||
"name": "Chinese Colony",
|
||||
"strength": 3
|
||||
},
|
||||
{
|
||||
"coordinates": [1200, 185],
|
||||
"flag": "jp",
|
||||
"name": "Japanese Colony",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [830, 1000],
|
||||
"flag": "in",
|
||||
"name": "Indian Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [75, 300],
|
||||
"flag": "",
|
||||
"name": "African Space Agency",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [750, 560],
|
||||
"flag": "au",
|
||||
"name": "Australian Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1075, 325],
|
||||
"flag": "",
|
||||
"name": "North Korean Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1920, 100],
|
||||
"flag": "",
|
||||
"name": "United Nations Base",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [25, 1245],
|
||||
"flag": "pl",
|
||||
"name": "Polish Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1170, 1080],
|
||||
"flag": "ie",
|
||||
"name": "Irish Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [1466, 1233],
|
||||
"flag": "cl",
|
||||
"name": "Chilean Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [560, 20],
|
||||
"flag": "za",
|
||||
"name": "South African Colony",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [2088, 1008],
|
||||
"flag": "",
|
||||
"name": "Free Pluto State",
|
||||
"strength": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 2500,
|
||||
"num_land_tiles": 3371126,
|
||||
"width": 2000
|
||||
},
|
||||
"mini_map": {
|
||||
"height": 1250,
|
||||
"num_land_tiles": 830966,
|
||||
"width": 1000
|
||||
},
|
||||
"name": "Yenisei",
|
||||
"nations": [
|
||||
{
|
||||
"coordinates": [1730, 900],
|
||||
"flag": "ru",
|
||||
"name": "Baikalovsk",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [1880, 2110],
|
||||
"flag": "ru",
|
||||
"name": "Mungui",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [560, 2020],
|
||||
"flag": "ru",
|
||||
"name": "Polykarpovsk",
|
||||
"strength": 1
|
||||
},
|
||||
{
|
||||
"coordinates": [580, 1270],
|
||||
"flag": "ru",
|
||||
"name": "Central Island",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [80, 460],
|
||||
"flag": "ru",
|
||||
"name": "West Coast",
|
||||
"strength": 2
|
||||
},
|
||||
{
|
||||
"coordinates": [725, 630],
|
||||
"flag": "ru",
|
||||
"name": "Northern Island",
|
||||
"strength": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 627 B |
@@ -1,5 +1,11 @@
|
||||
import { translateText } from "../client/Utils";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import {
|
||||
AutoUpgradeEvent,
|
||||
DoBoatAttackEvent,
|
||||
DoGroundAttackEvent,
|
||||
InputHandler,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
} from "./InputHandler";
|
||||
import {
|
||||
ClientID,
|
||||
GameID,
|
||||
@@ -8,11 +14,6 @@ import {
|
||||
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 {
|
||||
ErrorUpdate,
|
||||
GameUpdateType,
|
||||
@@ -20,30 +21,33 @@ import {
|
||||
HashUpdate,
|
||||
WinUpdate,
|
||||
} from "../core/game/GameUpdates";
|
||||
import { GameRenderer, createRenderer } from "./graphics/GameRenderer";
|
||||
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 {
|
||||
DoBoatAttackEvent,
|
||||
DoGroundAttackEvent,
|
||||
InputHandler,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
} from "./InputHandler";
|
||||
import { endGame, startGame, startTime } from "./LocalPersistantStats";
|
||||
import { getPersistentID } from "./Main";
|
||||
import { PlayerActions, UnitType } from "../core/game/Game";
|
||||
import {
|
||||
SendAttackIntentEvent,
|
||||
SendBoatAttackIntentEvent,
|
||||
SendHashEvent,
|
||||
SendSpawnIntentEvent,
|
||||
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 interface LobbyConfig {
|
||||
export type LobbyConfig = {
|
||||
serverConfig: ServerConfig;
|
||||
pattern: string | undefined;
|
||||
flag: string;
|
||||
@@ -55,15 +59,14 @@ export interface LobbyConfig {
|
||||
gameStartInfo?: GameStartInfo;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
}
|
||||
};
|
||||
|
||||
export function joinLobby(
|
||||
eventBus: EventBus,
|
||||
lobbyConfig: LobbyConfig,
|
||||
onPrestart: () => void,
|
||||
onJoin: () => void,
|
||||
): () => void {
|
||||
const eventBus = new EventBus();
|
||||
|
||||
console.log(
|
||||
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
|
||||
);
|
||||
@@ -82,7 +85,7 @@ export function joinLobby(
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
if (message.type === "prestart") {
|
||||
console.log(`lobby: game prestarting: ${JSON.stringify(message)}`);
|
||||
terrainLoad = loadTerrainMap(message.gameMap);
|
||||
terrainLoad = loadTerrainMap(message.gameMap, terrainMapFileLoader);
|
||||
onPrestart();
|
||||
}
|
||||
if (message.type === "start") {
|
||||
@@ -98,6 +101,7 @@ export function joinLobby(
|
||||
transport,
|
||||
userSettings,
|
||||
terrainLoad,
|
||||
terrainMapFileLoader,
|
||||
).then((r) => r.start());
|
||||
}
|
||||
if (message.type === "error") {
|
||||
@@ -125,6 +129,7 @@ async function createClientGame(
|
||||
transport: Transport,
|
||||
userSettings: UserSettings,
|
||||
terrainLoad: Promise<TerrainMapData> | null,
|
||||
mapLoader: GameMapLoader,
|
||||
): Promise<ClientGameRunner> {
|
||||
if (lobbyConfig.gameStartInfo === undefined) {
|
||||
throw new Error("missing gameStartInfo");
|
||||
@@ -139,7 +144,10 @@ async function createClientGame(
|
||||
if (terrainLoad) {
|
||||
gameMap = await terrainLoad;
|
||||
} else {
|
||||
gameMap = await loadTerrainMap(lobbyConfig.gameStartInfo.config.gameMap);
|
||||
gameMap = await loadTerrainMap(
|
||||
lobbyConfig.gameStartInfo.config.gameMap,
|
||||
mapLoader,
|
||||
);
|
||||
}
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameStartInfo,
|
||||
@@ -184,17 +192,17 @@ export class ClientGameRunner {
|
||||
|
||||
private lastMousePosition: { x: number; y: number } | null = null;
|
||||
|
||||
private lastMessageTime: number = 0;
|
||||
private connectionCheckInterval: NodeJS.Timeout | null = null;
|
||||
private lastMessageTime = 0;
|
||||
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();
|
||||
}
|
||||
@@ -221,7 +229,7 @@ export class ClientGameRunner {
|
||||
players,
|
||||
// Not saving turns locally
|
||||
[],
|
||||
startTime(),
|
||||
startTime() ?? 0,
|
||||
Date.now(),
|
||||
update.winner,
|
||||
this.lobby.serverConfig,
|
||||
@@ -242,6 +250,7 @@ export class ClientGameRunner {
|
||||
}, 20000);
|
||||
this.eventBus.on(MouseUpEvent, this.inputEvent.bind(this));
|
||||
this.eventBus.on(MouseMoveEvent, this.onMouseMove.bind(this));
|
||||
this.eventBus.on(AutoUpgradeEvent, this.autoUpgradeEvent.bind(this));
|
||||
this.eventBus.on(
|
||||
DoBoatAttackEvent,
|
||||
this.doBoatAttackUnderCursor.bind(this),
|
||||
@@ -279,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();
|
||||
@@ -355,7 +364,7 @@ export class ClientGameRunner {
|
||||
this.transport.connect(onconnect, onmessage);
|
||||
}
|
||||
|
||||
public stop(saveFullGame: boolean = false) {
|
||||
public stop(saveFullGame = false) {
|
||||
if (!this.isActive) return;
|
||||
|
||||
this.isActive = false;
|
||||
@@ -411,13 +420,83 @@ export class ClientGameRunner {
|
||||
|
||||
const owner = this.gameView.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
this.gameView.setFocusedPlayer(owner as PlayerView);
|
||||
this.gameView.setFocusedPlayer(owner);
|
||||
} else {
|
||||
this.gameView.setFocusedPlayer(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private autoUpgradeEvent(event: AutoUpgradeEvent) {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y,
|
||||
);
|
||||
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tile = this.gameView.ref(cell.x, cell.y);
|
||||
|
||||
if (this.myPlayer === null) {
|
||||
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
|
||||
if (myPlayer === null) return;
|
||||
this.myPlayer = myPlayer;
|
||||
}
|
||||
|
||||
if (this.gameView.inSpawnPhase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.findAndUpgradeNearestBuilding(tile);
|
||||
}
|
||||
|
||||
private findAndUpgradeNearestBuilding(clickedTile: TileRef) {
|
||||
this.myPlayer!.actions(clickedTile).then((actions) => {
|
||||
const upgradeUnits: {
|
||||
unitId: number;
|
||||
unitType: UnitType;
|
||||
distance: number;
|
||||
}[] = [];
|
||||
|
||||
for (const bu of actions.buildableUnits) {
|
||||
if (bu.canUpgrade !== false) {
|
||||
const existingUnit = this.gameView
|
||||
.units()
|
||||
.find((unit) => unit.id() === bu.canUpgrade);
|
||||
if (existingUnit) {
|
||||
const distance = this.gameView.manhattanDist(
|
||||
clickedTile,
|
||||
existingUnit.tile(),
|
||||
);
|
||||
|
||||
upgradeUnits.push({
|
||||
unitId: bu.canUpgrade,
|
||||
unitType: bu.type,
|
||||
distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradeUnits.length > 0) {
|
||||
upgradeUnits.sort((a, b) => a.distance - b.distance);
|
||||
const bestUpgrade = upgradeUnits[0];
|
||||
|
||||
this.eventBus.emit(
|
||||
new SendUpgradeStructureIntentEvent(
|
||||
bestUpgrade.unitId,
|
||||
bestUpgrade.unitType,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private doBoatAttackUnderCursor(): void {
|
||||
const tile = this.getTileUnderCursor();
|
||||
if (tile === null) {
|
||||
@@ -484,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 (
|
||||
@@ -544,7 +623,7 @@ export class ClientGameRunner {
|
||||
if (this.gameView.isLand(tile)) {
|
||||
const owner = this.gameView.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
this.gameView.setFocusedPlayer(owner as PlayerView);
|
||||
this.gameView.setFocusedPlayer(owner);
|
||||
} else {
|
||||
this.gameView.setFocusedPlayer(null);
|
||||
}
|
||||
@@ -558,7 +637,7 @@ export class ClientGameRunner {
|
||||
.sort((a, b) => a.distSquared - b.distSquared);
|
||||
|
||||
if (units.length > 0) {
|
||||
this.gameView.setFocusedPlayer(units[0].unit.owner() as PlayerView);
|
||||
this.gameView.setFocusedPlayer(units[0].unit.owner());
|
||||
} else {
|
||||
this.gameView.setFocusedPlayer(null);
|
||||
}
|
||||
|
||||
@@ -1,149 +1,96 @@
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { COSMETICS } from "../core/CosmeticSchemas";
|
||||
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
|
||||
import {
|
||||
StripeCreateCheckoutSessionResponseSchema,
|
||||
UserMeResponse,
|
||||
} from "../core/ApiSchemas";
|
||||
import { getApiBase, getAuthHeader } from "./jwt";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
interface StripeProduct {
|
||||
id: string;
|
||||
object: "product";
|
||||
active: boolean;
|
||||
created: number;
|
||||
description: string | null;
|
||||
images: string[];
|
||||
livemode: boolean;
|
||||
metadata: Record<string, string>;
|
||||
name: string;
|
||||
shippable: boolean | null;
|
||||
type: "good" | "service";
|
||||
updated: number;
|
||||
url: string | null;
|
||||
price: string;
|
||||
price_id: string;
|
||||
}
|
||||
|
||||
export interface Pattern {
|
||||
name: string;
|
||||
key: string;
|
||||
roles: string[];
|
||||
price?: string;
|
||||
priceId?: string;
|
||||
lockedReason?: string;
|
||||
notShown?: boolean;
|
||||
}
|
||||
import { z } from "zod";
|
||||
|
||||
export async function patterns(
|
||||
userMe: UserMeResponse | null,
|
||||
): Promise<Pattern[]> {
|
||||
const patterns: Pattern[] = Object.entries(COSMETICS.patterns).map(
|
||||
([key, patternData]) => {
|
||||
return {
|
||||
name: patternData.name,
|
||||
key,
|
||||
roles: patternData.role_group
|
||||
? (COSMETICS.role_groups[patternData.role_group] ?? [])
|
||||
: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
const cosmetics = await getCosmetics();
|
||||
|
||||
const products = await listAllProducts();
|
||||
patterns.forEach((pattern) => {
|
||||
addRestrictions(pattern, userMe, products);
|
||||
});
|
||||
if (cosmetics === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const patterns: Pattern[] = [];
|
||||
const playerFlares = new Set(userMe?.player.flares);
|
||||
|
||||
for (const name in cosmetics.patterns) {
|
||||
const patternData = cosmetics.patterns[name];
|
||||
const hasAccess = playerFlares.has(`pattern:${name}`);
|
||||
if (hasAccess) {
|
||||
// Remove product info because player already has access.
|
||||
patternData.product = null;
|
||||
patterns.push(patternData);
|
||||
} else if (patternData.product !== null) {
|
||||
// Player doesn't have access, but product is available for purchase.
|
||||
patterns.push(patternData);
|
||||
}
|
||||
// If player doesn't have access and product is null, don't show it.
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
function addRestrictions(
|
||||
pattern: Pattern,
|
||||
userMe: UserMeResponse | null,
|
||||
products: Map<string, StripeProduct>,
|
||||
) {
|
||||
if (userMe === null) {
|
||||
if (products.has(`pattern:${pattern.name}`)) {
|
||||
// Purchasable (flare-gated) patterns are shown as disabled
|
||||
pattern.lockedReason = translateText("territory_patterns.blocked.login");
|
||||
} else {
|
||||
// Role-gated patterns are not shown
|
||||
pattern.notShown = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const flares = userMe.player.flares ?? [];
|
||||
if (
|
||||
flares.includes("pattern:*") ||
|
||||
flares.includes(`pattern:${pattern.name}`)
|
||||
) {
|
||||
// Pattern is unlocked by flare
|
||||
return;
|
||||
}
|
||||
|
||||
const myRoles = userMe.player.roles ?? [];
|
||||
if (
|
||||
pattern.roles.some((authorizedRole) => myRoles.includes(authorizedRole))
|
||||
) {
|
||||
// Pattern is unlocked by role
|
||||
return;
|
||||
}
|
||||
|
||||
const product = products.get(`pattern:${pattern.name}`);
|
||||
if (product) {
|
||||
pattern.price = product.price;
|
||||
pattern.priceId = product.price_id;
|
||||
pattern.lockedReason = translateText("territory_patterns.blocked.purchase");
|
||||
return;
|
||||
}
|
||||
|
||||
// Pattern is locked by role group and not purchasable, don't show it.
|
||||
pattern.notShown = true;
|
||||
}
|
||||
|
||||
export async function handlePurchase(priceId: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getApiBase()}/stripe/create-checkout-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: getAuthHeader(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
priceId: priceId,
|
||||
successUrl: `${window.location.href}purchase-success`,
|
||||
cancelUrl: `${window.location.href}purchase-cancel`,
|
||||
}),
|
||||
const response = await fetch(
|
||||
`${getApiBase()}/stripe/create-checkout-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": getAuthHeader(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
priceId,
|
||||
successUrl: `${window.location.origin}#purchase-completed=true`,
|
||||
cancelUrl: `${window.location.origin}#purchase-completed=false`,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`Error purchasing pattern:${response.status} ${response.statusText}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
if (response.status === 401) {
|
||||
alert("You are not logged in. Please log in to purchase a pattern.");
|
||||
} else {
|
||||
alert("Something went wrong. Please try again later.");
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
|
||||
// Redirect to Stripe checkout
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error("Purchase error:", error);
|
||||
alert("Something went wrong. Please try again later.");
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const parsed = StripeCreateCheckoutSessionResponseSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
const error = z.prettifyError(parsed.error);
|
||||
console.error("Invalid checkout session response:", error);
|
||||
alert("Checkout failed. Please try again later.");
|
||||
return;
|
||||
}
|
||||
const { url } = parsed.data;
|
||||
|
||||
// Redirect to Stripe checkout
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Returns a map of flare -> product
|
||||
export async function listAllProducts(): Promise<Map<string, StripeProduct>> {
|
||||
async function getCosmetics(): Promise<Cosmetics | undefined> {
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/stripe/products`);
|
||||
const response = await fetch(`${getApiBase()}/cosmetics.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
console.error(`HTTP error! status: ${response.status}`);
|
||||
return;
|
||||
}
|
||||
const products = (await response.json()) as StripeProduct[];
|
||||
const productMap = new Map<string, StripeProduct>();
|
||||
products.forEach((product) => {
|
||||
productMap.set(product.metadata.flare, product);
|
||||
});
|
||||
return productMap;
|
||||
const result = CosmeticsSchema.safeParse(await response.json());
|
||||
if (!result.success) {
|
||||
console.error(`Invalid cosmetics: ${result.error.message}`);
|
||||
return;
|
||||
}
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch products:", error);
|
||||
return new Map();
|
||||
console.error("Error getting cosmetics:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,28 @@ 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() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("dark-mode-changed", this.handleDarkModeChanged);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("dark-mode-changed", this.handleDarkModeChanged);
|
||||
}
|
||||
|
||||
private readonly handleDarkModeChanged = (e: Event) => {
|
||||
const event = e as CustomEvent<{ darkMode: boolean }>;
|
||||
this.darkMode = event.detail.darkMode;
|
||||
};
|
||||
|
||||
toggleDarkMode() {
|
||||
this.userSettings.toggleDarkMode();
|
||||
this.darkMode = this.userSettings.darkMode();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import Countries from "./data/countries.json";
|
||||
const flagKey: string = "flag";
|
||||
import { FlagSchema } from "../core/Schemas";
|
||||
import { renderPlayerFlag } from "../core/CustomFlag";
|
||||
|
||||
const flagKey = "flag";
|
||||
|
||||
@customElement("flag-input")
|
||||
export class FlagInput extends LitElement {
|
||||
@state() private flag: string = "";
|
||||
@state() private search: string = "";
|
||||
@state() private showModal: boolean = false;
|
||||
@state() public flag = "";
|
||||
|
||||
static styles = css`
|
||||
@media (max-width: 768px) {
|
||||
@@ -21,19 +21,6 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private handleSearch(e: Event) {
|
||||
this.search = String((e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
if (flag === "xx") {
|
||||
flag = "";
|
||||
}
|
||||
this.flag = flag;
|
||||
this.showModal = false;
|
||||
this.storeFlag(flag);
|
||||
}
|
||||
|
||||
public getCurrentFlag(): string {
|
||||
return this.flag;
|
||||
}
|
||||
@@ -46,14 +33,6 @@ export class FlagInput extends LitElement {
|
||||
return "";
|
||||
}
|
||||
|
||||
private storeFlag(flag: string) {
|
||||
if (flag) {
|
||||
localStorage.setItem(flagKey, flag);
|
||||
} else if (flag === "") {
|
||||
localStorage.removeItem(flagKey);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchFlagEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-change", {
|
||||
@@ -64,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() {
|
||||
@@ -76,65 +69,46 @@ export class FlagInput extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full h-full ${this.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@click=${() => (this.showModal = false)}
|
||||
></div>
|
||||
<div class="flex relative">
|
||||
<button
|
||||
@click=${() => (this.showModal = !this.showModal)}
|
||||
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)]"
|
||||
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)]"
|
||||
title="Pick a flag!"
|
||||
>
|
||||
<img class="size-[48px]" src="/flags/${this.flag || "xx"}.svg" />
|
||||
<span
|
||||
id="flag-preview"
|
||||
style="display:inline-block; width:48px; height:64px;
|
||||
vertical-align:middle; background:#333; border-radius:6px;
|
||||
overflow:hidden;"
|
||||
></span>
|
||||
</button>
|
||||
${this.showModal
|
||||
? html`
|
||||
<div
|
||||
class="text-white flex flex-col gap-[0.5rem] absolute top-[60px] left-[0px] w-[780%] h-[500px] max-h-[50vh] max-w-[87vw] bg-gray-900/80 backdrop-blur-md p-[10px] rounded-[8px] z-[3] ${this
|
||||
.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
>
|
||||
<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"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@change=${this.handleSearch}
|
||||
@keyup=${this.handleSearch}
|
||||
/>
|
||||
<div
|
||||
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
${Countries.filter(
|
||||
(country) =>
|
||||
country.name
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase()) ||
|
||||
country.code
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase()),
|
||||
).map(
|
||||
(country) => html`
|
||||
<button
|
||||
@click=${() => this.setFlag(country.code)}
|
||||
class="text-center cursor-pointer border-none bg-none opacity-70 sm:w-[calc(33.3333%-15px) w-[calc(100%/3-15px)] md:w-[calc(100%/4-15px)]"
|
||||
>
|
||||
<img
|
||||
class="country-flag w-full h-auto"
|
||||
src="/flags/${country.code}.svg"
|
||||
/>
|
||||
<span class="country-name">${country.name}</span>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updated() {
|
||||
const preview = this.renderRoot.querySelector(
|
||||
"#flag-preview",
|
||||
) as HTMLElement;
|
||||
if (!preview) return;
|
||||
|
||||
preview.innerHTML = "";
|
||||
|
||||
if (this.flag?.startsWith("!")) {
|
||||
renderPlayerFlag(this.flag, preview);
|
||||
} else {
|
||||
const img = document.createElement("img");
|
||||
img.src = this.flag ? `/flags/${this.flag}.svg` : "/flags/xx.svg";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "contain";
|
||||
img.onerror = () => {
|
||||
if (!img.src.endsWith("/flags/xx.svg")) {
|
||||
img.src = "/flags/xx.svg";
|
||||
}
|
||||
};
|
||||
preview.appendChild(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import Countries from "./data/countries.json";
|
||||
|
||||
@customElement("flag-input-modal")
|
||||
export class FlagInputModal extends LitElement {
|
||||
@query("o-modal") private readonly modalEl!: HTMLElement & {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@state() private search = "";
|
||||
@state() private isModalOpen = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title="Flag Selector Modal" alwaysMaximized>
|
||||
<input
|
||||
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}
|
||||
@keyup=${this.handleSearch}
|
||||
/>
|
||||
<div
|
||||
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden h-[90%]"
|
||||
>
|
||||
${this.isModalOpen ? Countries.filter(
|
||||
(country) => !country.restricted && this.includedInSearch(country),
|
||||
).map(
|
||||
(country) => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
this.setFlag(country.code);
|
||||
this.close();
|
||||
}}
|
||||
class="text-center cursor-pointer border-none bg-none opacity-70
|
||||
w-[calc(100%/2-15px)] sm:w-[calc(100%/4-15px)]
|
||||
md:w-[calc(100%/6-15px)] lg:w-[calc(100%/8-15px)]
|
||||
xl:w-[calc(100%/10-15px)] min-w-[80px]"
|
||||
>
|
||||
<img
|
||||
class="country-flag w-full h-auto"
|
||||
src="/flags/${country.code}.svg"
|
||||
@error=${(e: Event) => {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
const fallback = "/flags/xx.svg";
|
||||
if (img.src && !img.src.endsWith(fallback)) {
|
||||
img.src = fallback;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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;
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
localStorage.setItem("flag", flag);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-change", {
|
||||
detail: { flag },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.isModalOpen = true;
|
||||
this.modalEl?.open();
|
||||
}
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
this.modalEl?.close();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface Window {
|
||||
adsbygoogle: unknown[];
|
||||
}
|
||||
@@ -108,7 +109,7 @@ const isElectron = () => {
|
||||
if (
|
||||
typeof navigator === "object" &&
|
||||
typeof navigator.userAgent === "string" &&
|
||||
navigator.userAgent.indexOf("Electron") >= 0
|
||||
navigator.userAgent.includes("Electron")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -15,6 +15,23 @@ export class HelpModal extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal
|
||||
@@ -121,6 +138,14 @@ export class HelpModal extends LitElement {
|
||||
</td>
|
||||
<td>${translateText("help_modal.action_reset_gfx")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="mouse-shell">
|
||||
<div class="mouse-wheel" id="highlighted-wheel"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${translateText("help_modal.action_auto_upgrade")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -166,11 +191,7 @@ export class HelpModal extends LitElement {
|
||||
<div>
|
||||
<p class="mb-4">${translateText("help_modal.ui_control_desc")}</p>
|
||||
<ul>
|
||||
<li class="mb-4">${translateText("help_modal.ui_pop")}</li>
|
||||
<li class="mb-4">${translateText("help_modal.ui_gold")}</li>
|
||||
<li class="mb-4">
|
||||
${translateText("help_modal.ui_troops_workers")}
|
||||
</li>
|
||||
<li class="mb-4">
|
||||
${translateText("help_modal.ui_attack_ratio")}
|
||||
</li>
|
||||
|
||||
@@ -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,18 +19,20 @@ import {
|
||||
UnitType,
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { GameConfig, GameInfo, 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;
|
||||
};
|
||||
@@ -33,50 +41,142 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private disableNPCs = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
@state() private bots: number = 400;
|
||||
@state() private infiniteGold: boolean = false;
|
||||
@state() private infiniteTroops: boolean = false;
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private bots = 400;
|
||||
@state() private infiniteGold = false;
|
||||
@state() private donateGold = false;
|
||||
@state() private infiniteTroops = false;
|
||||
@state() private donateTroops = false;
|
||||
@state() private instantBuild = false;
|
||||
@state() private lobbyId = "";
|
||||
@state() private copySuccess = false;
|
||||
@state() private players: string[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private clients: ClientInfo[] = [];
|
||||
@state() private useRandomMap = false;
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
@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 readonly userSettings: UserSettings = new UserSettings();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("host_modal.title")}>
|
||||
<div class="lobby-id-box">
|
||||
<button
|
||||
class="lobby-id-button"
|
||||
@click=${this.copyToClipboard}
|
||||
?disabled=${this.copySuccess}
|
||||
>
|
||||
<span class="lobby-id">${this.lobbyId}</span>
|
||||
<button class="lobby-id-button">
|
||||
<!-- Visibility toggle icon on the left -->
|
||||
${
|
||||
this.copySuccess
|
||||
? html`<span class="copy-success-icon">✓</span>`
|
||||
: html`
|
||||
<svg
|
||||
class="clipboard-icon"
|
||||
this.lobbyIdVisible
|
||||
? html`<svg
|
||||
class="visibility-icon"
|
||||
@click=${() => {
|
||||
this.lobbyIdVisible = !this.lobbyIdVisible;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="18px"
|
||||
width="18px"
|
||||
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"
|
||||
></path>
|
||||
</svg>`
|
||||
: html`<svg
|
||||
class="visibility-icon"
|
||||
@click=${() => {
|
||||
this.lobbyIdVisible = !this.lobbyIdVisible;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="18px"
|
||||
width="18px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M448 256s-64-128-192-128S64 256 64 256c32 64 96 128 192 128s160-64 192-128z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="18px"
|
||||
width="18px"
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
`
|
||||
stroke-width="32"
|
||||
></path>
|
||||
<path
|
||||
d="M144 256l224 0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="32"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>`
|
||||
}
|
||||
<!-- Lobby ID (conditionally shown) -->
|
||||
<span class="lobby-id" @click=${this.copyToClipboard} style="cursor: pointer;">
|
||||
${this.lobbyIdVisible ? this.lobbyId : "••••••••"}
|
||||
</span>
|
||||
|
||||
<!-- Copy icon/success indicator -->
|
||||
<div @click=${this.copyToClipboard} style="margin-left: 8px; cursor: pointer;">
|
||||
${
|
||||
this.copySuccess
|
||||
? html`<span class="copy-success-icon">✓</span>`
|
||||
: html`
|
||||
<svg
|
||||
class="clipboard-icon"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="18px"
|
||||
width="18px"
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="options-layout">
|
||||
@@ -96,7 +196,9 @@ export class HostLobbyModal extends LitElement {
|
||||
<div class="flex flex-row flex-wrap justify-center gap-4">
|
||||
${maps.map((mapValue) => {
|
||||
const mapKey = Object.keys(GameMapType).find(
|
||||
(key) => GameMapType[key] === mapValue,
|
||||
(key) =>
|
||||
GameMapType[key as keyof typeof GameMapType] ===
|
||||
mapValue,
|
||||
);
|
||||
return html`
|
||||
<div
|
||||
@@ -156,7 +258,7 @@ export class HostLobbyModal extends LitElement {
|
||||
></difficulty-display>
|
||||
<p class="option-card-title">
|
||||
${translateText(
|
||||
`difficulty.${DifficultyDescription[key]}`,
|
||||
`difficulty.${DifficultyDescription[key as keyof typeof DifficultyDescription]}`,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -210,8 +312,8 @@ export class HostLobbyModal extends LitElement {
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: o,
|
||||
})}
|
||||
num: o,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@@ -279,6 +381,38 @@ export class HostLobbyModal extends LitElement {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="donate-gold"
|
||||
class="option-card ${this.donateGold ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="donate-gold"
|
||||
@change=${this.handleDonateGoldChange}
|
||||
.checked=${this.donateGold}
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("host_modal.donate_gold")}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="donate-troops"
|
||||
class="option-card ${this.donateTroops ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="donate-troops"
|
||||
@change=${this.handleDonateTroopsChange}
|
||||
.checked=${this.donateTroops}
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("host_modal.donate_troops")}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="infinite-gold"
|
||||
class="option-card ${this.infiniteGold ? "selected" : ""}"
|
||||
@@ -323,9 +457,9 @@ export class HostLobbyModal extends LitElement {
|
||||
style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"
|
||||
>
|
||||
${renderUnitTypeOptions({
|
||||
disabledUnits: this.disabledUnits,
|
||||
toggleUnit: this.toggleUnit.bind(this),
|
||||
})}
|
||||
disabledUnits: this.disabledUnits,
|
||||
toggleUnit: this.toggleUnit.bind(this),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,29 +469,45 @@ export class HostLobbyModal extends LitElement {
|
||||
<!-- Lobby Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
${this.players.length}
|
||||
${this.clients.length}
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.player")
|
||||
: translateText("host_modal.players")
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="players-list">
|
||||
${this.players.map(
|
||||
(player) => html`<span class="player-tag">${player}</span>`,
|
||||
${this.clients.map(
|
||||
(client) => html`
|
||||
<span class="player-tag">
|
||||
${client.username}
|
||||
${client.clientID === this.lobbyCreatorClientID
|
||||
? html`<span class="host-badge"
|
||||
>(${translateText("host_modal.host_badge")})</span
|
||||
>`
|
||||
: html`
|
||||
<button
|
||||
class="remove-player-btn"
|
||||
@click=${() => this.kickPlayer(client.clientID)}
|
||||
title="Remove ${client.username}"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
`}
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-game-button-container">
|
||||
<button
|
||||
@click=${this.startGame}
|
||||
?disabled=${this.players.length < 2}
|
||||
?disabled=${this.clients.length < 2}
|
||||
class="start-game-button"
|
||||
>
|
||||
${
|
||||
this.players.length === 1
|
||||
this.clients.length === 1
|
||||
? translateText("host_modal.waiting")
|
||||
: translateText("host_modal.start")
|
||||
}
|
||||
@@ -374,7 +524,13 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
public open() {
|
||||
createLobby()
|
||||
this.lobbyCreatorClientID = generateID();
|
||||
this.lobbyIdVisible = this.userSettings.get(
|
||||
"settings.lobbyIdVisibility",
|
||||
true,
|
||||
);
|
||||
|
||||
createLobby(this.lobbyCreatorClientID)
|
||||
.then((lobby) => {
|
||||
this.lobbyId = lobby.gameID;
|
||||
// join lobby
|
||||
@@ -384,7 +540,7 @@ export class HostLobbyModal extends LitElement {
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: this.lobbyId,
|
||||
clientID: generateID(),
|
||||
clientID: this.lobbyCreatorClientID,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
@@ -457,11 +613,21 @@ export class HostLobbyModal extends LitElement {
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private handleDonateGoldChange(e: Event) {
|
||||
this.donateGold = Boolean((e.target as HTMLInputElement).checked);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private handleInfiniteTroopsChange(e: Event) {
|
||||
this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private handleDonateTroopsChange(e: Event) {
|
||||
this.donateTroops = Boolean((e.target as HTMLInputElement).checked);
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDisableNPCsChange(e: Event) {
|
||||
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
|
||||
console.log(`updating disable npcs to ${this.disableNPCs}`);
|
||||
@@ -493,7 +659,9 @@ export class HostLobbyModal extends LitElement {
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
donateGold: this.donateGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
donateTroops: this.donateTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
gameMode: this.gameMode,
|
||||
disabledUnits: this.disabledUnits,
|
||||
@@ -526,7 +694,9 @@ export class HostLobbyModal extends LitElement {
|
||||
|
||||
await this.putGameConfig();
|
||||
console.log(
|
||||
`Starting private game with map: ${GameMapType[this.selectedMap]} ${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();
|
||||
@@ -546,7 +716,7 @@ export class HostLobbyModal extends LitElement {
|
||||
try {
|
||||
//TODO: Convert id to url and copy
|
||||
await navigator.clipboard.writeText(
|
||||
`${location.origin}#join=${this.lobbyId}`,
|
||||
`${location.origin}/#join=${this.lobbyId}`,
|
||||
);
|
||||
this.copySuccess = true;
|
||||
setTimeout(() => {
|
||||
@@ -566,19 +736,32 @@ export class HostLobbyModal extends LitElement {
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(GameInfoSchema.parse)
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got game info response: ${JSON.stringify(data)}`);
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
|
||||
this.clients = data.clients ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
private kickPlayer(clientID: string) {
|
||||
// Dispatch event to be handled by WebSocket instead of HTTP
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("kick-player", {
|
||||
detail: { target: clientID },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createLobby(): Promise<GameInfo> {
|
||||
async function createLobby(creatorClientID: string): Promise<GameInfo> {
|
||||
const config = await getServerConfigFromClient();
|
||||
try {
|
||||
const id = generateID();
|
||||
const response = await fetch(
|
||||
`/${config.workerPath(id)}/api/create_game/${id}`,
|
||||
`/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -589,6 +772,8 @@ async function createLobby(): Promise<GameInfo> {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Server error response:", errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -598,6 +783,6 @@ async function createLobby(): Promise<GameInfo> {
|
||||
return data as GameInfo;
|
||||
} catch (error) {
|
||||
console.error("Error creating lobby:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -11,6 +11,13 @@ export class MouseUpEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class MouseOverEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted when a unit is selected or deselected
|
||||
*/
|
||||
@@ -63,7 +70,9 @@ 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 {}
|
||||
|
||||
export class ToggleStructureEvent implements GameEvent {
|
||||
constructor(public readonly structureType: UnitType | null) {}
|
||||
@@ -98,33 +107,40 @@ export class CenterCameraEvent implements GameEvent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export class AutoUpgradeEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class InputHandler {
|
||||
private lastPointerX: number = 0;
|
||||
private lastPointerY: number = 0;
|
||||
private lastPointerX = 0;
|
||||
private lastPointerY = 0;
|
||||
|
||||
private lastPointerDownX: number = 0;
|
||||
private lastPointerDownY: number = 0;
|
||||
private lastPointerDownX = 0;
|
||||
private lastPointerDownY = 0;
|
||||
|
||||
private pointers: Map<number, PointerEvent> = new Map();
|
||||
private readonly pointers: Map<number, PointerEvent> = new Map();
|
||||
|
||||
private lastPinchDistance: number = 0;
|
||||
private lastPinchDistance = 0;
|
||||
|
||||
private pointerDown: boolean = false;
|
||||
private pointerDown = false;
|
||||
|
||||
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() {
|
||||
@@ -143,11 +159,11 @@ export class InputHandler {
|
||||
groundAttack: "KeyG",
|
||||
modifierKey: "ControlLeft",
|
||||
altKey: "AltLeft",
|
||||
...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"),
|
||||
...(JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}") ?? {}),
|
||||
};
|
||||
|
||||
// Mac users might have different keybinds
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
const isMac = navigator.userAgent.includes("Mac");
|
||||
if (isMac) {
|
||||
this.keybinds.modifierKey = "MetaLeft"; // Use Command key on Mac
|
||||
}
|
||||
@@ -159,6 +175,7 @@ export class InputHandler {
|
||||
(e) => {
|
||||
this.onScroll(e);
|
||||
this.onShiftScroll(e);
|
||||
this.onTrackpadPan(e);
|
||||
e.preventDefault();
|
||||
},
|
||||
{ passive: false },
|
||||
@@ -170,12 +187,30 @@ export class InputHandler {
|
||||
this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY));
|
||||
}
|
||||
});
|
||||
|
||||
this.canvas.addEventListener("touchstart", (e) => this.onTouchStart(e), {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("touchmove", (e) => this.onTouchMove(e), {
|
||||
passive: false,
|
||||
});
|
||||
this.canvas.addEventListener("touchend", (e) => this.onTouchEnd(e), {
|
||||
passive: false,
|
||||
});
|
||||
this.pointers.clear();
|
||||
|
||||
this.moveInterval = setInterval(() => {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
// Skip if shift is held down
|
||||
if (
|
||||
this.activeKeys.has("ShiftLeft") ||
|
||||
this.activeKeys.has("ShiftRight")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.activeKeys.has(this.keybinds.moveUp) ||
|
||||
this.activeKeys.has("ArrowUp")
|
||||
@@ -251,6 +286,8 @@ export class InputHandler {
|
||||
this.keybinds.centerCamera,
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
"ShiftLeft",
|
||||
"ShiftRight",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.add(e.code);
|
||||
@@ -265,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) {
|
||||
@@ -293,11 +330,25 @@ export class InputHandler {
|
||||
this.eventBus.emit(new CenterCameraEvent());
|
||||
}
|
||||
|
||||
// Shift-D to toggle performance overlay
|
||||
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
|
||||
if (e.code === "KeyD" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
console.log("TogglePerformanceOverlayEvent");
|
||||
this.eventBus.emit(new TogglePerformanceOverlayEvent());
|
||||
}
|
||||
|
||||
this.activeKeys.delete(e.code);
|
||||
});
|
||||
}
|
||||
|
||||
private onPointerDown(event: PointerEvent) {
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
this.eventBus.emit(new AutoUpgradeEvent(event.clientX, event.clientY));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -319,6 +370,11 @@ export class InputHandler {
|
||||
}
|
||||
|
||||
onPointerUp(event: PointerEvent) {
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -357,19 +413,65 @@ export class InputHandler {
|
||||
const realCtrl =
|
||||
this.activeKeys.has("ControlLeft") ||
|
||||
this.activeKeys.has("ControlRight");
|
||||
const ratio = event.ctrlKey && !realCtrl ? 10 : 1; // Compensate pinch-zoom low sensitivity
|
||||
this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY * ratio));
|
||||
|
||||
const isZoomGesture =
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
Math.abs(event.deltaZ) > 0 ||
|
||||
(event.deltaMode === 1 && Math.abs(event.deltaY) > 0) ||
|
||||
(event.deltaMode === 0 && Math.abs(event.deltaY) >= 50);
|
||||
|
||||
if (isZoomGesture) {
|
||||
const ratio = event.ctrlKey && !realCtrl ? 10 : 1;
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(event.x, event.y, event.deltaY * ratio),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onShiftScroll(event: WheelEvent) {
|
||||
if (event.shiftKey) {
|
||||
const ratio = event.deltaY > 0 ? -10 : 10;
|
||||
const scrollValue = event.deltaY === 0 ? event.deltaX : event.deltaY;
|
||||
const ratio = scrollValue > 0 ? -10 : 10;
|
||||
this.eventBus.emit(new AttackRatioEvent(ratio));
|
||||
}
|
||||
}
|
||||
|
||||
private onTrackpadPan(event: WheelEvent) {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTrackpadPan =
|
||||
event.deltaMode === 0 &&
|
||||
(Math.abs(event.deltaX) > 0 || Math.abs(event.deltaY) > 0) &&
|
||||
((Math.abs(event.deltaX) > 0 && Math.abs(event.deltaY) > 0) ||
|
||||
event.deltaX % 1 !== 0 ||
|
||||
event.deltaY % 1 !== 0 ||
|
||||
(Math.abs(event.deltaX) < 30 && Math.abs(event.deltaY) < 30));
|
||||
|
||||
if (isTrackpadPan) {
|
||||
const panSensitivity = 1.0;
|
||||
const deltaX = -event.deltaX * panSensitivity;
|
||||
const deltaY = -event.deltaY * panSensitivity;
|
||||
|
||||
if (Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5) {
|
||||
this.eventBus.emit(new DragEvent(deltaX, deltaY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onPointerMove(event: PointerEvent) {
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.button > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -377,6 +479,7 @@ export class InputHandler {
|
||||
this.pointers.set(event.pointerId, event);
|
||||
|
||||
if (!this.pointerDown) {
|
||||
this.eventBus.emit(new MouseOverEvent(event.clientX, event.clientY));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,6 +510,42 @@ export class InputHandler {
|
||||
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
|
||||
}
|
||||
|
||||
private onTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length === 2) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private onTouchMove(event: TouchEvent) {
|
||||
if (event.touches.length === 2) {
|
||||
event.preventDefault();
|
||||
|
||||
const touch1 = event.touches[0];
|
||||
const touch2 = event.touches[1];
|
||||
const centerX = (touch1.clientX + touch2.clientX) / 2;
|
||||
const centerY = (touch1.clientY + touch2.clientY) / 2;
|
||||
|
||||
if (this.lastPointerX !== 0 && this.lastPointerY !== 0) {
|
||||
const deltaX = centerX - this.lastPointerX;
|
||||
const deltaY = centerY - this.lastPointerY;
|
||||
|
||||
if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {
|
||||
this.eventBus.emit(new DragEvent(deltaX, deltaY));
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPointerX = centerX;
|
||||
this.lastPointerY = centerY;
|
||||
}
|
||||
}
|
||||
|
||||
private onTouchEnd(event: TouchEvent) {
|
||||
if (event.touches.length < 2) {
|
||||
this.lastPointerX = 0;
|
||||
this.lastPointerY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private getPinchDistance(): number {
|
||||
const pointerEvents = Array.from(this.pointers.values());
|
||||
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { GameInfo, GameRecord } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { GameInfo, GameInfoSchema } from "../core/Schemas";
|
||||
import { LitElement, html } from "lit";
|
||||
import {
|
||||
WorkerApiArchivedGameLobbySchema,
|
||||
WorkerApiGameIdExistsSchema,
|
||||
} from "../core/WorkerSchemas";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
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;
|
||||
@state() private message: string = "";
|
||||
@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();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -45,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>
|
||||
@@ -88,7 +116,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
return this; // light DOM
|
||||
}
|
||||
|
||||
public open(id: string = "") {
|
||||
public open(id = "") {
|
||||
this.modalEl?.open();
|
||||
if (id) {
|
||||
this.setLobbyId(id);
|
||||
@@ -118,14 +146,25 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private setLobbyId(id: string) {
|
||||
if (id.startsWith("http")) {
|
||||
this.lobbyIdInput.value = id.split("join/")[1];
|
||||
private extractLobbyIdFromUrl(input: string): string {
|
||||
if (input.startsWith("http")) {
|
||||
if (input.includes("#join=")) {
|
||||
const params = new URLSearchParams(input.split("#")[1]);
|
||||
return params.get("join") ?? input;
|
||||
} else if (input.includes("join/")) {
|
||||
return input.split("join/")[1];
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
} else {
|
||||
this.lobbyIdInput.value = id;
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
private setLobbyId(id: string) {
|
||||
this.lobbyIdInput.value = this.extractLobbyIdFromUrl(id);
|
||||
}
|
||||
|
||||
private handleChange(e: Event) {
|
||||
const value = (e.target as HTMLInputElement).value.trim();
|
||||
this.setLobbyId(value);
|
||||
@@ -134,15 +173,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
private async pasteFromClipboard() {
|
||||
try {
|
||||
const clipText = await navigator.clipboard.readText();
|
||||
|
||||
let lobbyId: string;
|
||||
if (clipText.startsWith("http")) {
|
||||
lobbyId = clipText.split("join/")[1];
|
||||
} else {
|
||||
lobbyId = clipText;
|
||||
}
|
||||
|
||||
this.lobbyIdInput.value = lobbyId;
|
||||
this.setLobbyId(clipText);
|
||||
} catch (err) {
|
||||
console.error("Failed to read clipboard contents: ", err);
|
||||
}
|
||||
@@ -178,7 +209,8 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const gameInfo = await response.json();
|
||||
const json = await response.json();
|
||||
const gameInfo = WorkerApiGameIdExistsSchema.parse(json);
|
||||
|
||||
if (gameInfo.exists) {
|
||||
this.message = translateText("private_lobby.joined_waiting");
|
||||
@@ -211,7 +243,8 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const archiveData = await archiveResponse.json();
|
||||
const json = await archiveResponse.json();
|
||||
const archiveData = WorkerApiArchivedGameLobbySchema.parse(json);
|
||||
|
||||
if (
|
||||
archiveData.success === false &&
|
||||
@@ -227,13 +260,11 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
if (archiveData.exists) {
|
||||
const gameRecord = archiveData.gameRecord as GameRecord;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: gameRecord,
|
||||
gameRecord: archiveData.gameRecord,
|
||||
clientID: generateID(),
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
@@ -261,6 +292,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
},
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then(GameInfoSchema.parse)
|
||||
.then((data: GameInfo) => {
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
})
|
||||
|
||||
@@ -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";
|
||||
@@ -34,15 +37,17 @@ import zh_CN from "../../resources/lang/zh-CN.json";
|
||||
@customElement("lang-selector")
|
||||
export class LangSelector extends LitElement {
|
||||
@state() public translations: Record<string, string> | undefined;
|
||||
@state() private defaultTranslations: Record<string, string> | undefined;
|
||||
@state() private currentLang: string = "en";
|
||||
@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: boolean = false;
|
||||
@state() private debugMode: boolean = false;
|
||||
@state() private showModal = false;
|
||||
@state() private debugMode = false;
|
||||
|
||||
private dKeyPressed: boolean = 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() {
|
||||
@@ -85,10 +91,10 @@ export class LangSelector extends LitElement {
|
||||
|
||||
private setupDebugKey() {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key.toLowerCase() === "t") this.dKeyPressed = true;
|
||||
if (e.key.toLowerCase() === "t") this.debugKeyPressed = true;
|
||||
});
|
||||
window.addEventListener("keyup", (e) => {
|
||||
if (e.key.toLowerCase() === "t") this.dKeyPressed = false;
|
||||
if (e.key.toLowerCase() === "t") this.debugKeyPressed = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,8 +154,9 @@ export class LangSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let debugLang: any = null;
|
||||
if (this.dKeyPressed) {
|
||||
if (this.debugKeyPressed) {
|
||||
debugLang = {
|
||||
code: "debug",
|
||||
native: "Debug",
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -266,7 +279,7 @@ export class LangSelector extends LitElement {
|
||||
}
|
||||
|
||||
private openModal() {
|
||||
this.debugMode = this.dKeyPressed;
|
||||
this.debugMode = this.debugKeyPressed;
|
||||
this.showModal = true;
|
||||
this.loadLanguageList();
|
||||
}
|
||||
@@ -276,23 +289,28 @@ export class LangSelector extends LitElement {
|
||||
this.languageList.find((l) => l.code === this.currentLang) ??
|
||||
(this.currentLang === "debug"
|
||||
? {
|
||||
code: "debug",
|
||||
native: "Debug",
|
||||
en: "Debug",
|
||||
svg: "xx",
|
||||
}
|
||||
code: "debug",
|
||||
native: "Debug",
|
||||
en: "Debug",
|
||||
svg: "xx",
|
||||
}
|
||||
: {
|
||||
native: "English",
|
||||
en: "English",
|
||||
svg: "uk_us_flag",
|
||||
});
|
||||
native: "English",
|
||||
en: "English",
|
||||
svg: "uk_us_flag",
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="container__row">
|
||||
<button
|
||||
id="lang-selector"
|
||||
@click=${this.openModal}
|
||||
class="text-center appearance-none w-full bg-blue-100 hover:bg-blue-200 text-blue-900 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);
|
||||
|
||||
@@ -1,126 +1,20 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
/* 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";
|
||||
|
||||
@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";
|
||||
|
||||
static styles = css`
|
||||
.c-modal {
|
||||
position: fixed;
|
||||
padding: 1rem;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this; // Use Light DOM for TailwindCSS classes
|
||||
}
|
||||
|
||||
.c-modal__wrapper {
|
||||
background: #23232382;
|
||||
border-radius: 8px;
|
||||
min-width: 340px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c-modal__header {
|
||||
position: relative;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
font-size: 18px;
|
||||
background: #000000a1;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
padding: 1rem 2.4rem 1rem 1.4rem;
|
||||
}
|
||||
|
||||
.c-modal__close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-modal__content {
|
||||
position: relative;
|
||||
color: #fff;
|
||||
padding: 1.4rem;
|
||||
max-height: 60dvh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.lang-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.3s;
|
||||
border: 1px solid #aaa;
|
||||
background-color: #505050;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.lang-button:hover {
|
||||
background-color: #969696;
|
||||
}
|
||||
|
||||
.lang-button.active {
|
||||
background-color: #aaaaaa;
|
||||
border-color: #bbb;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.flag-icon {
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% {
|
||||
background-color: #990033;
|
||||
}
|
||||
20% {
|
||||
background-color: #996600;
|
||||
}
|
||||
40% {
|
||||
background-color: #336600;
|
||||
}
|
||||
60% {
|
||||
background-color: #008080;
|
||||
}
|
||||
80% {
|
||||
background-color: #1c3f99;
|
||||
}
|
||||
100% {
|
||||
background-color: #5e0099;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-button.debug {
|
||||
animation: rainbow 10s infinite;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border: 2px dashed aqua;
|
||||
box-shadow: 0 0 4px aqua;
|
||||
}
|
||||
`;
|
||||
|
||||
private close = () => {
|
||||
private readonly close = () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("close-modal", {
|
||||
bubbles: true,
|
||||
@@ -139,12 +33,25 @@ export class LanguageModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
|
||||
private selectLanguage = (lang: string) => {
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly selectLanguage = (lang: string) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("language-selected", {
|
||||
detail: { lang },
|
||||
@@ -158,27 +65,58 @@ export class LanguageModal extends LitElement {
|
||||
if (!this.visible) return null;
|
||||
|
||||
return html`
|
||||
<aside class="c-modal">
|
||||
<div class="c-modal__wrapper">
|
||||
<header class="c-modal__header">
|
||||
<aside
|
||||
class="fixed p-4 z-[1000] inset-0 bg-black/50 overflow-y-auto flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/80 dark:bg-gray-900/90 backdrop-blur-md rounded-lg min-w-[340px] max-w-[480px] w-full"
|
||||
>
|
||||
<header
|
||||
class="relative rounded-t-md text-lg bg-black/60 dark:bg-black/80 text-center text-white px-6 py-4 pr-10"
|
||||
>
|
||||
${translateText("select_lang.title")}
|
||||
<div class="c-modal__close" @click=${this.close}>✕</div>
|
||||
<div
|
||||
class="cursor-pointer absolute right-4 top-4 font-bold hover:text-gray-300"
|
||||
@click=${this.close}
|
||||
>
|
||||
✕
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="c-modal__content">
|
||||
<section
|
||||
class="relative text-white dark:text-gray-100 p-6 max-h-[60dvh] overflow-y-auto"
|
||||
>
|
||||
${this.languageList.map((lang) => {
|
||||
const isActive = this.currentLang === lang.code;
|
||||
const isDebug = lang.code === "debug";
|
||||
|
||||
let buttonClasses =
|
||||
"w-full flex items-center gap-2 p-2 mb-2 rounded-md transition-colors duration-300 border";
|
||||
|
||||
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";
|
||||
} 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";
|
||||
}
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="lang-button ${isActive ? "active" : ""} ${lang.code ===
|
||||
"debug"
|
||||
? "debug"
|
||||
: ""}"
|
||||
@click=${() => this.selectLanguage(lang.code)}
|
||||
class="${buttonClasses}"
|
||||
@click=${() =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
this.selectLanguage(lang.code)}
|
||||
>
|
||||
<img
|
||||
src="/flags/${lang.svg}.svg"
|
||||
class="flag-icon"
|
||||
class="w-6 h-4 object-contain"
|
||||
alt="${lang.code}"
|
||||
/>
|
||||
<span>${lang.native} (${lang.en})</span>
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import { GameConfig, GameID, GameRecord } from "../core/Schemas";
|
||||
import {
|
||||
GameConfig,
|
||||
GameConfigSchema,
|
||||
GameID,
|
||||
GameRecord,
|
||||
GameRecordSchema,
|
||||
ID,
|
||||
} from "../core/Schemas";
|
||||
import { replacer } from "../core/Util";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface LocalStatsData {
|
||||
[key: GameID]: {
|
||||
lobby: Partial<GameConfig>;
|
||||
const LocalStatsDataSchema = z.record(
|
||||
ID,
|
||||
z.object({
|
||||
lobby: GameConfigSchema.partial(),
|
||||
// Only once the game is over
|
||||
gameRecord?: GameRecord;
|
||||
};
|
||||
}
|
||||
gameRecord: GameRecordSchema.optional(),
|
||||
}),
|
||||
);
|
||||
type LocalStatsData = z.infer<typeof LocalStatsDataSchema>;
|
||||
|
||||
let _startTime: number;
|
||||
let _startTime: number | undefined;
|
||||
|
||||
function getStats(): LocalStatsData {
|
||||
const statsStr = localStorage.getItem("game-records");
|
||||
return statsStr ? JSON.parse(statsStr) : {};
|
||||
try {
|
||||
return LocalStatsDataSchema.parse(
|
||||
JSON.parse(localStorage.getItem("game-records") ?? "{}"),
|
||||
);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function save(stats: LocalStatsData) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { z } from "zod/v4";
|
||||
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() {
|
||||
@@ -97,11 +97,6 @@ export class LocalServer {
|
||||
return;
|
||||
}
|
||||
if (this.paused) {
|
||||
if (clientMsg.intent.type === "troop_ratio") {
|
||||
// Store troop change events because otherwise they are
|
||||
// not registered when game is paused.
|
||||
this.intents.push(clientMsg.intent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.intents.push(clientMsg.intent);
|
||||
@@ -122,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",
|
||||
@@ -174,7 +172,7 @@ export class LocalServer {
|
||||
});
|
||||
}
|
||||
|
||||
public endGame(saveFullGame: boolean = false) {
|
||||
public endGame(saveFullGame = false) {
|
||||
console.log("local server ending game");
|
||||
clearInterval(this.turnCheckInterval);
|
||||
if (this.isReplay) {
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
import favicon from "../../resources/images/Favicon.svg";
|
||||
import version from "../../resources/version.txt";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
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 { FlagInput } from "./FlagInput";
|
||||
import { GameStartingModal } from "./GameStartingModal";
|
||||
import "./GoogleAdElement";
|
||||
import { HelpModal } from "./HelpModal";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
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 { GameType } from "../core/game/Game";
|
||||
import { HelpModal } from "./HelpModal";
|
||||
import { HostLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
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 { 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
|
||||
interface Window {
|
||||
PageOS: {
|
||||
session: {
|
||||
@@ -53,14 +52,22 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
// Extend the global interfaces to include your custom events
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface DocumentEventMap {
|
||||
"join-lobby": CustomEvent<JoinLobbyEvent>;
|
||||
"kick-player": CustomEvent<KickPlayerEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface JoinLobbyEvent {
|
||||
export type JoinLobbyEvent = {
|
||||
clientID: string;
|
||||
// Multiplayer games only have gameID, gameConfig is not known until game starts.
|
||||
gameID: string;
|
||||
@@ -68,10 +75,15 @@ export interface JoinLobbyEvent {
|
||||
gameStartInfo?: GameStartInfo;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
}
|
||||
};
|
||||
|
||||
export type KickPlayerEvent = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
class Client {
|
||||
private gameStop: (() => void) | null = null;
|
||||
private readonly eventBus: EventBus = new EventBus();
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
@@ -79,7 +91,7 @@ class Client {
|
||||
|
||||
private joinModal: JoinPrivateLobbyModal;
|
||||
private publicLobby: PublicLobby;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
private readonly userSettings: UserSettings = new UserSettings();
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -155,14 +167,15 @@ class Client {
|
||||
}
|
||||
});
|
||||
|
||||
setFavicon();
|
||||
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
|
||||
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
|
||||
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
|
||||
|
||||
const spModal = document.querySelector(
|
||||
"single-player-modal",
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
|
||||
const singlePlayer = document.getElementById("single-player");
|
||||
if (singlePlayer === null) throw new Error("Missing single-player");
|
||||
singlePlayer.addEventListener("click", () => {
|
||||
@@ -185,6 +198,16 @@ class Client {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
const flagInputModal = document.querySelector(
|
||||
"flag-input-modal",
|
||||
) as FlagInputModal;
|
||||
flagInputModal instanceof FlagInputModal;
|
||||
const flgInput = document.getElementById("flag-input_");
|
||||
if (flgInput === null) throw new Error("Missing flag-input_");
|
||||
flgInput.addEventListener("click", () => {
|
||||
flagInputModal.open();
|
||||
});
|
||||
|
||||
const territoryModal = document.querySelector(
|
||||
"territory-patterns-modal",
|
||||
) as TerritoryPatternsModal;
|
||||
@@ -333,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", () => {
|
||||
@@ -383,14 +406,18 @@ class Client {
|
||||
window.addEventListener("popstate", onHashUpdate);
|
||||
window.addEventListener("hashchange", onHashUpdate);
|
||||
|
||||
function updateSliderProgress(slider) {
|
||||
function updateSliderProgress(slider: HTMLInputElement) {
|
||||
const percent =
|
||||
((slider.value - slider.min) / (slider.max - slider.min)) * 100;
|
||||
((Number(slider.value) - Number(slider.min)) /
|
||||
(Number(slider.max) - Number(slider.min))) *
|
||||
100;
|
||||
slider.style.setProperty("--progress", `${percent}%`);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll("#bots-count, #private-lobby-bots-count")
|
||||
.querySelectorAll<HTMLInputElement>(
|
||||
"#bots-count, #private-lobby-bots-count",
|
||||
)
|
||||
.forEach((slider) => {
|
||||
updateSliderProgress(slider);
|
||||
slider.addEventListener("input", () => updateSliderProgress(slider));
|
||||
@@ -399,8 +426,25 @@ class Client {
|
||||
|
||||
private handleHash() {
|
||||
const { hash } = window.location;
|
||||
|
||||
const alertAndStrip = (message: string) => {
|
||||
alert(message);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
window.location.pathname + window.location.search,
|
||||
);
|
||||
};
|
||||
|
||||
if (hash.startsWith("#")) {
|
||||
const params = new URLSearchParams(hash.slice(1));
|
||||
if (params.get("purchase-completed") === "true") {
|
||||
alertAndStrip("purchase succeeded");
|
||||
return;
|
||||
} else if (params.get("purchase-completed") === "false") {
|
||||
alertAndStrip("purchase failed");
|
||||
return;
|
||||
}
|
||||
const lobbyId = params.get("join");
|
||||
if (lobbyId && ID.safeParse(lobbyId).success) {
|
||||
this.joinModal.open(lobbyId);
|
||||
@@ -419,6 +463,7 @@ class Client {
|
||||
const config = await getServerConfigFromClient();
|
||||
|
||||
this.gameStop = joinLobby(
|
||||
this.eventBus,
|
||||
{
|
||||
gameID: lobby.gameID,
|
||||
serverConfig: config,
|
||||
@@ -450,6 +495,7 @@ class Client {
|
||||
"territory-patterns-modal",
|
||||
"language-modal",
|
||||
"news-modal",
|
||||
"flag-input-modal",
|
||||
].forEach((tag) => {
|
||||
const modal = document.querySelector(tag) as HTMLElement & {
|
||||
close?: () => void;
|
||||
@@ -504,6 +550,15 @@ class Client {
|
||||
this.gameStop = null;
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
|
||||
private handleKickPlayer(event: CustomEvent<KickPlayerEvent>) {
|
||||
const { target } = event.detail;
|
||||
|
||||
// Forward to eventBus if available
|
||||
if (this.eventBus) {
|
||||
this.eventBus.emit(new SendKickPlayerIntentEvent(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
@@ -511,14 +566,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
new Client().initialize();
|
||||
});
|
||||
|
||||
function setFavicon(): void {
|
||||
const link = document.createElement("link");
|
||||
link.type = "image/x-icon";
|
||||
link.rel = "shortcut icon";
|
||||
link.href = favicon;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// WARNING: DO NOT EXPOSE THIS ID
|
||||
function getPlayToken(): string {
|
||||
const result = isLoggedIn();
|
||||
@@ -566,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));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const LockSchema = z.object({
|
||||
owner: z.string(),
|
||||
timestamp: z.number(),
|
||||
});
|
||||
|
||||
export class MultiTabDetector {
|
||||
private readonly tabId = `${Date.now()}-${Math.random()}`;
|
||||
private readonly lockKey = "multi-tab-lock";
|
||||
@@ -60,7 +67,7 @@ export class MultiTabDetector {
|
||||
if (e.key === this.lockKey && e.newValue) {
|
||||
let other: { owner: string; timestamp: number };
|
||||
try {
|
||||
other = JSON.parse(e.newValue);
|
||||
other = LockSchema.parse(JSON.parse(e.newValue));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse lock", e);
|
||||
return;
|
||||
@@ -99,7 +106,7 @@ export class MultiTabDetector {
|
||||
const raw = localStorage.getItem(this.lockKey);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
return LockSchema.parse(JSON.parse(raw));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse lock", raw, e);
|
||||
return null;
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
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;
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
@property({ type: String }) markdown = "Loading...";
|
||||
|
||||
private initialized: boolean = false;
|
||||
private initialized = false;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
@@ -66,7 +83,7 @@ export class NewsModal extends LitElement {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
${translateText("news.full_changelog")}
|
||||
${translateText("news.see_all_releases")}
|
||||
<a
|
||||
href="https://github.com/openfrontio/OpenFrontIO/releases"
|
||||
target="_blank"
|
||||
@@ -88,6 +105,19 @@ export class NewsModal extends LitElement {
|
||||
this.initialized = true;
|
||||
fetch(changelog)
|
||||
.then((response) => (response.ok ? response.text() : "Failed to load"))
|
||||
.then((markdown) =>
|
||||
markdown
|
||||
.replace(
|
||||
/(?<!\()\bhttps:\/\/github\.com\/openfrontio\/OpenFrontIO\/pull\/(\d+)\b/g,
|
||||
(_match, prNumber) =>
|
||||
`[#${prNumber}](https://github.com/openfrontio/OpenFrontIO/pull/${prNumber})`,
|
||||
)
|
||||
.replace(
|
||||
/(?<!\()\bhttps:\/\/github\.com\/openfrontio\/OpenFrontIO\/compare\/([\w.-]+)\b/g,
|
||||
(_match, comparison) =>
|
||||
`[${comparison}](https://github.com/openfrontio/OpenFrontIO/compare/${comparison})`,
|
||||
),
|
||||
)
|
||||
.then((markdown) => (this.markdown = markdown));
|
||||
}
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -1,22 +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 { GameMapType, GameMode } from "../core/game/Game";
|
||||
import { terrainMapFileLoader } from "../core/game/TerrainMapFileLoader";
|
||||
import { GameID, GameInfo } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import { ApiPublicLobbiesResponseSchema } from "../core/ExpressSchemas";
|
||||
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: boolean = false;
|
||||
@state() private isButtonDebounced: boolean = false;
|
||||
@state() private mapImages: Map<GameID, string> = new Map();
|
||||
@state() public isLobbyHighlighted = false;
|
||||
@state() private isButtonDebounced = false;
|
||||
@state() private readonly mapImages: Map<GameID, string> = new Map();
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
private readonly debounceDelay = 750;
|
||||
private readonly lobbyIDToStart = new Map<GameID, number>();
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -74,10 +75,11 @@ 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 data = await response.json();
|
||||
const json = await response.json();
|
||||
const data = ApiPublicLobbiesResponseSchema.parse(json);
|
||||
return data.lobbies;
|
||||
} catch (error) {
|
||||
console.error("Error fetching lobbies:", error);
|
||||
@@ -119,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
|
||||
@@ -154,8 +157,8 @@ export class PublicLobby extends LitElement {
|
||||
? typeof teamCount === "string"
|
||||
? translateText(`public_lobby.teams_${teamCount}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: teamCount ?? 0,
|
||||
})
|
||||
num: teamCount ?? 0,
|
||||
})
|
||||
: translateText("game_mode.ffa")}</span
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -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,39 +13,58 @@ 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;
|
||||
};
|
||||
@state() private selectedMap: GameMapType = GameMapType.World;
|
||||
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
|
||||
@state() private disableNPCs: boolean = false;
|
||||
@state() private bots: number = 400;
|
||||
@state() private infiniteGold: boolean = false;
|
||||
@state() private infiniteTroops: boolean = false;
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disableNPCs = false;
|
||||
@state() private bots = 400;
|
||||
@state() private infiniteGold = false;
|
||||
@state() private readonly donateGold = false;
|
||||
@state() private infiniteTroops = false;
|
||||
@state() private readonly donateTroops = false;
|
||||
@state() private instantBuild = false;
|
||||
@state() private useRandomMap = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
private readonly userSettings: UserSettings = new UserSettings();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -67,7 +86,9 @@ export class SinglePlayerModal extends LitElement {
|
||||
<div class="flex flex-row flex-wrap justify-center gap-4">
|
||||
${maps.map((mapValue) => {
|
||||
const mapKey = Object.keys(GameMapType).find(
|
||||
(key) => GameMapType[key] === mapValue,
|
||||
(key) =>
|
||||
GameMapType[key as keyof typeof GameMapType] ===
|
||||
mapValue,
|
||||
);
|
||||
return html`
|
||||
<div
|
||||
@@ -129,7 +150,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
></difficulty-display>
|
||||
<p class="option-card-title">
|
||||
${translateText(
|
||||
`difficulty.${DifficultyDescription[key]}`,
|
||||
`difficulty.${DifficultyDescription[key as keyof typeof DifficultyDescription]}`,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -185,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>
|
||||
`,
|
||||
@@ -389,7 +410,8 @@ export class SinglePlayerModal extends LitElement {
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Starting single player game with map: ${GameMapType[this.selectedMap]}${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();
|
||||
@@ -408,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,
|
||||
@@ -432,7 +454,9 @@ export class SinglePlayerModal extends LitElement {
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
donateGold: this.donateGold,
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
donateTroops: this.donateTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
disabledUnits: this.disabledUnits
|
||||
.map((u) => Object.values(UnitType).find((ut) => ut === u))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { FetchGameMapLoader } from "../core/game/FetchGameMapLoader";
|
||||
import version from "../../resources/version.txt";
|
||||
|
||||
export const terrainMapFileLoader = new FetchGameMapLoader("/maps", version);
|
||||
@@ -1,24 +1,25 @@
|
||||
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 { UserSettings } from "../core/game/UserSettings";
|
||||
import { PatternDecoder } from "../core/PatternDecoder";
|
||||
import "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
import { handlePurchase, Pattern, patterns } from "./Cosmetics";
|
||||
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;
|
||||
};
|
||||
|
||||
public previewButton: HTMLElement | null = null;
|
||||
public buttonWidth: number = 150;
|
||||
public buttonWidth = 150;
|
||||
|
||||
@state() private selectedPattern: string | undefined;
|
||||
|
||||
@@ -35,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;
|
||||
|
||||
@@ -45,6 +46,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
this.selectedPattern = this.userSettings.getSelectedPattern();
|
||||
this.updateComplete.then(() => {
|
||||
const containers = this.renderRoot.querySelectorAll(".preview-container");
|
||||
@@ -59,6 +61,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -68,7 +71,12 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
private readonly handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-5);
|
||||
this.keySequence = nextSequence;
|
||||
@@ -100,14 +108,14 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
private renderTooltip(): TemplateResult | null {
|
||||
if (this.hoveredPattern && this.hoveredPattern.lockedReason) {
|
||||
if (this.hoveredPattern && this.hoveredPattern.product !== undefined) {
|
||||
return html`
|
||||
<div
|
||||
class="fixed z-[10000] px-3 py-2 rounded bg-black text-white text-sm pointer-events-none shadow-md"
|
||||
style="top: ${this.hoverPosition.y + 12}px; left: ${this.hoverPosition
|
||||
.x + 12}px;"
|
||||
>
|
||||
${this.hoveredPattern.lockedReason}
|
||||
${translateText("territory_patterns.blocked.purchase")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -115,7 +123,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
private renderPatternButton(pattern: Pattern): TemplateResult {
|
||||
const isSelected = this.selectedPattern === pattern.key;
|
||||
const isSelected = this.selectedPattern === pattern.pattern;
|
||||
|
||||
return html`
|
||||
<div style="flex: 0 1 calc(25% - 1rem); max-width: calc(25% - 1rem);">
|
||||
@@ -124,9 +132,9 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
${isSelected
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"}
|
||||
${pattern.lockedReason ? "opacity-50 cursor-not-allowed" : ""}"
|
||||
${pattern.product !== null ? "opacity-50 cursor-not-allowed" : ""}"
|
||||
@click=${() =>
|
||||
!pattern.lockedReason && this.selectPattern(pattern.key)}
|
||||
pattern.product === null && this.selectPattern(pattern.pattern)}
|
||||
@mouseenter=${(e: MouseEvent) => this.handleMouseEnter(pattern, e)}
|
||||
@mousemove=${(e: MouseEvent) => this.handleMouseMove(e)}
|
||||
@mouseleave=${() => this.handleMouseLeave()}
|
||||
@@ -148,23 +156,24 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
"
|
||||
>
|
||||
${this.renderPatternPreview(
|
||||
pattern.key,
|
||||
pattern.pattern,
|
||||
this.buttonWidth,
|
||||
this.buttonWidth,
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
${pattern.priceId !== undefined && pattern.lockedReason
|
||||
${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.priceId!);
|
||||
handlePurchase(pattern.product!.priceId);
|
||||
}}
|
||||
>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
(${pattern.price})
|
||||
(${pattern.product.price})
|
||||
</button>
|
||||
`
|
||||
: null}
|
||||
@@ -176,7 +185,6 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
const buttons: TemplateResult[] = [];
|
||||
for (const pattern of this.patterns) {
|
||||
if (!this.showChocoPattern && pattern.name === "choco") continue;
|
||||
if (pattern.notShown === true) continue;
|
||||
|
||||
const result = this.renderPatternButton(pattern);
|
||||
buttons.push(result);
|
||||
@@ -236,6 +244,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
this.modalEl?.open();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
this.isActive = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public close() {
|
||||
@@ -325,7 +334,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
private handleMouseEnter(pattern: Pattern, event: MouseEvent) {
|
||||
if (pattern.lockedReason) {
|
||||
if (pattern.product !== null) {
|
||||
this.hoveredPattern = pattern;
|
||||
this.hoverPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
@@ -380,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++) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { z } from "zod/v4";
|
||||
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) {}
|
||||
@@ -133,6 +133,10 @@ export class SendEmbargoIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendDeleteUnitIntentEvent implements GameEvent {
|
||||
constructor(public readonly unitId: number) {}
|
||||
}
|
||||
|
||||
export class CancelAttackIntentEvent implements GameEvent {
|
||||
constructor(public readonly attackID: string) {}
|
||||
}
|
||||
@@ -141,10 +145,6 @@ export class CancelBoatIntentEvent implements GameEvent {
|
||||
constructor(public readonly unitID: number) {}
|
||||
}
|
||||
|
||||
export class SendSetTargetTroopRatioEvent implements GameEvent {
|
||||
constructor(public readonly ratio: number) {}
|
||||
}
|
||||
|
||||
export class SendWinnerEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly winner: Winner,
|
||||
@@ -165,12 +165,16 @@ export class MoveWarshipIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendKickPlayerIntentEvent implements GameEvent {
|
||||
constructor(public readonly target: string) {}
|
||||
}
|
||||
|
||||
export class Transport {
|
||||
private socket: WebSocket | null = null;
|
||||
|
||||
private localServer: LocalServer;
|
||||
|
||||
private buffer: string[] = [];
|
||||
private readonly buffer: string[] = [];
|
||||
|
||||
private onconnect: () => void;
|
||||
private onmessage: (msg: ServerMessage) => void;
|
||||
@@ -178,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.
|
||||
@@ -223,9 +227,6 @@ export class Transport {
|
||||
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
|
||||
this.onSendEmbargoIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
|
||||
this.onSendSetTargetTroopRatioEvent(e),
|
||||
);
|
||||
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
|
||||
|
||||
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
|
||||
@@ -241,6 +242,14 @@ export class Transport {
|
||||
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
|
||||
this.onMoveWarshipEvent(e);
|
||||
});
|
||||
|
||||
this.eventBus.on(SendDeleteUnitIntentEvent, (e) =>
|
||||
this.onSendDeleteUnitIntent(e),
|
||||
);
|
||||
|
||||
this.eventBus.on(SendKickPlayerIntentEvent, (e) =>
|
||||
this.onSendKickPlayerIntent(e),
|
||||
);
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
@@ -319,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) {
|
||||
@@ -374,7 +384,7 @@ export class Transport {
|
||||
} satisfies ClientJoinMessage);
|
||||
}
|
||||
|
||||
leaveGame(saveFullGame: boolean = false) {
|
||||
leaveGame(saveFullGame = false) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.endGame(saveFullGame);
|
||||
return;
|
||||
@@ -525,14 +535,6 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
ratio: event.ratio,
|
||||
});
|
||||
}
|
||||
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
@@ -544,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) {
|
||||
@@ -611,11 +613,27 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendDeleteUnitIntent(event: SendDeleteUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "delete_unit",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
unitId: event.unitId,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendKickPlayerIntent(event: SendKickPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "kick_player",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
target: event.target,
|
||||
});
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
type: "intent",
|
||||
intent: intent,
|
||||
intent,
|
||||
} satisfies ClientIntentMessage;
|
||||
this.sendMsg(msg);
|
||||
} else {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
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> = {};
|
||||
@@ -25,14 +28,14 @@ export class UserSettingModal extends LitElement {
|
||||
const savedKeybinds = localStorage.getItem("settings.keybinds");
|
||||
if (savedKeybinds) {
|
||||
try {
|
||||
this.keybinds = JSON.parse(savedKeybinds);
|
||||
this.keybinds = KeybindSchema.parse(JSON.parse(savedKeybinds));
|
||||
} catch (e) {
|
||||
console.warn("Invalid keybinds JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@query("o-modal") private readonly modalEl!: HTMLElement & {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
isModalOpen: boolean;
|
||||
@@ -48,9 +51,14 @@ 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") {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-4);
|
||||
this.keySequence = nextSequence;
|
||||
@@ -90,6 +98,14 @@ export class UserSettingModal extends LitElement {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dark-mode-changed", {
|
||||
detail: { darkMode: enabled },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
@@ -120,6 +136,15 @@ export class UserSettingModal extends LitElement {
|
||||
console.log("💥 Special effects:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleStructureSprites(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.structureSprites", enabled);
|
||||
|
||||
console.log("🏠 Structure sprites:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleAnonymousNames(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
@@ -129,6 +154,14 @@ export class UserSettingModal extends LitElement {
|
||||
console.log("🙈 Anonymous Names:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleLobbyIdVisibility(e: CustomEvent<{ checked: boolean }>) {
|
||||
const hideIds = e.detail?.checked;
|
||||
if (typeof hideIds !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.lobbyIdVisibility", !hideIds); // Invert because checked=hide
|
||||
console.log("👁️ Hidden Lobby IDs:", hideIds ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
@@ -149,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;
|
||||
@@ -168,6 +191,13 @@ export class UserSettingModal extends LitElement {
|
||||
console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private togglePerformanceOverlay(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.performanceOverlay", enabled);
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{ action: string; value: string }>,
|
||||
) {
|
||||
@@ -204,8 +234,8 @@ export class UserSettingModal extends LitElement {
|
||||
<button
|
||||
class="w-1/2 text-center px-3 py-1 rounded-l
|
||||
${this.settingsMode === "basic"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
@click=${() => (this.settingsMode = "basic")}
|
||||
>
|
||||
${translateText("user_setting.tab_basic")}
|
||||
@@ -213,8 +243,8 @@ export class UserSettingModal extends LitElement {
|
||||
<button
|
||||
class="w-1/2 text-center px-3 py-1 rounded-r
|
||||
${this.settingsMode === "keybinds"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
? "bg-white/10 text-white"
|
||||
: "bg-transparent text-gray-400"}"
|
||||
@click=${() => (this.settingsMode = "keybinds")}
|
||||
>
|
||||
${translateText("user_setting.tab_keybinds")}
|
||||
@@ -271,6 +301,15 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.toggleFxLayer}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 🏠 Structure Sprites -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.structure_sprites_label")}"
|
||||
description="${translateText("user_setting.structure_sprites_desc")}"
|
||||
id="structure_sprites-toggle"
|
||||
.checked=${this.userSettings.structureSprites()}
|
||||
@change=${this.toggleStructureSprites}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 🖱️ Left Click Menu -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.left_click_label")}"
|
||||
@@ -289,6 +328,15 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.toggleAnonymousNames}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 👁️ Hidden Lobby IDs -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.lobby_id_visibility_label")}"
|
||||
description="${translateText("user_setting.lobby_id_visibility_desc")}"
|
||||
id="lobby-id-visibility-toggle"
|
||||
.checked=${!this.userSettings.get("settings.lobbyIdVisibility", true)}
|
||||
@change=${this.toggleLobbyIdVisibility}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 🏳️ Territory Patterns -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.territory_patterns_label")}"
|
||||
@@ -298,6 +346,15 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.toggleTerritoryPatterns}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 📱 Performance Overlay -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.performance_overlay_label")}"
|
||||
description="${translateText("user_setting.performance_overlay_desc")}"
|
||||
id="performance-overlay-toggle"
|
||||
.checked=${this.userSettings.performanceOverlay()}
|
||||
@change=${this.togglePerformanceOverlay}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- ⚔️ Attack Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.attack_ratio_label")}"
|
||||
@@ -309,17 +366,6 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.sliderAttackRatio}
|
||||
></setting-slider>
|
||||
|
||||
<!-- 🪖🛠️ Troop Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.troop_ratio_label")}"
|
||||
description="${translateText("user_setting.troop_ratio_desc")}"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${Number(localStorage.getItem("settings.troopRatio") ?? "0.95") *
|
||||
100}
|
||||
@change=${this.sliderTroopRatio}
|
||||
></setting-slider>
|
||||
|
||||
${this.showEasterEggSettings
|
||||
? html`
|
||||
<setting-slider
|
||||
@@ -333,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);
|
||||
@@ -352,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);
|
||||
|
||||