diff --git a/resources/lang/en.json b/resources/lang/en.json
index 64c491ee9..701c383fa 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -400,7 +400,8 @@
"sams": "SAMs",
"warships": "Warships",
"health": "Health",
- "attitude": "Attitude"
+ "attitude": "Attitude",
+ "levels": "Levels"
},
"events_display": {
"retreating": "retreating",
@@ -411,7 +412,9 @@
"unit_type_unknown": "Unknown",
"close": "Close",
"cooldown": "Cooldown",
- "type": "Type"
+ "type": "Type",
+ "upgrade": "Upgrade",
+ "level": "Level"
},
"relation": {
"hostile": "Hostile",
diff --git a/src/client/Transport.ts b/src/client/Transport.ts
index e9124b6f5..b1e73fded 100644
--- a/src/client/Transport.ts
+++ b/src/client/Transport.ts
@@ -44,6 +44,13 @@ export class SendBreakAllianceIntentEvent implements GameEvent {
) {}
}
+export class SendUpgradeStructureIntentEvent implements GameEvent {
+ constructor(
+ public readonly unitId: number,
+ public readonly unitType: UnitType,
+ ) {}
+}
+
export class SendAllianceReplyIntentEvent implements GameEvent {
constructor(
// The original alliance requestor
@@ -187,6 +194,9 @@ export class Transport {
this.onSendSpawnIntentEvent(e),
);
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e));
+ this.eventBus.on(SendUpgradeStructureIntentEvent, (e) =>
+ this.onSendUpgradeStructureIntent(e),
+ );
this.eventBus.on(SendBoatAttackIntentEvent, (e) =>
this.onSendBoatAttackIntent(e),
);
@@ -427,6 +437,15 @@ export class Transport {
});
}
+ private onSendUpgradeStructureIntent(event: SendUpgradeStructureIntentEvent) {
+ this.sendIntent({
+ type: "upgrade_structure",
+ unit: event.unitType,
+ clientID: this.lobbyConfig.clientID,
+ unitId: event.unitId,
+ });
+ }
+
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
this.sendIntent({
type: "targetPlayer",
diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts
index 099a865e2..261fd3f0f 100644
--- a/src/client/graphics/layers/PlayerInfoOverlay.ts
+++ b/src/client/graphics/layers/PlayerInfoOverlay.ts
@@ -240,18 +240,58 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
${translateText("player_info_overlay.ports")}:
${player.units(UnitType.Port).length}
+ ${player
+ .units(UnitType.Port)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0) > 1
+ ? html`(${translateText("player_info_overlay.levels")}:
+ ${player
+ .units(UnitType.Port)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0)})`
+ : ""}
${translateText("player_info_overlay.cities")}:
${player.units(UnitType.City).length}
+ ${player
+ .units(UnitType.City)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0) > 1
+ ? html`(${translateText("player_info_overlay.levels")}:
+ ${player
+ .units(UnitType.City)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0)})`
+ : ""}
${translateText("player_info_overlay.missile_launchers")}:
${player.units(UnitType.MissileSilo).length}
+ ${player
+ .units(UnitType.MissileSilo)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0) > 1
+ ? html`(${translateText("player_info_overlay.levels")}:
+ ${player
+ .units(UnitType.MissileSilo)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0)})`
+ : ""}
${translateText("player_info_overlay.sams")}:
${player.units(UnitType.SAMLauncher).length}
+ ${player
+ .units(UnitType.SAMLauncher)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0) > 1
+ ? html`(${translateText("player_info_overlay.levels")}:
+ ${player
+ .units(UnitType.SAMLauncher)
+ .map((unit) => unit.level())
+ .reduce((a, b) => a + b, 0)})`
+ : ""}
${translateText("player_info_overlay.warships")}:
diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts
index 180005d26..180e838ab 100644
--- a/src/client/graphics/layers/StructureLayer.ts
+++ b/src/client/graphics/layers/StructureLayer.ts
@@ -242,13 +242,13 @@ export class StructureLayer implements Layer {
const config = this.unitConfigs[unitType];
let icon: ImageData | undefined;
- if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
+ if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) {
icon = this.unitIcons.get("reloadingSam");
} else {
icon = this.unitIcons.get(iconType);
}
- if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
+ if (unitType === UnitType.MissileSilo && unit.isInCooldown()) {
icon = this.unitIcons.get("reloadingSilo");
} else {
icon = this.unitIcons.get(iconType);
@@ -268,13 +268,13 @@ export class StructureLayer implements Layer {
if (!unit.isActive()) return;
let borderColor = this.theme.borderColor(unit.owner());
- if (unitType === UnitType.SAMLauncher && unit.isCooldown()) {
+ if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) {
borderColor = reloadingColor;
} else if (unit.type() === UnitType.Construction) {
borderColor = underConstructionColor;
}
- if (unitType === UnitType.MissileSilo && unit.isCooldown()) {
+ if (unitType === UnitType.MissileSilo && unit.isInCooldown()) {
borderColor = reloadingColor;
} else if (unit.type() === UnitType.Construction) {
borderColor = underConstructionColor;
@@ -391,6 +391,7 @@ export class StructureLayer implements Layer {
const screenPos = this.transformHandler.worldToScreenCoordinates(cell);
const unitTile = clickedUnit.tile();
this.unitInfoModal?.onOpenStructureModal({
+ eventBus: this.eventBus,
unit: clickedUnit,
x: screenPos.x,
y: screenPos.y,
diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts
index 9918d8c4f..c9fa49933 100644
--- a/src/client/graphics/layers/UILayer.ts
+++ b/src/client/graphics/layers/UILayer.ts
@@ -128,7 +128,7 @@ export class UILayer implements Layer {
}
case UnitType.SAMLauncher:
case UnitType.MissileSilo:
- if (unit.isActive() && unit.isCooldown()) {
+ if (unit.isActive() && unit.isInCooldown()) {
const endTick = unit.ticksLeftInCooldown() || 0;
this.drawLoadingBar(unit, endTick);
}
diff --git a/src/client/graphics/layers/UnitInfoModal.ts b/src/client/graphics/layers/UnitInfoModal.ts
index 0066aa309..cee548768 100644
--- a/src/client/graphics/layers/UnitInfoModal.ts
+++ b/src/client/graphics/layers/UnitInfoModal.ts
@@ -1,8 +1,10 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
+import { EventBus } from "../../../core/EventBus";
import { UnitType } from "../../../core/game/Game";
import { GameView, UnitView } from "../../../core/game/GameView";
+import { SendUpgradeStructureIntentEvent } from "../../Transport";
import { Layer } from "./Layer";
import { StructureLayer } from "./StructureLayer";
@@ -15,6 +17,7 @@ export class UnitInfoModal extends LitElement implements Layer {
public game: GameView;
public structureLayer: StructureLayer | null = null;
+ private eventBus: EventBus;
constructor() {
super();
@@ -29,12 +32,14 @@ export class UnitInfoModal extends LitElement implements Layer {
}
public onOpenStructureModal = ({
+ eventBus,
unit,
x,
y,
tileX,
tileY,
}: {
+ eventBus: EventBus;
unit: UnitView;
x: number;
y: number;
@@ -44,6 +49,7 @@ export class UnitInfoModal extends LitElement implements Layer {
if (!this.game) return;
this.x = x;
this.y = y;
+ this.eventBus = eventBus;
const targetRef = this.game.ref(tileX, tileY);
const allUnitTypes = Object.values(UnitType);
@@ -119,12 +125,44 @@ export class UnitInfoModal extends LitElement implements Layer {
.close-button:hover {
background: #a00;
}
+
+ .upgrade-button {
+ background: #3a0;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+ padding: 6px 12px;
+ }
+
+ .upgrade-button:hover {
+ background: #0a0;
+ }
`;
render() {
if (!this.unit) return null;
- const cooldown = this.unit.ticksLeftInCooldown() ?? 0;
+ const ticksLeftInCooldown = this.unit.ticksLeftInCooldown();
+ let configTimer;
+ switch (this.unit.type()) {
+ case UnitType.MissileSilo:
+ configTimer = this.game.config().SiloCooldown();
+ break;
+ case UnitType.SAMLauncher:
+ configTimer = this.game.config().SAMCooldown();
+ break;
+ }
+ let cooldown = 0;
+ if (ticksLeftInCooldown !== undefined && configTimer !== undefined) {
+ cooldown = configTimer - (this.game.ticks() - ticksLeftInCooldown);
+ }
const secondsLeft = Math.ceil(cooldown / 10);
return html`
@@ -140,6 +178,16 @@ export class UnitInfoModal extends LitElement implements Layer {
${translateText("unit_info_modal.type")}:
${translateText(+"unit_type." + this.unit.type?.().toLowerCase()) ??
translateText("unit_info_modal.unit_type_unknown")}
+ ${translateText("unit_info_modal.level")}:
+ ${this.game.unitInfo(this.unit.type()).upgradable &&
+ this.unit.level?.()
+ ? this.unit.level?.()
+ : ""}
${secondsLeft > 0
? html`
@@ -147,7 +195,30 @@ export class UnitInfoModal extends LitElement implements Layer {
${secondsLeft}s
`
: ""}
-
+
+