diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b920e29e..092ec8b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: ๐Ÿงช CI on: + merge_group: pull_request: push: branches: [main] @@ -41,7 +42,7 @@ jobs: with: node-version: 20 - run: npm ci - - run: npm test + - run: npm run test:coverage eslint: name: ๐Ÿ” ESLint diff --git a/Dockerfile b/Dockerfile index 480ace982..364ed9b04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,11 @@ RUN npm run build-prod # https://openfront.io/commit.txt RUN echo "$GIT_COMMIT" > static/commit.txt +# Remove maps data from final image +FROM base AS prod-files +COPY . . +RUN rm -rf resources/maps + FROM dependencies AS npm-dependencies # Disable Husky hooks ENV HUSKY=0 @@ -67,7 +72,7 @@ COPY --from=npm-dependencies /usr/src/app/node_modules node_modules COPY package.json . # Copy the rest of the application code -COPY . . +COPY --from=prod-files /usr/src/app/ /usr/src/app/ # Copy frontend COPY --from=build /usr/src/app/static static diff --git a/jest.config.ts b/jest.config.ts index c619f7d52..2dcb0e851 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -29,10 +29,10 @@ export default { collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"], coverageThreshold: { global: { - branches: 0, - functions: 0, - lines: 0, - statements: 0, + statements: 21.5, + branches: 17.0, + lines: 22.0, + functions: 20.5, }, }, coverageReporters: ["text", "lcov", "html"], diff --git a/package-lock.json b/package-lock.json index ad931479f..c11cfa6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "nanoid": "^3.3.6", "obscenity": "^0.4.3", "ts-node": "^10.9.2", - "twemoji": "^14.0.2", "uuid": "^11.1.0", "winston": "^3.17.0", "ws": "^8.18.0", @@ -11664,29 +11663,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11937,6 +11913,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -14761,18 +14738,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", - "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", - "license": "MIT", - "dependencies": { - "universalify": "^0.1.2" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -19383,24 +19348,6 @@ "node": "*" } }, - "node_modules/twemoji": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz", - "integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==", - "license": "MIT", - "dependencies": { - "fs-extra": "^8.0.1", - "jsonfile": "^5.0.0", - "twemoji-parser": "14.0.0", - "universalify": "^0.1.2" - } - }, - "node_modules/twemoji-parser": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz", - "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==", - "license": "MIT" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -19536,15 +19483,6 @@ "node": ">=4" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 5311c5ecb..20fdc2a73 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "nanoid": "^3.3.6", "obscenity": "^0.4.3", "ts-node": "^10.9.2", - "twemoji": "^14.0.2", "uuid": "^11.1.0", "winston": "^3.17.0", "ws": "^8.18.0", diff --git a/resources/cosmetics/cosmetics.json b/resources/cosmetics/cosmetics.json index 6c6c8e495..e06d4e5fb 100644 --- a/resources/cosmetics/cosmetics.json +++ b/resources/cosmetics/cosmetics.json @@ -112,6 +112,9 @@ "name": "embelem", "role_group": "donor" }, + "AMo0AAAAAAAAAAAAAIAAJAACEAIIQCAgAAGCAAQgCBCAgEAAAggBCIAEIAAAAAAAAAAAAAAA": { + "name": "contributor" + }, "AMlNAAAAAAAAAAAAAPAfAACAHwDwgAcAAMQf4ADgAAAgwM8BAAwACEPABwDA__8xEACIAAAACAMOgCQAAGEwwAAoAPCAAQGMCCBhAAYIQPwAnwEYgADSB_QEQAAIkD_wJwABgAH8gx8ABAAYwE99ABgAAAcAACBwAADgBDCA-AAAADwQoH8AAAAA_v8DAAAAAAAMAAAAAAAAAAAAAAA": { "name": "grogu_head", "role_group": "donor" diff --git a/resources/images/ofm/MASTER_2025.png b/resources/images/ofm/MASTER_2025.png deleted file mode 100644 index 908de96f4..000000000 Binary files a/resources/images/ofm/MASTER_2025.png and /dev/null differ diff --git a/resources/images/ofm/logo_MASTER_2025.png b/resources/images/ofm/logo_MASTER_2025.png deleted file mode 100644 index c6e625982..000000000 Binary files a/resources/images/ofm/logo_MASTER_2025.png and /dev/null differ diff --git a/resources/lang/en.json b/resources/lang/en.json index 7bb19a434..92fad88cd 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -51,7 +51,6 @@ "ui_control_desc": "The control panel contains the following elements:", "ui_pop": "Pop - The amount of units you have, your max population and the rate at which you gain them.", "ui_gold": "Gold - The amount of gold you have and the rate at which you gain it.", - "ui_troops_workers": "Troops and Workers - The amount of allocated troops and workers. Troops are used to attack or defend against attacks. Workers are used to generate gold. You can adjust the number of troops and workers using the slider.", "ui_attack_ratio": "Attack ratio - The amount of troops that will be used when you attack. You can adjust the attack ratio using the slider. Having more attacking troops than defending troops will make you lose fewer troops in the attack, while having less will increase the damage dealt to your attacking troops. The effect doesn't go beyond ratios of 2:1.", "ui_events": "Event panel", "ui_events_desc": "The Event panel displays the latest events, requests and Quick Chat messages. Some examples are:", @@ -215,7 +214,8 @@ "player": "Player", "players": "Players", "waiting": "Waiting for players...", - "start": "Start Game" + "start": "Start Game", + "host_badge": "Host" }, "team_colors": { "red": "Red", @@ -288,10 +288,11 @@ "right_click_opens_menu": "Right click opens menu", "attack_ratio_label": "โš”๏ธ Attack Ratio", "attack_ratio_desc": "What percentage of your troops to send in an attack (1โ€“100%)", - "troop_ratio_label": "๐Ÿช–๐Ÿ› ๏ธ Troops and Workers Ratio", "troop_ratio_desc": "Adjust the balance between troops (for combat) and workers (for gold production) (1โ€“100%)", "territory_patterns_label": "๐Ÿณ๏ธ Territory Patterns", "territory_patterns_desc": "Choose whether to display territory pattern designs in game", + "performance_overlay_label": "Performance Overlay", + "performance_overlay_desc": "Toggle the performance overlay. When enabled, the performance overlay will be displayed. Press shift-D during game to toggle.", "easter_writing_speed_label": "Writing Speed Multiplier", "easter_writing_speed_desc": "Adjust how fast you pretend to code (x1โ€“x100)", "easter_bug_count_label": "Bug Count", @@ -501,10 +502,8 @@ "default": "Default" }, "control_panel": { - "pop": "Pop", "gold": "Gold", "troops": "Troops", - "workers": "Workers", "attack_ratio": "Attack Ratio" }, "player_panel": { @@ -577,6 +576,7 @@ "openfront": "OpenFront", "t_rex": "T-Rex", "embelem": "Emblem", + "contributor": "Contributor", "grogu_head": "Grogu Head", "grogu": "Grogu" } diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index b09d8198b..b0c3150d8 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -13,6 +13,7 @@ import { ServerConfig } from "../core/configuration/Config"; import { getConfig } from "../core/configuration/ConfigLoader"; import { PlayerActions, UnitType } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; +import { GameMapLoader } from "../core/game/GameMapLoader"; import { ErrorUpdate, GameUpdateType, @@ -33,6 +34,7 @@ import { } from "./InputHandler"; import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { getPersistentID } from "./Main"; +import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { SendAttackIntentEvent, SendBoatAttackIntentEvent, @@ -58,12 +60,11 @@ export interface LobbyConfig { } export function joinLobby( + eventBus: EventBus, lobbyConfig: LobbyConfig, onPrestart: () => void, onJoin: () => void, ): () => void { - const eventBus = new EventBus(); - console.log( `joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`, ); @@ -82,7 +83,7 @@ export function joinLobby( const onmessage = (message: ServerMessage) => { if (message.type === "prestart") { console.log(`lobby: game prestarting: ${JSON.stringify(message)}`); - terrainLoad = loadTerrainMap(message.gameMap); + terrainLoad = loadTerrainMap(message.gameMap, terrainMapFileLoader); onPrestart(); } if (message.type === "start") { @@ -98,6 +99,7 @@ export function joinLobby( transport, userSettings, terrainLoad, + terrainMapFileLoader, ).then((r) => r.start()); } if (message.type === "error") { @@ -125,6 +127,7 @@ async function createClientGame( transport: Transport, userSettings: UserSettings, terrainLoad: Promise | null, + mapLoader: GameMapLoader, ): Promise { if (lobbyConfig.gameStartInfo === undefined) { throw new Error("missing gameStartInfo"); @@ -139,7 +142,10 @@ async function createClientGame( if (terrainLoad) { gameMap = await terrainLoad; } else { - gameMap = await loadTerrainMap(lobbyConfig.gameStartInfo.config.gameMap); + gameMap = await loadTerrainMap( + lobbyConfig.gameStartInfo.config.gameMap, + mapLoader, + ); } const worker = new WorkerClient( lobbyConfig.gameStartInfo, diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index 08d7cad13..8ae13ca77 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -68,8 +68,21 @@ export class FlagInput extends LitElement { super.connectedCallback(); this.flag = this.getStoredFlag(); this.dispatchFlagEvent(); + window.addEventListener("keydown", this.handleKeyDown); } + disconnectedCallback() { + window.removeEventListener("keydown", this.handleKeyDown); + super.disconnectedCallback(); + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape") { + e.preventDefault(); + this.showModal = false; + } + }; + createRenderRoot() { return this; } diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts index ec53514b9..97e74be6d 100644 --- a/src/client/HelpModal.ts +++ b/src/client/HelpModal.ts @@ -15,6 +15,23 @@ export class HelpModal extends LitElement { return this; } + connectedCallback() { + super.connectedCallback(); + window.addEventListener("keydown", this.handleKeyDown); + } + + disconnectedCallback() { + window.removeEventListener("keydown", this.handleKeyDown); + super.disconnectedCallback(); + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape") { + e.preventDefault(); + this.close(); + } + }; + render() { return html`

