From e9723d7f0d8dbe4d72a6c2ed495b2512670dbcda Mon Sep 17 00:00:00 2001 From: Ulf Date: Sat, 18 Jul 2026 23:23:07 +0500 Subject: [PATCH 1/7] Initial implementation --- src/client/InputHandler.ts | 9 ++- src/client/Transport.ts | 2 + src/client/UIState.ts | 1 + .../controllers/BuildPreviewController.ts | 10 ++- src/client/hud/GameRenderer.ts | 1 + src/client/hud/layers/BuildMenu.ts | 80 ++++++++++++++++--- src/client/hud/layers/RadialMenuElements.ts | 50 +++++++++++- src/client/view/PlayerView.ts | 8 ++ src/core/Schemas.ts | 1 + src/core/configuration/Config.ts | 13 ++- src/core/execution/ExecutionManager.ts | 2 +- .../execution/UpgradeStructureExecution.ts | 17 ++-- src/core/game/Game.ts | 2 +- 13 files changed, 171 insertions(+), 25 deletions(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index bcd99d26ca..126cdd763f 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -927,7 +927,14 @@ export class InputHandler { } private setGhostStructure(ghostStructure: PlayerBuildableUnitType | null) { - this.uiState.ghostStructure = ghostStructure; + if (this.uiState.ghostStructure === ghostStructure && ghostStructure !== null) { + const multipliers = [1, 5, 10, 25, 50]; + const idx = multipliers.indexOf(this.uiState.upgradeMultiplier || 1); + this.uiState.upgradeMultiplier = multipliers[(idx + 1) % multipliers.length]; + } else { + this.uiState.upgradeMultiplier = 1; + this.uiState.ghostStructure = ghostStructure; + } } /** diff --git a/src/client/Transport.ts b/src/client/Transport.ts index f57e761c9f..6fdf8eecb2 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -57,6 +57,7 @@ export class SendUpgradeStructureIntentEvent implements GameEvent { constructor( public readonly unitId: number, public readonly unitType: UnitType, + public readonly amount: number = 1, ) {} } @@ -518,6 +519,7 @@ export class Transport { type: "upgrade_structure", unit: event.unitType, unitId: event.unitId, + amount: event.amount, }); } diff --git a/src/client/UIState.ts b/src/client/UIState.ts index 90b8194fd5..2bb2307129 100644 --- a/src/client/UIState.ts +++ b/src/client/UIState.ts @@ -4,4 +4,5 @@ export interface UIState { attackRatio: number; ghostStructure: PlayerBuildableUnitType | null; rocketDirectionUp: boolean; + upgradeMultiplier: number; } diff --git a/src/client/controllers/BuildPreviewController.ts b/src/client/controllers/BuildPreviewController.ts index cba6cb3972..a5803c7fad 100644 --- a/src/client/controllers/BuildPreviewController.ts +++ b/src/client/controllers/BuildPreviewController.ts @@ -457,7 +457,14 @@ export class BuildPreviewController implements Controller { radiusTileY = this.game.y(upgradeTargetTile); } - const cost = u.cost; + const multiplier = u.canUpgrade !== false ? (this.uiState.upgradeMultiplier || 1) : 1; + const cost = multiplier === 1 + ? u.cost + : this.game.config().unitInfo(u.type).cost( + this.game as any, + myPlayer as any, + multiplier + ); return { ghostType: u.type, tileX: this.game.x(tileRef), @@ -508,6 +515,7 @@ export class BuildPreviewController implements Controller { new SendUpgradeStructureIntentEvent( this.ghostUnit.buildableUnit.canUpgrade, this.ghostUnit.buildableUnit.type, + this.uiState.upgradeMultiplier || 1, ), ); this.removeGhostStructure(); diff --git a/src/client/hud/GameRenderer.ts b/src/client/hud/GameRenderer.ts index dc1f92b64b..1550886a92 100644 --- a/src/client/hud/GameRenderer.ts +++ b/src/client/hud/GameRenderer.ts @@ -59,6 +59,7 @@ export function createRenderer( attackRatio: 20, ghostStructure: null, rocketDirectionUp: true, + upgradeMultiplier: 1, }; //hide when the game renders diff --git a/src/client/hud/layers/BuildMenu.ts b/src/client/hud/layers/BuildMenu.ts index 0094f3ec30..8bcca31a9e 100644 --- a/src/client/hud/layers/BuildMenu.ts +++ b/src/client/hud/layers/BuildMenu.ts @@ -356,6 +356,9 @@ export class BuildMenu extends LitElement implements Controller { @state() private _hidden = true; + @state() + private _selectedUpgradeUnit: BuildableUnit | null = null; + public canBuildOrUpgrade(item: BuildItemDisplay): boolean { if (this.game?.myPlayer() === null || this.playerBuildables === null) { return false; @@ -382,14 +385,10 @@ export class BuildMenu extends LitElement implements Controller { return player.totalUnitLevels(item.unitType).toString(); } - public sendBuildOrUpgrade(buildableUnit: BuildableUnit, tile: TileRef): void { + public handleBuildClick(buildableUnit: BuildableUnit, tile: TileRef): void { if (buildableUnit.canUpgrade !== false) { - this.eventBus.emit( - new SendUpgradeStructureIntentEvent( - buildableUnit.canUpgrade, - buildableUnit.type, - ), - ); + this._selectedUpgradeUnit = buildableUnit; + this.requestUpdate(); } else if (buildableUnit.canBuild) { const rocketDirectionUp = buildableUnit.type === UnitType.AtomBomb || @@ -399,17 +398,79 @@ export class BuildMenu extends LitElement implements Controller { this.eventBus.emit( new BuildUnitIntentEvent(buildableUnit.type, tile, rocketDirectionUp), ); + this.hideMenu(); } + } + + public confirmUpgrade(amount: number): void { + if (!this._selectedUpgradeUnit || this._selectedUpgradeUnit.canUpgrade === false) { + this.hideMenu(); + return; + } + this.eventBus.emit( + new SendUpgradeStructureIntentEvent( + this._selectedUpgradeUnit.canUpgrade, + this._selectedUpgradeUnit.type, + amount + ), + ); this.hideMenu(); } + renderAmountPanel() { + if (!this._selectedUpgradeUnit) return html``; + const unitType = this._selectedUpgradeUnit.type; + const playerGold = this.game?.myPlayer()?.gold() ?? 0n; + + return html` +
+

Select Upgrade Amount

+
+ ${[1, 5, 10, 25, 50].map((amount) => { + const cost = amount === 1 + ? this._selectedUpgradeUnit!.cost + : this.game!.config()!.unitInfo(unitType).cost( + this.game as any, + this.game!.myPlayer() as any, + amount + ); + const canAfford = playerGold >= cost; + return html` + + `; + })} +
+
+ `; + } + render() { return html`
e.preventDefault()} > - ${this.filteredBuildTable.map( + ${this._selectedUpgradeUnit + ? this.renderAmountPanel() + : this.filteredBuildTable.map( (row) => html`
${row.map((item) => { @@ -426,7 +487,7 @@ export class BuildMenu extends LitElement implements Controller { - `; - })} -
- `, - )} + (row) => html` +
+ ${row.map((item) => { + const buildableUnit = this.playerBuildables?.find( + (bu) => bu.type === item.unitType, + ); + if (buildableUnit === undefined) { + return html``; + } + const enabled = + buildableUnit.canBuild !== false || + buildableUnit.canUpgrade !== false; + return html` + + `; + })} +
+ `, + )}
`; } diff --git a/src/client/hud/layers/RadialMenuElements.ts b/src/client/hud/layers/RadialMenuElements.ts index 671367b9e4..0ad704eef1 100644 --- a/src/client/hud/layers/RadialMenuElements.ts +++ b/src/client/hud/layers/RadialMenuElements.ts @@ -455,7 +455,11 @@ function createMenuElements( const buildableUnit = params.playerActions.buildableUnits.find( (bu) => bu.type === item.unitType, ); - if (!buildableUnit || buildableUnit.canUpgrade === false || !params.buildMenu.canBuildOrUpgrade(item)) { + if ( + !buildableUnit || + buildableUnit.canUpgrade === false || + !params.buildMenu.canBuildOrUpgrade(item) + ) { return []; } return [1, 5, 10, 25, 50].map((amount) => { @@ -465,26 +469,35 @@ function createMenuElements( name: `x${amount}`, text: `x${amount}`, fontSize: "20px", - color: (p: MenuElementParams) => (p.game.myPlayer()?.gold() ?? 0n) >= cost ? COLORS.building : COLORS.disabled, + color: (p: MenuElementParams) => + (p.game.myPlayer()?.gold() ?? 0n) >= cost + ? COLORS.building + : COLORS.disabled, icon: "", tooltipItems: [ - { text: `Upgrade x${amount}`, className: "title" }, + { + text: translateText("radial_menu.upgrade_x", { + amount: amount.toString(), + }), + className: "title", + }, { text: `${renderNumber(cost)} ${translateText("player_panel.gold")}`, className: "cost", }, ], - disabled: (p: MenuElementParams) => (p.game.myPlayer()?.gold() ?? 0n) < cost, + disabled: (p: MenuElementParams) => + (p.game.myPlayer()?.gold() ?? 0n) < cost, action: (p: MenuElementParams) => { p.eventBus.emit( new SendUpgradeStructureIntentEvent( buildableUnit.canUpgrade as number, buildableUnit.type, - amount - ) + amount, + ), ); p.closeMenu(); - } + }, }; }); }, diff --git a/src/client/render/gl/passes/WorldTextPass.ts b/src/client/render/gl/passes/WorldTextPass.ts index f230342e44..3ac1cf9d1f 100644 --- a/src/client/render/gl/passes/WorldTextPass.ts +++ b/src/client/render/gl/passes/WorldTextPass.ts @@ -351,8 +351,11 @@ export class WorldTextPass { g = 0.6; b = 0.6; } - - const topTextStr = label.multiplier && label.multiplier > 1 ? `x${label.multiplier}` : undefined; + + const topTextStr = + label.multiplier && label.multiplier > 1 + ? `x${label.multiplier}` + : undefined; // The vertex shader adds +0.5 to (x, y) for tile-center alignment, so we // pass raw tile coords here — same convention as the other popup entries. diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 68c044b665..f9c2260f78 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -107,7 +107,11 @@ export class Executor { } case "upgrade_structure": - return new UpgradeStructureExecution(player, intent.unitId, intent.amount); + return new UpgradeStructureExecution( + player, + intent.unitId, + intent.amount, + ); case "delete_unit": return new DeleteUnitExecution(player, intent.unitId); case "quick_chat": diff --git a/tests/client/graphics/RadialMenuElements.test.ts b/tests/client/graphics/RadialMenuElements.test.ts index 649c6fc50d..95bd75e255 100644 --- a/tests/client/graphics/RadialMenuElements.test.ts +++ b/tests/client/graphics/RadialMenuElements.test.ts @@ -115,6 +115,7 @@ describe("RadialMenuElements", () => { cost: vi.fn(() => 100), count: vi.fn(() => 5), sendBuildOrUpgrade: vi.fn(), + handleBuildClick: vi.fn(), }; mockPlayerActions = { @@ -470,7 +471,7 @@ describe("RadialMenuElements", () => { if (cityElement!.action) { cityElement!.action(mockParams); - expect(mockBuildMenu.sendBuildOrUpgrade).toHaveBeenCalled(); + expect(mockBuildMenu.handleBuildClick).toHaveBeenCalled(); expect(mockParams.closeMenu).toHaveBeenCalled(); } }); @@ -493,7 +494,7 @@ describe("RadialMenuElements", () => { if (atomBombElement!.action) { atomBombElement!.action(mockParams); - expect(mockBuildMenu.sendBuildOrUpgrade).toHaveBeenCalled(); + expect(mockBuildMenu.handleBuildClick).toHaveBeenCalled(); expect(mockParams.closeMenu).toHaveBeenCalled(); } }); diff --git a/tests/core/executions/UpgradeStructureExecution.test.ts b/tests/core/executions/UpgradeStructureExecution.test.ts new file mode 100644 index 0000000000..4a4c015840 --- /dev/null +++ b/tests/core/executions/UpgradeStructureExecution.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { UpgradeStructureExecution } from "../../../src/core/execution/UpgradeStructureExecution"; +import { GameImpl } from "../../../src/core/game/GameImpl"; +import { UnitType } from "../../../src/core/game/Game"; +import { TestConfig } from "../../util/TestConfig"; + +describe("UpgradeStructureExecution", () => { + it("upgrades a structure the specified amount of times", () => { + const game = new GameImpl(new TestConfig()); + const player = game.addPlayer({ + id: "player1", + smallID: 1, + colorIndex: 0, + }); + + const tile = game.ref(10, 10); + player.addGold(10_000_000n); + game.addUnit(UnitType.City, tile, player); + const city = game.unit(tile)!; + + for (let i = 0; i < 25; i++) { + game.executeNextTick(); + } + + expect(city.level()).toBe(1); + + const execution = new UpgradeStructureExecution(player, city.id(), 5); + game.addExecution(execution); + game.executeNextTick(); + + expect(city.level()).toBe(6); + }); + + it("stops upgrading early if player cannot afford remaining amounts", () => { + const game = new GameImpl(new TestConfig()); + const player = game.addPlayer({ + id: "player1", + smallID: 1, + colorIndex: 0, + }); + + const tile = game.ref(10, 10); + player.addGold(125_000n + 250_000n + 250_000n); + game.addUnit(UnitType.City, tile, player); + const city = game.unit(tile)!; + + for (let i = 0; i < 25; i++) { + game.executeNextTick(); + } + + expect(city.level()).toBe(1); + + const execution = new UpgradeStructureExecution(player, city.id(), 5); + game.addExecution(execution); + game.executeNextTick(); + + expect(city.level()).toBe(3); + }); +}); From 15434c3cbd90fd31b79850f179fbb903163b680f Mon Sep 17 00:00:00 2001 From: Ulf Date: Sun, 19 Jul 2026 01:09:29 +0500 Subject: [PATCH 5/7] Fix Github CI errors --- src/client/hud/layers/RadialMenuElements.ts | 1 + tests/InputHandler.test.ts | 1 + .../UpgradeStructureExecution.test.ts | 79 +++++++++++-------- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/client/hud/layers/RadialMenuElements.ts b/src/client/hud/layers/RadialMenuElements.ts index 0ad704eef1..b78e7cde85 100644 --- a/src/client/hud/layers/RadialMenuElements.ts +++ b/src/client/hud/layers/RadialMenuElements.ts @@ -87,6 +87,7 @@ export const COLORS = { build: "#e6c74a", building: "#1e3a5f", boat: "#2a82c9", + disabled: "#94a3b8", ally: "#4ade80", breakAlly: "#dc2626", breakAllyNoDebuff: "#d97706", diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index 3bf21fee78..440b810b7e 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -65,6 +65,7 @@ describe("InputHandler AutoUpgrade", () => { attackRatio: 20, ghostStructure: null, rocketDirectionUp: true, + upgradeMultiplier: 1, }, mockCanvas, eventBus, diff --git a/tests/core/executions/UpgradeStructureExecution.test.ts b/tests/core/executions/UpgradeStructureExecution.test.ts index 4a4c015840..8b11c5d546 100644 --- a/tests/core/executions/UpgradeStructureExecution.test.ts +++ b/tests/core/executions/UpgradeStructureExecution.test.ts @@ -1,27 +1,34 @@ import { describe, expect, it } from "vitest"; import { UpgradeStructureExecution } from "../../../src/core/execution/UpgradeStructureExecution"; -import { GameImpl } from "../../../src/core/game/GameImpl"; -import { UnitType } from "../../../src/core/game/Game"; -import { TestConfig } from "../../util/TestConfig"; +import { GameType, PlayerInfo, PlayerType, UnitType, TileRef } from "../../../src/core/game/Game"; +import { setup } from "../../util/Setup"; describe("UpgradeStructureExecution", () => { - it("upgrades a structure the specified amount of times", () => { - const game = new GameImpl(new TestConfig()); - const player = game.addPlayer({ - id: "player1", - smallID: 1, - colorIndex: 0, - }); - - const tile = game.ref(10, 10); - player.addGold(10_000_000n); - game.addUnit(UnitType.City, tile, player); - const city = game.unit(tile)!; + it("upgrades a structure the specified amount of times", async () => { + const game = await setup("ocean_and_land", { gameType: GameType.Singleplayer, instantBuild: true }, [], undefined, undefined, false); + const playerInfo = new PlayerInfo("player1", PlayerType.Human, null, "player1_id"); + game.addPlayer(playerInfo); + const player = game.player("player1_id")!; - for (let i = 0; i < 25; i++) { - game.executeNextTick(); + let landTile: TileRef | undefined = undefined; + for (let y = 0; y < game.map().height(); y++) { + for (let x = 0; x < game.map().width(); x++) { + const t = game.ref(x, y); + if (game.isLand(t)) { + landTile = t; + break; + } + } + if (landTile !== undefined) break; } + player.conquer(landTile!); + const city = player.buildUnit(UnitType.City, landTile!, {}); + + game.endSpawnPhase(); + + player.addGold(10_000_000n); + expect(city.level()).toBe(1); const execution = new UpgradeStructureExecution(player, city.id(), 5); @@ -31,23 +38,31 @@ describe("UpgradeStructureExecution", () => { expect(city.level()).toBe(6); }); - it("stops upgrading early if player cannot afford remaining amounts", () => { - const game = new GameImpl(new TestConfig()); - const player = game.addPlayer({ - id: "player1", - smallID: 1, - colorIndex: 0, - }); - - const tile = game.ref(10, 10); - player.addGold(125_000n + 250_000n + 250_000n); - game.addUnit(UnitType.City, tile, player); - const city = game.unit(tile)!; - - for (let i = 0; i < 25; i++) { - game.executeNextTick(); + it("stops upgrading early if player cannot afford remaining amounts", async () => { + const game = await setup("ocean_and_land", { gameType: GameType.Singleplayer, instantBuild: true }, [], undefined, undefined, false); + const playerInfo = new PlayerInfo("player1", PlayerType.Human, null, "player1_id"); + game.addPlayer(playerInfo); + const player = game.player("player1_id")!; + + let landTile: TileRef | undefined = undefined; + for (let y = 0; y < game.map().height(); y++) { + for (let x = 0; x < game.map().width(); x++) { + const t = game.ref(x, y); + if (game.isLand(t)) { + landTile = t; + break; + } + } + if (landTile !== undefined) break; } + player.conquer(landTile!); + const city = player.buildUnit(UnitType.City, landTile!, {}); + + game.endSpawnPhase(); + + player.addGold(750_000n); + expect(city.level()).toBe(1); const execution = new UpgradeStructureExecution(player, city.id(), 5); From 9b9385c58d85c16d0227b955d63b58047b110d9b Mon Sep 17 00:00:00 2001 From: Ulf Date: Sun, 19 Jul 2026 01:18:18 +0500 Subject: [PATCH 6/7] Resolve coderabbit issues --- resources/lang/en.json | 1 + src/client/hud/layers/BuildMenu.ts | 30 +++++++++++-------- src/client/hud/layers/RadialMenuElements.ts | 4 +-- src/client/render/gl/Renderer.ts | 5 +++- src/client/render/gl/passes/WorldTextPass.ts | 9 ++---- .../UpgradeStructureExecution.test.ts | 3 +- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 2ea015b5b1..5d6b5fde61 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -84,6 +84,7 @@ }, "not_enough_money": "Not enough money", "select_upgrade_amount": "Select Upgrade Amount", + "upgrade_amount": "x{amount}", "warship_shift_hint": "Hold Shift and drag to select multiple warships at once" }, "chat": { diff --git a/src/client/hud/layers/BuildMenu.ts b/src/client/hud/layers/BuildMenu.ts index d54104e779..d00ba84f67 100644 --- a/src/client/hud/layers/BuildMenu.ts +++ b/src/client/hud/layers/BuildMenu.ts @@ -357,7 +357,7 @@ export class BuildMenu extends LitElement implements Controller { private _hidden = true; @state() - private _selectedUpgradeUnit: BuildableUnit | null = null; + private _selectedUpgradeUnitType: UnitType | null = null; public canBuildOrUpgrade(item: BuildItemDisplay): boolean { if (this.game?.myPlayer() === null || this.playerBuildables === null) { @@ -387,7 +387,7 @@ export class BuildMenu extends LitElement implements Controller { public handleBuildClick(buildableUnit: BuildableUnit, tile: TileRef): void { if (buildableUnit.canUpgrade !== false) { - this._selectedUpgradeUnit = buildableUnit; + this._selectedUpgradeUnitType = buildableUnit.type; this.requestUpdate(); } else if (buildableUnit.canBuild) { const rocketDirectionUp = @@ -403,17 +403,19 @@ export class BuildMenu extends LitElement implements Controller { } public confirmUpgrade(amount: number): void { - if ( - !this._selectedUpgradeUnit || - this._selectedUpgradeUnit.canUpgrade === false - ) { + if (!this._selectedUpgradeUnitType) { + this.hideMenu(); + return; + } + const bu = this.playerBuildables?.find((u) => u.type === this._selectedUpgradeUnitType); + if (!bu || bu.canUpgrade === false) { this.hideMenu(); return; } this.eventBus.emit( new SendUpgradeStructureIntentEvent( - this._selectedUpgradeUnit.canUpgrade, - this._selectedUpgradeUnit.type, + bu.canUpgrade, + bu.type, amount, ), ); @@ -421,8 +423,10 @@ export class BuildMenu extends LitElement implements Controller { } renderAmountPanel() { - if (!this._selectedUpgradeUnit) return html``; - const baseCost = this._selectedUpgradeUnit.cost; + if (!this._selectedUpgradeUnitType) return html``; + const bu = this.playerBuildables?.find((u) => u.type === this._selectedUpgradeUnitType); + if (!bu) return html``; + const baseCost = bu.cost; const playerGold = this.game?.myPlayer()?.gold() ?? 0n; return html` @@ -449,7 +453,7 @@ export class BuildMenu extends LitElement implements Controller { : ""} > x${amount}${translateText("build_menu.upgrade_amount", { amount: amount.toString() })} e.preventDefault()} > - ${this._selectedUpgradeUnit + ${this._selectedUpgradeUnitType ? this.renderAmountPanel() : this.filteredBuildTable.map( (row) => html` @@ -553,7 +557,7 @@ export class BuildMenu extends LitElement implements Controller { hideMenu() { this._hidden = true; - this._selectedUpgradeUnit = null; + this._selectedUpgradeUnitType = null; this.requestUpdate(); } diff --git a/src/client/hud/layers/RadialMenuElements.ts b/src/client/hud/layers/RadialMenuElements.ts index b78e7cde85..7fdf0fa545 100644 --- a/src/client/hud/layers/RadialMenuElements.ts +++ b/src/client/hud/layers/RadialMenuElements.ts @@ -467,8 +467,8 @@ function createMenuElements( const cost = buildableUnit.cost * BigInt(amount); return { id: `upgrade_${item.unitType}_${amount}`, - name: `x${amount}`, - text: `x${amount}`, + name: translateText("build_menu.upgrade_amount", { amount: amount.toString() }), + text: translateText("build_menu.upgrade_amount", { amount: amount.toString() }), fontSize: "20px", color: (p: MenuElementParams) => (p.game.myPlayer()?.gold() ?? 0n) >= cost diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index 8945c6ebea..32df0b7de6 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -28,6 +28,7 @@ import type { } from "../types"; import { Camera } from "./Camera"; import { GLUnavailableError, initGL } from "./initGL"; +import { translateText } from "../../Utils"; import { BarPass } from "./passes/BarPass"; import { BorderComputePass } from "./passes/BorderComputePass"; import { BorderStampPass } from "./passes/BorderStampPass"; @@ -986,7 +987,9 @@ export class GPURenderer { cost: data.cost, canAfford: data.canAfford, canPlace: data.canBuild || data.canUpgrade, - multiplier: data.multiplier, + topText: data.multiplier && data.multiplier > 1 + ? translateText("build_menu.upgrade_amount", { amount: data.multiplier.toString() }) + : undefined, } : null, ); diff --git a/src/client/render/gl/passes/WorldTextPass.ts b/src/client/render/gl/passes/WorldTextPass.ts index 3ac1cf9d1f..8748dd5e5f 100644 --- a/src/client/render/gl/passes/WorldTextPass.ts +++ b/src/client/render/gl/passes/WorldTextPass.ts @@ -332,7 +332,7 @@ export class WorldTextPass { cost: number; canAfford: boolean; canPlace: boolean; - multiplier?: number; + topText?: string; } | null, ): void { if (label === null) { @@ -352,11 +352,6 @@ export class WorldTextPass { b = 0.6; } - const topTextStr = - label.multiplier && label.multiplier > 1 - ? `x${label.multiplier}` - : undefined; - // The vertex shader adds +0.5 to (x, y) for tile-center alignment, so we // pass raw tile coords here — same convention as the other popup entries. // Y offset is applied in rebuildInstances (zoom-relative). @@ -364,7 +359,7 @@ export class WorldTextPass { x: label.tileX, y: label.tileY, text: renderNumber(label.cost), - topText: topTextStr, + topText: label.topText, colorR: r, colorG: g, colorB: b, diff --git a/tests/core/executions/UpgradeStructureExecution.test.ts b/tests/core/executions/UpgradeStructureExecution.test.ts index 8b11c5d546..9080278745 100644 --- a/tests/core/executions/UpgradeStructureExecution.test.ts +++ b/tests/core/executions/UpgradeStructureExecution.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { UpgradeStructureExecution } from "../../../src/core/execution/UpgradeStructureExecution"; -import { GameType, PlayerInfo, PlayerType, UnitType, TileRef } from "../../../src/core/game/Game"; +import { GameType, PlayerInfo, PlayerType, UnitType } from "../../../src/core/game/Game"; +import type { TileRef } from "../../../src/core/game/GameMap"; import { setup } from "../../util/Setup"; describe("UpgradeStructureExecution", () => { From 9364ed26cf0644268f918ce81a1389fa1676bf8a Mon Sep 17 00:00:00 2001 From: Ulf Date: Sun, 19 Jul 2026 01:25:17 +0500 Subject: [PATCH 7/7] Fix Prettier font CI --- src/client/hud/layers/BuildMenu.ts | 18 +++---- src/client/hud/layers/RadialMenuElements.ts | 8 +++- src/client/render/gl/Renderer.ts | 11 +++-- .../UpgradeStructureExecution.test.ts | 47 +++++++++++++++---- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/client/hud/layers/BuildMenu.ts b/src/client/hud/layers/BuildMenu.ts index d00ba84f67..edfe4fed1d 100644 --- a/src/client/hud/layers/BuildMenu.ts +++ b/src/client/hud/layers/BuildMenu.ts @@ -407,24 +407,24 @@ export class BuildMenu extends LitElement implements Controller { this.hideMenu(); return; } - const bu = this.playerBuildables?.find((u) => u.type === this._selectedUpgradeUnitType); + const bu = this.playerBuildables?.find( + (u) => u.type === this._selectedUpgradeUnitType, + ); if (!bu || bu.canUpgrade === false) { this.hideMenu(); return; } this.eventBus.emit( - new SendUpgradeStructureIntentEvent( - bu.canUpgrade, - bu.type, - amount, - ), + new SendUpgradeStructureIntentEvent(bu.canUpgrade, bu.type, amount), ); this.hideMenu(); } renderAmountPanel() { if (!this._selectedUpgradeUnitType) return html``; - const bu = this.playerBuildables?.find((u) => u.type === this._selectedUpgradeUnitType); + const bu = this.playerBuildables?.find( + (u) => u.type === this._selectedUpgradeUnitType, + ); if (!bu) return html``; const baseCost = bu.cost; const playerGold = this.game?.myPlayer()?.gold() ?? 0n; @@ -453,7 +453,9 @@ export class BuildMenu extends LitElement implements Controller { : ""} > ${translateText("build_menu.upgrade_amount", { amount: amount.toString() })}${translateText("build_menu.upgrade_amount", { + amount: amount.toString(), + })} (p.game.myPlayer()?.gold() ?? 0n) >= cost diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index 32df0b7de6..581fc00fb3 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -10,6 +10,7 @@ */ import type { Config } from "../../../core/configuration/Config"; +import { translateText } from "../../Utils"; import type { SpiralRibbon } from "../frame/SpiralTrails"; import type { AttackRingInput, @@ -28,7 +29,6 @@ import type { } from "../types"; import { Camera } from "./Camera"; import { GLUnavailableError, initGL } from "./initGL"; -import { translateText } from "../../Utils"; import { BarPass } from "./passes/BarPass"; import { BorderComputePass } from "./passes/BorderComputePass"; import { BorderStampPass } from "./passes/BorderStampPass"; @@ -987,9 +987,12 @@ export class GPURenderer { cost: data.cost, canAfford: data.canAfford, canPlace: data.canBuild || data.canUpgrade, - topText: data.multiplier && data.multiplier > 1 - ? translateText("build_menu.upgrade_amount", { amount: data.multiplier.toString() }) - : undefined, + topText: + data.multiplier && data.multiplier > 1 + ? translateText("build_menu.upgrade_amount", { + amount: data.multiplier.toString(), + }) + : undefined, } : null, ); diff --git a/tests/core/executions/UpgradeStructureExecution.test.ts b/tests/core/executions/UpgradeStructureExecution.test.ts index 9080278745..8557f46d1e 100644 --- a/tests/core/executions/UpgradeStructureExecution.test.ts +++ b/tests/core/executions/UpgradeStructureExecution.test.ts @@ -1,13 +1,30 @@ import { describe, expect, it } from "vitest"; import { UpgradeStructureExecution } from "../../../src/core/execution/UpgradeStructureExecution"; -import { GameType, PlayerInfo, PlayerType, UnitType } from "../../../src/core/game/Game"; +import { + GameType, + PlayerInfo, + PlayerType, + UnitType, +} from "../../../src/core/game/Game"; import type { TileRef } from "../../../src/core/game/GameMap"; import { setup } from "../../util/Setup"; describe("UpgradeStructureExecution", () => { it("upgrades a structure the specified amount of times", async () => { - const game = await setup("ocean_and_land", { gameType: GameType.Singleplayer, instantBuild: true }, [], undefined, undefined, false); - const playerInfo = new PlayerInfo("player1", PlayerType.Human, null, "player1_id"); + const game = await setup( + "ocean_and_land", + { gameType: GameType.Singleplayer, instantBuild: true }, + [], + undefined, + undefined, + false, + ); + const playerInfo = new PlayerInfo( + "player1", + PlayerType.Human, + null, + "player1_id", + ); game.addPlayer(playerInfo); const player = game.player("player1_id")!; @@ -25,9 +42,9 @@ describe("UpgradeStructureExecution", () => { player.conquer(landTile!); const city = player.buildUnit(UnitType.City, landTile!, {}); - + game.endSpawnPhase(); - + player.addGold(10_000_000n); expect(city.level()).toBe(1); @@ -40,8 +57,20 @@ describe("UpgradeStructureExecution", () => { }); it("stops upgrading early if player cannot afford remaining amounts", async () => { - const game = await setup("ocean_and_land", { gameType: GameType.Singleplayer, instantBuild: true }, [], undefined, undefined, false); - const playerInfo = new PlayerInfo("player1", PlayerType.Human, null, "player1_id"); + const game = await setup( + "ocean_and_land", + { gameType: GameType.Singleplayer, instantBuild: true }, + [], + undefined, + undefined, + false, + ); + const playerInfo = new PlayerInfo( + "player1", + PlayerType.Human, + null, + "player1_id", + ); game.addPlayer(playerInfo); const player = game.player("player1_id")!; @@ -59,9 +88,9 @@ describe("UpgradeStructureExecution", () => { player.conquer(landTile!); const city = player.buildUnit(UnitType.City, landTile!, {}); - + game.endSpawnPhase(); - + player.addGold(750_000n); expect(city.level()).toBe(1);