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], }, }); });