mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-26 09:24:36 +00:00
Merge main into join-fail
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -44,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) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.[tj]s
|
||||
+1
-1
@@ -46,7 +46,7 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=1
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
# Install dependencies
|
||||
RUN npm ci --omit=dev
|
||||
RUN npm ci
|
||||
|
||||
# Final image
|
||||
FROM base
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
+104
-9
@@ -1,10 +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 { fileURLToPath } from "node:url";
|
||||
import pluginJs from "@eslint/js";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import eslintPluginLocal from "./eslint-plugin-local/plugin.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -25,6 +27,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,28 +43,119 @@ 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', 'single'], TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1788
|
||||
"@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",
|
||||
"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 }], // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1785
|
||||
"max-lines": ["error", { max: 1065, skipBlankLines: true, skipComments: true }],
|
||||
"max-lines-per-function": ["error", { max: 561 }],
|
||||
"no-loss-of-precision": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"object-curly-newline": ["error", { multiline: true, consistent: true }],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
|
||||
// "no-undef": "error", // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1786
|
||||
"no-unused-vars": "off", // @typescript-eslint/no-unused-vars
|
||||
"quote-props": ["error", "consistent-as-needed"],
|
||||
// 'sort-imports': 'error', // TODO: Enable this rule, https://github.com/openfrontio/OpenFrontIO/issues/1787
|
||||
"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}",
|
||||
"src/client/**/*.{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",
|
||||
"sort-keys": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"src/client/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
rules: {
|
||||
// Disabled rules for frontend
|
||||
"sort-keys": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
local: eslintPluginLocal,
|
||||
},
|
||||
rules: {
|
||||
"local/no-z-array": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Generated
+57
@@ -42,7 +42,9 @@
|
||||
"@datastructures-js/priority-queue": "^6.3.3",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@stylistic/eslint-plugin": "^5.2.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",
|
||||
@@ -6358,6 +6360,54 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.3.tgz",
|
||||
"integrity": "sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"eslint-visitor-keys": "^4.2.1",
|
||||
"espree": "^10.4.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz",
|
||||
@@ -6604,6 +6654,13 @@
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@total-typescript/ts-reset": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz",
|
||||
"integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"@datastructures-js/priority-queue": "^6.3.3",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@stylistic/eslint-plugin": "^5.2.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",
|
||||
|
||||
@@ -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"
|
||||
@@ -212,7 +214,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",
|
||||
"enables_title": "Enable Settings",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
import { createCanvas } from "./Utils";
|
||||
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
|
||||
|
||||
export interface LobbyConfig {
|
||||
export type LobbyConfig = {
|
||||
serverConfig: ServerConfig;
|
||||
pattern: string | undefined;
|
||||
flag: string;
|
||||
@@ -59,7 +59,7 @@ export interface LobbyConfig {
|
||||
gameStartInfo?: GameStartInfo;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
}
|
||||
};
|
||||
|
||||
export function joinLobby(
|
||||
eventBus: EventBus,
|
||||
@@ -192,7 +192,7 @@ export class ClientGameRunner {
|
||||
|
||||
private lastMousePosition: { x: number; y: number } | null = null;
|
||||
|
||||
private lastMessageTime: number = 0;
|
||||
private lastMessageTime = 0;
|
||||
private connectionCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
@@ -229,7 +229,7 @@ export class ClientGameRunner {
|
||||
players,
|
||||
// Not saving turns locally
|
||||
[],
|
||||
startTime(),
|
||||
startTime() ?? 0,
|
||||
Date.now(),
|
||||
update.winner,
|
||||
this.lobby.serverConfig,
|
||||
@@ -364,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;
|
||||
@@ -420,7 +420,7 @@ 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);
|
||||
}
|
||||
@@ -623,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);
|
||||
}
|
||||
@@ -637,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);
|
||||
}
|
||||
|
||||
+15
-3
@@ -1,4 +1,8 @@
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
StripeCreateCheckoutSessionResponseSchema,
|
||||
UserMeResponse,
|
||||
} from "../core/ApiSchemas";
|
||||
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
|
||||
import { getApiBase, getAuthHeader } from "./jwt";
|
||||
|
||||
@@ -37,7 +41,7 @@ export async function handlePurchase(priceId: string) {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: getAuthHeader(),
|
||||
"authorization": getAuthHeader(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
priceId: priceId,
|
||||
@@ -59,7 +63,15 @@ export async function handlePurchase(priceId: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { renderPlayerFlag } from "../core/CustomFlag";
|
||||
const flagKey: string = "flag";
|
||||
const flagKey = "flag";
|
||||
|
||||
@customElement("flag-input")
|
||||
export class FlagInput extends LitElement {
|
||||
@state() public flag: string = "";
|
||||
@state() public flag = "";
|
||||
|
||||
static styles = css`
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export class FlagInputModal extends LitElement {
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@state() private search: string = "";
|
||||
@state() private search = "";
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ClientInfo,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
GameInfoSchema,
|
||||
TeamCountConfig,
|
||||
} from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
@@ -39,17 +40,19 @@ 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 clients: ClientInfo[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private useRandomMap = false;
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private lobbyIdVisible: boolean = true;
|
||||
@state() private lobbyCreatorClientID = "";
|
||||
@state() private lobbyIdVisible = true;
|
||||
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
// Add a new timer for debouncing bot changes
|
||||
@@ -293,8 +296,8 @@ export class HostLobbyModal extends LitElement {
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: o,
|
||||
})}
|
||||
num: o,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@@ -362,6 +365,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" : ""}"
|
||||
@@ -406,9 +441,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>
|
||||
@@ -562,11 +597,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}`);
|
||||
@@ -598,7 +643,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,
|
||||
@@ -671,6 +718,7 @@ export class HostLobbyModal extends LitElement {
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(GameInfoSchema.parse)
|
||||
.then((data: GameInfo) => {
|
||||
console.log(`got game info response: ${JSON.stringify(data)}`);
|
||||
|
||||
|
||||
@@ -115,17 +115,17 @@ export class AutoUpgradeEvent implements GameEvent {
|
||||
}
|
||||
|
||||
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 lastPinchDistance: number = 0;
|
||||
private lastPinchDistance = 0;
|
||||
|
||||
private pointerDown: boolean = false;
|
||||
private pointerDown = false;
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
@@ -159,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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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 { GameInfo, GameInfoSchema } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import {
|
||||
WorkerApiArchivedGameLobbySchema,
|
||||
WorkerApiGameIdExistsSchema,
|
||||
} from "../core/WorkerSchemas";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
|
||||
@customElement("join-private-lobby-modal")
|
||||
export class JoinPrivateLobbyModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@@ -14,7 +19,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
close: () => void;
|
||||
};
|
||||
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
|
||||
@state() private message: string = "";
|
||||
@state() private message = "";
|
||||
@state() private hasJoined = false;
|
||||
@state() private players: string[] = [];
|
||||
|
||||
@@ -105,7 +110,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);
|
||||
@@ -198,7 +203,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");
|
||||
@@ -231,7 +237,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 &&
|
||||
@@ -247,13 +254,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,
|
||||
@@ -281,6 +286,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
},
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then(GameInfoSchema.parse)
|
||||
.then((data: GameInfo) => {
|
||||
this.players = data.clients?.map((p) => p.username) ?? [];
|
||||
})
|
||||
|
||||
+27
-13
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import "./LanguageModal";
|
||||
@@ -35,13 +37,15 @@ import zh_CN from "../../resources/lang/zh-CN.json";
|
||||
export class LangSelector extends LitElement {
|
||||
@state() public translations: Record<string, string> | undefined;
|
||||
@state() public defaultTranslations: Record<string, string> | undefined;
|
||||
@state() public currentLang: string = "en";
|
||||
@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 debugKeyPressed: boolean = false;
|
||||
private debugKeyPressed = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private languageMap: Record<string, any> = {
|
||||
ar,
|
||||
bg,
|
||||
@@ -123,6 +127,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 +135,7 @@ export class LangSelector extends LitElement {
|
||||
private async loadLanguageList() {
|
||||
try {
|
||||
const data = this.languageMap;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let list: any[] = [];
|
||||
|
||||
const browserLang = new Intl.Locale(navigator.language).language;
|
||||
@@ -146,6 +152,7 @@ export class LangSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let debugLang: any = null;
|
||||
if (this.debugKeyPressed) {
|
||||
debugLang = {
|
||||
@@ -177,10 +184,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 +245,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();
|
||||
}
|
||||
});
|
||||
@@ -276,16 +287,16 @@ 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">
|
||||
@@ -309,6 +320,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 +329,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 +341,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,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
@@ -5,6 +6,7 @@ import { translateText } from "../client/Utils";
|
||||
@customElement("language-modal")
|
||||
export class LanguageModal extends LitElement {
|
||||
@property({ type: Boolean }) visible = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@property({ type: Array }) languageList: any[] = [];
|
||||
@property({ type: String }) currentLang = "en";
|
||||
|
||||
@@ -105,7 +107,9 @@ export class LanguageModal extends LitElement {
|
||||
return html`
|
||||
<button
|
||||
class="${buttonClasses}"
|
||||
@click=${() => this.selectLanguage(lang.code)}
|
||||
@click=${() =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
this.selectLanguage(lang.code)}
|
||||
>
|
||||
<img
|
||||
src="/flags/${lang.svg}.svg"
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import { GameConfig, GameID, GameRecord } from "../core/Schemas";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
GameConfig,
|
||||
GameConfigSchema,
|
||||
GameID,
|
||||
GameRecord,
|
||||
GameRecordSchema,
|
||||
ID,
|
||||
} from "../core/Schemas";
|
||||
import { replacer } from "../core/Util";
|
||||
|
||||
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) {
|
||||
|
||||
@@ -169,7 +169,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) {
|
||||
|
||||
+11
-4
@@ -43,6 +43,7 @@ import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt";
|
||||
import "./styles.css";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface Window {
|
||||
PageOS: {
|
||||
session: {
|
||||
@@ -55,6 +56,7 @@ declare global {
|
||||
spaAddAds: (ads: Array<{ type: string; selectorId: string }>) => void;
|
||||
destroyUnits: (adType: string) => void;
|
||||
settings?: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
slots?: any;
|
||||
};
|
||||
spaNewPage: (url: string) => void;
|
||||
@@ -62,13 +64,14 @@ declare global {
|
||||
}
|
||||
|
||||
// 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;
|
||||
"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;
|
||||
@@ -76,7 +79,11 @@ 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;
|
||||
@@ -548,7 +555,7 @@ class Client {
|
||||
this.publicLobby.leaveLobby();
|
||||
}
|
||||
|
||||
private handleKickPlayer(event: CustomEvent) {
|
||||
private handleKickPlayer(event: CustomEvent<KickPlayerEvent>) {
|
||||
const { target } = event.detail;
|
||||
|
||||
// Forward to eventBus if available
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,7 +32,7 @@ export class NewsModal extends LitElement {
|
||||
|
||||
@property({ type: String }) markdown = "Loading...";
|
||||
|
||||
private initialized: boolean = false;
|
||||
private initialized = false;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { ApiPublicLobbiesResponseSchema } from "../core/ExpressSchemas";
|
||||
import { GameMapType, GameMode } from "../core/game/Game";
|
||||
import { GameID, GameInfo } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
@@ -10,12 +11,12 @@ import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
@customElement("public-lobby")
|
||||
export class PublicLobby extends LitElement {
|
||||
@state() private lobbies: GameInfo[] = [];
|
||||
@state() public isLobbyHighlighted: boolean = false;
|
||||
@state() private isButtonDebounced: boolean = false;
|
||||
@state() public isLobbyHighlighted = false;
|
||||
@state() private isButtonDebounced = false;
|
||||
@state() private mapImages: Map<GameID, string> = new Map();
|
||||
private lobbiesInterval: number | null = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private debounceDelay = 750;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -77,7 +78,8 @@ export class PublicLobby extends LitElement {
|
||||
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);
|
||||
@@ -154,8 +156,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
|
||||
|
||||
@@ -34,12 +34,14 @@ export class SinglePlayerModal extends LitElement {
|
||||
};
|
||||
@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 donateGold = false;
|
||||
@state() private infiniteTroops = false;
|
||||
@state() private donateTroops = false;
|
||||
@state() private instantBuild = false;
|
||||
@state() private useRandomMap = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
|
||||
@@ -451,7 +453,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))
|
||||
|
||||
@@ -19,7 +19,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
};
|
||||
|
||||
public previewButton: HTMLElement | null = null;
|
||||
public buttonWidth: number = 150;
|
||||
public buttonWidth = 150;
|
||||
|
||||
@state() private selectedPattern: string | undefined;
|
||||
|
||||
@@ -172,7 +172,7 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}}
|
||||
>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
(${pattern.product!.price})
|
||||
(${pattern.product.price})
|
||||
</button>
|
||||
`
|
||||
: null}
|
||||
|
||||
@@ -328,6 +328,7 @@ export class Transport {
|
||||
};
|
||||
this.socket.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const parsed = JSON.parse(event.data);
|
||||
const result = ServerMessageSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
@@ -383,7 +384,7 @@ export class Transport {
|
||||
} satisfies ClientJoinMessage);
|
||||
}
|
||||
|
||||
leaveGame(saveFullGame: boolean = false) {
|
||||
leaveGame(saveFullGame = false) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.endGame(saveFullGame);
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { z } from "zod";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/baseComponents/setting/SettingKeybind";
|
||||
@@ -8,6 +9,8 @@ import "./components/baseComponents/setting/SettingNumber";
|
||||
import "./components/baseComponents/setting/SettingSlider";
|
||||
import "./components/baseComponents/setting/SettingToggle";
|
||||
|
||||
const KeybindSchema = z.record(z.string(), z.string());
|
||||
|
||||
@customElement("user-setting")
|
||||
export class UserSettingModal extends LitElement {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
@@ -25,7 +28,7 @@ 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);
|
||||
}
|
||||
@@ -179,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;
|
||||
@@ -241,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")}
|
||||
@@ -250,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")}
|
||||
@@ -386,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);
|
||||
@@ -405,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);
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
validateUsername,
|
||||
} from "../core/validations/username";
|
||||
|
||||
const usernameKey: string = "username";
|
||||
const usernameKey = "username";
|
||||
|
||||
@customElement("username-input")
|
||||
export class UsernameInput extends LitElement {
|
||||
@state() private username: string = "";
|
||||
@property({ type: String }) validationError: string = "";
|
||||
private _isValid: boolean = true;
|
||||
@state() private username = "";
|
||||
@property({ type: String }) validationError = "";
|
||||
private _isValid = true;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
// Remove static styles since we're using Tailwind
|
||||
|
||||
+12
-2
@@ -57,6 +57,7 @@ export function generateCryptoRandomUUID(): string {
|
||||
|
||||
// Fallback using crypto.getRandomValues
|
||||
if (crypto !== undefined && "getRandomValues" in crypto) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c: number): string =>
|
||||
@@ -83,8 +84,11 @@ export const translateText = (
|
||||
key: string,
|
||||
params: Record<string, string | number> = {},
|
||||
): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
||||
const self = translateText as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
self.formatterCache ??= new Map();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
self.lastLang ??= null;
|
||||
|
||||
const langSelector = document.querySelector("lang-selector") as LangSelector;
|
||||
@@ -100,8 +104,11 @@ export const translateText = (
|
||||
return key;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (self.lastLang !== langSelector.currentLang) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
self.formatterCache.clear();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
self.lastLang = langSelector.currentLang;
|
||||
}
|
||||
|
||||
@@ -122,13 +129,16 @@ export const translateText = (
|
||||
? "en"
|
||||
: langSelector.currentLang;
|
||||
const cacheKey = `${key}:${locale}:${message}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
let formatter = self.formatterCache.get(cacheKey);
|
||||
|
||||
if (!formatter) {
|
||||
formatter = new IntlMessageFormat(message, locale);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
self.formatterCache.set(cacheKey, formatter);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return formatter.format(params) as string;
|
||||
} catch (e) {
|
||||
console.warn("ICU format error", e);
|
||||
@@ -192,7 +202,7 @@ export function getMessageTypeClasses(type: MessageType): string {
|
||||
}
|
||||
|
||||
export function getModifierKey(): string {
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
const isMac = navigator.userAgent.includes("Mac");
|
||||
if (isMac) {
|
||||
return "⌘"; // Command key
|
||||
} else {
|
||||
@@ -201,7 +211,7 @@ export function getModifierKey(): string {
|
||||
}
|
||||
|
||||
export function getAltKey(): string {
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
const isMac = navigator.userAgent.includes("Mac");
|
||||
if (isMac) {
|
||||
return "⌥"; // Option key
|
||||
} else {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const MapDescription: Record<keyof typeof GameMapType, string> = {
|
||||
export class MapDisplay extends LitElement {
|
||||
@property({ type: String }) mapKey = "";
|
||||
@property({ type: Boolean }) selected = false;
|
||||
@property({ type: String }) translation: string = "";
|
||||
@property({ type: String }) translation = "";
|
||||
@state() private mapWebpPath: string | null = null;
|
||||
@state() private mapName: string | null = null;
|
||||
@state() private isLoading = true;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("modal-overlay")
|
||||
export class ModalOverlay extends LitElement {
|
||||
@property({ reflect: true }) public visible: boolean = false;
|
||||
@property({ reflect: true }) public visible = false;
|
||||
|
||||
static styles = css`
|
||||
.overlay {
|
||||
|
||||
@@ -28,17 +28,6 @@ export class SettingSlider extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private handleSliderChange(e: Event) {
|
||||
const detail = (e as CustomEvent)?.detail;
|
||||
if (!detail || detail.value === undefined) {
|
||||
console.warn("Invalid slider change event", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = detail.value;
|
||||
console.log("Slider changed to", value);
|
||||
}
|
||||
|
||||
private updateSliderStyle(slider: HTMLInputElement) {
|
||||
const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
|
||||
slider.style.background = `linear-gradient(to right, #2196f3 ${percent}%, #444 ${percent}%)`;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export class AnimatedSprite {
|
||||
private frameHeight: number;
|
||||
private currentFrame: number = 0;
|
||||
private elapsedTime: number = 0;
|
||||
private active: boolean = true;
|
||||
private currentFrame = 0;
|
||||
private elapsedTime = 0;
|
||||
private active = true;
|
||||
|
||||
constructor(
|
||||
private image: CanvasImageSource,
|
||||
private frameWidth: number,
|
||||
private frameCount: number,
|
||||
private frameDuration: number, // in milliseconds
|
||||
private looping: boolean = false,
|
||||
private looping = false,
|
||||
private originX: number,
|
||||
private originY: number,
|
||||
) {
|
||||
|
||||
@@ -246,7 +246,7 @@ export function createRenderer(
|
||||
eventBus,
|
||||
game,
|
||||
transformHandler,
|
||||
emojiTable as EmojiTable,
|
||||
emojiTable,
|
||||
buildMenu,
|
||||
uiState,
|
||||
playerPanel,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Cell, Game, NameViewData, Player } from "../../core/game/Game";
|
||||
import { calculateBoundingBox } from "../../core/Util";
|
||||
|
||||
export interface Point {
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Rectangle {
|
||||
export type Rectangle = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
};
|
||||
|
||||
export function placeName(game: Game, player: Player): NameViewData {
|
||||
const boundingBox =
|
||||
@@ -81,9 +81,9 @@ export function createGrid(
|
||||
|
||||
const width = scaledBoundingBox.max.x - scaledBoundingBox.min.x + 1;
|
||||
const height = scaledBoundingBox.max.y - scaledBoundingBox.min.y + 1;
|
||||
const grid: boolean[][] = Array(width)
|
||||
.fill(null)
|
||||
.map(() => Array(height).fill(false));
|
||||
const grid: boolean[][] = Array<Array<boolean>>(width)
|
||||
.fill(null as unknown as boolean[])
|
||||
.map(() => Array<boolean>(height).fill(false));
|
||||
|
||||
for (let x = scaledBoundingBox.min.x; x <= scaledBoundingBox.max.x; x++) {
|
||||
for (let y = scaledBoundingBox.min.y; y <= scaledBoundingBox.max.y; y++) {
|
||||
@@ -102,7 +102,7 @@ export function createGrid(
|
||||
export function findLargestInscribedRectangle(grid: boolean[][]): Rectangle {
|
||||
const rows = grid[0].length;
|
||||
const cols = grid.length;
|
||||
const heights: number[] = new Array(cols).fill(0);
|
||||
const heights: number[] = new Array<number>(cols).fill(0);
|
||||
let largestRect: Rectangle = { x: 0, y: 0, width: 0, height: 0 };
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export class ProgressBar {
|
||||
private y: number,
|
||||
private w: number,
|
||||
private h: number,
|
||||
private progress: number = 0, // Progress from 0 to 1
|
||||
private progress = 0, // Progress from 0 to 1
|
||||
) {
|
||||
this.setProgress(progress);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ export const CAMERA_MAX_SPEED = 15;
|
||||
export const CAMERA_SMOOTHING = 0.03;
|
||||
|
||||
export class TransformHandler {
|
||||
public scale: number = 1.8;
|
||||
public scale = 1.8;
|
||||
private _boundingRect: DOMRect;
|
||||
private offsetX: number = -350;
|
||||
private offsetY: number = -200;
|
||||
private offsetX = -350;
|
||||
private offsetY = -200;
|
||||
private lastGoToCallTime: number | null = null;
|
||||
|
||||
private target: Cell | null;
|
||||
@@ -267,7 +267,7 @@ export class TransformHandler {
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
override(x: number = 0, y: number = 0, s: number = 1) {
|
||||
override(x = 0, y = 0, s = 1) {
|
||||
//hardset view position
|
||||
this.clearTarget();
|
||||
this.offsetX = x;
|
||||
@@ -276,7 +276,7 @@ export class TransformHandler {
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
centerAll(fit: number = 1) {
|
||||
centerAll(fit = 1) {
|
||||
//position entire map centered on the screen
|
||||
|
||||
const vpWidth = this.boundingRect().width;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface UIState {
|
||||
export type UIState = {
|
||||
attackRatio: number;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface Fx {
|
||||
export type Fx = {
|
||||
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export enum FxType {
|
||||
MiniFire = "MiniFire",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FadeFx, SpriteFx } from "./SpriteFx";
|
||||
* Shockwave effect: draw a growing 1px white circle
|
||||
*/
|
||||
export class ShockwaveFx implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
private lifeTime = 0;
|
||||
constructor(
|
||||
private x: number,
|
||||
private y: number,
|
||||
|
||||
@@ -4,11 +4,7 @@ import { AnimatedSprite } from "../AnimatedSprite";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
|
||||
function fadeInOut(
|
||||
t: number,
|
||||
fadeIn: number = 0.3,
|
||||
fadeOut: number = 0.7,
|
||||
): number {
|
||||
function fadeInOut(t: number, fadeIn = 0.3, fadeOut = 0.7): number {
|
||||
if (t < fadeIn) {
|
||||
const f = t / fadeIn; // Map to [0, 1]
|
||||
return f * f;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Fx } from "./Fx";
|
||||
|
||||
export class TextFx implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
private lifeTime = 0;
|
||||
|
||||
constructor(
|
||||
private text: string,
|
||||
private x: number,
|
||||
private y: number,
|
||||
private duration: number,
|
||||
private riseDistance: number = 30,
|
||||
private font: string = "11px sans-serif",
|
||||
private riseDistance = 30,
|
||||
private font = "11px sans-serif",
|
||||
private color: { r: number; g: number; b: number } = {
|
||||
r: 255,
|
||||
g: 255,
|
||||
|
||||
@@ -80,7 +80,7 @@ export class AlertFrame extends LitElement implements Layer {
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
|
||||
this.onBrokeAllianceUpdate(update as BrokeAllianceUpdate);
|
||||
this.onBrokeAllianceUpdate(update);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@ import { renderNumber } from "../../Utils";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export interface BuildItemDisplay {
|
||||
export type BuildItemDisplay = {
|
||||
unitType: UnitType;
|
||||
icon: string;
|
||||
description?: string;
|
||||
key?: string;
|
||||
countable?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const buildTable: BuildItemDisplay[][] = [
|
||||
[
|
||||
|
||||
@@ -12,22 +12,22 @@ import { GameView } from "../../../core/game/GameView";
|
||||
import { onlyImages } from "../../../core/Util";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
interface ChatEvent {
|
||||
type ChatEvent = {
|
||||
description: string;
|
||||
unsafeDescription?: boolean;
|
||||
createdAt: number;
|
||||
highlight?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
@customElement("chat-display")
|
||||
export class ChatDisplay extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
public game: GameView;
|
||||
|
||||
private active: boolean = false;
|
||||
private active = false;
|
||||
|
||||
@state() private _hidden: boolean = false;
|
||||
@state() private newEvents: number = 0;
|
||||
@state() private _hidden = false;
|
||||
@state() private newEvents = 0;
|
||||
@state() private chatEvents: ChatEvent[] = [];
|
||||
|
||||
private toggleHidden() {
|
||||
|
||||
@@ -32,9 +32,9 @@ export class ChatModal extends LitElement {
|
||||
|
||||
private players: PlayerView[] = [];
|
||||
|
||||
private playerSearchQuery: string = "";
|
||||
private playerSearchQuery = "";
|
||||
private previewText: string | null = null;
|
||||
private requiresPlayerSelection: boolean = false;
|
||||
private requiresPlayerSelection = false;
|
||||
private selectedCategory: string | null = null;
|
||||
private selectedPhraseText: string | null = null;
|
||||
private selectedPhraseTemplate: string | null = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
public uiState: UIState;
|
||||
|
||||
@state()
|
||||
private attackRatio: number = 0.2;
|
||||
private attackRatio = 0.2;
|
||||
|
||||
@state()
|
||||
private _maxTroops: number;
|
||||
@@ -35,7 +35,7 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private _gold: Gold;
|
||||
|
||||
private _troopRateIsIncreasing: boolean = true;
|
||||
private _troopRateIsIncreasing = true;
|
||||
|
||||
private _lastTroopIncreaseRate: number;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { DirectiveResult } from "lit/directive.js";
|
||||
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
|
||||
import { getMessageTypeClasses, translateText } from "../../Utils";
|
||||
|
||||
interface GameEvent {
|
||||
type GameEvent = {
|
||||
description: string;
|
||||
unsafeDescription?: boolean;
|
||||
buttons?: {
|
||||
@@ -66,14 +66,14 @@ interface GameEvent {
|
||||
duration?: Tick;
|
||||
focusID?: number;
|
||||
unitView?: UnitView;
|
||||
}
|
||||
};
|
||||
|
||||
@customElement("events-display")
|
||||
export class EventsDisplay extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
public game: GameView;
|
||||
|
||||
private active: boolean = false;
|
||||
private active = false;
|
||||
private events: GameEvent[] = [];
|
||||
|
||||
// allianceID -> last checked at tick
|
||||
@@ -82,11 +82,11 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
@state() private outgoingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingLandAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingBoats: UnitView[] = [];
|
||||
@state() private _hidden: boolean = false;
|
||||
@state() private _isVisible: boolean = false;
|
||||
@state() private newEvents: number = 0;
|
||||
@state() private _hidden = false;
|
||||
@state() private _isVisible = false;
|
||||
@state() private newEvents = 0;
|
||||
@state() private latestGoldAmount: bigint | null = null;
|
||||
@state() private goldAmountAnimating: boolean = false;
|
||||
@state() private goldAmountAnimating = false;
|
||||
private goldAmountTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
@state() private eventsFilters: Map<MessageCategory, boolean> = new Map([
|
||||
[MessageCategory.ATTACK, false],
|
||||
@@ -96,7 +96,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
]);
|
||||
|
||||
private renderButton(options: {
|
||||
content: any; // Can be string, TemplateResult, or other renderable content
|
||||
content: string | TemplateResult | DirectiveResult<typeof UnsafeHTMLDirective>;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
@@ -261,7 +261,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
this.alliancesCheckedAt.set(alliance.id, this.game.ticks());
|
||||
|
||||
const other = this.game.player(alliance.other) as PlayerView;
|
||||
const other = this.game.player(alliance.other);
|
||||
if (!other.isAlive()) continue;
|
||||
|
||||
this.addEvent({
|
||||
@@ -393,7 +393,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
let otherPlayerDiplayName: string = "";
|
||||
let otherPlayerDiplayName = "";
|
||||
if (event.recipient !== null) {
|
||||
//'recipient' parameter contains sender ID or recipient ID
|
||||
const player = this.game.player(event.recipient);
|
||||
@@ -529,8 +529,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
traitorDuration === 1
|
||||
? translateText("events_display.duration_second")
|
||||
: translateText("events_display.duration_seconds_plural", {
|
||||
seconds: traitorDuration,
|
||||
});
|
||||
seconds: traitorDuration,
|
||||
});
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.betrayal_description", {
|
||||
@@ -768,11 +768,11 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
})}
|
||||
${!attack.retreating
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitCancelAttackIntent(attack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: attack.retreating,
|
||||
})
|
||||
content: "❌",
|
||||
onClick: () => this.emitCancelAttackIntent(attack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: attack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
@@ -803,12 +803,12 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
})}
|
||||
${!landAttack.retreating
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () =>
|
||||
this.emitCancelAttackIntent(landAttack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: landAttack.retreating,
|
||||
})
|
||||
content: "❌",
|
||||
onClick: () =>
|
||||
this.emitCancelAttackIntent(landAttack.id),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: landAttack.retreating,
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
@@ -840,11 +840,11 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
})}
|
||||
${!boat.retreating()
|
||||
? this.renderButton({
|
||||
content: "❌",
|
||||
onClick: () => this.emitBoatCancelIntent(boat.id()),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: boat.retreating(),
|
||||
})
|
||||
content: "❌",
|
||||
onClick: () => this.emitBoatCancelIntent(boat.id()),
|
||||
className: "text-left flex-shrink-0",
|
||||
disabled: boat.retreating(),
|
||||
})
|
||||
: html`<span class="flex-shrink-0 text-blue-400"
|
||||
>(${translateText(
|
||||
"events_display.retreating",
|
||||
@@ -1033,24 +1033,24 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
${event.focusID
|
||||
? this.renderButton({
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.focusID &&
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.focusID &&
|
||||
this.emitGoToPlayerEvent(event.focusID);
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
: event.unitView
|
||||
? this.renderButton({
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.unitView &&
|
||||
content: this.getEventDescription(event),
|
||||
onClick: () => {
|
||||
event.unitView &&
|
||||
this.emitGoToUnitEvent(
|
||||
event.unitView,
|
||||
);
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
},
|
||||
className: "text-left",
|
||||
})
|
||||
: this.getEventDescription(event)}
|
||||
<!-- Events with buttons (Alliance requests) -->
|
||||
${event.buttons
|
||||
@@ -1061,12 +1061,12 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
<button
|
||||
class="inline-block px-3 py-1 text-white rounded text-md md:text-sm cursor-pointer transition-colors duration-300
|
||||
${btn.className.includes("btn-info")
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes(
|
||||
"btn-gray",
|
||||
)
|
||||
? "bg-gray-500 hover:bg-gray-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
? "bg-blue-500 hover:bg-blue-600"
|
||||
: btn.className.includes(
|
||||
"btn-gray",
|
||||
)
|
||||
? "bg-gray-500 hover:bg-gray-600"
|
||||
: "bg-green-600 hover:bg-green-700"}"
|
||||
@click=${() => {
|
||||
btn.action();
|
||||
if (!btn.preventClose) {
|
||||
|
||||
@@ -14,29 +14,29 @@ export class FPSDisplay extends LitElement implements Layer {
|
||||
public userSettings!: UserSettings;
|
||||
|
||||
@state()
|
||||
private currentFPS: number = 0;
|
||||
private currentFPS = 0;
|
||||
|
||||
@state()
|
||||
private averageFPS: number = 0;
|
||||
private averageFPS = 0;
|
||||
|
||||
@state()
|
||||
private frameTime: number = 0;
|
||||
private frameTime = 0;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
private isVisible = false;
|
||||
|
||||
@state()
|
||||
private isDragging: boolean = false;
|
||||
private isDragging = false;
|
||||
|
||||
@state()
|
||||
private position: { x: number; y: number } = { x: 50, y: 20 }; // Percentage values
|
||||
|
||||
private frameCount: number = 0;
|
||||
private lastTime: number = 0;
|
||||
private frameCount = 0;
|
||||
private lastTime = 0;
|
||||
private frameTimes: number[] = [];
|
||||
private fpsHistory: number[] = [];
|
||||
private lastSecondTime: number = 0;
|
||||
private framesThisSecond: number = 0;
|
||||
private lastSecondTime = 0;
|
||||
private framesThisSecond = 0;
|
||||
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -20,8 +20,8 @@ export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
private lastRefresh: number = 0;
|
||||
private refreshRate: number = 10;
|
||||
private lastRefresh = 0;
|
||||
private refreshRate = 10;
|
||||
private theme: Theme;
|
||||
private animatedSpriteLoader: AnimatedSpriteLoader =
|
||||
new AnimatedSpriteLoader();
|
||||
|
||||
@@ -22,19 +22,19 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
|
||||
@state()
|
||||
private _isSinglePlayer: boolean = false;
|
||||
private _isSinglePlayer = false;
|
||||
|
||||
@state()
|
||||
private _isReplayVisible: boolean = false;
|
||||
private _isReplayVisible = false;
|
||||
|
||||
@state()
|
||||
private _isVisible: boolean = true;
|
||||
private _isVisible = true;
|
||||
|
||||
@state()
|
||||
private isPaused: boolean = false;
|
||||
private isPaused = false;
|
||||
|
||||
@state()
|
||||
private timer: number = 0;
|
||||
private timer = 0;
|
||||
|
||||
private hasWinner = false;
|
||||
|
||||
@@ -99,7 +99,9 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private onSettingsButtonClick() {
|
||||
this.eventBus.emit(new ShowSettingsModalEvent(true));
|
||||
this.eventBus.emit(
|
||||
new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -13,16 +13,16 @@ export class GutterAdModal extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
private isVisible = false;
|
||||
|
||||
@state()
|
||||
private adLoaded: boolean = false;
|
||||
private adLoaded = false;
|
||||
|
||||
private leftAdType: string = "left_rail";
|
||||
private rightAdType: string = "right_rail";
|
||||
private leftContainerId: string = "gutter-ad-container-left";
|
||||
private rightContainerId: string = "gutter-ad-container-right";
|
||||
private margin: string = "10px";
|
||||
private leftAdType = "left_rail";
|
||||
private rightAdType = "right_rail";
|
||||
private leftContainerId = "gutter-ad-container-left";
|
||||
private rightContainerId = "gutter-ad-container-right";
|
||||
private margin = "10px";
|
||||
|
||||
// Override createRenderRoot to disable shadow DOM
|
||||
createRenderRoot() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface Layer {
|
||||
export type Layer = {
|
||||
init?: () => void;
|
||||
tick?: () => void;
|
||||
renderLayer?: (context: CanvasRenderingContext2D) => void;
|
||||
shouldTransform?: () => boolean;
|
||||
redraw?: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { renderNumber } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
interface Entry {
|
||||
type Entry = {
|
||||
name: string;
|
||||
position: number;
|
||||
score: string;
|
||||
@@ -15,7 +15,7 @@ interface Entry {
|
||||
troops: string;
|
||||
isMyPlayer: boolean;
|
||||
player: PlayerView;
|
||||
}
|
||||
};
|
||||
|
||||
export class GoToPlayerEvent implements GameEvent {
|
||||
constructor(public player: PlayerView) {}
|
||||
|
||||
@@ -110,7 +110,7 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
this.buildMenu.playerActions = actions;
|
||||
|
||||
const tileOwner = this.game.owner(tile);
|
||||
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
|
||||
const recipient = tileOwner.isPlayer() ? tileOwner : null;
|
||||
|
||||
if (myPlayer && recipient) {
|
||||
this.chatIntegration.setupChatModal(myPlayer, recipient);
|
||||
|
||||
@@ -13,12 +13,12 @@ export class MultiTabModal extends LitElement implements Layer {
|
||||
|
||||
private detector: MultiTabDetector;
|
||||
|
||||
@property({ type: Number }) duration: number = 5000;
|
||||
@state() private countdown: number = 5;
|
||||
@state() private isVisible: boolean = false;
|
||||
@state() private fakeIp: string = "";
|
||||
@state() private deviceFingerprint: string = "";
|
||||
@state() private reported: boolean = true;
|
||||
@property({ type: Number }) duration = 5000;
|
||||
@state() private countdown = 5;
|
||||
@state() private isVisible = false;
|
||||
@state() private fakeIp = "";
|
||||
@state() private deviceFingerprint = "";
|
||||
@state() private reported = true;
|
||||
|
||||
private intervalId?: number;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export class NameLayer implements Layer {
|
||||
private firstPlace: PlayerView | null = null;
|
||||
private theme: Theme = this.game.config().theme();
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
private isVisible: boolean = true;
|
||||
private isVisible = true;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
@@ -617,7 +617,7 @@ export class NameLayer implements Layer {
|
||||
src: string,
|
||||
size: number,
|
||||
id: string,
|
||||
center: boolean = false,
|
||||
center = false,
|
||||
): HTMLImageElement {
|
||||
const icon = document.createElement("img");
|
||||
icon.src = src;
|
||||
|
||||
@@ -48,23 +48,23 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
@state()
|
||||
private showPauseButton: boolean = true;
|
||||
private showPauseButton = true;
|
||||
|
||||
@state()
|
||||
private isPaused: boolean = false;
|
||||
private isPaused = false;
|
||||
|
||||
@state()
|
||||
private timer: number = 0;
|
||||
private timer = 0;
|
||||
|
||||
@state()
|
||||
private showSettings: boolean = false;
|
||||
private showSettings = false;
|
||||
|
||||
private isVisible = false;
|
||||
|
||||
private hasWinner = false;
|
||||
|
||||
@state()
|
||||
private alternateView: boolean = false;
|
||||
private alternateView = false;
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
this.alternateView = !this.alternateView;
|
||||
|
||||
@@ -97,8 +97,8 @@ export class PlayerActionHandler {
|
||||
this.eventBus.emit(new SendEmojiIntentEvent(targetPlayer, emojiIndex));
|
||||
}
|
||||
|
||||
handleQuickChat(recipient: PlayerView, chatKey: string, params: any = {}) {
|
||||
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, params));
|
||||
handleQuickChat(recipient: PlayerView, chatKey: string, target?: PlayerID) {
|
||||
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, target));
|
||||
}
|
||||
|
||||
handleDeleteUnit(unitId: number) {
|
||||
|
||||
@@ -60,12 +60,14 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
private unit: UnitView | null = null;
|
||||
|
||||
@state()
|
||||
private _isInfoVisible: boolean = false;
|
||||
private _isInfoVisible = false;
|
||||
|
||||
private _isActive = false;
|
||||
|
||||
private lastMouseUpdate = 0;
|
||||
|
||||
private showDetails = true;
|
||||
|
||||
init() {
|
||||
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
|
||||
this.onMouseEvent(e),
|
||||
@@ -105,7 +107,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
const owner = this.game.owner(tile);
|
||||
|
||||
if (owner && owner.isPlayer()) {
|
||||
this.player = owner as PlayerView;
|
||||
this.player = owner;
|
||||
this.player.profile().then((p) => {
|
||||
this.playerProfile = p;
|
||||
});
|
||||
@@ -219,13 +221,17 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div class="p-2">
|
||||
<div
|
||||
<button
|
||||
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex break-all ${isFriendly
|
||||
? "text-green-500"
|
||||
: "text-white"}"
|
||||
@click=${() => {
|
||||
this.showDetails = !this.showDetails;
|
||||
this.requestUpdate?.();
|
||||
}}
|
||||
>
|
||||
${player.cosmetics.flag
|
||||
? player.cosmetics.flag!.startsWith("!")
|
||||
? player.cosmetics.flag.startsWith("!")
|
||||
? html`<div
|
||||
class="h-8 mr-1 aspect-[3/4] player-flag"
|
||||
${ref((el) => {
|
||||
@@ -238,66 +244,73 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
></div>`
|
||||
: html`<img
|
||||
class="h-8 mr-1 aspect-[3/4]"
|
||||
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
|
||||
src=${"/flags/" + player.cosmetics.flag + ".svg"}
|
||||
/>`
|
||||
: html``}
|
||||
${player.name()}
|
||||
</div>
|
||||
${player.team() !== null
|
||||
? html`<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.team")}: ${player.team()}
|
||||
</div>`
|
||||
</button>
|
||||
|
||||
<!-- Collapsible section -->
|
||||
${this.showDetails
|
||||
? html`
|
||||
${player.team() !== null
|
||||
? html`<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.team")}:
|
||||
${player.team()}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.type")}: ${playerType}
|
||||
</div>
|
||||
${player.troops() >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.d_troops")}:
|
||||
${renderTroops(player.troops())}
|
||||
</div>`
|
||||
: ""}
|
||||
${attackingTroops >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.a_troops")}:
|
||||
${renderTroops(attackingTroops)}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.gold")}:
|
||||
${renderNumber(player.gold())}
|
||||
</div>
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Port,
|
||||
"player_info_overlay.ports",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.City,
|
||||
"player_info_overlay.cities",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Factory,
|
||||
"player_info_overlay.factories",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.MissileSilo,
|
||||
"player_info_overlay.missile_launchers",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.SAMLauncher,
|
||||
"player_info_overlay.sams",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Warship,
|
||||
"player_info_overlay.warships",
|
||||
)}
|
||||
${relationHtml}
|
||||
`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80">
|
||||
${translateText("player_info_overlay.type")}: ${playerType}
|
||||
</div>
|
||||
${player.troops() >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.d_troops")}:
|
||||
${renderTroops(player.troops())}
|
||||
</div>`
|
||||
: ""}
|
||||
${attackingTroops >= 1
|
||||
? html`<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.a_troops")}:
|
||||
${renderTroops(attackingTroops)}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="text-sm opacity-80" translate="no">
|
||||
${translateText("player_info_overlay.gold")}:
|
||||
${renderNumber(player.gold())}
|
||||
</div>
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Port,
|
||||
"player_info_overlay.ports",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.City,
|
||||
"player_info_overlay.cities",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Factory,
|
||||
"player_info_overlay.factories",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.MissileSilo,
|
||||
"player_info_overlay.missile_launchers",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.SAMLauncher,
|
||||
"player_info_overlay.sams",
|
||||
)}
|
||||
${this.displayUnitCount(
|
||||
player,
|
||||
UnitType.Warship,
|
||||
"player_info_overlay.warships",
|
||||
)}
|
||||
${relationHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private tile: TileRef | null = null;
|
||||
|
||||
@state()
|
||||
public isVisible: boolean = false;
|
||||
public isVisible = false;
|
||||
|
||||
@state()
|
||||
private allianceExpiryText: string | null = null;
|
||||
@@ -224,15 +224,15 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
const myPlayer = this.g.myPlayer();
|
||||
if (myPlayer === null) return;
|
||||
if (this.tile === null) return;
|
||||
let other = this.g.owner(this.tile);
|
||||
const other = this.g.owner(this.tile);
|
||||
if (!other.isPlayer()) {
|
||||
this.hide();
|
||||
console.warn("Tile is not owned by a player");
|
||||
return;
|
||||
}
|
||||
other = other as PlayerView;
|
||||
|
||||
const canDonate = this.actions?.interaction?.canDonate;
|
||||
const canDonateGold = this.actions?.interaction?.canDonateGold;
|
||||
const canDonateTroops = this.actions?.interaction?.canDonateTroops;
|
||||
const canSendAllianceRequest =
|
||||
this.actions?.interaction?.canSendAllianceRequest;
|
||||
const canSendEmoji =
|
||||
@@ -351,9 +351,9 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
>
|
||||
${other.allies().length > 0
|
||||
? other
|
||||
.allies()
|
||||
.map((p) => p.name())
|
||||
.join(", ")
|
||||
.allies()
|
||||
.map((p) => p.name())
|
||||
.join(", ")
|
||||
: translateText("player_panel.none")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,7 +421,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
<img src=${allianceIcon} alt="Alliance" class="w-6 h-6" />
|
||||
</button>`
|
||||
: ""}
|
||||
${canDonate
|
||||
${canDonateTroops
|
||||
? html`<button
|
||||
@click=${(e: MouseEvent) =>
|
||||
this.handleDonateTroopClick(e, myPlayer, other)}
|
||||
@@ -436,7 +436,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
/>
|
||||
</button>`
|
||||
: ""}
|
||||
${canDonate
|
||||
${canDonateGold
|
||||
? html`<button
|
||||
@click=${(e: MouseEvent) =>
|
||||
this.handleDonateGoldClick(e, myPlayer, other)}
|
||||
|
||||
@@ -15,12 +15,12 @@ export class CloseRadialMenuEvent implements GameEvent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
export interface TooltipItem {
|
||||
export type TooltipItem = {
|
||||
text: string;
|
||||
className: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RadialMenuConfig {
|
||||
export type RadialMenuConfig = {
|
||||
menuSize?: number;
|
||||
submenuScale?: number;
|
||||
centerButtonSize?: number;
|
||||
@@ -33,7 +33,7 @@ export interface RadialMenuConfig {
|
||||
maxNestedLevels?: number;
|
||||
innerRadiusIncrement?: number;
|
||||
tooltipStyle?: string;
|
||||
}
|
||||
};
|
||||
|
||||
type CenterButtonState = "default" | "back";
|
||||
|
||||
@@ -42,9 +42,9 @@ type RequiredRadialMenuConfig = Required<RadialMenuConfig>;
|
||||
export class RadialMenu implements Layer {
|
||||
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
|
||||
private tooltipElement: HTMLDivElement | null = null;
|
||||
private isVisible: boolean = false;
|
||||
private isVisible = false;
|
||||
|
||||
private currentLevel: number = 0; // Current menu level (0 = main menu, 1 = submenu, etc.)
|
||||
private currentLevel = 0; // Current menu level (0 = main menu, 1 = submenu, etc.)
|
||||
private menuStack: MenuElement[][] = []; // Stack to track menu navigation history
|
||||
private currentMenuItems: MenuElement[] = []; // Current active menu items (changes based on level)
|
||||
|
||||
@@ -53,9 +53,12 @@ export class RadialMenu implements Layer {
|
||||
|
||||
private centerButtonState: CenterButtonState = "default";
|
||||
|
||||
private isTransitioning: boolean = false;
|
||||
private lastHideTime: number = 0;
|
||||
private reopenCooldownMs: number = 300;
|
||||
private isTransitioning = false;
|
||||
private lastHideTime = 0;
|
||||
private reopenCooldownMs = 300;
|
||||
|
||||
private anchorX = 0;
|
||||
private anchorY = 0;
|
||||
|
||||
private menuGroups: Map<
|
||||
number,
|
||||
@@ -73,8 +76,8 @@ export class RadialMenu implements Layer {
|
||||
private selectedItemId: string | null = null;
|
||||
private submenuHoverTimeout: number | null = null;
|
||||
private backButtonHoverTimeout: number | null = null;
|
||||
private navigationInProgress: boolean = false;
|
||||
private originalCenterButtonIcon: string = "";
|
||||
private navigationInProgress = false;
|
||||
private originalCenterButtonIcon = "";
|
||||
|
||||
private params: MenuElementParams | null = null;
|
||||
|
||||
@@ -128,7 +131,7 @@ export class RadialMenu implements Layer {
|
||||
this.hideRadialMenu();
|
||||
this.eventBus.emit(new CloseRadialMenuEvent());
|
||||
})
|
||||
.on("contextmenu", (e) => {
|
||||
.on("contextmenu", (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.hideRadialMenu();
|
||||
this.eventBus.emit(new CloseRadialMenuEvent());
|
||||
@@ -146,6 +149,7 @@ export class RadialMenu implements Layer {
|
||||
.style("position", "absolute")
|
||||
.style("top", "50%")
|
||||
.style("left", "50%")
|
||||
.style("transition", `top ${this.config.menuTransitionDuration}ms ease, left ${this.config.menuTransitionDuration}ms ease`)
|
||||
.style("transform", "translate(-50%, -50%)")
|
||||
.style("pointer-events", "all")
|
||||
.on("click", (event) => this.hideRadialMenu());
|
||||
@@ -174,7 +178,7 @@ export class RadialMenu implements Layer {
|
||||
.attr("r", this.config.centerButtonSize)
|
||||
.attr("fill", "transparent")
|
||||
.style("cursor", "pointer")
|
||||
.on("click", (event) => {
|
||||
.on("click", (event: Event) => {
|
||||
event.stopPropagation();
|
||||
this.handleCenterButtonClick();
|
||||
})
|
||||
@@ -262,7 +266,7 @@ export class RadialMenu implements Layer {
|
||||
menuGroup.style("opacity", 0).style("transform", "scale(0.5)");
|
||||
}
|
||||
|
||||
this.menuGroups.set(level, menuGroup as any);
|
||||
this.menuGroups.set(level, menuGroup);
|
||||
|
||||
const offset = -Math.PI / items.length;
|
||||
|
||||
@@ -303,7 +307,7 @@ export class RadialMenu implements Layer {
|
||||
SVGGElement,
|
||||
unknown
|
||||
>,
|
||||
arc: d3.Arc<any, d3.PieArcDatum<MenuElement>>,
|
||||
arc: d3.Arc<unknown, d3.PieArcDatum<MenuElement>>,
|
||||
level: number,
|
||||
) {
|
||||
arcs
|
||||
@@ -344,7 +348,7 @@ export class RadialMenu implements Layer {
|
||||
arcs.each((d) => {
|
||||
const pathId = d.data.id;
|
||||
const path = d3.select(`path[data-id="${pathId}"]`);
|
||||
this.menuPaths.set(pathId, path as any);
|
||||
this.menuPaths.set(pathId, path as never);
|
||||
|
||||
if (
|
||||
pathId === this.selectedItemId &&
|
||||
@@ -384,7 +388,9 @@ export class RadialMenu implements Layer {
|
||||
>,
|
||||
level: number,
|
||||
) {
|
||||
const onHover = (d: d3.PieArcDatum<MenuElement>, path: any) => {
|
||||
const onHover = (d: d3.PieArcDatum<MenuElement>, path: d3.Selection<
|
||||
d3.BaseType, unknown, HTMLElement, unknown
|
||||
>) => {
|
||||
const disabled = this.params === null || d.data.disabled(this.params);
|
||||
if (d.data.tooltipItems && d.data.tooltipItems.length > 0) {
|
||||
this.showTooltip(d.data.tooltipItems);
|
||||
@@ -403,7 +409,9 @@ export class RadialMenu implements Layer {
|
||||
path.attr("stroke-width", "3");
|
||||
};
|
||||
|
||||
const onMouseOut = (d: d3.PieArcDatum<MenuElement>, path: any) => {
|
||||
const onMouseOut = (d: d3.PieArcDatum<MenuElement>, path: d3.Selection<
|
||||
d3.BaseType, unknown, HTMLElement, unknown
|
||||
>) => {
|
||||
const disabled = this.params === null || d.data.disabled(this.params);
|
||||
if (this.submenuHoverTimeout !== null) {
|
||||
window.clearTimeout(this.submenuHoverTimeout);
|
||||
@@ -483,15 +491,15 @@ export class RadialMenu implements Layer {
|
||||
onMouseOut(d, path);
|
||||
});
|
||||
|
||||
path.on("mousemove", function (event) {
|
||||
handleMouseMove(event as MouseEvent);
|
||||
path.on("mousemove", function (event: MouseEvent) {
|
||||
handleMouseMove(event);
|
||||
});
|
||||
|
||||
path.on("click", function (event) {
|
||||
path.on("click", function (event: Event) {
|
||||
onClick(d, event);
|
||||
});
|
||||
|
||||
path.on("touchstart", function (event) {
|
||||
path.on("touchstart", function (event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick(d, event);
|
||||
@@ -514,7 +522,7 @@ export class RadialMenu implements Layer {
|
||||
SVGGElement,
|
||||
unknown
|
||||
>,
|
||||
arc: d3.Arc<any, d3.PieArcDatum<MenuElement>>,
|
||||
arc: d3.Arc<unknown, d3.PieArcDatum<MenuElement>>,
|
||||
) {
|
||||
arcs
|
||||
.append("g")
|
||||
@@ -549,7 +557,7 @@ export class RadialMenu implements Layer {
|
||||
.attr("opacity", disabled ? 0.5 : 1);
|
||||
}
|
||||
|
||||
this.menuIcons.set(contentId, content as any);
|
||||
this.menuIcons.set(contentId, content as never);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -576,6 +584,7 @@ export class RadialMenu implements Layer {
|
||||
this.currentMenuItems = children;
|
||||
this.currentLevel++;
|
||||
|
||||
this.clampAndSetMenuPositionForLevel(this.currentLevel);
|
||||
this.renderMenuItems(this.currentMenuItems, this.currentLevel);
|
||||
this.updateMenuGroupVisibility();
|
||||
this.animatePreviousMenu();
|
||||
@@ -655,6 +664,7 @@ export class RadialMenu implements Layer {
|
||||
this.isTransitioning = true;
|
||||
|
||||
this.updateMenuLevels();
|
||||
this.clampAndSetMenuPositionForLevel(this.currentLevel);
|
||||
this.clearSelectedItemHoverState();
|
||||
this.updateMenuVisibility("backward");
|
||||
this.animateMenuTransitions();
|
||||
@@ -729,8 +739,8 @@ export class RadialMenu implements Layer {
|
||||
});
|
||||
}
|
||||
|
||||
private animateExistingMenu(
|
||||
previousMenu: d3.Selection<any, unknown, null, undefined>,
|
||||
private animateExistingMenu<T extends d3.BaseType>(
|
||||
previousMenu: d3.Selection<T, unknown, null, undefined>,
|
||||
) {
|
||||
previousMenu
|
||||
.transition()
|
||||
@@ -751,19 +761,17 @@ export class RadialMenu implements Layer {
|
||||
this.resetMenu();
|
||||
this.isTransitioning = false;
|
||||
this.selectedItemId = null;
|
||||
this.anchorX = x;
|
||||
this.anchorY = y;
|
||||
|
||||
this.menuElement.style("display", "block");
|
||||
|
||||
this.menuElement
|
||||
.select("svg")
|
||||
.style("top", `${y}px`)
|
||||
.style("left", `${x}px`)
|
||||
.style("transform", `translate(-50%, -50%)`);
|
||||
this.clampAndSetMenuPositionForLevel(this.currentLevel);
|
||||
|
||||
this.isVisible = true;
|
||||
|
||||
this.renderMenuItems(this.currentMenuItems, this.currentLevel);
|
||||
this.onCenterButtonHover(true);
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
}
|
||||
|
||||
public hideRadialMenu() {
|
||||
@@ -787,6 +795,7 @@ export class RadialMenu implements Layer {
|
||||
this.menuIcons.clear();
|
||||
|
||||
this.lastHideTime = Date.now();
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
}
|
||||
|
||||
private handleCenterButtonClick() {
|
||||
@@ -1038,4 +1047,26 @@ export class RadialMenu implements Layer {
|
||||
this.tooltipElement.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the menu's SVG center stays within viewport given the current level's outer radius
|
||||
private clampAndSetMenuPositionForLevel(level: number) {
|
||||
const outerRadius = this.getOuterRadiusForLevel(level);
|
||||
const margin = Math.max(outerRadius, this.config.centerButtonSize) + 10;
|
||||
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
// If the menu cannot fully fit on an axis, pin it to the viewport center on that axis.
|
||||
const clampedX = 2 * margin > vw ? vw / 2 : Math.min(Math.max(this.anchorX, margin), vw - margin);
|
||||
const clampedY = 2 * margin > vh ? vh / 2 : Math.min(Math.max(this.anchorY, margin), vh - margin);
|
||||
|
||||
const svgSel = this.menuElement.select("svg");
|
||||
svgSel
|
||||
.style("top", `${clampedY}px`)
|
||||
.style("left", `${clampedX}px`);
|
||||
}
|
||||
|
||||
private handleResize = () => {
|
||||
if (this.isVisible) this.clampAndSetMenuPositionForLevel(this.currentLevel);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
|
||||
import xIcon from "../../../../resources/images/XIcon.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
|
||||
export interface MenuElementParams {
|
||||
export type MenuElementParams = {
|
||||
myPlayer: PlayerView;
|
||||
selected: PlayerView | null;
|
||||
tile: TileRef;
|
||||
@@ -38,9 +38,9 @@ export interface MenuElementParams {
|
||||
chatIntegration: ChatIntegration;
|
||||
eventBus: EventBus;
|
||||
closeMenu: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MenuElement {
|
||||
export type MenuElement = {
|
||||
id: string;
|
||||
name: string;
|
||||
displayed?: boolean | ((params: MenuElementParams) => boolean);
|
||||
@@ -54,18 +54,18 @@ export interface MenuElement {
|
||||
disabled: (params: MenuElementParams) => boolean;
|
||||
action?: (params: MenuElementParams) => void; // For leaf items that perform actions
|
||||
subMenu?: (params: MenuElementParams) => MenuElement[]; // For non-leaf items that open submenus
|
||||
}
|
||||
};
|
||||
|
||||
export interface TooltipKey {
|
||||
export type TooltipKey = {
|
||||
key: string;
|
||||
className: string;
|
||||
params?: Record<string, string | number>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CenterButtonElement {
|
||||
export type CenterButtonElement = {
|
||||
disabled: (params: MenuElementParams) => boolean;
|
||||
action: (params: MenuElementParams) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const COLORS = {
|
||||
build: "#ebe250",
|
||||
@@ -208,7 +208,7 @@ const allyDonateGoldElement: MenuElement = {
|
||||
id: "ally_donate_gold",
|
||||
name: "donate gold",
|
||||
disabled: (params: MenuElementParams) =>
|
||||
!params.playerActions?.interaction?.canDonate,
|
||||
!params.playerActions?.interaction?.canDonateGold,
|
||||
color: COLORS.ally,
|
||||
icon: donateGoldIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
@@ -221,7 +221,7 @@ const allyDonateTroopsElement: MenuElement = {
|
||||
id: "ally_donate_troops",
|
||||
name: "donate troops",
|
||||
disabled: (params: MenuElementParams) =>
|
||||
!params.playerActions?.interaction?.canDonate,
|
||||
!params.playerActions?.interaction?.canDonateTroops,
|
||||
color: COLORS.ally,
|
||||
icon: donateTroopIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
@@ -566,8 +566,7 @@ export const rootMenuElement: MenuElement = {
|
||||
|
||||
const tileOwner = params.game.owner(params.tile);
|
||||
const isOwnTerritory =
|
||||
tileOwner.isPlayer() &&
|
||||
(tileOwner as PlayerView).id() === params.myPlayer.id();
|
||||
tileOwner.isPlayer() && tileOwner.id() === params.myPlayer.id();
|
||||
|
||||
const menuItems: (MenuElement | null)[] = [
|
||||
infoMenuElement,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RailTile,
|
||||
RailType,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Layer } from "./Layer";
|
||||
import { getRailroadRects } from "./RailroadSprites";
|
||||
|
||||
@@ -151,7 +151,7 @@ export class RailroadLayer implements Layer {
|
||||
const x = this.game.x(railRoad.tile);
|
||||
const y = this.game.y(railRoad.tile);
|
||||
const owner = this.game.owner(railRoad.tile);
|
||||
const recipient = owner.isPlayer() ? (owner as PlayerView) : null;
|
||||
const recipient = owner.isPlayer() ? owner : null;
|
||||
const color = recipient
|
||||
? this.theme.railroadColor(recipient)
|
||||
: new Colord({ r: 255, g: 255, b: 255, a: 1 });
|
||||
|
||||
@@ -12,8 +12,8 @@ import { Layer } from "./Layer";
|
||||
|
||||
export class ShowReplayPanelEvent {
|
||||
constructor(
|
||||
public visible: boolean = true,
|
||||
public isSingleplayer: boolean = false,
|
||||
public visible = true,
|
||||
public isSingleplayer = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
public eventBus: EventBus | undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
visible: boolean = false;
|
||||
visible = false;
|
||||
|
||||
@state()
|
||||
private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
|
||||
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
|
||||
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
|
||||
@@ -12,11 +12,16 @@ import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class ShowSettingsModalEvent {
|
||||
constructor(public readonly isVisible: boolean = true) {}
|
||||
constructor(
|
||||
public readonly isVisible = true,
|
||||
public readonly shouldPause = false,
|
||||
public readonly isPaused = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
@customElement("settings-modal")
|
||||
@@ -25,17 +30,26 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
public userSettings: UserSettings;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
private isVisible = false;
|
||||
|
||||
@state()
|
||||
private alternateView: boolean = false;
|
||||
private alternateView = false;
|
||||
|
||||
@query(".modal-overlay")
|
||||
private modalOverlay!: HTMLElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
shouldPause = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
wasPausedWhenOpened = false;
|
||||
|
||||
init() {
|
||||
this.eventBus.on(ShowSettingsModalEvent, (event) => {
|
||||
this.isVisible = event.isVisible;
|
||||
this.shouldPause = event.shouldPause;
|
||||
this.wasPausedWhenOpened = event.isPaused;
|
||||
this.pauseGame(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +95,12 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
this.isVisible = false;
|
||||
document.body.style.overflow = "";
|
||||
this.requestUpdate();
|
||||
this.pauseGame(false);
|
||||
}
|
||||
|
||||
private pauseGame(pause: boolean) {
|
||||
if (this.shouldPause && !this.wasPausedWhenOpened)
|
||||
this.eventBus.emit(new PauseGameEvent(pause));
|
||||
}
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
@@ -354,8 +374,8 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
${this.userSettings.performanceOverlay()
|
||||
? translateText("user_setting.performance_overlay_enabled")
|
||||
: translateText(
|
||||
"user_setting.performance_overlay_disabled",
|
||||
)}
|
||||
"user_setting.performance_overlay_disabled",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
|
||||
@@ -13,12 +13,12 @@ export class SpawnAd extends LitElement implements Layer {
|
||||
public g: GameView;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
private isVisible = false;
|
||||
|
||||
@state()
|
||||
private adLoaded: boolean = false;
|
||||
private adLoaded = false;
|
||||
|
||||
private gamesPlayed: number = 0;
|
||||
private gamesPlayed = 0;
|
||||
|
||||
// Override createRenderRoot to disable shadow DOM
|
||||
createRenderRoot() {
|
||||
|
||||
@@ -19,15 +19,15 @@ import { Layer } from "./Layer";
|
||||
type ShapeType = "triangle" | "square" | "pentagon" | "octagon" | "circle";
|
||||
|
||||
class StructureRenderInfo {
|
||||
public isOnScreen: boolean = false;
|
||||
public isOnScreen = false;
|
||||
constructor(
|
||||
public unit: UnitView,
|
||||
public owner: PlayerID,
|
||||
public iconContainer: PIXI.Container,
|
||||
public levelContainer: PIXI.Container,
|
||||
public dotContainer: PIXI.Container,
|
||||
public level: number = 0,
|
||||
public underConstruction: boolean = true,
|
||||
public level = 0,
|
||||
public underConstruction = true,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class StructureIconsLayer implements Layer {
|
||||
private iconsStage: PIXI.Container;
|
||||
private levelsStage: PIXI.Container;
|
||||
private dotsStage: PIXI.Container;
|
||||
private shouldRedraw: boolean = true;
|
||||
private shouldRedraw = true;
|
||||
private textureCache: Map<string, PIXI.Texture> = new Map();
|
||||
private theme: Theme;
|
||||
private renderer: PIXI.Renderer;
|
||||
@@ -353,12 +353,12 @@ export class StructureIconsLayer implements Layer {
|
||||
const shape = STRUCTURE_SHAPES[structureType];
|
||||
const texture = shape
|
||||
? this.createIcon(
|
||||
unit.owner(),
|
||||
structureType,
|
||||
isConstruction,
|
||||
shape,
|
||||
renderIcon,
|
||||
)
|
||||
unit.owner(),
|
||||
structureType,
|
||||
isConstruction,
|
||||
shape,
|
||||
renderIcon,
|
||||
)
|
||||
: PIXI.Texture.EMPTY;
|
||||
|
||||
this.textureCache.set(cacheKey, texture);
|
||||
|
||||
@@ -23,11 +23,11 @@ const BASE_TERRITORY_RADIUS = 13.5;
|
||||
const RADIUS_SCALE_FACTOR = 0.5;
|
||||
const ZOOM_THRESHOLD = 4.3; // below this zoom level, structures are not rendered
|
||||
|
||||
interface UnitRenderConfig {
|
||||
type UnitRenderConfig = {
|
||||
icon: string;
|
||||
borderRadius: number;
|
||||
territoryRadius: number;
|
||||
}
|
||||
};
|
||||
|
||||
export class StructureLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
|
||||
@@ -6,14 +6,14 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
interface TeamEntry {
|
||||
type TeamEntry = {
|
||||
teamName: string;
|
||||
totalScoreStr: string;
|
||||
totalGold: string;
|
||||
totalTroops: string;
|
||||
totalScoreSort: number;
|
||||
players: PlayerView[];
|
||||
}
|
||||
};
|
||||
|
||||
@customElement("team-stats")
|
||||
export class TeamStats extends LitElement implements Layer {
|
||||
|
||||
@@ -391,7 +391,7 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
paintTerritory(tile: TileRef, isBorder: boolean = false) {
|
||||
paintTerritory(tile: TileRef, isBorder = false) {
|
||||
if (isBorder && !this.game.hasOwner(tile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export function getAuthHeader(): string {
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
|
||||
export async function logOut(allSessions: boolean = false) {
|
||||
export async function logOut(allSessions = false) {
|
||||
const token = getToken();
|
||||
if (token === null) return;
|
||||
clearToken();
|
||||
|
||||
@@ -3,10 +3,10 @@ import { html, TemplateResult } from "lit";
|
||||
import { UnitType } from "../../core/game/Game";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
export interface UnitTypeRenderContext {
|
||||
export type UnitTypeRenderContext = {
|
||||
disabledUnits: UnitType[];
|
||||
toggleUnit: (unit: UnitType, checked: boolean) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const unitOptions: { type: UnitType; translationKey: string }[] = [
|
||||
{ type: UnitType.City, translationKey: "unit_type.city" },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file contains schemas for api.openfront.io
|
||||
import { z } from "zod";
|
||||
import { base64urlToUuid } from "./Base64";
|
||||
|
||||
@@ -48,3 +49,19 @@ export const UserMeResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
|
||||
|
||||
export const StripeCreateCheckoutSessionResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
object: z.literal("checkout.session"),
|
||||
url: z.string(),
|
||||
payment_status: z.enum(["paid", "unpaid", "no_payment_required"]),
|
||||
status: z.enum(["open", "complete", "expired"]),
|
||||
client_reference_id: z.string().optional(),
|
||||
customer: z.string().optional(),
|
||||
payment_intent: z.string().optional(),
|
||||
subscription: z.string().optional(),
|
||||
metadata: z.partialRecord(z.string(), z.string()),
|
||||
});
|
||||
export type StripeCreateCheckoutSessionResponse = z.infer<
|
||||
typeof StripeCreateCheckoutSessionResponseSchema
|
||||
>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod/v4";
|
||||
import { z } from "zod";
|
||||
import { RequiredPatternSchema } from "./Schemas";
|
||||
|
||||
export const ProductSchema = z.object({
|
||||
@@ -25,7 +25,7 @@ export const CosmeticsSchema = z.object({
|
||||
z.string(),
|
||||
z.object({
|
||||
name: z.string(),
|
||||
flares: z.array(z.string()).optional(),
|
||||
flares: z.string().array().optional(),
|
||||
}),
|
||||
),
|
||||
color: z.record(
|
||||
@@ -33,7 +33,7 @@ export const CosmeticsSchema = z.object({
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: z.string(),
|
||||
flares: z.array(z.string()).optional(),
|
||||
flares: z.string().array().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Cosmetics } from "./CosmeticSchemas";
|
||||
|
||||
const ANIMATION_DURATIONS: Record<string, number> = {
|
||||
rainbow: 4000,
|
||||
/* eslint-disable sort-keys */
|
||||
"bright-rainbow": 4000,
|
||||
"copper-glow": 3000,
|
||||
"silver-glow": 3000,
|
||||
"gold-glow": 3000,
|
||||
neon: 3000,
|
||||
lava: 6000,
|
||||
/* eslint-enable sort-keys */
|
||||
water: 6200,
|
||||
"lava": 6000,
|
||||
"neon": 3000,
|
||||
"rainbow": 4000,
|
||||
"silver-glow": 3000,
|
||||
"water": 6200,
|
||||
};
|
||||
|
||||
// TODO: Pass in cosmetics as a parameter when
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export type GameEvent = object;
|
||||
|
||||
export interface EventConstructor<T extends GameEvent = GameEvent> {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
export type EventConstructor<T extends GameEvent = GameEvent> = new (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...args: any[]
|
||||
) => T;
|
||||
|
||||
export class EventBus {
|
||||
private listeners: Map<EventConstructor, Array<(event: GameEvent) => void>> =
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// This file contians schemas for the primary openfront express server
|
||||
import { z } from "zod";
|
||||
import { GameInfoSchema } from "./Schemas";
|
||||
|
||||
export const ApiEnvResponseSchema = z.object({
|
||||
game_env: z.string(),
|
||||
});
|
||||
export type ApiEnvResponse = z.infer<typeof ApiEnvResponseSchema>;
|
||||
|
||||
export const ApiPublicLobbiesResponseSchema = z.object({
|
||||
lobbies: GameInfoSchema.array(),
|
||||
});
|
||||
export type ApiPublicLobbiesResponse = z.infer<
|
||||
typeof ApiPublicLobbiesResponseSchema
|
||||
>;
|
||||
+10
-9
@@ -57,13 +57,13 @@ export async function createGameRunner(
|
||||
const nations = gameStart.config.disableNPCs
|
||||
? []
|
||||
: gameMap.manifest.nations.map(
|
||||
(n) =>
|
||||
new Nation(
|
||||
new Cell(n.coordinates[0], n.coordinates[1]),
|
||||
n.strength,
|
||||
new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
|
||||
),
|
||||
);
|
||||
(n) =>
|
||||
new Nation(
|
||||
new Cell(n.coordinates[0], n.coordinates[1]),
|
||||
n.strength,
|
||||
new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
|
||||
),
|
||||
);
|
||||
|
||||
const game: Game = createGame(
|
||||
humans,
|
||||
@@ -190,14 +190,15 @@ export class GameRunner {
|
||||
const other = this.game.owner(tile) as Player;
|
||||
actions.interaction = {
|
||||
canBreakAlliance: player.isAlliedWith(other),
|
||||
canDonate: player.canDonate(other),
|
||||
canDonateGold: player.canDonateGold(other),
|
||||
canDonateTroops: player.canDonateTroops(other),
|
||||
canEmbargo: !player.hasEmbargoAgainst(other),
|
||||
canSendAllianceRequest: player.canSendAllianceRequest(other),
|
||||
canSendEmoji: player.canSendEmoji(other),
|
||||
canTarget: player.canTarget(other),
|
||||
sharedBorder: player.sharesBorderWith(other),
|
||||
};
|
||||
const alliance = player.allianceWith(other as Player);
|
||||
const alliance = player.allianceWith(other);
|
||||
if (alliance) {
|
||||
actions.interaction.allianceExpiresAt = alliance.expiresAt();
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ export class PseudoRandom {
|
||||
private state1: number;
|
||||
|
||||
// Keep these variables to maintain the exact same interface
|
||||
private m: number = 0x80000000; // 2**31
|
||||
private a: number = 1103515245;
|
||||
private c: number = 12345;
|
||||
private m = 0x80000000; // 2**31
|
||||
private a = 1103515245;
|
||||
private c = 12345;
|
||||
private state: number;
|
||||
|
||||
private static readonly POW36_8 = Math.pow(36, 8); // Pre-compute 36^8
|
||||
|
||||
+18
-11
@@ -112,17 +112,6 @@ export type Player = z.infer<typeof PlayerSchema>;
|
||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||
const PlayerTypeSchema = z.enum(PlayerType);
|
||||
|
||||
export interface GameInfo {
|
||||
gameID: GameID;
|
||||
clients?: ClientInfo[];
|
||||
numClients?: number;
|
||||
msUntilStart?: number;
|
||||
gameConfig?: GameConfig;
|
||||
}
|
||||
export interface ClientInfo {
|
||||
clientID: ClientID;
|
||||
username: string;
|
||||
}
|
||||
export enum LogSeverity {
|
||||
Debug = "DEBUG",
|
||||
Info = "INFO",
|
||||
@@ -148,6 +137,8 @@ export const GameConfigSchema = z.object({
|
||||
difficulty: z.enum(Difficulty),
|
||||
disableNPCs: z.boolean(),
|
||||
disabledUnits: z.enum(UnitType).array().optional(),
|
||||
donateGold: z.boolean(),
|
||||
donateTroops: z.boolean(),
|
||||
gameMap: z.enum(GameMapType),
|
||||
gameMode: z.enum(GameMode),
|
||||
gameType: z.enum(GameType),
|
||||
@@ -192,6 +183,22 @@ export const ID = z
|
||||
export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
|
||||
|
||||
export const UsernameSchema = SafeString;
|
||||
|
||||
export const ClientInfoSchema = z.object({
|
||||
clientID: ID,
|
||||
username: UsernameSchema,
|
||||
});
|
||||
export type ClientInfo = z.infer<typeof ClientInfoSchema>;
|
||||
|
||||
export const GameInfoSchema = z.object({
|
||||
clients: ClientInfoSchema.array().optional(),
|
||||
gameConfig: GameConfigSchema.optional(),
|
||||
gameID: ID,
|
||||
msUntilStart: z.number().int().nonnegative().optional(),
|
||||
numClients: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export type GameInfo = z.infer<typeof GameInfoSchema>;
|
||||
|
||||
const countryCodes = countries.map((c) => c.code);
|
||||
export const FlagSchema = z
|
||||
.string()
|
||||
|
||||
@@ -285,6 +285,7 @@ export const flattenedEmojiTable: string[] = emojiTable.flat();
|
||||
/**
|
||||
* JSON.stringify replacer function that converts bigint values to strings.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function replacer(_key: string, value: any): any {
|
||||
return typeof value === "bigint" ? value.toString() : value;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// This file contians schemas for the openfront worker express server
|
||||
import { z } from "zod";
|
||||
import { GameConfigSchema } from "./Schemas";
|
||||
import { GameConfigSchema, GameRecordSchema } from "./Schemas";
|
||||
|
||||
export const CreateGameInputSchema = GameConfigSchema.or(
|
||||
z
|
||||
@@ -9,3 +10,33 @@ export const CreateGameInputSchema = GameConfigSchema.or(
|
||||
);
|
||||
|
||||
export const GameInputSchema = GameConfigSchema.partial();
|
||||
|
||||
export const WorkerApiGameIdExistsSchema = z.object({
|
||||
exists: z.boolean(),
|
||||
});
|
||||
export type WorkerApiGameIdExists = z.infer<typeof WorkerApiGameIdExistsSchema>;
|
||||
|
||||
export const WorkerApiArchivedGameLobbySchema = z.union([
|
||||
z.object({
|
||||
error: z.literal("Game not found"),
|
||||
exists: z.literal(false),
|
||||
success: z.literal(false),
|
||||
}),
|
||||
z.object({
|
||||
details: z.object({
|
||||
actualCommit: z.string(),
|
||||
expectedCommit: z.string(),
|
||||
}),
|
||||
error: z.literal("Version mismatch"),
|
||||
exists: z.literal(true),
|
||||
success: z.literal(false),
|
||||
}),
|
||||
z.object({
|
||||
exists: z.literal(true),
|
||||
gameRecord: GameRecordSchema,
|
||||
success: z.literal(true),
|
||||
}),
|
||||
]);
|
||||
export type WorkerApiArchivedGameLobby = z.infer<
|
||||
typeof WorkerApiArchivedGameLobbySchema
|
||||
>;
|
||||
|
||||
@@ -26,7 +26,7 @@ export enum GameEnv {
|
||||
Prod,
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
export type ServerConfig = {
|
||||
turnIntervalMs(): number;
|
||||
gameCreationRate(): number;
|
||||
lobbyMaxPlayers(
|
||||
@@ -62,14 +62,14 @@ export interface ServerConfig {
|
||||
cloudflareCredsPath(): string;
|
||||
stripePublishableKey(): string;
|
||||
allowedFlares(): string[] | undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export interface NukeMagnitude {
|
||||
export type NukeMagnitude = {
|
||||
inner: number;
|
||||
outer: number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
export type Config = {
|
||||
samHittingChance(): number;
|
||||
samWarheadHittingChance(): number;
|
||||
spawnImmunityDuration(): Tick;
|
||||
@@ -82,7 +82,9 @@ export interface Config {
|
||||
isUnitDisabled(unitType: UnitType): boolean;
|
||||
bots(): number;
|
||||
infiniteGold(): boolean;
|
||||
donateGold(): boolean;
|
||||
infiniteTroops(): boolean;
|
||||
donateTroops(): boolean;
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
@@ -168,9 +170,9 @@ export interface Config {
|
||||
structureMinDist(): number;
|
||||
isReplay(): boolean;
|
||||
allianceExtensionPromptOffset(): number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Theme {
|
||||
export type Theme = {
|
||||
teamColor(team: Team): Colord;
|
||||
territoryColor(playerInfo: PlayerView): Colord;
|
||||
specialBuildingColor(playerInfo: PlayerView): Colord;
|
||||
@@ -188,4 +190,4 @@ export interface Theme {
|
||||
allyColor(): Colord;
|
||||
enemyColor(): Colord;
|
||||
spawnHighlightColor(): Colord;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiEnvResponseSchema } from "../ExpressSchemas";
|
||||
import { UserSettings } from "../game/UserSettings";
|
||||
import { GameConfig } from "../Schemas";
|
||||
import { Config, GameEnv, ServerConfig } from "./Config";
|
||||
@@ -11,7 +12,7 @@ export let cachedSC: ServerConfig | null = null;
|
||||
export async function getConfig(
|
||||
gameConfig: GameConfig,
|
||||
userSettings: UserSettings | null,
|
||||
isReplay: boolean = false,
|
||||
isReplay = false,
|
||||
): Promise<Config> {
|
||||
const sc = await getServerConfigFromClient();
|
||||
switch (sc.env()) {
|
||||
@@ -36,7 +37,8 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
|
||||
`Failed to fetch server config: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const config = await response.json();
|
||||
const json = await response.json();
|
||||
const config = ApiEnvResponseSchema.parse(json);
|
||||
// Log the retrieved configuration
|
||||
console.log("Server config loaded:", config);
|
||||
|
||||
|
||||
@@ -323,9 +323,15 @@ export class DefaultConfig implements Config {
|
||||
infiniteGold(): boolean {
|
||||
return this._gameConfig.infiniteGold;
|
||||
}
|
||||
donateGold(): boolean {
|
||||
return this._gameConfig.donateGold;
|
||||
}
|
||||
infiniteTroops(): boolean {
|
||||
return this._gameConfig.infiniteTroops;
|
||||
}
|
||||
donateTroops(): boolean {
|
||||
return this._gameConfig.donateTroops;
|
||||
}
|
||||
trainSpawnRate(numberOfStations: number): number {
|
||||
return Math.min(1400, Math.round(20 * Math.pow(numberOfStations, 0.5)));
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const malusForRetreat = 25;
|
||||
export class AttackExecution implements Execution {
|
||||
private breakAlliance = false;
|
||||
private wasAlliedAtInit = false; // Store alliance state at initialization
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
private toConquer = new FlatBinaryHeap();
|
||||
|
||||
private random = new PseudoRandom(123);
|
||||
@@ -34,7 +34,7 @@ export class AttackExecution implements Execution {
|
||||
private _owner: Player,
|
||||
private _targetID: PlayerID | null,
|
||||
private sourceTile: TileRef | null = null,
|
||||
private removeTroops: boolean = true,
|
||||
private removeTroops = true,
|
||||
) {}
|
||||
|
||||
public targetID(): PlayerID | null {
|
||||
@@ -69,7 +69,7 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
if (this.target && this.target.isPlayer()) {
|
||||
const targetPlayer = this.target as Player;
|
||||
const targetPlayer = this.target;
|
||||
if (
|
||||
targetPlayer.type() !== PlayerType.Bot &&
|
||||
this._owner.type() !== PlayerType.Bot
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TrainStationExecution } from "./TrainStationExecution";
|
||||
export class CityExecution implements Execution {
|
||||
private mg: Game;
|
||||
private city: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
@@ -48,7 +48,7 @@ export class CityExecution implements Execution {
|
||||
createStation(): void {
|
||||
if (this.city !== null) {
|
||||
const nearbyFactory = this.mg.hasUnitNearby(
|
||||
this.city.tile()!,
|
||||
this.city.tile(),
|
||||
this.mg.config().trainStationMaxRange(),
|
||||
UnitType.Factory,
|
||||
this.player.id(),
|
||||
|
||||
@@ -20,7 +20,7 @@ import { WarshipExecution } from "./WarshipExecution";
|
||||
|
||||
export class ConstructionExecution implements Execution {
|
||||
private construction: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
|
||||
private ticksUntilComplete: Tick;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ShellExecution } from "./ShellExecution";
|
||||
export class DefensePostExecution implements Execution {
|
||||
private mg: Game;
|
||||
private post: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
|
||||
private target: Unit | null = null;
|
||||
private lastShellAttack = 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Execution, Game, MessageType, Player } from "../game/Game";
|
||||
|
||||
export class DeleteUnitExecution implements Execution {
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
private mg: Game;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -25,7 +25,7 @@ export class DonateGoldExecution implements Execution {
|
||||
tick(ticks: number): void {
|
||||
if (this.gold === null) throw new Error("not initialized");
|
||||
if (
|
||||
this.sender.canDonate(this.recipient) &&
|
||||
this.sender.canDonateGold(this.recipient) &&
|
||||
this.sender.donateGold(this.recipient, this.gold)
|
||||
) {
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DonateTroopsExecution implements Execution {
|
||||
tick(ticks: number): void {
|
||||
if (this.troops === null) throw new Error("not initialized");
|
||||
if (
|
||||
this.sender.canDonate(this.recipient) &&
|
||||
this.sender.canDonateTroops(this.recipient) &&
|
||||
this.sender.donateTroops(this.recipient, this.troops)
|
||||
) {
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TrainStationExecution } from "./TrainStationExecution";
|
||||
|
||||
export class FactoryExecution implements Execution {
|
||||
private factory: Unit | null = null;
|
||||
private active: boolean = true;
|
||||
private active = true;
|
||||
private game: Game;
|
||||
constructor(
|
||||
private player: Player,
|
||||
@@ -47,7 +47,7 @@ export class FactoryExecution implements Execution {
|
||||
createStation(): void {
|
||||
if (this.factory !== null) {
|
||||
const structures = this.game.nearbyUnits(
|
||||
this.factory.tile()!,
|
||||
this.factory.tile(),
|
||||
this.game.config().trainStationMaxRange(),
|
||||
[UnitType.City, UnitType.Port, UnitType.Factory],
|
||||
);
|
||||
|
||||
@@ -297,7 +297,7 @@ export class FakeHumanExecution implements Execution {
|
||||
UnitType.SAMLauncher,
|
||||
);
|
||||
const structureTiles = structures.map((u) => u.tile());
|
||||
const randomTiles: (TileRef | null)[] = new Array(10);
|
||||
const randomTiles: (TileRef | null)[] = new Array<TileRef | null>(10).fill(null);
|
||||
for (let i = 0; i < randomTiles.length; i++) {
|
||||
randomTiles[i] = this.randTerritoryTile(other);
|
||||
}
|
||||
@@ -455,8 +455,8 @@ export class FakeHumanExecution implements Execution {
|
||||
const tiles =
|
||||
type === UnitType.Port
|
||||
? Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t),
|
||||
)
|
||||
this.mg.isOceanShore(t),
|
||||
)
|
||||
: Array.from(this.player.tiles());
|
||||
if (tiles.length === 0) return null;
|
||||
return this.random.randElement(tiles);
|
||||
|
||||
@@ -31,7 +31,7 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private separateDst: TileRef;
|
||||
|
||||
private speed: number = -1;
|
||||
private speed = -1;
|
||||
|
||||
constructor(
|
||||
private player: Player,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user