Files
evanpelle 488ffc5c9e Improve unit updates & reloading (#1394)
## Description:

Previously upgrading a unit would have it reload immediately, eg
upgrading missile silo 1=>2 gives it 2 missiles immediately. With this
change it must reload the missile. Same with SAMs. This prevents users
from spamming upgrades as missiles are coming in.

Fix the progress reloading bar. Previously it only showed the SAM/silo
as reloading when it was completely out of missiles. Now it shows
roughly how many missiles are available (eg level 5 fires 3 missiles,
now progress bar is at 40%). It also shows progress of missiles
reloading. If no missiles are available, progress bar is empty

There was a bug where if a silo of SAM was in cooldown it would be
updated each tick, causing the sprite/icon to be rerendered each tick.

## 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
2025-07-10 10:06:05 -07:00

159 lines
4.5 KiB
TypeScript

/**
* @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,
createdAt: () => 1,
} 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 remove health bars for inactive units", () => {
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);
// an inactive unit doesnt have a health bar
unit.isActive = () => false;
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.createLoadingBar(unit);
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,
createdAt: () => 1,
} 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);
});
});