diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 1fc2d5257..3ebd6aca2 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -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 = 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, }); } diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index 8ff9fabc3..ad05dfd1c 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -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(); diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 538e759ee..35fe39e93 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -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(); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index d81e2e037..86f1631df 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -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 diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index c3150fa8b..97cfa2531 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -116,7 +116,6 @@ export interface UnitUpdate { health?: number; constructionType?: UnitType; missileTimerQueue: number[]; - readyMissileCount: number; level: number; hasTrainStation: boolean; trainType?: TrainType; // Only for trains diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 6063acd81..e6bf174a8 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -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; } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 80527e53b..0e137d534 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -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()); } diff --git a/tests/client/graphics/UILayer.test.ts b/tests/client/graphics/UILayer.test.ts index 0a31d69be..c899ca079 100644 --- a/tests/client/graphics/UILayer.test.ts +++ b/tests/client/graphics/UILayer.test.ts @@ -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);