Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
123 changes: 121 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,32 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private _maxTroops: number;

/**
* Maximum troops available to the player from territory tiles.
*/
@state()
private _maxTroopsTerritory: number = 0;

/**
* Maximum troops available to the player from City units.
*/
@state()
private _maxTroopsCity: number = 0;

/**
* Proportional allocation of troops that belong to territory-derived maximum troops.
* This value is computed from the config.estimatedTroopsTerritory helper.
*/
@state()
private _troopsTerritoryEstimate: number = 0;

/**
* Proportional allocation of troops available to the player that belong to city-derived maximum troops.
* This value is computed from the config.estimatedTroopsCity helper.
*/
@state()
private _troopsCityEstimate: number = 0;

@state()
private troopRate: number;

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

@state()
private _troopsOnMission: number = 0;

@state()
private _playerColor: string = "";

private _troopRateIsIncreasing: boolean = true;

private _lastTroopIncreaseRate: number;
Expand Down Expand Up @@ -85,10 +117,45 @@ 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;

const outgoingAttacks = player.outgoingAttacks();

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

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

this._troopsOnMission = attackTroops + boatTroops;

try {
const config = this.game.config();
this._maxTroopsTerritory = Math.round(config.maxTroopsTerritory(player));
this._maxTroopsCity = Math.round(config.maxTroopsCity(player));
this._maxTroops = Math.round(config.maxTroops(player));

// Get estimated breakdown of current troops
this._troopsTerritoryEstimate = config.estimatedTroopsTerritory(player);
this._troopsCityEstimate = config.estimatedTroopsCity(player);
} catch (e) {
console.warn("Failed to calculate capacity breakdown:", e);
this._maxTroopsTerritory = 0;
this._maxTroopsCity = 0;
this._maxTroops = 0;
this._troopsTerritoryEstimate = 0;
this._troopsCityEstimate = 0;
}

this._playerColor =
player.territoryColor()?.toRgbString() ??
this.game.config().theme().neutralColor().toRgbString();

this.requestUpdate();
}

Expand Down Expand Up @@ -119,6 +186,8 @@ export class ControlPanel extends LitElement implements Layer {
}

