From 4e8aa1f0662d4823d26696f6d96ebde876b98fe9 Mon Sep 17 00:00:00 2001 From: bijx Date: Fri, 19 Dec 2025 22:06:21 -0500 Subject: [PATCH] Feat: Add cost to ghost structure icon when using keyboard shortcuts (#2650) ## Description: Introduces a dynamic textbox under the cursor and populates it with price when a keyboard hotkey is pressed. Prices update correctly based on current value of the structure or strike being purchased, even if the value is 0 (during `Infinite Gold` mode). Price value updates live even if the price box is currently being shown (for example, when voluntarily removing a structure causes the price to change. See video below). ### Video Demo https://github.com/user-attachments/assets/3f974268-c14b-4129-9629-5a0f7db8b30c The more in depth demo was too big for GitHub, but I uploaded it on the Discord https://discord.com/channels/1284581928254701718/1447907175522504704/1451483322260914297 ### Live price updates on tooltip https://github.com/user-attachments/assets/0d98739c-6f24-4fcd-a047-cc304e7e86aa ### Works with `Infinite Gold` mode https://github.com/user-attachments/assets/25bd2919-77cd-4735-8c3f-043306f53b8f ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: bijx --- .../graphics/layers/StructureDrawingUtils.ts | 42 ++++++++++++++++- .../graphics/layers/StructureIconsLayer.ts | 47 ++++++++++++++++--- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index db34d496a..81a14aba5 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -110,7 +110,12 @@ export class SpriteFactory { ghostStage: PIXI.Container, pos: { x: number; y: number }, structureType: UnitType, - ): PIXI.Container { + ): { + container: PIXI.Container; + priceText: PIXI.BitmapText; + priceBg: PIXI.Graphics; + priceBox: { height: number; y: number; paddingX: number; minWidth: number }; + } { const parentContainer = new PIXI.Container(); const texture = this.createTexture( structureType, @@ -123,12 +128,45 @@ export class SpriteFactory { sprite.anchor.set(0.5); sprite.alpha = 0.5; parentContainer.addChild(sprite); + + const priceText = new PIXI.BitmapText({ + text: "125K", + style: { fontFamily: "round_6x6_modified", fontSize: 12 }, + }); + priceText.anchor.set(0.5); + const priceGroup = new PIXI.Container(); + const boxHeight = 18; + const boxY = + (sprite.height > 0 ? sprite.height / 2 : 16) + boxHeight / 2 + 4; + + // a way to resize the pill horizontally based on the text width + const paddingX = 8; + const minWidth = 32; + const textWidth = priceText.width; + const boxWidth = Math.max(minWidth, textWidth + paddingX * 2); + + const priceBg = new PIXI.Graphics(); + priceBg + .roundRect(-boxWidth / 2, boxY - boxHeight / 2, boxWidth, boxHeight, 4) + .fill({ color: 0x000000, alpha: 0.65 }); + + priceText.position.set(0, boxY); + + priceGroup.addChild(priceBg); + priceGroup.addChild(priceText); + parentContainer.addChild(priceGroup); + parentContainer.position.set(pos.x, pos.y); parentContainer.scale.set( Math.min(1, this.transformHandler.scale / ICON_SCALE_FACTOR_ZOOMED_OUT), ); ghostStage.addChild(parentContainer); - return parentContainer; + return { + container: parentContainer, + priceText, + priceBg, + priceBox: { height: boxHeight, y: boxY, paddingX, minWidth }, + }; } // --- internal helpers --- diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 1458e7aff..31d1caf6f 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -25,6 +25,7 @@ import { BuildUnitIntentEvent, SendUpgradeStructureIntentEvent, } from "../../Transport"; +import { renderNumber } from "../../Utils"; import { TransformHandler } from "../TransformHandler"; import { UIState } from "../UIState"; import { Layer } from "./Layer"; @@ -58,6 +59,9 @@ class StructureRenderInfo { export class StructureIconsLayer implements Layer { private ghostUnit: { container: PIXI.Container; + priceText: PIXI.BitmapText; + priceBg: PIXI.Graphics; + priceBox: { height: number; y: number; paddingX: number; minWidth: number }; range: PIXI.Container | null; rangeLevel?: number; buildableUnit: BuildableUnit; @@ -271,6 +275,7 @@ export class StructureIconsLayer implements Layer { canBuild: false, canUpgrade: false, }); + this.updateGhostPrice(0); this.ghostUnit.container.filters = [ new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }), ]; @@ -278,6 +283,7 @@ export class StructureIconsLayer implements Layer { } this.ghostUnit.buildableUnit = unit; + this.updateGhostPrice(unit.cost ?? 0); const targetLevel = this.resolveGhostRangeLevel(unit); this.updateGhostRange(targetLevel); @@ -312,6 +318,30 @@ export class StructureIconsLayer implements Layer { }); } + private updateGhostPrice(cost: bigint | number) { + if (!this.ghostUnit) return; + const { priceText, priceBg, priceBox } = this.ghostUnit; + priceText.text = renderNumber(cost); + priceText.position.set(0, priceBox.y); + + const textWidth = priceText.width; + const boxWidth = Math.max( + priceBox.minWidth, + textWidth + priceBox.paddingX * 2, + ); + + priceBg.clear(); + priceBg + .roundRect( + -boxWidth / 2, + priceBox.y - priceBox.height / 2, + boxWidth, + priceBox.height, + 4, + ) + .fill({ color: 0x000000, alpha: 0.65 }); + } + private createStructure(e: MouseUpEvent) { if (!this.ghostUnit) return; if ( @@ -367,16 +397,21 @@ export class StructureIconsLayer implements Layer { const rect = this.transformHandler.boundingRect(); const localX = this.mousePos.x - rect.left; const localY = this.mousePos.y - rect.top; + const ghost = this.factory.createGhostContainer( + player, + this.ghostStage, + { x: localX, y: localY }, + type, + ); this.ghostUnit = { - container: this.factory.createGhostContainer( - player, - this.ghostStage, - { x: localX, y: localY }, - type, - ), + container: ghost.container, + priceText: ghost.priceText, + priceBg: ghost.priceBg, + priceBox: ghost.priceBox, range: null, buildableUnit: { type, canBuild: false, canUpgrade: false, cost: 0n }, }; + this.updateGhostPrice(0); const baseLevel = this.resolveGhostRangeLevel(this.ghostUnit.buildableUnit); this.updateGhostRange(baseLevel); }