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 <evanpelle@gmail.com>
This commit is contained in:
Vivacious Box
2025-06-10 23:50:31 +03:00
committed by evanpelle
parent 1cfeaf8c2a
commit c4895df40f
6 changed files with 1657 additions and 19 deletions
+61
View File
@@ -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;
}
}
+127 -5
View File
@@ -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<number, ProgressBar> = 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) {