Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 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
0a3ee08
Merge branch 'openfrontio:main' into feature/troop-breakdown-visualiz…
binmogit Nov 9, 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
109 changes: 107 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,18 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private _maxTroops: number;

@state()
private _maxTroopsTerritory: number = 0;

@state()
private _maxTroopsCity: number = 0;

@state()
private _troopsTerritory: number = 0;

@state()
private _troopsCity: number = 0;

@state()
private troopRate: number;

Expand All @@ -35,6 +47,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 +103,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._troopsTerritory = config.estimatedTroopsTerritory(player);
this._troopsCity = config.estimatedTroopsCity(player);
} catch (e) {
console.warn("Failed to calculate capacity breakdown:", e);
this._maxTroopsTerritory = 0;
this._maxTroopsCity = 0;
this._maxTroops = 0;
this._troopsTerritory = 0;
this._troopsCity = 0;
}

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

this.requestUpdate();
}

Expand Down Expand Up @@ -119,6 +172,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 +236,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
._troopsTerritory}"
></div>
${this._troopsCity > 0
? html`<div
class="h-full opacity-80"
style="background-color: ${playerColor}; flex-grow: ${this
._troopsCity}"
></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
8 changes: 8 additions & 0 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ export interface Config {
// are twice more likely to be selected. X is determined below.
proximityBonusPortsNb(totalPorts: number): number;
maxTroops(player: Player | PlayerView): number;
maxTroopsTerritory(player: Player | PlayerView): number;
maxTroopsCity(player: Player | PlayerView): number;

// Helper methods to proportionally attribute current troops to their capacity sources
// Note: These are estimates based on capacity ratio, not actual tracking
estimatedTroopsTerritory(player: Player | PlayerView): number;
estimatedTroopsCity(player: Player | PlayerView): number;

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

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

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

estimatedTroopsTerritory(player: Player | PlayerView): number {
const maxTroops = this.maxTroops(player);
if (maxTroops === 0) return 0;

const maxTroopsTerritory = this.maxTroopsTerritory(player);
const currentTroops = player.troops();

// Proportionally attribute current troops based on territory's share of max capacity
return Math.round((maxTroopsTerritory / maxTroops) * currentTroops);
}

estimatedTroopsCity(player: Player | PlayerView): number {
const maxTroops = this.maxTroops(player);
if (maxTroops === 0) return 0;

const maxTroopsCity = this.maxTroopsCity(player);
const currentTroops = player.troops();

// Proportionally attribute current troops based on cities' share of max capacity
return Math.round((maxTroopsCity / maxTroops) * currentTroops);
}

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