${translateText("help_modal.ui_control_desc")}

    -
  • ${translateText("help_modal.ui_pop")}
  • ${translateText("help_modal.ui_gold")}
  • -
  • - ${translateText("help_modal.ui_troops_workers")} -
  • ${translateText("help_modal.ui_attack_ratio")}
  • diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index eeba83454..07212ef6b 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -14,7 +14,12 @@ import { mapCategories, } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; -import { GameConfig, GameInfo, TeamCountConfig } from "../core/Schemas"; +import { + ClientInfo, + GameConfig, + GameInfo, + TeamCountConfig, +} from "../core/Schemas"; import { generateID } from "../core/Util"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; @@ -40,9 +45,10 @@ export class HostLobbyModal extends LitElement { @state() private instantBuild: boolean = false; @state() private lobbyId = ""; @state() private copySuccess = false; - @state() private players: string[] = []; + @state() private clients: ClientInfo[] = []; @state() private useRandomMap: boolean = false; @state() private disabledUnits: UnitType[] = [UnitType.Factory]; + @state() private lobbyCreatorClientID: string = ""; @state() private lobbyIdVisible: boolean = true; private playersInterval: NodeJS.Timeout | null = null; @@ -50,6 +56,23 @@ export class HostLobbyModal extends LitElement { private botsUpdateTimer: number | null = null; private userSettings: UserSettings = new UserSettings(); + connectedCallback() { + super.connectedCallback(); + window.addEventListener("keydown", this.handleKeyDown); + } + + disconnectedCallback() { + window.removeEventListener("keydown", this.handleKeyDown); + super.disconnectedCallback(); + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape") { + e.preventDefault(); + this.close(); + } + }; + render() { return html` @@ -111,7 +134,7 @@ export class HostLobbyModal extends LitElement { ${this.lobbyIdVisible ? this.lobbyId : "โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข"} - +
    ${ @@ -395,29 +418,45 @@ export class HostLobbyModal extends LitElement {
    - ${this.players.length} + ${this.clients.length} ${ - this.players.length === 1 + this.clients.length === 1 ? translateText("host_modal.player") : translateText("host_modal.players") }
    - ${this.players.map( - (player) => html`${player}`, + ${this.clients.map( + (client) => html` + + ${client.username} + ${client.clientID === this.lobbyCreatorClientID + ? html`(${translateText("host_modal.host_badge")})` + : html` + + `} + + `, )} -