diff --git a/resources/lang/en.json b/resources/lang/en.json
index c9adfcf33..3cb7cfba5 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -233,7 +233,8 @@
"naval_trade": "Tradeship",
"conquest_gold": "Conquered player gold",
"stolen_gold": "Stolen with warships",
- "num_of_conquests": "Number of conquered players",
+ "num_of_conquests_humans": "Player kills",
+ "num_of_conquests_bots": "Bot kills",
"duration": "Duration",
"survival_time": "Survival time",
"war": "War",
diff --git a/src/client/components/baseComponents/ranking/GameInfoRanking.ts b/src/client/components/baseComponents/ranking/GameInfoRanking.ts
index fa78d36f0..dff65db0f 100644
--- a/src/client/components/baseComponents/ranking/GameInfoRanking.ts
+++ b/src/client/components/baseComponents/ranking/GameInfoRanking.ts
@@ -5,10 +5,14 @@ import {
GOLD_INDEX_TRAIN_OTHER,
GOLD_INDEX_TRAIN_SELF,
GOLD_INDEX_WAR,
+ PLAYER_INDEX_BOT,
+ PLAYER_INDEX_HUMAN,
+ PLAYER_INDEX_NATION,
} from "../../../../core/StatsSchemas";
export enum RankType {
- Conquests = "Conquests",
+ ConquestHumans = "ConquestHumans",
+ ConquestBots = "ConquestBots",
Atoms = "Atoms",
Hydros = "Hydros",
MIRV = "MIRV",
@@ -27,7 +31,7 @@ export interface PlayerInfo {
tag?: string;
killedAt?: number;
gold: bigint[];
- conquests: number;
+ conquests: bigint[];
flag?: string;
winner: boolean;
atoms: number;
@@ -79,12 +83,13 @@ export class Ranking {
username = match[2];
}
const gold = (stats.gold ?? []).map((v) => BigInt(v ?? 0));
+ const conquests = (stats.conquests ?? []).map((v) => BigInt(v ?? 0));
players[player.clientID] = {
id: player.clientID,
rawUsername: player.username,
username,
tag: player.clanTag,
- conquests: Number(stats.conquests) || 0,
+ conquests,
flag: player.cosmetics?.flag ?? undefined,
killedAt: stats.killedAt !== null ? Number(stats.killedAt) : undefined,
gold,
@@ -125,8 +130,13 @@ export class Ranking {
return (player.killedAt / Math.max(this.duration, 1)) * 10;
}
return 100;
- case RankType.Conquests:
- return player.conquests;
+ case RankType.ConquestHumans:
+ return Number(player.conquests[PLAYER_INDEX_HUMAN] ?? 0n);
+ case RankType.ConquestBots:
+ return (
+ Number(player.conquests[PLAYER_INDEX_BOT] ?? 0n) +
+ Number(player.conquests[PLAYER_INDEX_NATION] ?? 0n)
+ );
case RankType.Atoms:
return player.atoms;
case RankType.Hydros:
diff --git a/src/client/components/baseComponents/ranking/PlayerRow.ts b/src/client/components/baseComponents/ranking/PlayerRow.ts
index 773188c8b..ed3cfc6ba 100644
--- a/src/client/components/baseComponents/ranking/PlayerRow.ts
+++ b/src/client/components/baseComponents/ranking/PlayerRow.ts
@@ -63,7 +63,8 @@ export class PlayerRow extends LitElement {
private renderPlayerInfo() {
switch (this.rankType) {
case RankType.Lifetime:
- case RankType.Conquests:
+ case RankType.ConquestHumans:
+ case RankType.ConquestBots:
return this.renderScoreAsBar();
case RankType.Atoms:
case RankType.Hydros:
diff --git a/src/client/components/baseComponents/ranking/RankingControls.ts b/src/client/components/baseComponents/ranking/RankingControls.ts
index e32933efe..527681c7f 100644
--- a/src/client/components/baseComponents/ranking/RankingControls.ts
+++ b/src/client/components/baseComponents/ranking/RankingControls.ts
@@ -10,19 +10,25 @@ const economyRankings = new Set([
RankType.NavalTrade,
RankType.TrainTrade,
]);
-const tradeRankings = new Set([RankType.NavalTrade, RankType.TrainTrade]);
-const bombRankings = new Set([RankType.Atoms, RankType.Hydros, RankType.MIRV]);
const warRankings = new Set([
- RankType.Conquests,
+ RankType.ConquestHumans,
+ RankType.ConquestBots,
RankType.Atoms,
RankType.Hydros,
RankType.MIRV,
]);
+const tradeRankings = new Set([RankType.NavalTrade, RankType.TrainTrade]);
+const bombRankings = new Set([RankType.Atoms, RankType.Hydros, RankType.MIRV]);
+const conquestRankings = new Set([
+ RankType.ConquestHumans,
+ RankType.ConquestBots,
+]);
const isEconomyRanking = (t: RankType) => economyRankings.has(t);
const isTradeRanking = (t: RankType) => tradeRankings.has(t);
const isBombRanking = (t: RankType) => bombRankings.has(t);
const isWarRanking = (t: RankType) => warRankings.has(t);
+const isConquestRanking = (t: RankType) => conquestRankings.has(t);
@customElement("ranking-controls")
export class RankingControls extends LitElement {
@@ -41,7 +47,7 @@ export class RankingControls extends LitElement {
"game_info_modal.duration",
)}
${this.renderButton(
- RankType.Conquests,
+ RankType.ConquestHumans,
isWarRanking(this.rankType),
"game_info_modal.war",
)}
@@ -78,8 +84,8 @@ export class RankingControls extends LitElement {
"game_info_modal.bombs",
)}
${this.renderSubButton(
- RankType.Conquests,
- this.rankType === RankType.Conquests,
+ RankType.ConquestHumans,
+ isConquestRanking(this.rankType),
"game_info_modal.conquests",
)}
diff --git a/src/client/components/baseComponents/ranking/RankingHeader.ts b/src/client/components/baseComponents/ranking/RankingHeader.ts
index dae3db3c1..cd1270d15 100644
--- a/src/client/components/baseComponents/ranking/RankingHeader.ts
+++ b/src/client/components/baseComponents/ranking/RankingHeader.ts
@@ -14,7 +14,7 @@ export class RankingHeader extends LitElement {
render() {
return html`
${this.renderHeaderContent()}
@@ -27,10 +27,21 @@ export class RankingHeader extends LitElement {
return html`
${translateText("game_info_modal.survival_time")}
`;
- case RankType.Conquests:
- return html`
- ${translateText("game_info_modal.num_of_conquests")}
-
`;
+ case RankType.ConquestHumans:
+ case RankType.ConquestBots:
+ return html`
+
+ ${this.renderMultipleChoiceHeaderButton(
+ translateText("game_info_modal.num_of_conquests_humans"),
+ RankType.ConquestHumans,
+ )}
+ /
+ ${this.renderMultipleChoiceHeaderButton(
+ translateText("game_info_modal.num_of_conquests_bots"),
+ RankType.ConquestBots,
+ )}
+
+ `;
case RankType.Atoms:
case RankType.Hydros:
case RankType.MIRV:
diff --git a/src/client/components/baseComponents/stats/PlayerStatsTree.ts b/src/client/components/baseComponents/stats/PlayerStatsTree.ts
index e703ee2a2..1f412e00f 100644
--- a/src/client/components/baseComponents/stats/PlayerStatsTree.ts
+++ b/src/client/components/baseComponents/stats/PlayerStatsTree.ts
@@ -149,7 +149,7 @@ export class PlayerStatsTreeView extends LitElement {
attacks: this.mergeStatArrays(base.attacks, next.attacks),
betrayals: this.mergeStatValue(base.betrayals, next.betrayals),
killedAt: this.mergeStatValue(base.killedAt, next.killedAt),
- conquests: this.mergeStatValue(base.conquests, next.conquests),
+ conquests: this.mergeStatArrays(base.conquests, next.conquests),
boats: this.mergeStatRecord(base.boats, next.boats),
bombs: this.mergeStatRecord(base.bombs, next.bombs),
gold: this.mergeStatArrays(base.gold, next.gold),
@@ -203,7 +203,7 @@ export class PlayerStatsTreeView extends LitElement {
attacks: stats.attacks ? [...stats.attacks] : undefined,
betrayals: stats.betrayals,
killedAt: stats.killedAt,
- conquests: stats.conquests,
+ conquests: stats.conquests ? [...stats.conquests] : undefined,
boats: stats.boats ? { ...stats.boats } : undefined,
bombs: stats.bombs ? { ...stats.bombs } : undefined,
gold: stats.gold ? [...stats.gold] : undefined,
diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts
index 7596e9fbf..c37607bd6 100644
--- a/src/core/StatsSchemas.ts
+++ b/src/core/StatsSchemas.ts
@@ -62,6 +62,11 @@ export const ATTACK_INDEX_SENT = 0; // Outgoing attack troops
export const ATTACK_INDEX_RECV = 1; // Incmoing attack troops
export const ATTACK_INDEX_CANCEL = 2; // Cancelled attack troops
+// Player types
+export const PLAYER_INDEX_HUMAN = 0;
+export const PLAYER_INDEX_NATION = 1;
+export const PLAYER_INDEX_BOT = 2;
+
// Boats
export const BOAT_INDEX_SENT = 0; // Boats launched
export const BOAT_INDEX_ARRIVE = 1; // Boats arrived
@@ -102,7 +107,7 @@ export const PlayerStatsSchema = z
attacks: AtLeastOneNumberSchema.optional(),
betrayals: BigIntStringSchema.optional(),
killedAt: BigIntStringSchema.optional(),
- conquests: BigIntStringSchema.optional(),
+ conquests: AtLeastOneNumberSchema.optional(),
boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(),
gold: AtLeastOneNumberSchema.optional(),
diff --git a/src/core/game/StatsImpl.ts b/src/core/game/StatsImpl.ts
index 56e394769..c2195bf72 100644
--- a/src/core/game/StatsImpl.ts
+++ b/src/core/game/StatsImpl.ts
@@ -24,11 +24,14 @@ import {
OTHER_INDEX_LOST,
OTHER_INDEX_UPGRADE,
OtherUnitType,
+ PLAYER_INDEX_BOT,
+ PLAYER_INDEX_HUMAN,
+ PLAYER_INDEX_NATION,
PlayerStats,
unitTypeToBombUnit,
unitTypeToOtherUnit,
} from "../StatsSchemas";
-import { Player, TerraNullius, UnitType } from "./Game";
+import { Player, PlayerType, TerraNullius, UnitType } from "./Game";
import { Stats } from "./Stats";
type BigIntLike = bigint | number;
@@ -41,6 +44,12 @@ function _bigint(value: BigIntLike): bigint {
}
}
+const conquest_by_type: Record = {
+ [PlayerType.Human]: PLAYER_INDEX_HUMAN,
+ [PlayerType.Nation]: PLAYER_INDEX_NATION,
+ [PlayerType.Bot]: PLAYER_INDEX_BOT,
+};
+
export class StatsImpl implements Stats {
private readonly data: AllPlayersStats = {};
@@ -138,14 +147,12 @@ export class StatsImpl implements Stats {
p.units[type][index] += _bigint(value);
}
- private _addConquest(player: Player) {
+ private _addConquest(player: Player, index: number) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
- if (p.conquests === undefined) {
- p.conquests = _bigint(1);
- } else {
- p.conquests += _bigint(1);
- }
+ p.conquests ??= [0n];
+ while (p.conquests.length <= index) p.conquests.push(0n);
+ p.conquests[index] += _bigint(1);
}
private _addPlayerKilled(player: Player, tick: number) {
@@ -249,7 +256,10 @@ export class StatsImpl implements Stats {
goldWar(player: Player, captured: Player, gold: BigIntLike): void {
this._addGold(player, GOLD_INDEX_WAR, gold);
- this._addConquest(player);
+ const conquestType = conquest_by_type[captured.type()];
+ if (conquestType !== undefined) {
+ this._addConquest(player, conquestType);
+ }
}
unitBuild(player: Player, type: OtherUnitType): void {
diff --git a/tests/GameInfoRanking.test.ts b/tests/GameInfoRanking.test.ts
index 1e523ae93..1eeefebb9 100644
--- a/tests/GameInfoRanking.test.ts
+++ b/tests/GameInfoRanking.test.ts
@@ -56,7 +56,7 @@ describe("Ranking class", () => {
cosmetics: { flag: "USA" },
stats: {
units: { port: [2n, 0n, 0n, 2n] },
- conquests: 5n,
+ conquests: [5n],
gold: [0n, 100n, 20n, 0n, 15n, 5n], // total 140
bombs: {
abomb: [1n],
@@ -71,7 +71,7 @@ describe("Ranking class", () => {
username: "Bob",
stats: {
units: { city: [2n, 0n, 0n, 2n] },
- conquests: 8n,
+ conquests: [8n],
gold: [0n, 50n, 10n, 5n], // total 65, no train trade
bombs: {
abomb: [0n],
@@ -86,7 +86,7 @@ describe("Ranking class", () => {
username: "Charlie",
stats: {
// no units, but has conquests/killedAt to count as played
- conquests: 8n,
+ conquests: [8n],
killedAt: BigInt(600),
gold: [0n, 10n, 2n, 10n, 0n, 5n], // total 27
bombs: {},
@@ -110,21 +110,21 @@ describe("Ranking class", () => {
test("summarizes players correctly", () => {
const r = new Ranking(makeSession());
- const players = r.sortedBy(RankType.Conquests);
+ const players = r.sortedBy(RankType.ConquestHumans);
expect(players.length).toBe(3);
const p1 = players.find((p) => p.id === "p1")!;
expect(p1.username).toBe("Alice");
expect(p1.flag).toBe("USA");
- expect(p1.conquests).toBe(5);
+ expect(p1.conquests).toStrictEqual([5n]);
expect(p1.atoms).toBe(1);
expect(p1.mirv).toBe(2);
});
test("correctly identifies winner", () => {
const r = new Ranking(makeSession());
- const p2 = r.sortedBy(RankType.Conquests).find((p) => p.id === "p2")!;
+ const p2 = r.sortedBy(RankType.ConquestHumans).find((p) => p.id === "p2")!;
expect(p2.winner).toBe(true);
});
@@ -157,7 +157,7 @@ describe("Ranking class", () => {
test("lifetime score is percentage of duration", () => {
const r = new Ranking(makeSession());
- const p3 = r.sortedBy(RankType.Conquests).find((p) => p.id === "p3")!;
+ const p3 = r.sortedBy(RankType.ConquestHumans).find((p) => p.id === "p3")!;
const expected = Number(BigInt(600)) / gameDuration;
expect(r.score(p3, RankType.Lifetime)).toBe(expected);
});
@@ -170,7 +170,7 @@ describe("Ranking class", () => {
test("winners should be ahead of players with same score", () => {
const r = new Ranking(makeSession());
- const sortedPlayers = r.sortedBy(RankType.Conquests);
+ const sortedPlayers = r.sortedBy(RankType.ConquestHumans);
expect(sortedPlayers[0].id).toBe("p2"); // p2 & p3 same score but winner first
});
diff --git a/tests/Stats.test.ts b/tests/Stats.test.ts
index 1b654210b..4cab4b2fe 100644
--- a/tests/Stats.test.ts
+++ b/tests/Stats.test.ts
@@ -162,14 +162,14 @@ describe("Stats", () => {
expect(stats.stats()).toStrictEqual({
client1: {
gold: [0n, 1n],
- conquests: 1n,
+ conquests: [1n],
},
});
stats.goldWar(player1, player2, 1);
expect(stats.stats()).toStrictEqual({
client1: {
gold: [0n, 2n],
- conquests: 2n,
+ conquests: [2n],
},
});
});