Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
70314d0
Add visual troop capacity breakdown bar
binmogit Nov 4, 2025
d6f561f
Fix troop breakdown bar rendering
binmogit Nov 4, 2025
ab45db9
Rename variables for clarity: _territoryMax/_cityMax → _territoryCapa…
binmogit Nov 4, 2025
154a26f
Refactor: extract base capacity methods to avoid formula duplication
binmogit Nov 4, 2025
0763b09
Add error handling and accessibility to troop breakdown bar
binmogit Nov 4, 2025
dbd2c87
Reset _maxTroops in error handler for consistent state
binmogit Nov 4, 2025
9695ee7
Add troops on mission visual indicator to capacity bar
binmogit Nov 5, 2025
b44eca2
Merge main into feature/troops-on-mission
binmogit Nov 5, 2025
dfc90ae
Remove tooltip from troop capacity bar for simplicity
binmogit Nov 5, 2025
fe15279
Use totalUnitLevels() helper method in baseCityCapacity()
binmogit Nov 5, 2025
d23e688
Refactor: Calculate troops on mission directly from PlayerView data
binmogit Nov 5, 2025
86c853d
Apply code review optimizations
binmogit Nov 5, 2025
0b556cb
Apply micro-optimizations and remove verbose comments
binmogit Nov 5, 2025
0c9257a
Remove unnecessary comments from ControlPanel
binmogit Nov 6, 2025
8608dfc
Refactor troop capacity: rename methods and add estimation helpers
binmogit Nov 6, 2025
fb18014
Merge upstream/main into feature/troop-breakdown-visualization-with-m…
binmogit Nov 6, 2025
0e4727c
docs: add JSDoc for fork-introduced symbols
binmogit Nov 6, 2025
67b92c1
Fix capacity estimates for non-human players.
binmogit Nov 6, 2025
098ddca
Merge branch 'main' into feature/troop-breakdown-visualization-with-m…
binmogit Nov 6, 2025
42698de
Merge branch 'main' into feature/troop-breakdown-visualization-with-m…
binmogit Nov 7, 2025
bbe0457
More accurate doc
binmogit Nov 7, 2025
c17c037
Refactor: extract estimateTroopSources helper for estimatedTroopsTerr…
binmogit Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions src/client/graphics/layers/ControlPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Gold } from "../../../core/game/Game";
import { Gold, UnitType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
Expand All @@ -23,6 +23,12 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private _maxTroops: number;

@state()
private _territoryCapacity: number = 0;

@state()
private _cityCapacity: number = 0;

@state()
private troopRate: number;

Expand All @@ -35,6 +41,9 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private _gold: Gold;

@state()
private _troopsOnMission: number = 0;

private _troopRateIsIncreasing: boolean = true;

private _lastTroopIncreaseRate: number;
Expand Down Expand Up @@ -85,10 +94,44 @@ export class ControlPanel extends LitElement implements Layer {
this.updateTroopIncrease();
}

this._maxTroops = this.game.config().maxTroops(player);
this._gold = player.gold();
this._troops = player.troops();
this.troopRate = this.game.config().troopIncreaseRate(player) * 10;

// Calculate troops on mission directly from player data
const outgoingAttacks = player.outgoingAttacks();

const attackTroops = outgoingAttacks
.filter((a) => a.targetID !== 0)
.reduce((sum, attack) => sum + attack.troops, 0);

const landAttackTroops = outgoingAttacks
.filter((a) => a.targetID === 0)
.reduce((sum, attack) => sum + attack.troops, 0);

const boatTroops = player
.units(UnitType.TransportShip)
.reduce((sum, boat) => sum + boat.troops(), 0);

this._troopsOnMission = attackTroops + landAttackTroops + boatTroops;

// Compute breakdown of max troops into territory and city contributions
// Uses config methods to ensure consistency with maxTroops calculation
try {
const config = this.game.config();
this._territoryCapacity = Math.round(
config.baseTerritoryCapacity(player),
);
this._cityCapacity = Math.round(config.baseCityCapacity(player));
this._maxTroops = config.maxTroops(player);
} catch (e) {
// Fallback: clear breakdown if anything unexpected
console.warn("Failed to calculate capacity breakdown:", e);
this._territoryCapacity = 0;
this._cityCapacity = 0;
this._maxTroops = 0;
}

this.requestUpdate();
}

