From 5e821bec06c3f43eae4940e535134fa8e91e441f Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:28:51 +0200 Subject: [PATCH 01/19] Optimizations for botbehaviour (#1114) ## Description: Some optimizations for bot & fakehuman execution. Better performance measured using profiling, at the start of a game or in singple player, but since the circumstances differ it is not a very reliable comparison. --- In BotExecution: Added getNeighborTraitorToAttack in Botbehavior to use in maybeAttack. No caching for playerNeighbors array, because although it could be re-used up to two times in selectRandomEnemy, maybeAttack runs every tick and selectEnemy only every 10 seconds so the cache overhead would not be worth it. --- In Botbehavior: Put repeated code in dedicated functions for readability, maintainance ease, and reduce the chances of forgetting to update the timestamp for enemyUpdated. Can be seen as a follow-up to PRs #434 and #946. -- In both selectEnemy and selectRandomEnemy: Put the first if this.enemy === null statement parenthesis, around the two checks below. Because if there's already an enemy (if forgetEnemies hasn't nullified it, enemy === null is false at the first if statement), then enemy === null will still be false for the two checks below the first. No need to check it thrice then. Once inside the first if statement (when enemy === null is true), then we need to check it two times more in case the code inside already set an enemy. -- In selectEnemy under 'Prefer neighboring bots': Removed unneccessary check for enemy === null. Replaced sort with more performant for loop. -- In selectRandomEnemy: Under 'Select a traitor as an enemy', if a traitor is a friendly player, they got less odds to be chosen as enemy over an unfriendly player, but they weren't ruled out. While below in the Sanity Check, we specifically rule out friendly players as enemy and set enemy=null. So excluded friendly players under 'Select a traitor as an enemy' instead of only giving them lower odds. Use the new shared method getNeighborTraitorToAttack. -- In checkIncomingAttacks, since we're only interested in the max value, replaced sort with loop that makes a single pass through the attacks to find the largest one. -- For shouldAcceptAllianceRequest, #1049 added tests and improved readability of Alliance requests. It may have also deoptimized it a bit. Const notTooManyAlliances would, in the past, not be computed if requestorIsMuchLarger was already found to be true. Since the readability changes, tooManyAlliances was always computed regardless of the value of requestorIsMuchLarger. This PR attempts to address this too, while still keeping it as readable hopefully. Also choose for early returns to optimize a bit more. So if isTraitor is true, don't compute any further and just return false immediately etc. Switched the order of testing for noMalice and isTraitor. Traitor status used to be throughout the game, now it's only 30 seconds or will be maybe 45 seconds to 1 minute. So the chances of someone asking for alliance and being a traitor at the same time have decreased. isMalice chances could be bigger. Removed the named constants and choose to put them in comments, so readability should be about the same as #1049 intended. Kept the braces for the if statements because otherwise Prettier would misalign the comments.) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --- src/core/execution/BotExecution.ts | 7 +- src/core/execution/utils/BotBehavior.ts | 155 ++++++++++++++---------- 2 files changed, 96 insertions(+), 66 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 84d46768c..71141929d 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -58,11 +58,8 @@ export class BotExecution implements Execution { if (this.behavior === null) { throw new Error("not initialized"); } - const traitors = this.bot - .neighbors() - .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; - if (traitors.length > 0) { - const toAttack = this.random.randElement(traitors); + const toAttack = this.behavior.getNeighborTraitorToAttack(); + if (toAttack !== null) { const odds = this.bot.isFriendly(toAttack) ? 6 : 3; if (this.random.chance(odds)) { this.behavior.sendAttack(toAttack); diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 39ff00907..2cfc95a52 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -43,22 +43,48 @@ export class BotBehavior { this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji)); } + private setNewEnemy(newEnemy: Player | null) { + this.enemy = newEnemy; + this.enemyUpdated = this.game.ticks(); + } + + private clearEnemy() { + this.enemy = null; + } + forgetOldEnemies() { // Forget old enemies if (this.game.ticks() - this.enemyUpdated > 100) { - this.enemy = null; + this.clearEnemy(); } } + private hasSufficientTroops(): boolean { + const maxPop = this.game.config().maxPopulation(this.player); + const ratio = this.player.population() / maxPop; + return ratio >= this.triggerRatio; + } + private checkIncomingAttacks() { // Switch enemies if we're under attack const incomingAttacks = this.player.incomingAttacks(); - if (incomingAttacks.length > 0) { - this.enemy = incomingAttacks - .sort((a, b) => b.troops() - a.troops())[0] - .attacker(); - this.enemyUpdated = this.game.ticks(); + let largestAttack = 0; + let largestAttacker: Player | undefined; + for (const attack of incomingAttacks) { + if (attack.troops() <= largestAttack) continue; + largestAttack = attack.troops(); + largestAttacker = attack.attacker(); } + if (largestAttacker !== undefined) { + this.setNewEnemy(largestAttacker); + } + } + + getNeighborTraitorToAttack(): Player | null { + const traitors = this.player + .neighbors() + .filter((n): n is Player => n.isPlayer() && n.isTraitor()); + return traitors.length > 0 ? this.random.randElement(traitors) : null; } assistAllies() { @@ -79,8 +105,7 @@ export class BotBehavior { } // All checks passed, assist them this.player.updateRelation(ally, -20); - this.enemy = target; - this.enemyUpdated = this.game.ticks(); + this.setNewEnemy(target); this.emoji(ally, this.assistAcceptEmoji); break outer; } @@ -90,50 +115,55 @@ export class BotBehavior { selectEnemy(): Player | null { if (this.enemy === null) { // Save up troops until we reach the trigger ratio - const maxPop = this.game.config().maxPopulation(this.player); - const ratio = this.player.population() / maxPop; - if (ratio < this.triggerRatio) return null; - } + if (!this.hasSufficientTroops()) return null; - // Prefer neighboring bots - if (this.enemy === null) { + // Prefer neighboring bots const bots = this.player .neighbors() .filter((n) => n.isPlayer() && n.type() === PlayerType.Bot) as Player[]; if (bots.length > 0) { const density = (p: Player) => p.troops() / p.numTilesOwned(); - this.enemy = bots.sort((a, b) => density(a) - density(b))[0]; - this.enemyUpdated = this.game.ticks(); + let lowestDensityBot: Player | undefined; + let lowestDensity = Infinity; + + for (const bot of bots) { + const currentDensity = density(bot); + if (currentDensity < lowestDensity) { + lowestDensity = currentDensity; + lowestDensityBot = bot; + } + } + + if (lowestDensityBot !== undefined) { + this.setNewEnemy(lowestDensityBot); + } } - } - // Retaliate against incoming attacks - if (this.enemy === null) { - this.checkIncomingAttacks(); - } + // Retaliate against incoming attacks + if (this.enemy === null) { + this.checkIncomingAttacks(); + } - // Select the most hated player - if (this.enemy === null) { - const mostHated = this.player.allRelationsSorted()[0]; - if (mostHated !== undefined && mostHated.relation === Relation.Hostile) { - this.enemy = mostHated.player; - this.enemyUpdated = this.game.ticks(); + // Select the most hated player + if (this.enemy === null) { + const mostHated = this.player.allRelationsSorted()[0]; + if ( + mostHated !== undefined && + mostHated.relation === Relation.Hostile + ) { + this.setNewEnemy(mostHated.player); + } } } // Sanity check, don't attack our allies or teammates - if (this.enemy && this.player.isFriendly(this.enemy)) { - this.enemy = null; - } - return this.enemy; + return this.enemySanityCheck(); } selectRandomEnemy(): Player | TerraNullius | null { if (this.enemy === null) { // Save up troops until we reach the trigger ratio - const maxPop = this.game.config().maxPopulation(this.player); - const ratio = this.player.population() / maxPop; - if (ratio < this.triggerRatio) return null; + if (!this.hasSufficientTroops()) return null; // Choose a new enemy randomly const neighbors = this.player.neighbors(); @@ -145,34 +175,32 @@ export class BotBehavior { continue; } } - this.enemy = neighbor; - this.enemyUpdated = this.game.ticks(); + this.setNewEnemy(neighbor); } - } - // Retaliate against incoming attacks - if (this.enemy === null) { - this.checkIncomingAttacks(); - } + // Retaliate against incoming attacks + if (this.enemy === null) { + this.checkIncomingAttacks(); + } - // Select a traitor as an enemy - if (this.enemy === null) { - const traitors = this.player - .neighbors() - .filter((n) => n.isPlayer() && n.isTraitor()) as Player[]; - if (traitors.length > 0) { - const toAttack = this.random.randElement(traitors); - const odds = this.player.isFriendly(toAttack) ? 6 : 3; - if (this.random.chance(odds)) { - this.enemy = toAttack; - this.enemyUpdated = this.game.ticks(); + // Select a traitor as an enemy + if (this.enemy === null) { + const toAttack = this.getNeighborTraitorToAttack(); + if (toAttack !== null) { + if (!this.player.isFriendly(toAttack) && this.random.chance(3)) { + this.setNewEnemy(toAttack); + } } } } // Sanity check, don't attack our allies or teammates + return this.enemySanityCheck(); + } + + private enemySanityCheck(): Player | null { if (this.enemy && this.player.isFriendly(this.enemy)) { - this.enemy = null; + this.clearEnemy(); } return this.enemy; } @@ -200,12 +228,17 @@ export class BotBehavior { } function shouldAcceptAllianceRequest(player: Player, request: AllianceRequest) { - const isTraitor = request.requestor().isTraitor(); - const hasMalice = player.relation(request.requestor()) < Relation.Neutral; - const requestorIsMuchLarger = - request.requestor().numTilesOwned() > player.numTilesOwned() * 3; - const tooManyAlliances = request.requestor().alliances().length >= 3; - return ( - !isTraitor && !hasMalice && (requestorIsMuchLarger || !tooManyAlliances) - ); + if (player.relation(request.requestor()) < Relation.Neutral) { + return false; // Reject if hasMalice + } + if (request.requestor().isTraitor()) { + return false; // Reject if isTraitor + } + if (request.requestor().numTilesOwned() > player.numTilesOwned() * 3) { + return true; // Accept if requestorIsMuchLarger + } + if (request.requestor().alliances().length >= 3) { + return false; // Reject if tooManyAlliances + } + return true; // Accept otherwise } From 063574c224f84294dc312e872f87f62612fd670b Mon Sep 17 00:00:00 2001 From: Christopher Mesona <45428623+Ble4Ch@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:41:47 +0200 Subject: [PATCH 02/19] fix: correct mac modifier and emoji key detection in input handler (#1118) ## Description: Fixes Command and Option Keys for Mac. Shows this modification in help ![image](https://github.com/user-attachments/assets/81f884c2-6929-48cf-a8e5-3fde6290df34) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: George Co-authored-by: cmesona --- src/client/HelpModal.ts | 13 ++++--- src/client/InputHandler.ts | 76 ++++++++++++++++++++++++++------------ src/client/Utils.ts | 18 +++++++++ 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts index bd1807f59..0f2aebe60 100644 --- a/src/client/HelpModal.ts +++ b/src/client/HelpModal.ts @@ -1,6 +1,6 @@ import { LitElement, html } from "lit"; import { customElement, query } from "lit/decorators.js"; -import { translateText } from "../client/Utils"; +import { getAltKey, getModifierKey, translateText } from "../client/Utils"; import "./components/Difficulties"; import "./components/Maps"; @@ -41,7 +41,7 @@ export class HelpModal extends LitElement {
- Shift + ⇧ Shift +
@@ -54,7 +54,7 @@ export class HelpModal extends LitElement {
- Ctrl + ${getModifierKey()} +
@@ -67,7 +67,7 @@ export class HelpModal extends LitElement {
- Alt + ${getAltKey()} +
@@ -99,7 +99,7 @@ export class HelpModal extends LitElement {
- Shift + ⇧ Shift +
@@ -116,7 +116,8 @@ export class HelpModal extends LitElement { - ALT + R + ${getAltKey()} + + R ${translateText("help_modal.action_reset_gfx")} diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 597733705..a4ba1eae6 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -103,6 +103,7 @@ export class InputHandler { private moveInterval: NodeJS.Timeout | null = null; private activeKeys = new Set(); + private keybinds: Record = {}; private readonly PAN_SPEED = 5; private readonly ZOOM_SPEED = 10; @@ -115,7 +116,7 @@ export class InputHandler { ) {} initialize() { - const keybinds = { + this.keybinds = { toggleView: "Space", centerCamera: "KeyC", moveUp: "KeyW", @@ -127,8 +128,17 @@ export class InputHandler { attackRatioDown: "Digit1", attackRatioUp: "Digit2", boatAttack: "KeyB", + modifierKey: "ControlLeft", + altKey: "AltLeft", ...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"), }; + + // Mac users might have different keybinds + const isMac = /Mac/.test(navigator.userAgent); + if (isMac) { + this.keybinds.modifierKey = "MetaLeft"; // Use Command key on Mac + } + this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); window.addEventListener("pointerup", (e) => this.onPointerUp(e)); this.canvas.addEventListener( @@ -154,22 +164,22 @@ export class InputHandler { let deltaY = 0; if ( - this.activeKeys.has(keybinds.moveUp) || + this.activeKeys.has(this.keybinds.moveUp) || this.activeKeys.has("ArrowUp") ) deltaY += this.PAN_SPEED; if ( - this.activeKeys.has(keybinds.moveDown) || + this.activeKeys.has(this.keybinds.moveDown) || this.activeKeys.has("ArrowDown") ) deltaY -= this.PAN_SPEED; if ( - this.activeKeys.has(keybinds.moveLeft) || + this.activeKeys.has(this.keybinds.moveLeft) || this.activeKeys.has("ArrowLeft") ) deltaX += this.PAN_SPEED; if ( - this.activeKeys.has(keybinds.moveRight) || + this.activeKeys.has(this.keybinds.moveRight) || this.activeKeys.has("ArrowRight") ) deltaX -= this.PAN_SPEED; @@ -182,13 +192,13 @@ export class InputHandler { const cy = window.innerHeight / 2; if ( - this.activeKeys.has(keybinds.zoomOut) || + this.activeKeys.has(this.keybinds.zoomOut) || this.activeKeys.has("Minus") ) { this.eventBus.emit(new ZoomEvent(cx, cy, this.ZOOM_SPEED)); } if ( - this.activeKeys.has(keybinds.zoomIn) || + this.activeKeys.has(this.keybinds.zoomIn) || this.activeKeys.has("Equal") ) { this.eventBus.emit(new ZoomEvent(cx, cy, -this.ZOOM_SPEED)); @@ -196,7 +206,7 @@ export class InputHandler { }, 1); window.addEventListener("keydown", (e) => { - if (e.code === keybinds.toggleView) { + if (e.code === this.keybinds.toggleView) { e.preventDefault(); if (!this.alternateView) { this.alternateView = true; @@ -211,21 +221,21 @@ export class InputHandler { if ( [ - keybinds.moveUp, - keybinds.moveDown, - keybinds.moveLeft, - keybinds.moveRight, - keybinds.zoomOut, - keybinds.zoomIn, + this.keybinds.moveUp, + this.keybinds.moveDown, + this.keybinds.moveLeft, + this.keybinds.moveRight, + this.keybinds.zoomOut, + this.keybinds.zoomIn, "ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight", "Minus", "Equal", - keybinds.attackRatioDown, - keybinds.attackRatioUp, - keybinds.centerCamera, + this.keybinds.attackRatioDown, + this.keybinds.attackRatioUp, + this.keybinds.centerCamera, "ControlLeft", "ControlRight", ].includes(e.code) @@ -234,7 +244,7 @@ export class InputHandler { } }); window.addEventListener("keyup", (e) => { - if (e.code === keybinds.toggleView) { + if (e.code === this.keybinds.toggleView) { e.preventDefault(); this.alternateView = false; this.eventBus.emit(new AlternateViewEvent(false)); @@ -245,22 +255,22 @@ export class InputHandler { this.eventBus.emit(new RefreshGraphicsEvent()); } - if (e.code === keybinds.boatAttack) { + if (e.code === this.keybinds.boatAttack) { e.preventDefault(); this.eventBus.emit(new DoBoatAttackEvent()); } - if (e.code === keybinds.attackRatioDown) { + if (e.code === this.keybinds.attackRatioDown) { e.preventDefault(); this.eventBus.emit(new AttackRatioEvent(-10)); } - if (e.code === keybinds.attackRatioUp) { + if (e.code === this.keybinds.attackRatioUp) { e.preventDefault(); this.eventBus.emit(new AttackRatioEvent(10)); } - if (e.code === keybinds.centerCamera) { + if (e.code === this.keybinds.centerCamera) { e.preventDefault(); this.eventBus.emit(new CenterCameraEvent()); } @@ -297,11 +307,11 @@ export class InputHandler { this.pointerDown = false; this.pointers.clear(); - if (event.ctrlKey) { + if (this.isModifierKeyPressed(event)) { this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY)); return; } - if (event.altKey) { + if (this.isAltKeyPressed(event)) { this.eventBus.emit(new ShowEmojiMenuEvent(event.clientX, event.clientY)); return; } @@ -400,4 +410,22 @@ export class InputHandler { } this.activeKeys.clear(); } + + isModifierKeyPressed(event: PointerEvent): boolean { + return ( + (this.keybinds.modifierKey === "AltLeft" && event.altKey) || + (this.keybinds.modifierKey === "ControlLeft" && event.ctrlKey) || + (this.keybinds.modifierKey === "ShiftLeft" && event.shiftKey) || + (this.keybinds.modifierKey === "MetaLeft" && event.metaKey) + ); + } + + isAltKeyPressed(event: PointerEvent): boolean { + return ( + (this.keybinds.altKey === "AltLeft" && event.altKey) || + (this.keybinds.altKey === "ControlLeft" && event.ctrlKey) || + (this.keybinds.altKey === "ShiftLeft" && event.shiftKey) || + (this.keybinds.altKey === "MetaLeft" && event.metaKey) + ); + } } diff --git a/src/client/Utils.ts b/src/client/Utils.ts index b60dcaeb6..d8e782f16 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -150,3 +150,21 @@ export function getMessageTypeClasses(type: MessageType): string { return severityColors["white"]; } } + +export function getModifierKey(): string { + const isMac = /Mac/.test(navigator.userAgent); + if (isMac) { + return "⌘"; // Command key + } else { + return "Ctrl"; + } +} + +export function getAltKey(): string { + const isMac = /Mac/.test(navigator.userAgent); + if (isMac) { + return "⌥"; // Option key + } else { + return "Alt"; + } +} From bf9b9a4b0dc208ca910a68fc14d987f2f326eba6 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 9 Jun 2025 14:53:34 -0700 Subject: [PATCH 03/19] fix duplicate websocket handler (#1124) ## Description: Turns out ws.on adds a new listener, it doesn't remove the existing listener. so it turns out we had both worker & game server listening to messages. this only got noticed because we close web socket connection on parsing failure. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/server/GameServer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 88db909dd..57fced752 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -182,6 +182,7 @@ export class GameServer { this.allClients.set(client.clientID, client); + client.ws.removeAllListeners("message"); client.ws.on( "message", gatekeeper.wsHandler(client.ip, async (message: string) => { @@ -239,6 +240,7 @@ export class GameServer { } }), ); + client.ws.removeAllListeners("close"); client.ws.on("close", () => { this.log.info("client disconnected", { clientID: client.clientID, @@ -248,6 +250,7 @@ export class GameServer { (c) => c.clientID !== client.clientID, ); }); + client.ws.removeAllListeners("error"); client.ws.on("error", (error: Error) => { if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") { client.ws.close(1002); From 9401f204360dc3070dbe82ad447b3c0183769b4b Mon Sep 17 00:00:00 2001 From: its-sii Date: Mon, 9 Jun 2025 18:34:58 -0400 Subject: [PATCH 04/19] Adding unit info modal translation support. (#1122) ## Description: Adding translation support for unit info modal. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors Regression test to make sure UI is still displaying as expected ![image](https://github.com/user-attachments/assets/bad18084-a4bb-479c-a3dd-74c9c541fdb0) ## Please put your Discord username so you can be contacted if a bug or regression is found: sii --- resources/lang/en.json | 7 +++++++ src/client/graphics/layers/UnitInfoModal.ts | 14 +++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 34d9b0d4b..030f08916 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -406,6 +406,13 @@ "retreating": "retreating", "boat": "Boat" }, + "unit_info_modal": { + "structure_info": "Structure Info", + "unit_type_unknown": "Unknown", + "close": "Close", + "cooldown": "Cooldown", + "type": "Type" + }, "relation": { "hostile": "Hostile", "distrustful": "Distrustful", diff --git a/src/client/graphics/layers/UnitInfoModal.ts b/src/client/graphics/layers/UnitInfoModal.ts index 131f9c028..0066aa309 100644 --- a/src/client/graphics/layers/UnitInfoModal.ts +++ b/src/client/graphics/layers/UnitInfoModal.ts @@ -1,5 +1,6 @@ import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { translateText } from "../../../client/Utils"; import { UnitType } from "../../../core/game/Game"; import { GameView, UnitView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; @@ -133,14 +134,17 @@ export class UnitInfoModal extends LitElement implements Layer { .x}px; top: ${this.y}px; position: absolute;" >
- Structure Info + ${translateText("unit_info_modal.structure_info")}
- Type: ${this.unit.type?.() ?? "Unknown"} + ${translateText("unit_info_modal.type")}: + ${translateText(+"unit_type." + this.unit.type?.().toLowerCase()) ?? + translateText("unit_info_modal.unit_type_unknown")}
${secondsLeft > 0 ? html`
- Cooldown: ${secondsLeft}s + ${translateText("unit_info_modal.cooldown")} + ${secondsLeft}s
` : ""}
@@ -152,10 +156,10 @@ export class UnitInfoModal extends LitElement implements Layer { } }} class="close-button" - title="Close" + title="${translateText("unit_info_modal.close")}" style="width: 100px; height: 32px;" > - CLOSE + ${translateText("unit_info_modal.close")}
From a19bfec40aa850e23ed79909632bed662b435140 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 9 Jun 2025 20:02:51 -0700 Subject: [PATCH 05/19] increase nuke speed from 4 to 6 (#1125) ## Description: Since nukes take a curved/longer paths nukes take too long to reach their target. This PR increases their speed by 50%. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/core/configuration/DefaultConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c59841798..dc88b18c4 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -752,7 +752,7 @@ export class DefaultConfig implements Config { } defaultNukeSpeed(): number { - return 4; + return 6; } // Humans can be population, soldiers attacking, soldiers in boat etc. From cfbed15fade5d66ab34155e04f54f98b02155770 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Tue, 10 Jun 2025 00:59:31 -0400 Subject: [PATCH 06/19] Avoid using as to cast values (#1115) ## Description: - Use ` is ` return type declarations in favor of `as`. - Use `satisfies` instead of `as`. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- src/client/graphics/layers/NameLayer.ts | 4 ++-- src/core/execution/utils/BotBehavior.ts | 4 +++- src/core/game/PlayerImpl.ts | 18 ++++++++++-------- tests/MessageTypeClasses.test.ts | 8 ++++---- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 23f596c01..d322f76f4 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -12,7 +12,7 @@ import targetIcon from "../../../../resources/images/TargetIcon.svg"; import traitorIcon from "../../../../resources/images/TraitorIcon.svg"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { Theme } from "../../../core/configuration/Config"; -import { AllPlayers, Cell, nukeTypes, UnitType } from "../../../core/game/Game"; +import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { UserSettings } from "../../../core/game/UserSettings"; import { createCanvas, renderNumber, renderTroops } from "../../Utils"; @@ -516,7 +516,7 @@ export class NameLayer implements Layer { const isSendingNuke = render.player.id() === unit.owner().id(); const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id(); return ( - (nukeTypes as UnitType[]).includes(unit.type()) && + nukeTypes.includes(unit.type()) && isSendingNuke && notMyPlayer && unit.isActive() diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 2cfc95a52..5e393d967 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -120,7 +120,9 @@ export class BotBehavior { // Prefer neighboring bots const bots = this.player .neighbors() - .filter((n) => n.isPlayer() && n.type() === PlayerType.Bot) as Player[]; + .filter( + (n): n is Player => n.isPlayer() && n.type() === PlayerType.Bot, + ); if (bots.length > 0) { const density = (p: Player) => p.troops() / p.numTilesOwned(); let lowestDensityBot: Player | undefined; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 580e38bb6..7a528b284 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -157,7 +157,7 @@ export class PlayerImpl implements Player { troops: a.troops(), id: a.id(), retreating: a.retreating(), - } as AttackUpdate; + } satisfies AttackUpdate; }), incomingAttacks: this._incomingAttacks.map((a) => { return { @@ -166,7 +166,7 @@ export class PlayerImpl implements Player { troops: a.troops(), id: a.id(), retreating: a.retreating(), - } as AttackUpdate; + } satisfies AttackUpdate; }), outgoingAllianceRequests: outgoingAllianceRequests, hasSpawned: this.hasSpawned(), @@ -252,7 +252,9 @@ export class PlayerImpl implements Player { if (this.mg.map().isLand(neighbor)) { const owner = this.mg.map().ownerID(neighbor); if (owner !== this.smallID()) { - ns.add(this.mg.playerBySmallID(owner) as Player | TerraNullius); + ns.add( + this.mg.playerBySmallID(owner) satisfies Player | TerraNullius, + ); } } } @@ -394,7 +396,7 @@ export class PlayerImpl implements Player { if (this.isAlliedWith(recipient)) { throw new Error(`cannot create alliance request, already allies`); } - return this.mg.createAllianceRequest(this, recipient as Player); + return this.mg.createAllianceRequest(this, recipient satisfies Player); } relation(other: Player): Relation { @@ -481,7 +483,7 @@ export class PlayerImpl implements Player { .map((a) => a.other(this)) .flatMap((ally) => ally.targets()); ts.push(...this.targets()); - return [...new Set(ts)] as Player[]; + return [...new Set(ts)] satisfies Player[]; } sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void { @@ -1007,8 +1009,8 @@ export class PlayerImpl implements Player { if (this.mg.owner(tile) === this) { return false; } - if (this.mg.hasOwner(tile)) { - const other = this.mg.owner(tile) as Player; + const other = this.mg.owner(tile); + if (other.isPlayer()) { if (this.isFriendly(other)) { return false; } @@ -1018,7 +1020,7 @@ export class PlayerImpl implements Player { return false; } if (this.mg.hasOwner(tile)) { - return this.sharesBorderWith(this.mg.owner(tile)); + return this.sharesBorderWith(other); } else { for (const t of this.mg.bfs( tile, diff --git a/tests/MessageTypeClasses.test.ts b/tests/MessageTypeClasses.test.ts index 706600a03..3b7368fc6 100644 --- a/tests/MessageTypeClasses.test.ts +++ b/tests/MessageTypeClasses.test.ts @@ -15,8 +15,8 @@ describe("getMessageTypeClasses", () => { it("should return a valid CSS class for every MessageType", () => { const messageTypes = Object.values(MessageType).filter( - (value) => typeof value === "number", - ) as MessageType[]; + (value): value is MessageType => typeof value === "number", + ); messageTypes.forEach((messageType) => { const result = getMessageTypeClasses(messageType); @@ -30,8 +30,8 @@ describe("getMessageTypeClasses", () => { it("should not trigger console.warn for any MessageType", () => { const messageTypes = Object.values(MessageType).filter( - (value) => typeof value === "number", - ) as MessageType[]; + (value): value is MessageType => typeof value === "number", + ); messageTypes.forEach((messageType) => { getMessageTypeClasses(messageType); From aee6c0e72f806ec0fce8271b1541a315d54e6ed8 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:34:03 +0200 Subject: [PATCH 07/19] =?UTF-8?q?Fix=20M=C4=81ori=20flag=20name=20(#1133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Removed unicode in Māori flag file name to prevent error, which lead to not be able to start a game with it. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --- resources/flags/{Māori flag.svg => Maori flag.svg} | 2 +- src/client/data/countries.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename resources/flags/{Māori flag.svg => Maori flag.svg} (99%) diff --git a/resources/flags/Māori flag.svg b/resources/flags/Maori flag.svg similarity index 99% rename from resources/flags/Māori flag.svg rename to resources/flags/Maori flag.svg index a04f55679..7ab870b4d 100644 --- a/resources/flags/Māori flag.svg +++ b/resources/flags/Maori flag.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/src/client/data/countries.json b/src/client/data/countries.json index 90d67d102..043f2963b 100644 --- a/src/client/data/countries.json +++ b/src/client/data/countries.json @@ -1238,7 +1238,7 @@ "name": "Malta" }, { - "code": "Māori flag", + "code": "Maori flag", "continent": "Oceania", "name": "Māori Flag" }, From f508e0dd1539444e62d56961f5ef15721cd9e949 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 10 Jun 2025 10:01:46 -0700 Subject: [PATCH 08/19] use newer attack, delete existing attack (#1134) ## Description: Also when a new attack is sent when there's an existing attack, have the new attack delete the existing attack. This causes attack to "reset" and use the entire border when a new attack is sent. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: --- src/core/execution/AttackExecution.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index bc8c78dff..1926d9991 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -140,13 +140,11 @@ export class AttackExecution implements Execution { if ( outgoing !== this.attack && outgoing.target() === this.attack.target() && - outgoing.sourceTile() === this.attack.sourceTile() + // Boat attacks (sourceTile is not null) are not combined with other attacks + this.attack.sourceTile() === null ) { - // Existing attack on same target, add troops - outgoing.setTroops(outgoing.troops() + this.attack.troops()); - this.active = false; - this.attack.delete(); - return; + this.attack.setTroops(this.attack.troops() + outgoing.troops()); + outgoing.delete(); } } From a0d17ed85ebbbff192b9d21091934bffe65b32d8 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 10 Jun 2025 10:03:02 -0700 Subject: [PATCH 09/19] counter attack doesn't cancel out attack (#1132) ## Description: Instead of a counter attack cancelling out the initial attack, both attacks run concurrently. This results in a more dynamic frontline. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: --- src/core/execution/AttackExecution.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 1926d9991..51c07c212 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -122,20 +122,6 @@ export class AttackExecution implements Execution { // Record stats this.mg.stats().attack(this._owner, this.target, this.startTroops); - for (const incoming of this._owner.incomingAttacks()) { - if (incoming.attacker() === this.target) { - // Target has opposing attack, cancel them out - if (incoming.troops() > this.attack.troops()) { - incoming.setTroops(incoming.troops() - this.attack.troops()); - this.attack.delete(); - this.active = false; - return; - } else { - this.attack.setTroops(this.attack.troops() - incoming.troops()); - incoming.delete(); - } - } - } for (const outgoing of this._owner.outgoingAttacks()) { if ( outgoing !== this.attack && From 790b052ca29d1ed0ca5312d9241d2f395d406c57 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Tue, 10 Jun 2025 13:07:26 -0400 Subject: [PATCH 10/19] Move version and changelog to files (#1109) ## Description: Move these assets to files so that they can be replaced with generated assets in the future. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- Dockerfile | 6 +- package-lock.json | 160 ++++++++++++++++++++++++++++++++++++++-- package.json | 1 + resources/changelog.md | 3 + resources/version.txt | 1 + src/client/Main.ts | 15 +++- src/client/NewsModal.ts | 25 ++----- src/client/index.html | 4 +- 8 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 resources/changelog.md create mode 100644 resources/version.txt diff --git a/Dockerfile b/Dockerfile index d28764cb8..80e8cba5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ FROM base COPY --from=dependencies / / ARG GIT_COMMIT=unknown -ENV GIT_COMMIT=$GIT_COMMIT +ENV GIT_COMMIT="$GIT_COMMIT" # Set the working directory in the container WORKDIR /usr/src/app @@ -35,7 +35,7 @@ COPY package*.json ./ # Install dependencies while bypassing Husky hooks ENV HUSKY=0 ENV NPM_CONFIG_IGNORE_SCRIPTS=1 -RUN mkdir -p .git && npm install +RUN mkdir -p .git && npm ci # Copy the rest of the application code COPY . . @@ -45,7 +45,7 @@ RUN npm run build-prod # So we can see which commit was used to build the container # https://openfront.io/commit.txt -RUN echo $GIT_COMMIT > static/commit.txt +RUN echo "$GIT_COMMIT" > static/commit.txt # Copy Nginx configuration and ensure it's used instead of the default COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/package-lock.json b/package-lock.json index faad9e243..260fdaa72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "jimp": "^0.22.12", "jose": "^6.0.10", "lit": "^3.2.1", + "lit-markdown": "^1.3.2", "msgpack5": "^6.0.2", "nanoid": "^3.3.6", "node-addon-api": "^8.1.0", @@ -11303,7 +11304,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11777,7 +11777,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -11932,7 +11931,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -15582,6 +15580,57 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/lit-markdown": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lit-markdown/-/lit-markdown-1.3.2.tgz", + "integrity": "sha512-51M4QRR2UmJIKck3kRQClD8CAM2Ox7BBZ9qzQLA1ppkr5tFfyNJDXnM0A2xmpNNfFrELZrnTmD5ST6VEdcemPg==", + "license": "MIT", + "dependencies": { + "lit": "^2.6.1", + "marked": "^4.2.12", + "sanitize-html": "^2.9.0" + } + }, + "node_modules/lit-markdown/node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/lit-markdown/node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-markdown/node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-markdown/node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/load-bmfont": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", @@ -15958,6 +16007,18 @@ "tmpl": "1.0.5" } }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -17019,6 +17080,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/parse5": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", @@ -17411,7 +17478,6 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -18558,6 +18624,91 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-html": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", + "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -19121,7 +19272,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index 6eb96b8c3..25f0fd8a7 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "jimp": "^0.22.12", "jose": "^6.0.10", "lit": "^3.2.1", + "lit-markdown": "^1.3.2", "msgpack5": "^6.0.2", "nanoid": "^3.3.6", "node-addon-api": "^8.1.0", diff --git a/resources/changelog.md b/resources/changelog.md new file mode 100644 index 000000000..76e4aeec3 --- /dev/null +++ b/resources/changelog.md @@ -0,0 +1,3 @@ +# header + +changelog here diff --git a/resources/version.txt b/resources/version.txt new file mode 100644 index 000000000..00f522f66 --- /dev/null +++ b/resources/version.txt @@ -0,0 +1 @@ +v0.24.0-dev diff --git a/src/client/Main.ts b/src/client/Main.ts index 9e5c6dfc1..a8fbf98e8 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -59,11 +59,19 @@ class Client { constructor() {} initialize(): void { + const gameVersion = document.getElementById( + "game-version", + ) as HTMLDivElement; + if (!gameVersion) { + console.warn("Game version element not found"); + } + fetch("/version.txt") + .then((response) => (response.ok ? response.text() : "Failed to load")) + .then((version) => (gameVersion.innerText = version)); + const newsModal = document.querySelector("news-modal") as NewsModal; if (!newsModal) { console.warn("News modal element not found"); - } else { - console.log("News modal element found"); } newsModal instanceof NewsModal; const newsButton = document.querySelector("news-button") as NewsButton; @@ -72,6 +80,9 @@ class Client { } else { console.log("News button element found"); } + fetch("/changelog.md") + .then((response) => (response.ok ? response.text() : "Failed to load")) + .then((changelog) => (newsModal.markdown = changelog)); // Comment out to show news button. // newsButton.hidden = true; diff --git a/src/client/NewsModal.ts b/src/client/NewsModal.ts index 13ddb67bb..4f08b2ee1 100644 --- a/src/client/NewsModal.ts +++ b/src/client/NewsModal.ts @@ -1,5 +1,6 @@ import { LitElement, css, html } from "lit"; -import { customElement, query } from "lit/decorators.js"; +import { resolveMarkdown } from "lit-markdown"; +import { customElement, property, query } from "lit/decorators.js"; import { translateText } from "../client/Utils"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; @@ -11,6 +12,8 @@ export class NewsModal extends LitElement { close: () => void; }; + @property({ type: String }) markdown = "Loading..."; + static styles = css` :host { display: block; @@ -51,22 +54,10 @@ export class NewsModal extends LitElement {
-

Main things to note:

-
-
    -
  • Workers reproduce faster than troops.
  • -
  • Defense = troops divided how much land you have.
  • -
  • Attacking troops count toward your population limit.
  • -
-
-
- See full changelog - here. + ${resolveMarkdown(this.markdown, { + includeImages: true, + includeCodeBlockClassNames: true, + })}
diff --git a/src/client/index.html b/src/client/index.html index ae94e0ae7..99978c9b8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -203,7 +203,9 @@ /> -
v23.0
+
+ Loading version... +
From bf26557dac279c154937daa206e47d7c5cad5aea Mon Sep 17 00:00:00 2001 From: Ghis <23282302+ghisloufou@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:39:41 +0200 Subject: [PATCH 11/19] Fix non valid SafeString flag codes (#1135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Removed unicode in the following flag file names to prevent error, which lead to not be able to start a game with it. - Ceará - 1_Northern Uí Néill - Pará - São Paulo - 1_Southern Uí Néill related to https://github.com/openfrontio/OpenFrontIO/pull/1133 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: ghisloufou --- ...1_Northern Uí Néill.svg => 1_Northern Ui Neill.svg} | 0 ...1_Southern Uí Néill.svg => 1_Southern Ui Neill.svg} | 0 resources/flags/{Ceará.svg => Ceara.svg} | 0 resources/flags/{Pará.svg => Para.svg} | 0 resources/flags/{São Paulo.svg => Sao Paulo.svg} | 0 resources/flags/para.svg | 3 --- resources/maps/Britannia.json | 4 ++-- src/client/data/countries.json | 10 +++++----- 8 files changed, 7 insertions(+), 10 deletions(-) rename resources/flags/{1_Northern Uí Néill.svg => 1_Northern Ui Neill.svg} (100%) rename resources/flags/{1_Southern Uí Néill.svg => 1_Southern Ui Neill.svg} (100%) rename resources/flags/{Ceará.svg => Ceara.svg} (100%) rename resources/flags/{Pará.svg => Para.svg} (100%) rename resources/flags/{São Paulo.svg => Sao Paulo.svg} (100%) delete mode 100644 resources/flags/para.svg diff --git a/resources/flags/1_Northern Uí Néill.svg b/resources/flags/1_Northern Ui Neill.svg similarity index 100% rename from resources/flags/1_Northern Uí Néill.svg rename to resources/flags/1_Northern Ui Neill.svg diff --git a/resources/flags/1_Southern Uí Néill.svg b/resources/flags/1_Southern Ui Neill.svg similarity index 100% rename from resources/flags/1_Southern Uí Néill.svg rename to resources/flags/1_Southern Ui Neill.svg diff --git a/resources/flags/Ceará.svg b/resources/flags/Ceara.svg similarity index 100% rename from resources/flags/Ceará.svg rename to resources/flags/Ceara.svg diff --git a/resources/flags/Pará.svg b/resources/flags/Para.svg similarity index 100% rename from resources/flags/Pará.svg rename to resources/flags/Para.svg diff --git a/resources/flags/São Paulo.svg b/resources/flags/Sao Paulo.svg similarity index 100% rename from resources/flags/São Paulo.svg rename to resources/flags/Sao Paulo.svg diff --git a/resources/flags/para.svg b/resources/flags/para.svg deleted file mode 100644 index aae5eb835..000000000 --- a/resources/flags/para.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/maps/Britannia.json b/resources/maps/Britannia.json index 79c5533ac..614d3615a 100644 --- a/resources/maps/Britannia.json +++ b/resources/maps/Britannia.json @@ -115,7 +115,7 @@ "coordinates": [564, 845], "name": "Southern Uí Néill", "strength": 3, - "flag": "1_Southern Uí Néill" + "flag": "1_Southern Ui Neill" }, { "coordinates": [639, 680], @@ -133,7 +133,7 @@ "coordinates": [416, 678], "name": "Northern Uí Néill", "strength": 3, - "flag": "1_Northern Uí Néill" + "flag": "1_Northern Ui Neill" }, { "coordinates": [1869, 1308], diff --git a/src/client/data/countries.json b/src/client/data/countries.json index 043f2963b..61b7c0251 100644 --- a/src/client/data/countries.json +++ b/src/client/data/countries.json @@ -404,7 +404,7 @@ "name": "Cayman Islands" }, { - "code": "Ceará", + "code": "Ceara", "continent": "South America", "name": "Ceará" }, @@ -1501,7 +1501,7 @@ "name": "Northern Mariana Islands" }, { - "code": "1_Northern Uí Néill", + "code": "1_Northern Ui Neill", "continent": "Europe", "name": "Northern Uí Néill" }, @@ -1566,7 +1566,7 @@ "name": "Papua New Guinea" }, { - "code": "Pará", + "code": "Para", "continent": "South America", "name": "Pará" }, @@ -1803,7 +1803,7 @@ "name": "Sao Tome and Principe" }, { - "code": "São Paulo", + "code": "Sao Paulo", "continent": "South America", "name": "São Paulo" }, @@ -1941,7 +1941,7 @@ "name": "South Sudan" }, { - "code": "1_Southern Uí Néill", + "code": "1_Southern Ui Neill", "continent": "Europe", "name": "Southern Uí Néill" }, From 22e0de95267d4d6002fc2adce9a79e751514c074 Mon Sep 17 00:00:00 2001 From: Ghis <23282302+ghisloufou@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:40:20 +0200 Subject: [PATCH 12/19] Add a Replay speed control feature (#1106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: PR for https://github.com/openfrontio/OpenFrontIO/issues/1105 This pull request introduces a new replay-panel under options-menu to control singleplayer and replay speed. ![image](https://github.com/user-attachments/assets/2f83b969-eff1-4a87-bdca-867f6a98e46b) User can select 4 different speed multipliers based on the turnIntervalMs config. I locally tested the feature in singleplayer mode. I couldn't find a way to test replay mode. Tested with the pause button, working as you would expect. Here is an example: [replay-speed.webm](https://github.com/user-attachments/assets/7b3a7616-5f8f-4fbb-88ba-0b2414c6f2ea) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ⚠️ I need help to test this feature in real conditions with a replayed game in dev env. - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: ghisloufou --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: evanpelle --- resources/lang/en.json | 3 + src/client/InputHandler.ts | 5 + src/client/LangSelector.ts | 1 + src/client/LocalServer.ts | 30 +++-- src/client/Transport.ts | 1 + src/client/graphics/GameRenderer.ts | 9 ++ src/client/graphics/layers/OptionsMenu.ts | 2 +- src/client/graphics/layers/ReplayPanel.ts | 123 ++++++++++++++++++ src/client/index.html | 1 + src/client/utilities/ReplaySpeedMultiplier.ts | 8 ++ 10 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 src/client/graphics/layers/ReplayPanel.ts create mode 100644 src/client/utilities/ReplaySpeedMultiplier.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 030f08916..64c491ee9 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -442,6 +442,9 @@ "none": "None", "alliances": "Alliances" }, + "replay_panel": { + "replay_speed": "Replay speed" + }, "error_modal": { "crashed": "Game crashed!", "paste_discord": "Please paste the following in your bug report in Discord:", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index a4ba1eae6..96fde5beb 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -1,6 +1,7 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { UnitView } from "../core/game/GameView"; import { UserSettings } from "../core/game/UserSettings"; +import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; export class MouseUpEvent implements GameEvent { constructor( @@ -82,6 +83,10 @@ export class AttackRatioEvent implements GameEvent { constructor(public readonly attackRatio: number) {} } +export class ReplaySpeedChangeEvent implements GameEvent { + constructor(public readonly replaySpeedMultiplier: ReplaySpeedMultiplier) {} +} + export class CenterCameraEvent implements GameEvent { constructor() {} } diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 27b7f9399..ad1486b77 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -186,6 +186,7 @@ export class LangSelector extends LitElement { "game-starting-modal", "top-bar", "player-panel", + "replay-panel", "help-modal", "username-input", "public-lobby", diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index e3bc08dcf..73e4961d2 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,3 +1,4 @@ +import { EventBus } from "../core/EventBus"; import { AllPlayersStats, ClientMessage, @@ -12,7 +13,9 @@ import { } from "../core/Schemas"; import { createGameRecord, decompressGameRecord, replacer } from "../core/Util"; import { LobbyConfig } from "./ClientGameRunner"; +import { ReplaySpeedChangeEvent } from "./InputHandler"; import { getPersistentID } from "./Main"; +import { defaultReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; export class LocalServer { // All turns from the game record on replay. @@ -24,6 +27,7 @@ export class LocalServer { private startedAt: number; private paused = false; + private replaySpeedMultiplier = defaultReplaySpeedMultiplier; private winner: ClientSendWinnerMessage | null = null; private allPlayersStats: AllPlayersStats = {}; @@ -38,23 +42,29 @@ export class LocalServer { private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void, private isReplay: boolean, + private eventBus: EventBus, ) {} start() { this.turnCheckInterval = setInterval(() => { - if (this.turnsExecuted === this.turns.length) { - if ( - this.isReplay || - Date.now() > - this.turnStartTime + this.lobbyConfig.serverConfig.turnIntervalMs() - ) { - this.turnStartTime = Date.now(); - // End turn on the server means the client will start processing the turn. - this.endTurn(); - } + const turnIntervalMs = + this.lobbyConfig.serverConfig.turnIntervalMs() * + this.replaySpeedMultiplier; + + if ( + this.turnsExecuted === this.turns.length && + Date.now() > this.turnStartTime + turnIntervalMs + ) { + this.turnStartTime = Date.now(); + // End turn on the server means the client will start processing the turn. + this.endTurn(); } }, 5); + this.eventBus.on(ReplaySpeedChangeEvent, (event) => { + this.replaySpeedMultiplier = event.replaySpeedMultiplier; + }); + this.startedAt = Date.now(); this.clientConnect(); if (this.lobbyConfig.gameRecord) { diff --git a/src/client/Transport.ts b/src/client/Transport.ts index f208a55a1..e9124b6f5 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -266,6 +266,7 @@ export class Transport { onconnect, onmessage, this.lobbyConfig.gameRecord !== undefined, + this.eventBus, ); this.localServer.start(); } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index ee3bbc4b9..8699c2741 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -21,6 +21,7 @@ import { OptionsMenu } from "./layers/OptionsMenu"; import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay"; import { PlayerPanel } from "./layers/PlayerPanel"; import { PlayerTeamLabel } from "./layers/PlayerTeamLabel"; +import { ReplayPanel } from "./layers/ReplayPanel"; import { SpawnTimer } from "./layers/SpawnTimer"; import { StructureLayer } from "./layers/StructureLayer"; import { TeamStats } from "./layers/TeamStats"; @@ -126,6 +127,13 @@ export function createRenderer( optionsMenu.eventBus = eventBus; optionsMenu.game = game; + const replayPanel = document.querySelector("replay-panel") as ReplayPanel; + if (!(replayPanel instanceof ReplayPanel)) { + console.error("ReplayPanel element not found in the DOM"); + } + replayPanel.eventBus = eventBus; + replayPanel.game = game; + const topBar = document.querySelector("top-bar") as TopBar; if (!(topBar instanceof TopBar)) { console.error("top bar not found"); @@ -215,6 +223,7 @@ export function createRenderer( playerInfo, winModel, optionsMenu, + replayPanel, teamStats, topBar, playerPanel, diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index 56e71605a..79aa4dad5 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { GameType } from "../../../core/game/Game"; diff --git a/src/client/graphics/layers/ReplayPanel.ts b/src/client/graphics/layers/ReplayPanel.ts new file mode 100644 index 000000000..529a52ca1 --- /dev/null +++ b/src/client/graphics/layers/ReplayPanel.ts @@ -0,0 +1,123 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { EventBus } from "../../../core/EventBus"; +import { GameType } from "../../../core/game/Game"; +import { GameView } from "../../../core/game/GameView"; +import { ReplaySpeedChangeEvent } from "../../InputHandler"; +import { + defaultReplaySpeedMultiplier, + ReplaySpeedMultiplier, +} from "../../utilities/ReplaySpeedMultiplier"; +import { translateText } from "../../Utils"; +import { Layer } from "./Layer"; + +@customElement("replay-panel") +export class ReplayPanel extends LitElement implements Layer { + public game: GameView | undefined; + public eventBus: EventBus | undefined; + + @state() + private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier; + + @state() + private _isVisible = false; + + init() { + if (this.game?.config().gameConfig().gameType === GameType.Singleplayer) { + this.setVisible(true); + } + } + + tick() { + if (!this._isVisible && this.game?.config().isReplay()) { + this.setVisible(true); + } + + this.requestUpdate(); + } + + onReplaySpeedChange(value: ReplaySpeedMultiplier) { + this._replaySpeedMultiplier = value; + this.eventBus?.emit(new ReplaySpeedChangeEvent(value)); + } + + renderLayer(context: CanvasRenderingContext2D) { + // Render any necessary canvas elements + } + + shouldTransform(): boolean { + return false; + } + + setVisible(visible: boolean) { + this._isVisible = visible; + this.requestUpdate(); + } + + render() { + if (!this._isVisible) { + return html``; + } + + return html` +
e.preventDefault()} + > + +
+ + + + +
+
+ `; + } + + createRenderRoot() { + return this; // Disable shadow DOM to allow Tailwind styles + } +} diff --git a/src/client/index.html b/src/client/index.html index 99978c9b8..1b9c2c46e 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -306,6 +306,7 @@ class="flex flex-column gap-2 fixed right-[10px] top-[10px] z-50 flex flex-col w-32 sm:w-32 lg:w-48" > +
Date: Tue, 10 Jun 2025 23:50:31 +0300 Subject: [PATCH 13/19] Add progress bars to show loading time and healthbars (#1107) ## Description: Add progress bars to show construction time, loading time and health bars in the UI layer The progress bars always show at least one pixel of progression (better visuals) ![buildcity](https://github.com/user-attachments/assets/7181642a-742d-4996-8ca9-748b55c04a58) ![launchNuke](https://github.com/user-attachments/assets/85fbed8f-3d91-4d7e-9c01-737ee5868992) ![ships2](https://github.com/user-attachments/assets/9fd53e6a-b2c7-4044-8b65-6f61231775b1) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: Vivacious Box --------- Co-authored-by: evanpelle --- package-lock.json | 1287 ++++++++++++++++++++- package.json | 2 + src/client/graphics/ProgressBar.ts | 61 + src/client/graphics/layers/UILayer.ts | 132 ++- tests/client/graphics/ProgressBar.test.ts | 58 + tests/client/graphics/UILayer.test.ts | 136 +++ 6 files changed, 1657 insertions(+), 19 deletions(-) create mode 100644 src/client/graphics/ProgressBar.ts create mode 100644 tests/client/graphics/ProgressBar.test.ts create mode 100644 tests/client/graphics/UILayer.test.ts diff --git a/package-lock.json b/package-lock.json index 260fdaa72..74acea471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ "autoprefixer": "^10.4.20", "babel-jest": "^29.7.0", "binary-base64-loader": "^1.0.0", + "canvas": "^3.1.0", "chai": "^5.1.1", "concurrently": "^8.2.2", "cross-env": "^7.0.3", @@ -110,6 +111,7 @@ "html-loader": "^5.1.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-environment-jsdom": "^30.0.0-beta.3", "lint-staged": "^15.4.3", "mrmime": "^2.0.0", "postcss": "^8.5.1", @@ -159,6 +161,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -2930,6 +2953,121 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -4381,6 +4519,228 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.0.0-beta.3.tgz", + "integrity": "sha512-+zbcQyBD2IhxWkv0koB1PnEpcsDRwlmTRQIz6/+mzdT3sit3nd15C8g4bH9kW2+EPA5WIthZ2GsXiV+dJO+igA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.0-beta.3", + "@jest/fake-timers": "30.0.0-beta.3", + "@jest/types": "30.0.0-beta.3", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.0.0-beta.3", + "jest-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.0-beta.3.tgz", + "integrity": "sha512-1qcDVc37nlItrkYXrWC9FFXU0CxNUe/PGYpHzOz6zIX7FKFv7i8Z0LKBs27iN6XIhczrmQtFzs9JUnHGARWRoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.0-beta.3", + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "jest-mock": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.0-beta.3.tgz", + "integrity": "sha512-bSYm4Q42Obgzs8tnDcd5zMsgNSZFTtydZJQxEFoaHOSnNuSnC03CvJbZuKs5Gcjqm94eHYoOL7HDvo1W5UMVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.0-beta.3", + "jest-mock": "30.0.0-beta.3", + "jest-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0-beta.3.tgz", + "integrity": "sha512-tiT79EKOlJGT5v8fYr9UKLSyjlA3Ek+nk0cVZwJGnRqVp26EQSOTYXBCzj0dGMegkgnPTt3f7wP1kGGI8q/e0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0-beta.3.tgz", + "integrity": "sha512-x7GyHD8rxZ4Ygmp4rea3uPDIPZ6Jglcglaav8wQNqXsVUAByapDwLF52Cp3wEYMPMnvH4BicEj56j8fqZx5jng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.0-beta.3", + "@jest/schemas": "30.0.0-beta.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.33", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", + "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.0-beta.3.tgz", + "integrity": "sha512-AMfuhrcOs+Nlyk3HBywn1cIO5KusDxelLP6HTgMlggYWNODm2yNENVnYBuBw78x1uK5f/DQNYlTioq5ub6TXlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "30.0.0-beta.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.8", + "pretty-format": "30.0.0-beta.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.0-beta.3.tgz", + "integrity": "sha512-g7w/Wjxefrq7MNTGstemMP20PTiSpABJlkl/4L1lAAAy15ZM4BDEl1D9aBEz2qcfUJAS9690HVIB4bJ/V+5sTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "jest-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kob8YNaO1UPrG0TgGdH5l0ciNGuXDX93Yn2b2VCkALuqOXbqzT2xCr6O7dBuwhM7tmzBbpM6CkcK7Qyf/JmLZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^4.0.0", + "graceful-fs": "^4.2.9", + "picomatch": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.0-beta.3.tgz", + "integrity": "sha512-MR29N2jVpfzRkuW6XbZsYYIqpoU/CzlsLwNO0h4/D5OZlu3c4WkIxaIiUxyuw25GEB8B6KNqOC6WQrqAzhkA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.0-beta.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, "node_modules/@jest/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", @@ -4442,6 +4802,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.0-beta.3.tgz", + "integrity": "sha512-IuB9mweyJI5ToVBRdptKb2w97LGnNHFI+V9/cGaYeFareL7BYD6KiUH022OC51K1841c6YzgYjyQmJHFxELZSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -8272,6 +8656,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -9086,13 +9482,10 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, "engines": { "node": ">= 14" } @@ -9970,6 +10363,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz", + "integrity": "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/canvas/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/centra": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", @@ -10833,6 +11248,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.4.0.tgz", + "integrity": "sha512-W0Y2HOXlPkb2yaKrCVRjinYKciu/qSLEmK0K9mcfDei3zwlnHFEHAs/Du3cIRwPqY+J4JsiBzUjoHyc8RsJ03A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/d3": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", @@ -11234,6 +11663,57 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -11268,6 +11748,29 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -11293,6 +11796,16 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -12427,6 +12940,16 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -12979,6 +13502,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "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", @@ -13201,6 +13731,13 @@ "omggif": "^1.0.10" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -13533,6 +14070,19 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -13800,12 +14350,12 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -14009,6 +14559,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -14225,6 +14782,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -14588,6 +15152,225 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-environment-jsdom": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.0.0-beta.3.tgz", + "integrity": "sha512-YLBmv46sn5CYQR/iX+seZJ7FsuUAM4tf7Pm5ymP8XQKzrKEPdDv1f1V/z2b9XSTniR+OlIuGpnP3G3RbpbsceA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.0-beta.3", + "@jest/environment-jsdom-abstract": "30.0.0-beta.3", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.0-beta.3.tgz", + "integrity": "sha512-1qcDVc37nlItrkYXrWC9FFXU0CxNUe/PGYpHzOz6zIX7FKFv7i8Z0LKBs27iN6XIhczrmQtFzs9JUnHGARWRoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.0-beta.3", + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "jest-mock": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.0-beta.3.tgz", + "integrity": "sha512-bSYm4Q42Obgzs8tnDcd5zMsgNSZFTtydZJQxEFoaHOSnNuSnC03CvJbZuKs5Gcjqm94eHYoOL7HDvo1W5UMVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.0-beta.3", + "jest-mock": "30.0.0-beta.3", + "jest-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0-beta.3.tgz", + "integrity": "sha512-tiT79EKOlJGT5v8fYr9UKLSyjlA3Ek+nk0cVZwJGnRqVp26EQSOTYXBCzj0dGMegkgnPTt3f7wP1kGGI8q/e0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0-beta.3.tgz", + "integrity": "sha512-x7GyHD8rxZ4Ygmp4rea3uPDIPZ6Jglcglaav8wQNqXsVUAByapDwLF52Cp3wEYMPMnvH4BicEj56j8fqZx5jng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.0-beta.3", + "@jest/schemas": "30.0.0-beta.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.33", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", + "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.0-beta.3.tgz", + "integrity": "sha512-AMfuhrcOs+Nlyk3HBywn1cIO5KusDxelLP6HTgMlggYWNODm2yNENVnYBuBw78x1uK5f/DQNYlTioq5ub6TXlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "30.0.0-beta.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.8", + "pretty-format": "30.0.0-beta.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.0-beta.3.tgz", + "integrity": "sha512-g7w/Wjxefrq7MNTGstemMP20PTiSpABJlkl/4L1lAAAy15ZM4BDEl1D9aBEz2qcfUJAS9690HVIB4bJ/V+5sTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "jest-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kob8YNaO1UPrG0TgGdH5l0ciNGuXDX93Yn2b2VCkALuqOXbqzT2xCr6O7dBuwhM7tmzBbpM6CkcK7Qyf/JmLZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^4.0.0", + "graceful-fs": "^4.2.9", + "picomatch": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.0-beta.3.tgz", + "integrity": "sha512-MR29N2jVpfzRkuW6XbZsYYIqpoU/CzlsLwNO0h4/D5OZlu3c4WkIxaIiUxyuw25GEB8B6KNqOC6WQrqAzhkA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.0-beta.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -15054,6 +15837,83 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -16158,6 +17018,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -16366,6 +17239,13 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -16468,6 +17348,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -16534,6 +17421,32 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz", @@ -16748,6 +17661,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -17087,18 +18007,31 @@ "license": "MIT" }, "node_modules/parse5": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", - "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -17776,6 +18709,33 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -17996,6 +18956,17 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -18180,6 +19151,32 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -18552,6 +19549,13 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -18715,6 +19719,19 @@ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -19086,6 +20103,53 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -19719,6 +20783,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.25.11", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.25.11.tgz", @@ -19829,6 +20900,95 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -20141,6 +21301,26 @@ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -20186,6 +21366,19 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -20474,6 +21667,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/twemoji": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz", @@ -20809,6 +22015,19 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -21212,12 +22431,35 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -21457,6 +22699,16 @@ "xtend": "^4.0.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", @@ -21485,6 +22737,13 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 25f0fd8a7..1ab7dee0f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "autoprefixer": "^10.4.20", "babel-jest": "^29.7.0", "binary-base64-loader": "^1.0.0", + "canvas": "^3.1.0", "chai": "^5.1.1", "concurrently": "^8.2.2", "cross-env": "^7.0.3", @@ -57,6 +58,7 @@ "html-loader": "^5.1.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-environment-jsdom": "^30.0.0-beta.3", "lint-staged": "^15.4.3", "mrmime": "^2.0.0", "postcss": "^8.5.1", diff --git a/src/client/graphics/ProgressBar.ts b/src/client/graphics/ProgressBar.ts new file mode 100644 index 000000000..52500bb87 --- /dev/null +++ b/src/client/graphics/ProgressBar.ts @@ -0,0 +1,61 @@ +export class ProgressBar { + private static readonly CLEAR_PADDING = 2; + constructor( + private colors: string[] = [], + private ctx: CanvasRenderingContext2D, + private x: number, + private y: number, + private w: number, + private h: number, + private progress: number = 0, // Progress from 0 to 1 + ) { + this.setProgress(progress); + } + + setProgress(progress: number): void { + progress = Math.max(0, Math.min(1, progress)); + this.clear(); + // Draw the loading bar background + this.ctx.fillStyle = "rgba(0, 0, 0, 1)"; + this.ctx.fillRect(this.x - 1, this.y - 1, this.w, this.h); + + // Draw the loading progress + if (this.colors.length === 0) { + this.ctx.fillStyle = "#808080"; // default gray + } else { + const idx = Math.min( + this.colors.length - 1, + Math.floor(progress * this.colors.length), + ); + this.ctx.fillStyle = this.colors[idx]; + } + this.ctx.fillRect( + this.x, + this.y, + Math.max(1, Math.floor(progress * (this.w - 2))), + this.h - 2, + ); + this.progress = progress; + } + + clear() { + this.ctx.clearRect( + this.x - ProgressBar.CLEAR_PADDING, + this.y - ProgressBar.CLEAR_PADDING, + this.w + ProgressBar.CLEAR_PADDING, + this.h + ProgressBar.CLEAR_PADDING, + ); + } + + getX(): number { + return this.x; + } + + getY(): number { + return this.y; + } + + getProgress(): number { + return this.progress; + } +} diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index e8b8a54a0..9918d8c4f 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -1,12 +1,24 @@ import { Colord } from "colord"; import { EventBus } from "../../../core/EventBus"; import { Theme } from "../../../core/configuration/Config"; -import { UnitType } from "../../../core/game/Game"; +import { Tick, UnitType } from "../../../core/game/Game"; +import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; import { UnitSelectionEvent } from "../../InputHandler"; +import { ProgressBar } from "../ProgressBar"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; +const COLOR_PROGRESSION = [ + "rgb(232, 25, 25)", + "rgb(240, 122, 25)", + "rgb(202, 231, 15)", + "rgb(44, 239, 18)", +]; +const HEALTHBAR_WIDTH = 11; // Width of the health bar +const LOADINGBAR_WIDTH = 18; // Width of the loading bar +const PROGRESSBAR_HEIGHT = 3; // Height of a bar + /** * Layer responsible for drawing UI elements that overlay the game * such as selection boxes, health bars, etc. @@ -17,7 +29,11 @@ export class UILayer implements Layer { private theme: Theme | null = null; private selectionAnimTime = 0; - + private allProgressBars: Map< + number, + { unit: UnitView; startTick: Tick; endTick: Tick; progressBar: ProgressBar } + > = new Map(); + private allHealthBars: Map = new Map(); // Keep track of currently selected unit private selectedUnit: UnitView | null = null; @@ -51,6 +67,16 @@ export class UILayer implements Layer { if (this.selectedUnit && this.selectedUnit.type() === UnitType.Warship) { this.drawSelectionBox(this.selectedUnit); } + + this.game + .updatesSinceLastTick() + ?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id)) + ?.forEach((unitView) => { + if (unitView === undefined) return; + this.onUnitEvent(unitView); + }); + + this.updateProgressBars(); } init() { @@ -76,6 +102,42 @@ export class UILayer implements Layer { this.canvas.height = this.game.height(); } + onUnitEvent(unit: UnitView) { + switch (unit.type()) { + case UnitType.Construction: { + const playerId = this.game.myPlayer()?.id(); + if ( + unit.isActive() && + playerId !== undefined && + unit.owner().id() === playerId + ) { + const constructionType = unit.constructionType(); + if (constructionType === undefined) { + // Skip units without construction type + return; + } + const endTick = + this.game.unitInfo(constructionType).constructionDuration || 0; + this.drawLoadingBar(unit, endTick); + } + break; + } + case UnitType.Warship: { + this.drawHealthBar(unit); + break; + } + case UnitType.SAMLauncher: + case UnitType.MissileSilo: + if (unit.isActive() && unit.isCooldown()) { + const endTick = unit.ticksLeftInCooldown() || 0; + this.drawLoadingBar(unit, endTick); + } + break; + default: + return; + } + } + /** * Handle the unit selection event */ @@ -187,11 +249,71 @@ export class UILayer implements Layer { } /** - * Draw health bar for a unit (placeholder for future implementation) + * Draw health bar for a unit */ public drawHealthBar(unit: UnitView) { - // This is a placeholder for future health bar implementation - // It would draw a health bar above units that have health + const maxHealth = this.game.unitInfo(unit.type()).maxHealth; + if (maxHealth === undefined || this.context === null) { + return; + } + if ( + this.allHealthBars.has(unit.id()) && + (unit.health() >= maxHealth || unit.health() <= 0 || !unit.isActive()) + ) { + // full hp/dead warships dont need a hp bar + this.allHealthBars.get(unit.id())?.clear(); + this.allHealthBars.delete(unit.id()); + } else if (unit.health() < maxHealth && unit.health() > 0) { + this.allHealthBars.get(unit.id())?.clear(); + const healthBar = new ProgressBar( + COLOR_PROGRESSION, + this.context, + this.game.x(unit.tile()) - 4, + this.game.y(unit.tile()) - 6, + HEALTHBAR_WIDTH, + PROGRESSBAR_HEIGHT, + unit.health() / maxHealth, + ); + // keep track of units that have health bars for clearing purposes + this.allHealthBars.set(unit.id(), healthBar); + } + } + + private updateProgressBars() { + const currentTick = this.game.ticks(); + this.allProgressBars.forEach((progressBarInfo, unitId) => { + const progress = + (currentTick - progressBarInfo.startTick) / progressBarInfo.endTick; + if (progress >= 1 || !progressBarInfo.unit.isActive()) { + this.allProgressBars.get(unitId)?.progressBar.clear(); + this.allProgressBars.delete(unitId); + return; + } + progressBarInfo.progressBar.setProgress(progress); + }); + } + + public drawLoadingBar(unit: UnitView, endTick: Tick) { + if (!this.context) { + return; + } + if (!this.allProgressBars.has(unit.id())) { + const progressBar = new ProgressBar( + COLOR_PROGRESSION, + this.context, + this.game.x(unit.tile()) - 8, + this.game.y(unit.tile()) - 10, + LOADINGBAR_WIDTH, + PROGRESSBAR_HEIGHT, + 0, + ); + this.allProgressBars.set(unit.id(), { + unit, + startTick: this.game.ticks(), + endTick, + progressBar, + }); + } } paintCell(x: number, y: number, color: Colord, alpha: number) { diff --git a/tests/client/graphics/ProgressBar.test.ts b/tests/client/graphics/ProgressBar.test.ts new file mode 100644 index 000000000..50189a9e3 --- /dev/null +++ b/tests/client/graphics/ProgressBar.test.ts @@ -0,0 +1,58 @@ +/** + * @jest-environment jsdom + */ +import { ProgressBar } from "../../../src/client/graphics/ProgressBar"; + +describe("ProgressBar", () => { + let ctx: CanvasRenderingContext2D; + let canvas: HTMLCanvasElement; + + beforeEach(() => { + canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 20; + ctx = canvas.getContext("2d")!; + }); + + it("should initialize and draw the background", () => { + const spyClearRect = jest.spyOn(ctx, "clearRect"); + const spyFillRect = jest.spyOn(ctx, "fillRect"); + const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set"); + const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5); + expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12); + expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10); + expect(spyFillStyle).toHaveBeenCalledWith("#00ff00"); + expect(bar.getX()).toBe(2); + expect(bar.getY()).toBe(2); + }); + + it("should set progress and draw the progress bar", () => { + const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10); + const spyFillRect = jest.spyOn(ctx, "fillRect"); + bar.setProgress(0.5); + expect(bar.getProgress()).toBe(0.5); + expect(spyFillRect).toHaveBeenCalledWith( + 2, + 2, + Math.floor(0.5 * (80 - 2)), + 8, + ); + expect(ctx.fillStyle).toBe("#00ff00"); + + bar.setProgress(0.1); + expect(ctx.fillStyle).toBe("#ff0000"); + }); + + it("should clamp progress between 0 and 1 on init", () => { + const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, -1); + expect(bar.getProgress()).toBe(0); + const bar2 = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 2); + expect(bar2.getProgress()).toBe(1); + }); + + it("should handle empty colors array gracefully", () => { + const bar = new ProgressBar([], ctx, 2, 2, 80, 10, 0.5); + expect(() => bar.setProgress(0.5)).not.toThrow(); + expect(ctx.fillStyle).toBe("#808080"); + }); +}); diff --git a/tests/client/graphics/UILayer.test.ts b/tests/client/graphics/UILayer.test.ts new file mode 100644 index 000000000..dbc131cec --- /dev/null +++ b/tests/client/graphics/UILayer.test.ts @@ -0,0 +1,136 @@ +/** + * @jest-environment jsdom + */ +import { UILayer } from "../../../src/client/graphics/layers/UILayer"; +import { UnitSelectionEvent } from "../../../src/client/InputHandler"; +import { UnitView } from "../../../src/core/game/GameView"; + +describe("UILayer", () => { + let game: any; + let eventBus: any; + let transformHandler: any; + + beforeEach(() => { + game = { + width: () => 100, + height: () => 100, + config: () => ({ + theme: () => ({ + territoryColor: () => ({ + lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }), + }), + }), + }), + x: () => 10, + y: () => 10, + unitInfo: () => ({ maxHealth: 10, constructionDuration: 5 }), + myPlayer: () => ({ id: () => 1 }), + ticks: () => 1, + updatesSinceLastTick: () => undefined, + }; + eventBus = { on: jest.fn() }; + transformHandler = {}; + }); + + it("should initialize and redraw canvas", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + expect(ui["canvas"].width).toBe(100); + expect(ui["canvas"].height).toBe(100); + expect(ui["context"]).not.toBeNull(); + }); + + it("should handle unit selection event", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + type: () => "Warship", + isActive: () => true, + tile: () => ({}), + owner: () => ({}), + }; + const event = { isSelected: true, unit }; + ui.drawSelectionBox = jest.fn(); + ui["onUnitSelection"](event as UnitSelectionEvent); + expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit); + }); + + it("should add and clear health bars", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + id: () => 1, + type: () => "Warship", + health: () => 5, + tile: () => ({}), + owner: () => ({}), + isActive: () => true, + } as unknown as UnitView; + ui.drawHealthBar(unit); + expect(ui["allHealthBars"].has(1)).toBe(true); + + // a full hp unit doesnt have a health bar + unit.health = () => 10; + ui.drawHealthBar(unit); + expect(ui["allHealthBars"].has(1)).toBe(false); + + // a dead unit doesnt have a health bar + unit.health = () => 5; + ui.drawHealthBar(unit); + expect(ui["allHealthBars"].has(1)).toBe(true); + unit.health = () => 0; + ui.drawHealthBar(unit); + expect(ui["allHealthBars"].has(1)).toBe(false); + }); + + it("should add loading bar for unit", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + id: () => 2, + tile: () => ({}), + isActive: () => true, + } as unknown as UnitView; + ui.drawLoadingBar(unit, 5); + expect(ui["allProgressBars"].has(2)).toBe(true); + }); + + it("should remove loading bar for inactive unit", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + id: () => 2, + type: () => "Construction", + constructionType: () => "City", + owner: () => ({ id: () => 1 }), + tile: () => ({}), + isActive: () => true, + } as unknown as UnitView; + ui.onUnitEvent(unit); + expect(ui["allProgressBars"].has(2)).toBe(true); + + // an inactive unit should not have a loading bar + unit.isActive = () => false; + ui.tick(); + expect(ui["allProgressBars"].has(2)).toBe(false); + }); + + it("should remove loading bar for a finished progress bar", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + id: () => 2, + type: () => "Construction", + constructionType: () => "City", + owner: () => ({ id: () => 1 }), + tile: () => ({}), + isActive: () => true, + } as unknown as UnitView; + ui.onUnitEvent(unit); + expect(ui["allProgressBars"].has(2)).toBe(true); + + game.ticks = () => 6; // simulate enough ticks for completion + ui.tick(); + expect(ui["allProgressBars"].has(2)).toBe(false); + }); +}); From cd799b514c201584d7d64611bbd169ed7040a48d Mon Sep 17 00:00:00 2001 From: Christopher Mesona <45428623+Ble4Ch@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:55:53 +0200 Subject: [PATCH 14/19] feat: assign unique colors for players (#1063) ## Description: * adds 100+ colors for players * assigns a unique color for each player * bot and team colors assignment unchanged ![image](https://github.com/user-attachments/assets/75061a50-7166-4c0b-8f53-b35074a85706) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: George --------- Co-authored-by: cmesona --- src/core/configuration/Colors.ts | 314 +++++++++++++++------- src/core/configuration/PastelTheme.ts | 47 +--- src/core/configuration/PastelThemeDark.ts | 47 +--- tests/Colors.test.ts | 81 ++++++ 4 files changed, 320 insertions(+), 169 deletions(-) create mode 100644 tests/Colors.test.ts diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index f2bbfd0fe..747862cff 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -1,4 +1,6 @@ import { colord, Colord } from "colord"; +import { ColoredTeams, Team } from "../game/Game"; +import { simpleHash } from "../Util"; export const red: Colord = colord({ r: 235, g: 53, b: 53 }); // Bright Red export const blue: Colord = colord({ r: 41, g: 98, b: 255 }); // Royal Blue @@ -9,7 +11,7 @@ export const orange = colord({ h: 25, s: 95, l: 53 }); export const green = colord({ h: 128, s: 49, l: 50 }); export const botColor: Colord = colord({ r: 210, g: 206, b: 200 }); // Muted Beige Gray -export const territoryColors: Colord[] = [ +export const nationColors: Colord[] = [ colord({ r: 230, g: 100, b: 100 }), // Bright Red colord({ r: 100, g: 180, b: 230 }), // Sky Blue colord({ r: 230, g: 180, b: 80 }), // Golden Yellow @@ -109,110 +111,71 @@ export const territoryColors: Colord[] = [ colord({ r: 170, g: 150, b: 170 }), // Dusty Rose ]; +// Bright pastel theme with 64 colors export const humanColors: Colord[] = [ - // Original set - colord({ r: 235, g: 75, b: 75 }), // Bright Red - colord({ r: 67, g: 190, b: 84 }), // Fresh Green - colord({ r: 59, g: 130, b: 246 }), // Royal Blue - colord({ r: 245, g: 158, b: 11 }), // Amber - colord({ r: 236, g: 72, b: 153 }), // Deep Pink - colord({ r: 48, g: 178, b: 180 }), // Teal - colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple - colord({ r: 251, g: 191, b: 36 }), // Marigold - colord({ r: 74, g: 222, b: 128 }), // Mint - colord({ r: 239, g: 68, b: 68 }), // Crimson - colord({ r: 34, g: 197, b: 94 }), // Emerald - colord({ r: 96, g: 165, b: 250 }), // Sky Blue - colord({ r: 249, g: 115, b: 22 }), // Tangerine - colord({ r: 192, g: 132, b: 252 }), // Lavender - colord({ r: 45, g: 212, b: 191 }), // Turquoise - colord({ r: 244, g: 114, b: 182 }), // Rose - colord({ r: 132, g: 204, b: 22 }), // Lime - colord({ r: 56, g: 189, b: 248 }), // Light Blue - colord({ r: 234, g: 179, b: 8 }), // Sunflower - colord({ r: 217, g: 70, b: 239 }), // Fuchsia colord({ r: 16, g: 185, b: 129 }), // Sea Green - colord({ r: 251, g: 146, b: 60 }), // Light Orange - colord({ r: 147, g: 51, b: 234 }), // Bright Purple - colord({ r: 79, g: 70, b: 229 }), // Indigo - colord({ r: 245, g: 101, b: 101 }), // Coral - colord({ r: 134, g: 239, b: 172 }), // Light Green - colord({ r: 59, g: 130, b: 246 }), // Cerulean - colord({ r: 253, g: 164, b: 175 }), // Salmon Pink - colord({ r: 147, g: 197, b: 253 }), // Powder Blue - colord({ r: 252, g: 211, b: 77 }), // Golden - colord({ r: 190, g: 92, b: 251 }), // Amethyst - colord({ r: 82, g: 183, b: 136 }), // Jade - colord({ r: 248, g: 113, b: 113 }), // Warm Red - colord({ r: 99, g: 202, b: 253 }), // Azure - colord({ r: 240, g: 171, b: 252 }), // Orchid - colord({ r: 163, g: 230, b: 53 }), // Yellow Green - colord({ r: 234, g: 88, b: 12 }), // Burnt Orange - colord({ r: 125, g: 211, b: 252 }), // Crystal Blue - colord({ r: 251, g: 113, b: 133 }), // Watermelon + colord({ r: 34, g: 197, b: 94 }), // Emerald + colord({ r: 45, g: 212, b: 191 }), // Turquoise + colord({ r: 48, g: 178, b: 180 }), // Teal colord({ r: 52, g: 211, b: 153 }), // Spearmint - colord({ r: 167, g: 139, b: 250 }), // Periwinkle - colord({ r: 245, g: 158, b: 11 }), // Honey + colord({ r: 56, g: 189, b: 248 }), // Light Blue + colord({ r: 59, g: 130, b: 246 }), // Royal Blue + colord({ r: 67, g: 190, b: 84 }), // Fresh Green + colord({ r: 74, g: 222, b: 128 }), // Mint + colord({ r: 79, g: 70, b: 229 }), // Indigo + colord({ r: 82, g: 183, b: 136 }), // Jade + colord({ r: 96, g: 165, b: 250 }), // Sky Blue + colord({ r: 99, g: 202, b: 253 }), // Azure colord({ r: 110, g: 231, b: 183 }), // Seafoam - colord({ r: 233, g: 213, b: 255 }), // Light Lilac - colord({ r: 202, g: 138, b: 4 }), // Rich Gold - colord({ r: 151, g: 255, b: 187 }), // Fresh Mint - colord({ r: 220, g: 38, b: 38 }), // Ruby colord({ r: 124, g: 58, b: 237 }), // Royal Purple - colord({ r: 45, g: 212, b: 191 }), // Ocean - colord({ r: 252, g: 165, b: 165 }), // Peach - - // Additional 50 colors - colord({ r: 179, g: 136, b: 255 }), // Light Purple + colord({ r: 125, g: 211, b: 252 }), // Crystal Blue + colord({ r: 132, g: 204, b: 22 }), // Lime colord({ r: 133, g: 77, b: 14 }), // Chocolate - colord({ r: 52, g: 211, b: 153 }), // Aquamarine - colord({ r: 234, g: 179, b: 8 }), // Mustard - colord({ r: 236, g: 72, b: 153 }), // Hot Pink - colord({ r: 147, g: 197, b: 253 }), // Sky - colord({ r: 249, g: 115, b: 22 }), // Pumpkin - colord({ r: 167, g: 139, b: 250 }), // Iris - colord({ r: 16, g: 185, b: 129 }), // Pine - colord({ r: 251, g: 146, b: 60 }), // Mango - colord({ r: 192, g: 132, b: 252 }), // Wisteria - colord({ r: 79, g: 70, b: 229 }), // Sapphire - colord({ r: 245, g: 101, b: 101 }), // Salmon - colord({ r: 134, g: 239, b: 172 }), // Spring Green - colord({ r: 59, g: 130, b: 246 }), // Ocean Blue - colord({ r: 253, g: 164, b: 175 }), // Rose Gold - colord({ r: 16, g: 185, b: 129 }), // Forest - colord({ r: 252, g: 211, b: 77 }), // Sunshine - colord({ r: 190, g: 92, b: 251 }), // Grape - colord({ r: 82, g: 183, b: 136 }), // Eucalyptus - colord({ r: 248, g: 113, b: 113 }), // Cherry - colord({ r: 99, g: 202, b: 253 }), // Arctic - colord({ r: 240, g: 171, b: 252 }), // Lilac - colord({ r: 163, g: 230, b: 53 }), // Chartreuse - colord({ r: 234, g: 88, b: 12 }), // Rust - colord({ r: 125, g: 211, b: 252 }), // Ice Blue - colord({ r: 251, g: 113, b: 133 }), // Strawberry - colord({ r: 52, g: 211, b: 153 }), // Sage - colord({ r: 167, g: 139, b: 250 }), // Violet - colord({ r: 245, g: 158, b: 11 }), // Apricot - colord({ r: 110, g: 231, b: 183 }), // Mint Green - colord({ r: 233, g: 213, b: 255 }), // Thistle - colord({ r: 202, g: 138, b: 4 }), // Bronze - colord({ r: 151, g: 255, b: 187 }), // Pistachio - colord({ r: 220, g: 38, b: 38 }), // Fire Engine - colord({ r: 124, g: 58, b: 237 }), // Electric Purple - colord({ r: 45, g: 212, b: 191 }), // Caribbean - colord({ r: 252, g: 165, b: 165 }), // Melon - colord({ r: 168, g: 85, b: 247 }), // Byzantium - colord({ r: 74, g: 222, b: 128 }), // Kelly Green - colord({ r: 239, g: 68, b: 68 }), // Cardinal - colord({ r: 34, g: 197, b: 94 }), // Shamrock - colord({ r: 96, g: 165, b: 250 }), // Marina - colord({ r: 249, g: 115, b: 22 }), // Carrot - colord({ r: 192, g: 132, b: 252 }), // Heliotrope - colord({ r: 45, g: 212, b: 191 }), // Lagoon - colord({ r: 244, g: 114, b: 182 }), // Bubble Gum - colord({ r: 132, g: 204, b: 22 }), // Apple - colord({ r: 56, g: 189, b: 248 }), // Electric Blue - colord({ r: 234, g: 179, b: 8 }), // Daffodil + colord({ r: 134, g: 239, b: 172 }), // Light Green + colord({ r: 147, g: 51, b: 234 }), // Bright Purple + colord({ r: 147, g: 197, b: 253 }), // Powder Blue + colord({ r: 151, g: 255, b: 187 }), // Fresh Mint + colord({ r: 163, g: 230, b: 53 }), // Yellow Green + colord({ r: 167, g: 139, b: 250 }), // Periwinkle + colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple + colord({ r: 179, g: 136, b: 255 }), // Light Purple + colord({ r: 186, g: 255, b: 201 }), // Pale Emerald + colord({ r: 190, g: 92, b: 251 }), // Amethyst + colord({ r: 192, g: 132, b: 252 }), // Lavender + colord({ r: 202, g: 138, b: 4 }), // Rich Gold + colord({ r: 202, g: 225, b: 255 }), // Baby Blue + colord({ r: 204, g: 204, b: 255 }), // Soft Lavender Blue + colord({ r: 217, g: 70, b: 239 }), // Fuchsia + colord({ r: 220, g: 38, b: 38 }), // Ruby + colord({ r: 220, g: 220, b: 255 }), // Meringue Blue + colord({ r: 220, g: 240, b: 250 }), // Ice Blue + colord({ r: 230, g: 250, b: 210 }), // Pastel Lime + colord({ r: 230, g: 255, b: 250 }), // Mint Whisper + colord({ r: 233, g: 213, b: 255 }), // Light Lilac + colord({ r: 234, g: 88, b: 12 }), // Burnt Orange + colord({ r: 234, g: 179, b: 8 }), // Sunflower + colord({ r: 235, g: 75, b: 75 }), // Bright Red + colord({ r: 236, g: 72, b: 153 }), // Deep Pink + colord({ r: 239, g: 68, b: 68 }), // Crimson + colord({ r: 240, g: 171, b: 252 }), // Orchid + colord({ r: 240, g: 240, b: 200 }), // Light Khaki + colord({ r: 244, g: 114, b: 182 }), // Rose + colord({ r: 245, g: 101, b: 101 }), // Coral + colord({ r: 245, g: 158, b: 11 }), // Amber + colord({ r: 248, g: 113, b: 113 }), // Warm Red + colord({ r: 249, g: 115, b: 22 }), // Tangerine + colord({ r: 250, g: 215, b: 225 }), // Cotton Candy + colord({ r: 250, g: 250, b: 210 }), // Pastel Lemon + colord({ r: 251, g: 113, b: 133 }), // Watermelon + colord({ r: 251, g: 146, b: 60 }), // Light Orange + colord({ r: 251, g: 191, b: 36 }), // Marigold + colord({ r: 251, g: 235, b: 245 }), // Rose Powder + colord({ r: 252, g: 165, b: 165 }), // Peach + colord({ r: 252, g: 211, b: 77 }), // Golden + colord({ r: 253, g: 164, b: 175 }), // Salmon Pink + colord({ r: 255, g: 204, b: 229 }), // Blush Pink + colord({ r: 255, g: 223, b: 186 }), // Apricot Cream + colord({ r: 255, g: 240, b: 200 }), // Vanilla ]; export const botColors: Colord[] = [ @@ -266,3 +229,156 @@ export const botColors: Colord[] = [ colord({ r: 150, g: 160, b: 140 }), // Muted Dark Olive Green colord({ r: 150, g: 140, b: 150 }), // Muted Dusty Rose ]; + +// Fallback colors for when the color palette is exhausted. Currently 100 colors. +export const fallbackColors: Colord[] = [ + colord({ r: 0, g: 5, b: 0 }), // Black Mint + colord({ r: 0, g: 15, b: 0 }), // Deep Forest + colord({ r: 0, g: 25, b: 0 }), // Jungle + colord({ r: 0, g: 35, b: 0 }), // Dark Emerald + colord({ r: 0, g: 45, b: 0 }), // Green Moss + colord({ r: 0, g: 55, b: 0 }), // Moss Shadow + colord({ r: 0, g: 65, b: 0 }), // Dark Meadow + colord({ r: 0, g: 75, b: 0 }), // Forest Fern + colord({ r: 0, g: 85, b: 0 }), // Pine Leaf + colord({ r: 0, g: 95, b: 0 }), // Shadow Grass + colord({ r: 0, g: 105, b: 0 }), // Classic Green + colord({ r: 0, g: 115, b: 0 }), // Deep Lime + colord({ r: 0, g: 125, b: 0 }), // Dense Leaf + colord({ r: 0, g: 135, b: 0 }), // Basil Green + colord({ r: 0, g: 145, b: 0 }), // Organic Green + colord({ r: 0, g: 155, b: 0 }), // Bitter Herb + colord({ r: 0, g: 165, b: 0 }), // Raw Spinach + colord({ r: 0, g: 175, b: 0 }), // Woodland + colord({ r: 0, g: 185, b: 0 }), // Spring Weed + colord({ r: 0, g: 195, b: 5 }), // Apple Stem + colord({ r: 0, g: 205, b: 10 }), // Crisp Lettuce + colord({ r: 0, g: 215, b: 15 }), // Vibrant Green + colord({ r: 0, g: 225, b: 20 }), // Bright Herb + colord({ r: 0, g: 235, b: 25 }), // Green Splash + colord({ r: 0, g: 245, b: 30 }), // Mint Leaf + colord({ r: 0, g: 255, b: 35 }), // Fresh Mint + colord({ r: 10, g: 255, b: 45 }), // Neon Grass + colord({ r: 20, g: 255, b: 55 }), // Lemon Balm + colord({ r: 30, g: 255, b: 65 }), // Juicy Green + colord({ r: 40, g: 255, b: 75 }), // Pear Tint + colord({ r: 50, g: 255, b: 85 }), // Avocado Pastel + colord({ r: 60, g: 255, b: 95 }), // Lime Glow + colord({ r: 70, g: 255, b: 105 }), // Light Leaf + colord({ r: 80, g: 255, b: 115 }), // Soft Fern + colord({ r: 90, g: 255, b: 125 }), // Pastel Green + colord({ r: 100, g: 255, b: 135 }), // Green Melon + colord({ r: 110, g: 255, b: 145 }), // Herbal Mist + colord({ r: 120, g: 255, b: 155 }), // Kiwi Foam + colord({ r: 130, g: 255, b: 165 }), // Aloe Fresh + colord({ r: 140, g: 255, b: 175 }), // Light Mint + colord({ r: 150, g: 200, b: 255 }), // Cornflower Mist + colord({ r: 150, g: 255, b: 185 }), // Green Sorbet + colord({ r: 160, g: 215, b: 255 }), // Powder Blue + colord({ r: 160, g: 255, b: 195 }), // Pastel Apple + colord({ r: 170, g: 190, b: 255 }), // Periwinkle Ice + colord({ r: 170, g: 225, b: 255 }), // Baby Sky + colord({ r: 170, g: 255, b: 205 }), // Aloe Breeze + colord({ r: 180, g: 180, b: 255 }), // Pale Indigo + colord({ r: 180, g: 235, b: 250 }), // Aqua Pastel + colord({ r: 180, g: 255, b: 215 }), // Pale Mint + colord({ r: 190, g: 140, b: 195 }), // Fuchsia Tint + colord({ r: 190, g: 245, b: 240 }), // Ice Mint + colord({ r: 190, g: 255, b: 225 }), // Mint Water + colord({ r: 195, g: 145, b: 200 }), // Dusky Rose + colord({ r: 200, g: 150, b: 205 }), // Plum Frost + colord({ r: 200, g: 170, b: 255 }), // Lilac Bloom + colord({ r: 200, g: 255, b: 215 }), // Cool Aloe + colord({ r: 200, g: 255, b: 235 }), // Cool Mist + colord({ r: 205, g: 155, b: 210 }), // Berry Foam + colord({ r: 210, g: 160, b: 215 }), // Grape Cloud + colord({ r: 210, g: 255, b: 245 }), // Sea Mist + colord({ r: 215, g: 165, b: 220 }), // Light Bloom + colord({ r: 215, g: 255, b: 200 }), // Fresh Mint + colord({ r: 220, g: 160, b: 255 }), // Violet Mist + colord({ r: 220, g: 170, b: 225 }), // Cherry Blossom + colord({ r: 220, g: 255, b: 255 }), // Pale Aqua + colord({ r: 225, g: 175, b: 230 }), // Faded Rose + colord({ r: 225, g: 255, b: 175 }), // Soft Lime + colord({ r: 230, g: 180, b: 235 }), // Dreamy Mauve + colord({ r: 230, g: 250, b: 255 }), // Sky Haze + colord({ r: 235, g: 150, b: 255 }), // Orchid Glow + colord({ r: 235, g: 185, b: 240 }), // Powder Violet + colord({ r: 240, g: 190, b: 245 }), // Pastel Violet + colord({ r: 240, g: 240, b: 255 }), // Frosted Lilac + colord({ r: 240, g: 250, b: 160 }), // Citrus Wash + colord({ r: 245, g: 160, b: 240 }), // Rose Lilac + colord({ r: 245, g: 195, b: 250 }), // Soft Magenta + colord({ r: 245, g: 245, b: 175 }), // Lemon Mist + colord({ r: 250, g: 200, b: 255 }), // Lilac Cream + colord({ r: 250, g: 230, b: 255 }), // Misty Mauve + colord({ r: 255, g: 170, b: 225 }), // Bubblegum Pink + colord({ r: 255, g: 185, b: 215 }), // Blush Mist + colord({ r: 255, g: 195, b: 235 }), // Faded Fuchsia + colord({ r: 255, g: 200, b: 220 }), // Cotton Rose + colord({ r: 255, g: 205, b: 245 }), // Pastel Orchid + colord({ r: 255, g: 205, b: 255 }), // Violet Bloom + colord({ r: 255, g: 210, b: 230 }), // Pastel Blush + colord({ r: 255, g: 210, b: 250 }), // Lavender Mist + colord({ r: 255, g: 210, b: 255 }), // Orchid Mist + colord({ r: 255, g: 215, b: 195 }), // Apricot Glow + colord({ r: 255, g: 215, b: 245 }), // Rose Whisper + colord({ r: 255, g: 220, b: 235 }), // Pink Mist + colord({ r: 255, g: 220, b: 250 }), // Powder Petal + colord({ r: 255, g: 225, b: 180 }), // Butter Peach + colord({ r: 255, g: 225, b: 255 }), // Petal Mist + colord({ r: 255, g: 230, b: 245 }), // Light Rose + colord({ r: 255, g: 235, b: 200 }), // Cream Peach + colord({ r: 255, g: 235, b: 235 }), // Blushed Petal + colord({ r: 255, g: 240, b: 220 }), // Pastel Sand + colord({ r: 255, g: 245, b: 210 }), // Soft Banana +]; + +export class ColorAllocator { + private availableColors: Colord[]; + private fallbackColors: Colord[]; + private assigned = new Map(); + + constructor(colors: Colord[], fallback: Colord[]) { + this.availableColors = [...colors]; + this.fallbackColors = [...fallback]; + } + + assignColor(id: string): Colord { + if (this.assigned.has(id)) { + return this.assigned.get(id)!; + } + if (this.availableColors.length === 0) { + this.availableColors = this.fallbackColors; + } + const index = 0; + const color = this.availableColors.splice(index, 1)[0]; + this.assigned.set(id, color); + return color; + } + + assignTeamColor(team: Team): Colord { + switch (team) { + case ColoredTeams.Blue: + return blue; + case ColoredTeams.Red: + return red; + case ColoredTeams.Teal: + return teal; + case ColoredTeams.Purple: + return purple; + case ColoredTeams.Yellow: + return yellow; + case ColoredTeams.Orange: + return orange; + case ColoredTeams.Green: + return green; + case ColoredTeams.Bot: + return botColor; + default: + return this.availableColors[ + simpleHash(team) % this.availableColors.length + ]; + } + } +} diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 8846348d0..c723d9bd3 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,21 +1,14 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; -import { simpleHash } from "../Util"; -import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; +import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { - blue, - botColor, botColors, - green, + ColorAllocator, + fallbackColors, humanColors, - orange, - purple, - red, - teal, - territoryColors, - yellow, + nationColors, } from "./Colors"; import { Theme } from "./Config"; @@ -24,9 +17,12 @@ type ColorCache = Map; export class PastelTheme implements Theme { private borderColorCache: ColorCache = new Map(); private rand = new PseudoRandom(123); + private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private botColorAllocator = new ColorAllocator(botColors, botColors); + private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private nationColorAllocator = new ColorAllocator(nationColors, nationColors); private background = colord({ r: 60, g: 60, b: 60 }); - private land = colord({ r: 194, g: 193, b: 148 }); private shore = colord({ r: 204, g: 203, b: 158 }); private falloutColors = [ colord({ r: 120, g: 255, b: 71 }), // Original color @@ -45,26 +41,7 @@ export class PastelTheme implements Theme { private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); teamColor(team: Team): Colord { - switch (team) { - case ColoredTeams.Blue: - return blue; - case ColoredTeams.Red: - return red; - case ColoredTeams.Teal: - return teal; - case ColoredTeams.Purple: - return purple; - case ColoredTeams.Yellow: - return yellow; - case ColoredTeams.Orange: - return orange; - case ColoredTeams.Green: - return green; - case ColoredTeams.Bot: - return botColor; - default: - return humanColors[simpleHash(team) % humanColors.length]; - } + return this.teamColorAllocator.assignTeamColor(team); } territoryColor(player: PlayerView): Colord { @@ -73,12 +50,12 @@ export class PastelTheme implements Theme { return this.teamColor(team); } if (player.type() === PlayerType.Human) { - return humanColors[simpleHash(player.id()) % humanColors.length]; + return this.humanColorAllocator.assignColor(player.id()); } if (player.type() === PlayerType.Bot) { - return botColors[simpleHash(player.id()) % botColors.length]; + return this.botColorAllocator.assignColor(player.id()); } - return territoryColors[simpleHash(player.id()) % territoryColors.length]; + return this.nationColorAllocator.assignColor(player.id()); } textColor(player: PlayerView): string { diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index 3d428c447..467205cea 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -1,21 +1,14 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; -import { simpleHash } from "../Util"; -import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; +import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { - blue, - botColor, botColors, - green, + ColorAllocator, + fallbackColors, humanColors, - orange, - purple, - red, - teal, - territoryColors, - yellow, + nationColors, } from "./Colors"; import { Theme } from "./Config"; @@ -24,9 +17,12 @@ type ColorCache = Map; export class PastelThemeDark implements Theme { private borderColorCache: ColorCache = new Map(); private rand = new PseudoRandom(123); + private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private botColorAllocator = new ColorAllocator(botColors, botColors); + private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private nationColorAllocator = new ColorAllocator(nationColors, nationColors); private background = colord({ r: 0, g: 0, b: 0 }); - private land = colord({ r: 194, g: 193, b: 148 }); private shore = colord({ r: 134, g: 133, b: 88 }); private falloutColors = [ colord({ r: 120, g: 255, b: 71 }), // Original color @@ -45,26 +41,7 @@ export class PastelThemeDark implements Theme { private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); teamColor(team: Team): Colord { - switch (team) { - case ColoredTeams.Blue: - return blue; - case ColoredTeams.Red: - return red; - case ColoredTeams.Teal: - return teal; - case ColoredTeams.Purple: - return purple; - case ColoredTeams.Yellow: - return yellow; - case ColoredTeams.Orange: - return orange; - case ColoredTeams.Green: - return green; - case ColoredTeams.Bot: - return botColor; - default: - return humanColors[simpleHash(team) % humanColors.length]; - } + return this.teamColorAllocator.assignTeamColor(team); } territoryColor(player: PlayerView): Colord { @@ -73,12 +50,12 @@ export class PastelThemeDark implements Theme { return this.teamColor(team); } if (player.type() === PlayerType.Human) { - return humanColors[simpleHash(player.id()) % humanColors.length]; + return this.humanColorAllocator.assignColor(player.id()); } if (player.type() === PlayerType.Bot) { - return botColors[simpleHash(player.id()) % botColors.length]; + return this.botColorAllocator.assignColor(player.id()); } - return territoryColors[simpleHash(player.id()) % territoryColors.length]; + return this.nationColorAllocator.assignColor(player.id()); } textColor(player: PlayerView): string { diff --git a/tests/Colors.test.ts b/tests/Colors.test.ts new file mode 100644 index 000000000..eb04cea0c --- /dev/null +++ b/tests/Colors.test.ts @@ -0,0 +1,81 @@ +import { colord, Colord } from "colord"; +import { + blue, + botColor, + ColorAllocator, + red, + teal, +} from "../src/core/configuration/Colors"; +import { ColoredTeams } from "../src/core/game/Game"; + +const mockColors: Colord[] = [ + colord({ r: 255, g: 0, b: 0 }), + colord({ r: 0, g: 255, b: 0 }), + colord({ r: 0, g: 0, b: 255 }), +]; + +const fallbackMockColors: Colord[] = [ + colord({ r: 0, g: 0, b: 0 }), + colord({ r: 255, g: 255, b: 255 }), +]; + +describe("ColorAllocator", () => { + let allocator: ColorAllocator; + + beforeEach(() => { + allocator = new ColorAllocator(mockColors, fallbackMockColors); + }); + + test("returns a unique color for each new ID", () => { + const c1 = allocator.assignColor("a"); + const c2 = allocator.assignColor("b"); + const c3 = allocator.assignColor("c"); + + expect(c1.isEqual(c2)).toBe(false); + expect(c1.isEqual(c3)).toBe(false); + expect(c2.isEqual(c3)).toBe(false); + }); + + test("returns the same color for the same ID", () => { + const c1 = allocator.assignColor("a"); + const c2 = allocator.assignColor("a"); + + expect(c1.isEqual(c2)).toBe(true); + }); + + test("falls back when colors are exhausted", () => { + allocator.assignColor("1"); + allocator.assignColor("2"); + allocator.assignColor("3"); + const fallback = allocator.assignColor("4"); + const fallback2 = allocator.assignColor("5"); + + const match = fallbackMockColors.some((color) => color.isEqual(fallback)); + expect(match).toBe(true); + + const match2 = fallback.isEqual(fallback2); + expect(match2).toBe(false); + }); + + test("assignBotColor returns deterministic color from botColors", () => { + const allocator = new ColorAllocator(mockColors, mockColors); + + const id1 = "bot123"; + const id2 = "bot456"; + + const c1 = allocator.assignColor(id1); + const c2 = allocator.assignColor(id2); + const c1Again = allocator.assignColor(id1); + const c2Again = allocator.assignColor(id2); + + expect(c1.isEqual(c1Again)).toBe(true); + expect(c2.isEqual(c2Again)).toBe(true); + }); + + test("assignTeamColor returns the expected static color for known teams", () => { + expect(allocator.assignTeamColor(ColoredTeams.Blue)).toEqual(blue); + expect(allocator.assignTeamColor(ColoredTeams.Red)).toEqual(red); + expect(allocator.assignTeamColor(ColoredTeams.Teal)).toEqual(teal); + expect(allocator.assignTeamColor(ColoredTeams.Bot)).toEqual(botColor); + }); +}); From f0c37ec9c5a78d450de7c7e4052c8b574895db65 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 10 Jun 2025 14:43:37 -0700 Subject: [PATCH 15/19] bugfix: ColorAllocator not copying fallbackColors causing it to be empty, causing npes --- src/core/configuration/Colors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index 747862cff..d09db865e 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -349,7 +349,7 @@ export class ColorAllocator { return this.assigned.get(id)!; } if (this.availableColors.length === 0) { - this.availableColors = this.fallbackColors; + this.availableColors = [...this.fallbackColors]; } const index = 0; const color = this.availableColors.splice(index, 1)[0]; From 15a895c724c368cf2ae0bff4e511c80e04e7edf8 Mon Sep 17 00:00:00 2001 From: falc <76709589+falcolnic@users.noreply.github.com> Date: Wed, 11 Jun 2025 04:26:40 +0200 Subject: [PATCH 16/19] lazy loading and current data var (#988) ## Description: Improved loading perfomance a little bit ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: ![Screenshot from 2025-06-01 01-58-58](https://github.com/user-attachments/assets/6d74edc8-4de3-4b1f-ab9e-a61afb449a08) @qqkedsi --- src/client/ClientGameRunner.ts | 42 +++++++++++++++++++--------------- src/client/HelpModal.ts | 16 +++++++++++++ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 1bde53319..401ea2653 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -268,8 +268,10 @@ export class ClientGameRunner { }); const worker = this.worker; const keepWorkerAlive = () => { - worker.sendHeartbeat(); - requestAnimationFrame(keepWorkerAlive); + if (this.isActive) { + worker.sendHeartbeat(); + requestAnimationFrame(keepWorkerAlive); + } }; requestAnimationFrame(keepWorkerAlive); @@ -329,8 +331,10 @@ export class ClientGameRunner { } public stop(saveFullGame: boolean = false) { - this.worker.cleanup(); + if (!this.isActive) return; + this.isActive = false; + this.worker.cleanup(); this.transport.leaveGame(saveFullGame); if (this.connectionCheckInterval) { clearInterval(this.connectionCheckInterval); @@ -516,12 +520,13 @@ export class ClientGameRunner { if (this.transport.isLocal) { return; } - const timeSinceLastMessage = Date.now() - this.lastMessageTime; + const now = Date.now(); + const timeSinceLastMessage = now - this.lastMessageTime; if (timeSinceLastMessage > 5000) { console.log( `No message from server for ${timeSinceLastMessage} ms, reconnecting`, ); - this.lastMessageTime = Date.now(); + this.lastMessageTime = now; this.transport.reconnect(); } } @@ -554,26 +559,25 @@ function showErrorModal( const button = document.createElement("button"); button.textContent = translateText("error_modal.copy_clipboard"); button.className = "copy-btn"; - button.addEventListener("click", () => { - navigator.clipboard - .writeText(content) - .then(() => (button.textContent = translateText("error_modal.copied"))) - .catch( - () => (button.textContent = translateText("error_modal.failed_copy")), - ); - }); - - const closeButton = document.createElement("button"); - closeButton.textContent = "X"; - closeButton.className = "close-btn"; - closeButton.addEventListener("click", () => { - modal.style.display = "none"; + button.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(content); + button.textContent = translateText("error_modal.copied"); + } catch { + button.textContent = translateText("error_modal.failed_copy"); + } }); // Add to modal modal.appendChild(pre); modal.appendChild(button); if (closable) { + const closeButton = document.createElement("button"); + closeButton.textContent = "X"; + closeButton.className = "close-btn"; + closeButton.addEventListener("click", () => { + modal.remove(); + }); modal.appendChild(closeButton); } diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts index 0f2aebe60..ec53514b9 100644 --- a/src/client/HelpModal.ts +++ b/src/client/HelpModal.ts @@ -140,6 +140,7 @@ export class HelpModal extends LitElement { alt="Leaderboard" title="Leaderboard" class="default-image" + loading="lazy" />
@@ -159,6 +160,7 @@ export class HelpModal extends LitElement { alt="Control panel" title="Control panel" class="default-image" + loading="lazy" />
@@ -189,12 +191,14 @@ export class HelpModal extends LitElement { alt="Event panel" title="Event panel" class="default-image" + loading="lazy" /> Event panel
@@ -226,6 +230,7 @@ export class HelpModal extends LitElement { alt="Options" title="Options" class="default-image" + loading="lazy" />
@@ -253,6 +258,7 @@ export class HelpModal extends LitElement { alt="Player info overlay" title="Player info overlay" class="default-image" + loading="lazy" />
@@ -275,12 +281,14 @@ export class HelpModal extends LitElement { alt="Radial menu" title="Radial menu" class="default-image" + loading="lazy" /> Radial menu ally
@@ -295,6 +303,7 @@ export class HelpModal extends LitElement { src="/images/InfoIcon.svg" class="inline-block icon" style="fill: white; background: transparent;" + loading="lazy" /> ${translateText("help_modal.radial_info")} @@ -331,6 +340,7 @@ export class HelpModal extends LitElement { alt="Enemy info panel" title="Enemy info panel" class="info-panel-img" + loading="lazy" />
@@ -374,6 +384,7 @@ export class HelpModal extends LitElement { alt="Ally info panel" title="Ally info panel" class="info-panel-img" + loading="lazy" />
@@ -483,6 +494,7 @@ export class HelpModal extends LitElement { alt="Number 1 player" title="Number 1 player" class="player-icon-img w-full" + loading="lazy" />
@@ -499,6 +511,7 @@ export class HelpModal extends LitElement { alt="Traitor" title="Traitor" class="player-icon-img w-full" + loading="lazy" />
@@ -515,6 +528,7 @@ export class HelpModal extends LitElement { alt="Ally" title="Ally" class="player-icon-img w-full" + loading="lazy" />
@@ -533,6 +547,7 @@ export class HelpModal extends LitElement { alt="Stopped trading" title="Stopped trading" class="player-icon-img w-full" + loading="lazy" />
@@ -549,6 +564,7 @@ export class HelpModal extends LitElement { alt="Alliance Request" title="Alliance Request" class="player-icon-img w-full" + loading="lazy" /> From 9c3b828fc81b848d589de17a960027f457da9172 Mon Sep 17 00:00:00 2001 From: Ghis <23282302+ghisloufou@users.noreply.github.com> Date: Wed, 11 Jun 2025 04:54:04 +0200 Subject: [PATCH 17/19] fix(client): use the right language-modal selector (#1136) ## Description: Use the proper language-modal selector tag in initialize for the language modal. fixes https://github.com/openfrontio/OpenFrontIO/issues/865 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: ghisloufou --- src/client/Main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index a8fbf98e8..cab14d6a0 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -90,13 +90,13 @@ class Client { const langSelector = document.querySelector( "lang-selector", ) as LangSelector; - const LanguageModal = document.querySelector( - "lang-selector", + const languageModal = document.querySelector( + "language-modal", ) as LanguageModal; if (!langSelector) { console.warn("Lang selector element not found"); } - if (!LanguageModal) { + if (!languageModal) { console.warn("Language modal element not found"); } From 9b2c6cc1f6abe220969f2f0895a25b7a4a5a045d Mon Sep 17 00:00:00 2001 From: Ethienne Graveline <28783306+Egraveline@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:04:17 -0400 Subject: [PATCH 18/19] Simple Upgradable Structures (Cities, Ports, SAMs and Silos) (#1012) ## Description: https://github.com/openfrontio/OpenFrontIO/issues/776 I've implemented upgradable structures for cities and ports. As of right now this is just meant as a QOL change for structure stacking that currently happens and no gameplay changes are intended. Structure upgrades cost the same as making a new structure of that type and function the same as making a new structure of that type. I'm putting up a draft PR for this now since adding support for SAMs and Silos will take more time to handle the cooldowns and I want to make sure I'm on the right track for getting this merged. I also still need to add bot behavior for this and re-enable min distance for structures. I didn't see translations for the UnitInfoModal so I've left that out for now. I've tested locally in a single player game so far but will document and test more thoroughly before merging. ![image](https://github.com/user-attachments/assets/321a17cf-26a5-4152-aae1-6b6a691638bb) ![image](https://github.com/user-attachments/assets/8cfdabe6-f0a1-435a-a5a3-05b442427c2f) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: # Poutine --------- Co-authored-by: Scott Anderson --- resources/lang/en.json | 7 +- src/client/Transport.ts | 19 +++++ .../graphics/layers/PlayerInfoOverlay.ts | 40 ++++++++++ src/client/graphics/layers/StructureLayer.ts | 9 ++- src/client/graphics/layers/UILayer.ts | 2 +- src/client/graphics/layers/UnitInfoModal.ts | 75 ++++++++++++++++++- src/core/Schemas.ts | 14 +++- src/core/StatsSchemas.ts | 1 + src/core/configuration/DefaultConfig.ts | 14 +++- src/core/execution/ExecutionManager.ts | 3 + src/core/execution/MissileSiloExecution.ts | 12 ++- src/core/execution/SAMLauncherExecution.ts | 20 +++-- .../execution/UpgradeStructureExecution.ts | 44 +++++++++++ src/core/game/Game.ts | 9 ++- src/core/game/GameUpdates.ts | 4 +- src/core/game/GameView.ts | 10 ++- src/core/game/PlayerImpl.ts | 6 ++ src/core/game/Stats.ts | 3 + src/core/game/StatsImpl.ts | 5 ++ src/core/game/UnitImpl.ts | 47 +++++++----- tests/MissileSilo.test.ts | 19 ++++- tests/SAM.test.ts | 17 +++++ 22 files changed, 334 insertions(+), 46 deletions(-) create mode 100644 src/core/execution/UpgradeStructureExecution.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 64c491ee9..701c383fa 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -400,7 +400,8 @@ "sams": "SAMs", "warships": "Warships", "health": "Health", - "attitude": "Attitude" + "attitude": "Attitude", + "levels": "Levels" }, "events_display": { "retreating": "retreating", @@ -411,7 +412,9 @@ "unit_type_unknown": "Unknown", "close": "Close", "cooldown": "Cooldown", - "type": "Type" + "type": "Type", + "upgrade": "Upgrade", + "level": "Level" }, "relation": { "hostile": "Hostile", diff --git a/src/client/Transport.ts b/src/client/Transport.ts index e9124b6f5..b1e73fded 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -44,6 +44,13 @@ export class SendBreakAllianceIntentEvent implements GameEvent { ) {} } +export class SendUpgradeStructureIntentEvent implements GameEvent { + constructor( + public readonly unitId: number, + public readonly unitType: UnitType, + ) {} +} + export class SendAllianceReplyIntentEvent implements GameEvent { constructor( // The original alliance requestor @@ -187,6 +194,9 @@ export class Transport { this.onSendSpawnIntentEvent(e), ); this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e)); + this.eventBus.on(SendUpgradeStructureIntentEvent, (e) => + this.onSendUpgradeStructureIntent(e), + ); this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e), ); @@ -427,6 +437,15 @@ export class Transport { }); } + private onSendUpgradeStructureIntent(event: SendUpgradeStructureIntentEvent) { + this.sendIntent({ + type: "upgrade_structure", + unit: event.unitType, + clientID: this.lobbyConfig.clientID, + unitId: event.unitId, + }); + } + private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) { this.sendIntent({ type: "targetPlayer", diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 099a865e2..261fd3f0f 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -240,18 +240,58 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
${translateText("player_info_overlay.ports")}: ${player.units(UnitType.Port).length} + ${player + .units(UnitType.Port) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0) > 1 + ? html`(${translateText("player_info_overlay.levels")}: + ${player + .units(UnitType.Port) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0)})` + : ""}
${translateText("player_info_overlay.cities")}: ${player.units(UnitType.City).length} + ${player + .units(UnitType.City) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0) > 1 + ? html`(${translateText("player_info_overlay.levels")}: + ${player + .units(UnitType.City) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0)})` + : ""}
${translateText("player_info_overlay.missile_launchers")}: ${player.units(UnitType.MissileSilo).length} + ${player + .units(UnitType.MissileSilo) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0) > 1 + ? html`(${translateText("player_info_overlay.levels")}: + ${player + .units(UnitType.MissileSilo) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0)})` + : ""}
${translateText("player_info_overlay.sams")}: ${player.units(UnitType.SAMLauncher).length} + ${player + .units(UnitType.SAMLauncher) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0) > 1 + ? html`(${translateText("player_info_overlay.levels")}: + ${player + .units(UnitType.SAMLauncher) + .map((unit) => unit.level()) + .reduce((a, b) => a + b, 0)})` + : ""}
${translateText("player_info_overlay.warships")}: diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 180005d26..180e838ab 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -242,13 +242,13 @@ export class StructureLayer implements Layer { const config = this.unitConfigs[unitType]; let icon: ImageData | undefined; - if (unitType === UnitType.SAMLauncher && unit.isCooldown()) { + if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) { icon = this.unitIcons.get("reloadingSam"); } else { icon = this.unitIcons.get(iconType); } - if (unitType === UnitType.MissileSilo && unit.isCooldown()) { + if (unitType === UnitType.MissileSilo && unit.isInCooldown()) { icon = this.unitIcons.get("reloadingSilo"); } else { icon = this.unitIcons.get(iconType); @@ -268,13 +268,13 @@ export class StructureLayer implements Layer { if (!unit.isActive()) return; let borderColor = this.theme.borderColor(unit.owner()); - if (unitType === UnitType.SAMLauncher && unit.isCooldown()) { + if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) { borderColor = reloadingColor; } else if (unit.type() === UnitType.Construction) { borderColor = underConstructionColor; } - if (unitType === UnitType.MissileSilo && unit.isCooldown()) { + if (unitType === UnitType.MissileSilo && unit.isInCooldown()) { borderColor = reloadingColor; } else if (unit.type() === UnitType.Construction) { borderColor = underConstructionColor; @@ -391,6 +391,7 @@ export class StructureLayer implements Layer { const screenPos = this.transformHandler.worldToScreenCoordinates(cell); const unitTile = clickedUnit.tile(); this.unitInfoModal?.onOpenStructureModal({ + eventBus: this.eventBus, unit: clickedUnit, x: screenPos.x, y: screenPos.y, diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 9918d8c4f..c9fa49933 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -128,7 +128,7 @@ export class UILayer implements Layer { } case UnitType.SAMLauncher: case UnitType.MissileSilo: - if (unit.isActive() && unit.isCooldown()) { + if (unit.isActive() && unit.isInCooldown()) { const endTick = unit.ticksLeftInCooldown() || 0; this.drawLoadingBar(unit, endTick); } diff --git a/src/client/graphics/layers/UnitInfoModal.ts b/src/client/graphics/layers/UnitInfoModal.ts index 0066aa309..cee548768 100644 --- a/src/client/graphics/layers/UnitInfoModal.ts +++ b/src/client/graphics/layers/UnitInfoModal.ts @@ -1,8 +1,10 @@ import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { translateText } from "../../../client/Utils"; +import { EventBus } from "../../../core/EventBus"; import { UnitType } from "../../../core/game/Game"; import { GameView, UnitView } from "../../../core/game/GameView"; +import { SendUpgradeStructureIntentEvent } from "../../Transport"; import { Layer } from "./Layer"; import { StructureLayer } from "./StructureLayer"; @@ -15,6 +17,7 @@ export class UnitInfoModal extends LitElement implements Layer { public game: GameView; public structureLayer: StructureLayer | null = null; + private eventBus: EventBus; constructor() { super(); @@ -29,12 +32,14 @@ export class UnitInfoModal extends LitElement implements Layer { } public onOpenStructureModal = ({ + eventBus, unit, x, y, tileX, tileY, }: { + eventBus: EventBus; unit: UnitView; x: number; y: number; @@ -44,6 +49,7 @@ export class UnitInfoModal extends LitElement implements Layer { if (!this.game) return; this.x = x; this.y = y; + this.eventBus = eventBus; const targetRef = this.game.ref(tileX, tileY); const allUnitTypes = Object.values(UnitType); @@ -119,12 +125,44 @@ export class UnitInfoModal extends LitElement implements Layer { .close-button:hover { background: #a00; } + + .upgrade-button { + background: #3a0; + color: #fff; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + padding: 6px 12px; + } + + .upgrade-button:hover { + background: #0a0; + } `; render() { if (!this.unit) return null; - const cooldown = this.unit.ticksLeftInCooldown() ?? 0; + const ticksLeftInCooldown = this.unit.ticksLeftInCooldown(); + let configTimer; + switch (this.unit.type()) { + case UnitType.MissileSilo: + configTimer = this.game.config().SiloCooldown(); + break; + case UnitType.SAMLauncher: + configTimer = this.game.config().SAMCooldown(); + break; + } + let cooldown = 0; + if (ticksLeftInCooldown !== undefined && configTimer !== undefined) { + cooldown = configTimer - (this.game.ticks() - ticksLeftInCooldown); + } const secondsLeft = Math.ceil(cooldown / 10); return html` @@ -140,6 +178,16 @@ export class UnitInfoModal extends LitElement implements Layer { ${translateText("unit_info_modal.type")}: ${translateText(+"unit_type." + this.unit.type?.().toLowerCase()) ?? translateText("unit_info_modal.unit_type_unknown")} + ${translateText("unit_info_modal.level")}: + ${this.game.unitInfo(this.unit.type()).upgradable && + this.unit.level?.() + ? this.unit.level?.() + : ""}
${secondsLeft > 0 ? html`
@@ -147,7 +195,30 @@ export class UnitInfoModal extends LitElement implements Layer { ${secondsLeft}s
` : ""} -
+
+