mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:20:43 +00:00
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
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Colord } from "colord";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { Tick, UnitType } from "../../../core/game/Game";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
@@ -32,7 +32,7 @@ export class UILayer implements Layer {
|
||||
private selectionAnimTime = 0;
|
||||
private allProgressBars: Map<
|
||||
number,
|
||||
{ unit: UnitView; startTick: Tick; endTick: Tick; progressBar: ProgressBar }
|
||||
{ unit: UnitView; progressBar: ProgressBar }
|
||||
> = new Map();
|
||||
private allHealthBars: Map<number, ProgressBar> = new Map();
|
||||
// Keep track of currently selected unit
|
||||
@@ -105,21 +105,12 @@ export class UILayer implements Layer {
|
||||
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);
|
||||
const constructionType = unit.constructionType();
|
||||
if (constructionType === undefined) {
|
||||
// Skip units without construction type
|
||||
return;
|
||||
}
|
||||
this.createLoadingBar(unit);
|
||||
break;
|
||||
}
|
||||
case UnitType.Warship: {
|
||||
@@ -127,24 +118,10 @@ export class UILayer implements Layer {
|
||||
break;
|
||||
}
|
||||
case UnitType.MissileSilo:
|
||||
if (
|
||||
unit.isActive() &&
|
||||
unit.isInCooldown() &&
|
||||
!this.allProgressBars.has(unit.id())
|
||||
) {
|
||||
const endTick = this.game.config().SiloCooldown();
|
||||
this.drawLoadingBar(unit, endTick);
|
||||
}
|
||||
this.createLoadingBar(unit);
|
||||
break;
|
||||
case UnitType.SAMLauncher:
|
||||
if (
|
||||
unit.isActive() &&
|
||||
unit.isInCooldown() &&
|
||||
!this.allProgressBars.has(unit.id())
|
||||
) {
|
||||
const endTick = this.game.config().SAMCooldown();
|
||||
this.drawLoadingBar(unit, endTick);
|
||||
}
|
||||
this.createLoadingBar(unit);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@@ -318,20 +295,41 @@ export class UILayer implements Layer {
|
||||
}
|
||||
|
||||
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()) {
|
||||
const progress = this.getProgress(progressBarInfo.unit);
|
||||
if (progress >= 1) {
|
||||
this.allProgressBars.get(unitId)?.progressBar.clear();
|
||||
this.allProgressBars.delete(unitId);
|
||||
return;
|
||||
} else {
|
||||
progressBarInfo.progressBar.setProgress(progress);
|
||||
}
|
||||
progressBarInfo.progressBar.setProgress(progress);
|
||||
});
|
||||
}
|
||||
|
||||
public drawLoadingBar(unit: UnitView, endTick: Tick) {
|
||||
private getProgress(unit: UnitView): number {
|
||||
if (!unit.isActive()) {
|
||||
return 1;
|
||||
}
|
||||
switch (unit.type()) {
|
||||
case UnitType.Construction:
|
||||
const constructionType = unit.constructionType();
|
||||
if (constructionType === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return (
|
||||
(this.game.ticks() - unit.createdAt()) /
|
||||
(this.game.unitInfo(constructionType).constructionDuration || 1)
|
||||
);
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.SAMLauncher:
|
||||
return unit.missileReadinesss();
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public createLoadingBar(unit: UnitView) {
|
||||
if (!this.context) {
|
||||
return;
|
||||
}
|
||||
@@ -347,8 +345,6 @@ export class UILayer implements Layer {
|
||||
);
|
||||
this.allProgressBars.set(unit.id(), {
|
||||
unit,
|
||||
startTick: this.game.ticks(),
|
||||
endTick,
|
||||
progressBar,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,16 +34,14 @@ export class MissileSiloExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const frontTime = this.silo.ticksLeftInCooldown();
|
||||
// frontTime is the time the earliest missile fired.
|
||||
const frontTime = this.silo.missileTimerQueue()[0];
|
||||
if (frontTime === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cooldown =
|
||||
this.mg.config().SiloCooldown() - (this.mg.ticks() - frontTime);
|
||||
if (typeof cooldown === "number" && cooldown >= 0) {
|
||||
this.silo.touch();
|
||||
}
|
||||
|
||||
if (cooldown <= 0) {
|
||||
this.silo.reloadMissile();
|
||||
|
||||
@@ -190,16 +190,13 @@ export class SAMLauncherExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
const frontTime = this.sam.ticksLeftInCooldown();
|
||||
const frontTime = this.sam.missileTimerQueue()[0];
|
||||
if (frontTime === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cooldown =
|
||||
this.mg.config().SAMCooldown() - (this.mg.ticks() - frontTime);
|
||||
if (typeof cooldown === "number" && cooldown >= 0) {
|
||||
this.sam.touch();
|
||||
}
|
||||
|
||||
if (cooldown <= 0) {
|
||||
this.sam.reloadMissile();
|
||||
|
||||
@@ -438,7 +438,7 @@ export interface Unit {
|
||||
launch(): void;
|
||||
reloadMissile(): void;
|
||||
isInCooldown(): boolean;
|
||||
ticksLeftInCooldown(): Tick | undefined;
|
||||
missileTimerQueue(): number[];
|
||||
|
||||
// Trade Ships
|
||||
setSafeFromPirates(): void; // Only for trade ships
|
||||
|
||||
@@ -116,7 +116,6 @@ export interface UnitUpdate {
|
||||
health?: number;
|
||||
constructionType?: UnitType;
|
||||
missileTimerQueue: number[];
|
||||
readyMissileCount: number;
|
||||
level: number;
|
||||
hasTrainStation: boolean;
|
||||
trainType?: TrainType; // Only for trains
|
||||
|
||||
@@ -47,12 +47,18 @@ interface PlayerCosmetics {
|
||||
export class UnitView {
|
||||
public _wasUpdated = true;
|
||||
public lastPos: TileRef[] = [];
|
||||
private _createdAt: Tick;
|
||||
|
||||
constructor(
|
||||
private gameView: GameView,
|
||||
private data: UnitUpdate,
|
||||
) {
|
||||
this.lastPos.push(data.pos);
|
||||
this._createdAt = this.gameView.ticks();
|
||||
}
|
||||
|
||||
createdAt(): Tick {
|
||||
return this._createdAt;
|
||||
}
|
||||
|
||||
wasUpdated(): boolean {
|
||||
@@ -123,12 +129,40 @@ export class UnitView {
|
||||
targetTile(): TileRef | undefined {
|
||||
return this.data.targetTile;
|
||||
}
|
||||
ticksLeftInCooldown(): Tick | undefined {
|
||||
return this.data.missileTimerQueue?.[0];
|
||||
}
|
||||
isInCooldown(): boolean {
|
||||
return this.data.readyMissileCount === 0;
|
||||
|
||||
// How "ready" this unit is from 0 to 1.
|
||||
missileReadinesss(): number {
|
||||
const maxMissiles = this.data.level;
|
||||
const missilesReloading = this.data.missileTimerQueue.length;
|
||||
|
||||
if (missilesReloading === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const missilesReady = maxMissiles - missilesReloading;
|
||||
|
||||
if (missilesReady === 0 && maxMissiles > 1) {
|
||||
// Unless we have just one missile (level 1),
|
||||
// show 0% readiness so user knows no missiles are ready.
|
||||
return 0;
|
||||
}
|
||||
|
||||
let readiness = missilesReady / maxMissiles;
|
||||
|
||||
const cooldownDuration =
|
||||
this.data.unitType === UnitType.SAMLauncher
|
||||
? this.gameView.config().SAMCooldown()
|
||||
: this.gameView.config().SiloCooldown();
|
||||
|
||||
for (const cooldown of this.data.missileTimerQueue) {
|
||||
const cooldownProgress = this.gameView.ticks() - cooldown;
|
||||
const cooldownRatio = cooldownProgress / cooldownDuration;
|
||||
const adjusted = cooldownRatio / maxMissiles;
|
||||
readiness += adjusted;
|
||||
}
|
||||
return readiness;
|
||||
}
|
||||
|
||||
level(): number {
|
||||
return this.data.level;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export class UnitImpl implements Unit {
|
||||
private _constructionType: UnitType | undefined;
|
||||
private _lastOwner: PlayerImpl | null = null;
|
||||
private _troops: number;
|
||||
// Number of missiles in cooldown, if empty all missiles are ready.
|
||||
private _missileTimerQueue: number[] = [];
|
||||
private _readyMissileCount: number = 1;
|
||||
private _hasTrainStation: boolean = false;
|
||||
private _patrolTile: TileRef | undefined;
|
||||
private _level: number = 1;
|
||||
@@ -128,7 +128,6 @@ export class UnitImpl implements Unit {
|
||||
targetUnitId: this._targetUnit?.id() ?? undefined,
|
||||
targetTile: this.targetTile() ?? undefined,
|
||||
missileTimerQueue: this._missileTimerQueue,
|
||||
readyMissileCount: this._readyMissileCount,
|
||||
level: this.level(),
|
||||
hasTrainStation: this._hasTrainStation,
|
||||
trainType: this._trainType,
|
||||
@@ -297,7 +296,6 @@ export class UnitImpl implements Unit {
|
||||
|
||||
launch(): void {
|
||||
this._missileTimerQueue.push(this.mg.ticks());
|
||||
this._readyMissileCount--;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
@@ -306,12 +304,15 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
isInCooldown(): boolean {
|
||||
return this._readyMissileCount === 0;
|
||||
return this._missileTimerQueue.length === this._level;
|
||||
}
|
||||
|
||||
missileTimerQueue(): number[] {
|
||||
return this._missileTimerQueue;
|
||||
}
|
||||
|
||||
reloadMissile(): void {
|
||||
this._missileTimerQueue.shift();
|
||||
this._readyMissileCount++;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
@@ -374,7 +375,7 @@ export class UnitImpl implements Unit {
|
||||
increaseLevel(): void {
|
||||
this._level++;
|
||||
if ([UnitType.MissileSilo, UnitType.SAMLauncher].includes(this.type())) {
|
||||
this._readyMissileCount++;
|
||||
this._missileTimerQueue.push(this.mg.ticks());
|
||||
}
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("UILayer", () => {
|
||||
tile: () => ({}),
|
||||
owner: () => ({}),
|
||||
isActive: () => true,
|
||||
createdAt: () => 1,
|
||||
} as unknown as UnitView;
|
||||
ui.drawHealthBar(unit);
|
||||
expect(ui["allHealthBars"].has(1)).toBe(true);
|
||||
@@ -111,7 +112,7 @@ describe("UILayer", () => {
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
} as unknown as UnitView;
|
||||
ui.drawLoadingBar(unit, 5);
|
||||
ui.createLoadingBar(unit);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -145,6 +146,7 @@ describe("UILayer", () => {
|
||||
owner: () => ({ id: () => 1 }),
|
||||
tile: () => ({}),
|
||||
isActive: () => true,
|
||||
createdAt: () => 1,
|
||||
} as unknown as UnitView;
|
||||
ui.onUnitEvent(unit);
|
||||
expect(ui["allProgressBars"].has(2)).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user