mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:10:45 +00:00
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
This commit is contained in:
@@ -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 ---
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user