Expand Down Expand Up @@ -181,6 +224,56 @@ export class ControlPanel extends LitElement implements Layer {
></span
>
</div>
<!-- Max troops breakdown bar -->
<div
role="progressbar"
aria-valuenow="${this._troops + this._troopsOnMission}"
aria-valuemin="0"
aria-valuemax="${this._maxTroops}"
aria-label="Troop capacity: ${this._troops} available, ${this
._troopsOnMission} on mission, ${this._maxTroops} maximum"
class="h-1 bg-black/50 rounded-full overflow-hidden mt-2 mb-3"
>
<div
class="flex h-full"
style="width: ${this._maxTroops > 0
? Math.min(
((this._troops + this._troopsOnMission) / this._maxTroops) *
100,
100,
)
: 0}%"
>
<!-- Available troops (territory + cities) -->
<div class="flex" style="flex-grow: ${this._troops}">
<div
class="h-full opacity-60"
style="background-color: ${this.game
?.myPlayer()
?.territoryColor()
.toRgbString() ?? "rgb(147, 51, 234)"}; flex-grow: ${this
._territoryCapacity}"
></div>
${this._cityCapacity > 0
? html`<div
class="h-full opacity-80"
style="background-color: ${this.game
?.myPlayer()
?.territoryColor()
.toRgbString() ??
"rgb(59, 130, 246)"}; flex-grow: ${this._cityCapacity}"
></div>`
: ""}
</div>
<!-- Troops on mission (red with pattern for distinction) -->
${this._troopsOnMission > 0
? html`<div
class="h-full bg-red-600 opacity-50"
style="flex-grow: ${this._troopsOnMission}"
></div>`
: ""}
</div>
</div>
<div class="flex justify-between">
<span class="font-bold"
>${translateText("control_panel.gold")}:</span
Expand Down
2 changes: 2 additions & 0 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export interface Config {
// are twice more likely to be selected. X is determined below.
proximityBonusPortsNb(totalPorts: number): number;
maxTroops(player: Player | PlayerView): number;
baseTerritoryCapacity(player: Player | PlayerView): number;
baseCityCapacity(player: Player | PlayerView): number;
cityTroopIncrease(): number;
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
shellLifetime(): number;
Expand Down
15 changes: 9 additions & 6 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,16 +822,19 @@ export class DefaultConfig implements Config {
return this.infiniteTroops() ? 1_000_000 : 25_000;
}

baseTerritoryCapacity(player: Player | PlayerView): number {
return 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000);
}

baseCityCapacity(player: Player | PlayerView): number {
return player.totalUnitLevels(UnitType.City) * this.cityTroopIncrease();
}

maxTroops(player: Player | PlayerView): number {
const maxTroops =
player.type() === PlayerType.Human && this.infiniteTroops()
? 1_000_000_000
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
player
.units(UnitType.City)
.map((city) => city.level())
.reduce((a, b) => a + b, 0) *
this.cityTroopIncrease();
: this.baseTerritoryCapacity(player) + this.baseCityCapacity(player);

if (player.type() === PlayerType.Bot) {
return maxTroops / 3;
Expand Down
1 change: 1 addition & 0 deletions src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ export interface Player {
unitCount(type: UnitType): number;
unitsConstructed(type: UnitType): number;
unitsOwned(type: UnitType): number;
totalUnitLevels(type: UnitType): number;
buildableUnits(tile: TileRef | null): BuildableUnit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit<T extends UnitType>(
Expand Down
6 changes: 6 additions & 0 deletions src/core/game/PlayerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ export class PlayerImpl implements Player {
return total;
}

totalUnitLevels(type: UnitType): number {
return this.units(type)
.map((unit) => unit.level())
.reduce((a, b) => a + b, 0);
}

sharesBorderWith(other: Player | TerraNullius): boolean {
for (const border of this._borderTiles) {
for (const neighbor of this.mg.map().neighbors(border)) {
Expand Down
Loading