Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@
"leaderboard": {
"cities": "Cities",
"gold": "Gold",
"gold_per_min": "Gold/min",
"launchers": "Launchers",
"maxtroops": "Max troops",
"owned": "Owned",
Expand All @@ -812,6 +813,7 @@
"show_control": "Show Control",
"show_units": "Show Units",
"team": "Team",
"troops": "Troops",
"warships": "Warships"
},
"leaderboard_modal": {
Expand Down
103 changes: 96 additions & 7 deletions src/client/hud/layers/Leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,29 @@ import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { renderTroops, translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { UnitType } from "../../../core/game/Game";
import { Controller } from "../../Controller";
import { GoToPlayerEvent } from "../../TransformHandler";
import { formatPercentage, renderNumber } from "../../Utils";
import { GameView, PlayerView } from "../../view";

type SortKey =
| "tiles"
| "gold"
| "goldPerMinute"
| "troops"
| "maxtroops"
| "cities";

interface Entry {
name: string;
position: number;
score: string;
gold: string;
goldPerMinute: string;
troops: string;
maxTroops: string;
cities: string;
isMyPlayer: boolean;
isOnSameTeam: boolean;
player: PlayerView;
Expand All @@ -30,7 +42,7 @@ export class Leaderboard extends LitElement implements Controller {
private showTopFive = true;

@state()
private _sortKey: "tiles" | "gold" | "maxtroops" = "tiles";
private _sortKey: SortKey = "tiles";

@state()
private _sortOrder: "asc" | "desc" = "desc";
Expand All @@ -57,7 +69,7 @@ export class Leaderboard extends LitElement implements Controller {
this.updateLeaderboard();
}

private setSort(key: "tiles" | "gold" | "maxtroops") {
private setSort(key: SortKey) {
if (this._sortKey === key) {
this._sortOrder = this._sortOrder === "asc" ? "desc" : "asc";
} else {
Expand All @@ -83,6 +95,7 @@ export class Leaderboard extends LitElement implements Controller {

const sorted: PlayerViewTroopsCache[] = this.game
.playerViews()
.filter((p) => p.isAlive())
.map((p) => ({ pv: p, maxTroops: maxTroops(p) }));

switch (this._sortKey) {
Expand All @@ -91,9 +104,25 @@ export class Leaderboard extends LitElement implements Controller {
compare(Number(a.pv.gold()), Number(b.pv.gold())),
);
break;
case "goldPerMinute":
sorted.sort((a, b) =>
compare(a.pv.goldPerMinute(), b.pv.goldPerMinute()),
);
break;
case "troops":
sorted.sort((a, b) => compare(a.pv.troops(), b.pv.troops()));
break;
case "maxtroops":
sorted.sort((a, b) => compare(a.maxTroops, b.maxTroops));
break;
case "cities":
sorted.sort((a, b) =>
compare(
a.pv.totalUnitLevels(UnitType.City),
b.pv.totalUnitLevels(UnitType.City),
),
);
break;
default:
sorted.sort((a, b) =>
compare(a.pv.numTilesOwned(), b.pv.numTilesOwned()),
Expand All @@ -103,10 +132,7 @@ export class Leaderboard extends LitElement implements Controller {
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();

const alivePlayers = sorted.filter((player) => player.pv.isAlive());
const playersToShow = this.showTopFive
? alivePlayers.slice(0, 5)
: alivePlayers;
const playersToShow = this.showTopFive ? sorted.slice(0, 5) : sorted;

this.players = playersToShow.map((playerCache, index) => {
const player = playerCache.pv;
Expand All @@ -118,7 +144,10 @@ export class Leaderboard extends LitElement implements Controller {
player.numTilesOwned() / numTilesWithoutFallout,
),
gold: renderNumber(player.gold()),
goldPerMinute: renderNumber(player.goldPerMinute()),
troops: renderTroops(player.troops()),
maxTroops: renderTroops(maxTroops),
cities: renderNumber(player.totalUnitLevels(UnitType.City)),
isMyPlayer: player === myPlayer,
isOnSameTeam:
myPlayer !== null &&
Expand Down Expand Up @@ -149,7 +178,10 @@ export class Leaderboard extends LitElement implements Controller {
myPlayer.numTilesOwned() / this.game.numLandTiles(),
),
gold: renderNumber(myPlayer.gold()),
goldPerMinute: renderNumber(myPlayer.goldPerMinute()),
troops: renderTroops(myPlayer.troops()),
maxTroops: renderTroops(myPlayerMaxTroops),
cities: renderNumber(myPlayer.totalUnitLevels(UnitType.City)),
isMyPlayer: true,
isOnSameTeam: true,
player: myPlayer,
Expand Down Expand Up @@ -179,7 +211,7 @@ export class Leaderboard extends LitElement implements Controller {
>
<div
class="grid bg-gray-800/85 w-full text-xs md:text-xs lg:text-sm rounded-lg overflow-hidden"
style="grid-template-columns: minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px);"
style="grid-template-columns: minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(56px, 82px) minmax(55px, 95px) minmax(55px, 105px) minmax(42px, 62px);"
>
<div class="contents font-bold bg-gray-700/60">
<div class="py-1 md:py-2 text-center border-b border-slate-500">
Expand Down Expand Up @@ -212,6 +244,28 @@ export class Leaderboard extends LitElement implements Controller {
: "⬇️"
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
@click=${() => this.setSort("goldPerMinute")}
>
${translateText("leaderboard.gold_per_min")}
${this._sortKey === "goldPerMinute"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
@click=${() => this.setSort("troops")}
>
${translateText("leaderboard.troops")}
${this._sortKey === "troops"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
@click=${() => this.setSort("maxtroops")}
Expand All @@ -223,6 +277,17 @@ export class Leaderboard extends LitElement implements Controller {
: "⬇️"
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
@click=${() => this.setSort("cities")}
>
${translateText("leaderboard.cities")}
${this._sortKey === "cities"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</div>
</div>

${repeat(
Expand Down Expand Up @@ -267,6 +332,22 @@ export class Leaderboard extends LitElement implements Controller {
>
${player.gold}
</div>
<div
class="py-1 md:py-2 text-center ${index <
this.players.length - 1
? "border-b border-slate-500"
: ""}"
>
${player.goldPerMinute}
</div>
<div
class="py-1 md:py-2 text-center ${index <
this.players.length - 1
? "border-b border-slate-500"
: ""}"
>
${player.troops}
</div>
<div
class="py-1 md:py-2 text-center ${index <
this.players.length - 1
Expand All @@ -275,6 +356,14 @@ export class Leaderboard extends LitElement implements Controller {
>
${player.maxTroops}
</div>
<div
class="py-1 md:py-2 text-center ${index <
this.players.length - 1
? "border-b border-slate-500"
: ""}"
>
${player.cities}
</div>
</div>
`,
)}
Expand Down
1 change: 1 addition & 0 deletions src/client/render/types/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface PlayerState {
isDisconnected: boolean;
tilesOwned: number;
gold: number;
goldPerMinute: number;
troops: number;
isTraitor: boolean;
traitorRemainingTicks: number;
Expand Down
7 changes: 4 additions & 3 deletions src/client/view/GameView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,19 +394,20 @@ export class GameView implements GameMap {
player.setEmbargoSmallIDs(smallIDs);
});

// Packed per-player stats: [smallID, tilesOwned, gold, troops] quads for
// Packed per-player stats: [smallID, tilesOwned, gold, goldPerMinute, troops] records for
// every player whose stats changed this tick (the per-tick churn that no
// longer travels in PlayerUpdate objects). Applied after pass 1 so
// first-emission players exist; their quad carries the same values as
// the full update, so double-applying is harmless.
const packedStats = gu.packedPlayerUpdates;
if (packedStats !== undefined) {
for (let i = 0; i + 3 < packedStats.length; i += 4) {
for (let i = 0; i + 4 < packedStats.length; i += 5) {
const state = this._playerStates.get(packedStats[i]);
if (state === undefined) continue;
state.tilesOwned = packedStats[i + 1];
state.gold = packedStats[i + 2];
state.troops = packedStats[i + 3];
state.goldPerMinute = packedStats[i + 3];
state.troops = packedStats[i + 4];
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/client/view/PlayerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function stateFromUpdate(pu: PlayerUpdate): PlayerState {
isDisconnected: pu.isDisconnected!,
tilesOwned: pu.tilesOwned!,
gold: Number(pu.gold!),
goldPerMinute: pu.goldPerMinute!,
troops: pu.troops!,
isTraitor: pu.isTraitor!,
traitorRemainingTicks: Math.max(0, pu.traitorRemainingTicks ?? 0),
Expand Down Expand Up @@ -472,6 +473,10 @@ export class PlayerView {
return BigInt(this.state.gold);
}

goldPerMinute(): number {
return this.state.goldPerMinute;
}

troops(): number {
return this.state.troops;
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ export interface Player {

// Resources & Troops
gold(): Gold;
addGold(toAdd: Gold, tile?: TileRef): void;
addGold(toAdd: Gold, tile?: TileRef, countAsIncome?: boolean): void;
removeGold(toRemove: Gold): Gold;
troops(): number;
setTroops(troops: number): void;
Expand Down
2 changes: 1 addition & 1 deletion src/core/game/GameImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class GameImpl implements Game {

private updates: GameUpdates = createGameUpdatesMap();
private tileUpdatePairs: number[] = [];
/** [smallID, tilesOwned, gold, troops] quads — see PlayerImpl.toUpdate. */
/** [smallID, tilesOwned, gold, goldPerMinute, troops] records — see PlayerImpl.toUpdate. */
private playerStatsQuads: number[] = [];
/** [smallID, direction, index, troops] quads — see packAttackTroopDeltas. */
private attackTroopsQuads: number[] = [];
Expand Down
5 changes: 3 additions & 2 deletions src/core/game/GameUpdateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
* apply line in applyStateUpdate below. A field missing here is never diffed,
* so its changes silently never reach the main thread after the first update.
*
* EXCEPTION: tilesOwned / gold / troops are deliberately NOT diffed here.
* EXCEPTION: tilesOwned / gold / goldPerMinute / troops are deliberately NOT diffed here.
* They change for nearly every alive player every tick, so they travel on
* the transferable `GameUpdateViewData.packedPlayerUpdates` channel instead
* (see PlayerImpl.toUpdate) and appear in PlayerUpdate objects only on a
Expand Down Expand Up @@ -52,7 +52,7 @@ export function diffPlayerUpdate(
setIfDifferent("playerType", prev.playerType === next.playerType);
setIfDifferent("isAlive", prev.isAlive === next.isAlive);
setIfDifferent("isDisconnected", prev.isDisconnected === next.isDisconnected);
// tilesOwned / gold / troops intentionally absent — see EXCEPTION above.
// tilesOwned / gold / goldPerMinute / troops intentionally absent — see EXCEPTION above.
setIfDifferent("isTraitor", prev.isTraitor === next.isTraitor);
setIfDifferent(
"traitorRemainingTicks",
Expand Down Expand Up @@ -114,6 +114,7 @@ export function applyStateUpdate(target: PlayerState, pu: PlayerUpdate): void {
target.isDisconnected = pu.isDisconnected;
if (pu.tilesOwned !== undefined) target.tilesOwned = pu.tilesOwned;
if (pu.gold !== undefined) target.gold = Number(pu.gold);
if (pu.goldPerMinute !== undefined) target.goldPerMinute = pu.goldPerMinute;
if (pu.troops !== undefined) target.troops = pu.troops;
if (pu.isTraitor !== undefined) target.isTraitor = pu.isTraitor;
if (pu.traitorRemainingTicks !== undefined) {
Expand Down
6 changes: 4 additions & 2 deletions src/core/game/GameUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ export interface GameUpdateViewData {
*/
packedMotionPlans?: Uint32Array;
/**
* Packed per-player numeric stats as `[smallID, tilesOwned, gold, troops]`
* float64 quads — the fields that change for nearly every alive player
* Packed per-player numeric stats as
* `[smallID, tilesOwned, gold, goldPerMinute, troops]` float64 records —
* the fields that change for nearly every alive player
* every tick. They travel here (transferred, not structured-cloned) instead
* of in `PlayerUpdate` object diffs, which only carry them on a player's
* first emission. Gold is exact in a float64 (game values stay far below
Expand Down Expand Up @@ -228,6 +229,7 @@ export interface PlayerUpdate {
isDisconnected?: boolean;
tilesOwned?: number;
gold?: Gold;
goldPerMinute?: number;
troops?: number;
allies?: number[];
embargoes?: Set<PlayerID>;
Expand Down
Loading
Loading