render() {
const playerColor = this._playerColor;

return html`
<style>
input[type="range"] {
Expand Down Expand Up @@ -181,6 +250,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"
aria-describedby="troop-capacity-description"
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: ${playerColor}; flex-grow: ${this
._troopsTerritoryEstimate}"
></div>
${this._troopsCityEstimate > 0
? html`<div
class="h-full opacity-80"
style="background-color: ${playerColor}; flex-grow: ${this
._troopsCityEstimate}"
></div>`
: ""}
</div>
<!-- Troops on mission -->
${this._troopsOnMission > 0
? html`<div
class="h-full bg-red-600 opacity-50"
style="flex-grow: ${this._troopsOnMission}"
></div>`
: ""}
</div>
</div>
<span id="troop-capacity-description" class="sr-only">
Colored bar segments represent territory capacity (lighter shade)
and city capacity (darker shade). Red segment shows troops currently
on mission.
</span>
<div class="flex justify-between">
<span class="font-bold"
>${translateText("control_panel.gold")}:</span
Expand Down
41 changes: 41 additions & 0 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,48 @@ export interface Config {
// When computing likelihood of trading for any given port, the X closest port
// are twice more likely to be selected. X is determined below.
proximityBonusPortsNb(totalPorts: number): number;
/**
* Returns the total maximum troop capacity for the given player.
* This is the sum of all capacity sources (territory + cities) and may be
* adjusted by player type (bot/human) or game difficulty.
* @param player The player or player view to compute capacity for.
* @returns Maximum troop capacity as a number.
*/
maxTroops(player: Player | PlayerView): number;

/**
* Maximum troop capacity derived from territory ownership (tiles).
* This represents how many troops the player's territories can hold.
* @param player The player or player view to compute territory capacity for.
* @returns The maximum number of troops capacity derived from the player's territories.
*/
maxTroopsTerritory(player: Player | PlayerView): number;

/**
* Maximum troop capacity derived from cities (unit levels of UnitType.City).
* @param player The player or player view to compute city capacity for.
* @returns The maximum number of troops capacity derived from the player's cities.
*/
maxTroopsCity(player: Player | PlayerView): number;

/**
* Helper method to estimate how many of the player's current troops are
* attributable to territory-derived capacity. These are proportional
* estimates (based on capacity ratios) and do not represent exact tracking.
* @param player The player or player view to compute an estimate for.
* @returns Estimated number of the player's troops attributable to territory capacity as a number.
*/
estimatedTroopsTerritory(player: Player | PlayerView): number;

/**
* Helper method to estimate how many of the player's current troops are
* attributable to city-derived capacity. These are proportional estimates
* (based on capacity ratios) and do not represent exact tracking.
* @param player The player or player view to compute an estimate for.
* @returns Estimated number of player's troops attributable to city capacity as a number.
*/
estimatedTroopsCity(player: Player | PlayerView): number;

cityTroopIncrease(): number;
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
shellLifetime(): number;
Expand Down
80 changes: 74 additions & 6 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,16 +825,84 @@ export class DefaultConfig implements Config {
return this.infiniteTroops() ? 1_000_000 : 25_000;
}

/**
* Compute maximum troop capacity contributed by territory (tiles owned).
* The formula scales with the number of tiles owned and includes a base offset.
* @param player Player or PlayerView used to read tiles owned.
* @returns Territory-derived troop capacity.
*/
maxTroopsTerritory(player: Player | PlayerView): number {
return 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000);
}

/**
* Compute maximum troop capacity contributed by cities.
* Capacity is proportional to the total city unit levels owned by the player.
* @param player Player or PlayerView used to read city unit levels.
* @returns City-derived troop capacity.
*/
maxTroopsCity(player: Player | PlayerView): number {
return player.totalUnitLevels(UnitType.City) * this.cityTroopIncrease();
}

/**
* Private helper to estimate troop source breakdowns for a player.
* Returns both territory and city estimates as an object.
* @param player Player or PlayerView used to compute the estimate.
*/
private estimateTroopSources(player: Player | PlayerView): {
territory: number;
city: number;
} {
const maxTroopsTerritory = this.maxTroopsTerritory(player);
const maxTroopsCity = this.maxTroopsCity(player);
const baseCapacity = maxTroopsTerritory + maxTroopsCity;
if (baseCapacity === 0) return { territory: 0, city: 0 };
const currentTroops = player.troops();
return {
territory: Math.round(
(maxTroopsTerritory / baseCapacity) * currentTroops,
),
city: Math.round((maxTroopsCity / baseCapacity) * currentTroops),
};
}

/**
* Estimate how many of the player's current troops are attributable to
* territory-derived capacity. This is a proportional estimate based on the
* ratio of territory capacity to total capacity, multiplied by current troops.
* Note: this is only an estimate and not precise tracking of troop sources.
* @param player Player or PlayerView used to compute the estimate.
* @returns Estimated number of the player's troops attributable to territory-derived capacity (proportional, not exact).
*/
estimatedTroopsTerritory(player: Player | PlayerView): number {
return this.estimateTroopSources(player).territory;
}

/**
* Estimate how many of the player's current troops are attributable to
* city-derived capacity. This is a proportional estimate based on the
* ratio of city capacity to total capacity, multiplied by current troops.
* Note: this is only an estimate and not precise tracking of troop sources.
* @param player Player or PlayerView used to compute the estimate.
* @returns Estimated number of troops attributable to city capacity as a number.
*/
estimatedTroopsCity(player: Player | PlayerView): number {
return this.estimateTroopSources(player).city;
}

/**
* Compute the player's effective maximum troop capacity. This sums territory
* and city capacities, applies special handling for infiniteTroops and
* adjusts for player type (bot/human) and game difficulty.
* @param player Player or PlayerView used to compute capacity.
* @returns Effective maximum troop capacity for the player.
*/
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.maxTroopsTerritory(player) + this.maxTroopsCity(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