mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
dedupe
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { AllPlayers, ColoredTeams, nukeTypes } from "../../core/game/Game";
|
||||
import { AllPlayers, nukeTypes } from "../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../core/game/GameView";
|
||||
import { computeTeamTiles, findCrownTeam } from "../../core/game/TeamUtils";
|
||||
import allianceIcon from "/images/AllianceIcon.svg?url";
|
||||
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
|
||||
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.svg?url";
|
||||
@@ -59,24 +60,7 @@ export function getFirstPlacePlayer(game: GameView): PlayerView | null {
|
||||
|
||||
/** Returns the team with the most total tiles, or null if no team leads. */
|
||||
export function getCrownTeam(game: GameView): string | null {
|
||||
const teamToTiles = new Map<string, number>();
|
||||
for (const player of game.playerViews()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
let maxTiles = 0;
|
||||
let crownTeam: string | null = null;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crownTeam = team;
|
||||
}
|
||||
}
|
||||
return crownTeam;
|
||||
return findCrownTeam(computeTeamTiles(game.playerViews()));
|
||||
}
|
||||
|
||||
export function getPlayerIcons(
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import {
|
||||
computeTeamTiles,
|
||||
findCrownTeam,
|
||||
normalizeCrownSeconds,
|
||||
} from "../../../core/game/TeamUtils";
|
||||
import { SendWinnerEvent } from "../../Transport";
|
||||
import {
|
||||
formatPercentage,
|
||||
@@ -98,15 +103,9 @@ export class TeamStats extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private trackMetrics() {
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of this.game.playerViews()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
const teamToTiles = computeTeamTiles(this.game.playerViews());
|
||||
for (const team of teamToTiles.keys()) {
|
||||
this._knownTeams.add(team);
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
const prev = this._peakTiles.get(team) ?? 0;
|
||||
@@ -119,14 +118,9 @@ export class TeamStats extends LitElement implements Layer {
|
||||
private freezeScores() {
|
||||
const numTilesWithoutFallout =
|
||||
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
||||
for (const player of this.game.playerViews()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
this._frozenCurrentPercent.set(
|
||||
team,
|
||||
(this._frozenCurrentPercent.get(team) ?? 0) +
|
||||
player.numTilesOwned() / numTilesWithoutFallout,
|
||||
);
|
||||
const teamToTiles = computeTeamTiles(this.game.playerViews());
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
this._frozenCurrentPercent.set(team, tiles / numTilesWithoutFallout);
|
||||
}
|
||||
for (const [team, peakTiles] of this._peakTiles) {
|
||||
this._frozenPeakPercent.set(team, peakTiles / numTilesWithoutFallout);
|
||||
@@ -174,32 +168,19 @@ export class TeamStats extends LitElement implements Layer {
|
||||
? Math.min(Math.floor(elapsedGameTicks / 10), maxTimerValue * 60)
|
||||
: Math.floor(elapsedGameTicks / 10);
|
||||
const serverCrownTicks = this.game.teamCrownTicks() ?? {};
|
||||
// Crown holder = team with most tiles (same logic as WinCheckExecution)
|
||||
let crownHolder: string | null = null;
|
||||
let maxTilesNow = 0;
|
||||
for (const [team, players] of Object.entries(grouped)) {
|
||||
const teamTotal = players.reduce((sum, p) => sum + p.numTilesOwned(), 0);
|
||||
if (teamTotal > maxTilesNow) {
|
||||
maxTilesNow = teamTotal;
|
||||
crownHolder = team;
|
||||
}
|
||||
}
|
||||
// Non-holder teams get floor(ticks/10), holder gets remainder
|
||||
const normalizedCrownTicks = new Map<string, number>();
|
||||
let othersSeconds = 0;
|
||||
for (const [team, ticks] of Object.entries(serverCrownTicks)) {
|
||||
if (team !== crownHolder) {
|
||||
const secs = Math.floor(ticks / 10);
|
||||
normalizedCrownTicks.set(team, secs * 10);
|
||||
othersSeconds += secs;
|
||||
}
|
||||
}
|
||||
if (crownHolder !== null) {
|
||||
normalizedCrownTicks.set(
|
||||
crownHolder,
|
||||
Math.max(0, elapsedSeconds - othersSeconds) * 10,
|
||||
);
|
||||
}
|
||||
const allTeamKeys = Object.keys(grouped);
|
||||
const crownHolder = findCrownTeam(
|
||||
computeTeamTiles(this.game.playerViews()),
|
||||
);
|
||||
const serverTicksMap = new Map<Team, number>(
|
||||
Object.entries(serverCrownTicks),
|
||||
);
|
||||
const crownSecondsMap = normalizeCrownSeconds(
|
||||
allTeamKeys,
|
||||
serverTicksMap,
|
||||
crownHolder,
|
||||
elapsedSeconds,
|
||||
);
|
||||
|
||||
this.teams = Object.entries(grouped)
|
||||
.map(([teamStr, teamPlayers]) => {
|
||||
@@ -245,7 +226,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
this._gameOver && this.game.competitiveScores()
|
||||
? (this.game.competitiveScores()!.find((s) => s.team === teamStr)
|
||||
?.crownTimeSeconds ?? 0)
|
||||
: Math.floor((normalizedCrownTicks.get(teamStr) ?? 0) / 10),
|
||||
: (crownSecondsMap.get(teamStr) ?? 0),
|
||||
|
||||
totalLaunchers: renderNumber(totalLaunchers),
|
||||
totalSAMs: renderNumber(totalSAMs),
|
||||
|
||||
@@ -121,6 +121,7 @@ export class AttackExecution implements Execution {
|
||||
|
||||
// CrownBreakPoint: 25% troop bonus when attacking the crown-holding team
|
||||
if (
|
||||
this.mg.config().gameConfig().competitiveScoring &&
|
||||
this.mg.config().gameConfig().gameMode === GameMode.Team &&
|
||||
this.target.isPlayer()
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ColoredTeams, Execution, Game, GameMode, Team } from "../game/Game";
|
||||
import { Execution, Game, GameMode } from "../game/Game";
|
||||
import { computeTeamTiles, findCrownTeam } from "../game/TeamUtils";
|
||||
|
||||
/**
|
||||
* Tracks team-level competitive metrics every 10 ticks:
|
||||
@@ -30,30 +31,13 @@ export class TeamMetricsExecution implements Execution {
|
||||
if (elapsedSeconds >= maxTimerValue * 60) return;
|
||||
}
|
||||
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of this.mg.players()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
const teamToTiles = computeTeamTiles(this.mg.players());
|
||||
|
||||
// Track peak tiles for each team
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
this.mg.updateTeamPeakTiles(team, tiles);
|
||||
}
|
||||
|
||||
// Track crown (team with most tiles)
|
||||
let maxTiles = 0;
|
||||
let crownTeam: Team | null = null;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crownTeam = team;
|
||||
}
|
||||
}
|
||||
const crownTeam = findCrownTeam(teamToTiles);
|
||||
if (crownTeam !== null) {
|
||||
this.mg.addCrownTick(crownTeam, 10);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
RankedType,
|
||||
Team,
|
||||
} from "../game/Game";
|
||||
import {
|
||||
computeTeamTiles,
|
||||
findCrownTeam,
|
||||
normalizeCrownSeconds,
|
||||
} from "../game/TeamUtils";
|
||||
|
||||
export class WinEvent implements GameEvent {
|
||||
constructor(public readonly winner: Player) {}
|
||||
@@ -148,7 +153,7 @@ export class WinCheckExecution implements Execution {
|
||||
if (winner === undefined) return;
|
||||
|
||||
const scores = this.mg.config().gameConfig().competitiveScoring
|
||||
? this.computeScores(teamToTiles, numTilesWithoutFallout)
|
||||
? this.computeScores(this.mg, teamToTiles, numTilesWithoutFallout)
|
||||
: undefined;
|
||||
this.mg.setWinner(winner[0], this.mg.stats().stats(), scores);
|
||||
console.log(`${winner[0]} has won the game`);
|
||||
@@ -157,17 +162,15 @@ export class WinCheckExecution implements Execution {
|
||||
}
|
||||
|
||||
private computeScores(
|
||||
mg: Game,
|
||||
teamToTiles: Map<Team, number>,
|
||||
numTilesWithoutFallout: number,
|
||||
) {
|
||||
if (this.mg === null) return undefined;
|
||||
|
||||
const eliminationOrder = this.mg.teamEliminationOrder();
|
||||
const eliminationOrder = mg.teamEliminationOrder();
|
||||
const allTeams = Array.from(teamToTiles.keys()).filter(
|
||||
(t) => t !== ColoredTeams.Bot,
|
||||
);
|
||||
const totalGameTicks =
|
||||
this.mg.ticks() - this.mg.config().numSpawnPhaseTurns();
|
||||
const totalGameTicks = mg.ticks() - mg.config().numSpawnPhaseTurns();
|
||||
|
||||
// Rank surviving teams by current tiles (more tiles = better placement)
|
||||
const survivingTeams = allTeams.filter(
|
||||
@@ -177,37 +180,19 @@ export class WinCheckExecution implements Execution {
|
||||
(a, b) => (teamToTiles.get(b) ?? 0) - (teamToTiles.get(a) ?? 0),
|
||||
);
|
||||
|
||||
// Normalize crown seconds: non-crown teams get floor(ticks/10),
|
||||
// crown holder gets the remainder so the sum matches the game timer.
|
||||
const nonBotTiles = computeTeamTiles(mg.allPlayers());
|
||||
const crownHolder = findCrownTeam(nonBotTiles);
|
||||
const elapsedSeconds = Math.floor(Math.max(0, totalGameTicks) / 10);
|
||||
let crownHolder: Team | null = null;
|
||||
let maxTiles = 0;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (team !== ColoredTeams.Bot && tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crownHolder = team;
|
||||
}
|
||||
}
|
||||
const allCrownTicks = this.mg!.allTeamCrownTicks();
|
||||
const teamCrownSeconds = new Map<Team, number>();
|
||||
let othersSum = 0;
|
||||
for (const team of allTeams) {
|
||||
const ticks = allCrownTicks.get(team) ?? 0;
|
||||
if (team !== crownHolder) {
|
||||
const secs = Math.floor(ticks / 10);
|
||||
teamCrownSeconds.set(team, secs);
|
||||
othersSum += secs;
|
||||
}
|
||||
}
|
||||
if (crownHolder !== null) {
|
||||
teamCrownSeconds.set(
|
||||
crownHolder,
|
||||
Math.max(0, elapsedSeconds - othersSum),
|
||||
);
|
||||
}
|
||||
const allCrownTicks = mg.allTeamCrownTicks();
|
||||
const teamCrownSeconds = normalizeCrownSeconds(
|
||||
allTeams,
|
||||
allCrownTicks,
|
||||
crownHolder,
|
||||
elapsedSeconds,
|
||||
);
|
||||
|
||||
const metrics: TeamRawMetrics[] = allTeams.map((team) => {
|
||||
const peakTiles = this.mg!.teamPeakTiles(team);
|
||||
const peakTiles = mg.teamPeakTiles(team);
|
||||
const peakTilePercentage = (peakTiles / numTilesWithoutFallout) * 100;
|
||||
const crownTicks = allCrownTicks.get(team) ?? 0;
|
||||
const crownRatio = totalGameTicks > 0 ? crownTicks / totalGameTicks : 0;
|
||||
|
||||
@@ -51,6 +51,7 @@ import { createRailNetwork } from "./RailNetworkImpl";
|
||||
import { Stats } from "./Stats";
|
||||
import { StatsImpl } from "./StatsImpl";
|
||||
import { assignTeams } from "./TeamAssignment";
|
||||
import { computeTeamTiles, findCrownTeam } from "./TeamUtils";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
||||
|
||||
@@ -1256,28 +1257,11 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
crownTeam(): Team | null {
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of this.players()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
let maxTiles = 0;
|
||||
let crown: Team | null = null;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crown = team;
|
||||
}
|
||||
}
|
||||
return crown;
|
||||
return findCrownTeam(computeTeamTiles(this.players()));
|
||||
}
|
||||
|
||||
allTeamCrownTicks(): ReadonlyMap<Team, number> {
|
||||
return new Map(this._teamCrownTicks);
|
||||
return this._teamCrownTicks;
|
||||
}
|
||||
|
||||
addCrownTick(team: Team, amount: number): void {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ColoredTeams, Team } from "./Game";
|
||||
|
||||
/**
|
||||
* Any object that exposes a team and a tile count —
|
||||
* satisfied by both server-side `Player` and client-side `PlayerView`.
|
||||
*/
|
||||
interface HasTeamAndTiles {
|
||||
team(): Team | null;
|
||||
numTilesOwned(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums tile counts per team from the given players.
|
||||
* By default bots and null-team players are excluded.
|
||||
*/
|
||||
export function computeTeamTiles(
|
||||
players: Iterable<HasTeamAndTiles>,
|
||||
excludeBots = true,
|
||||
): Map<Team, number> {
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of players) {
|
||||
const team = player.team();
|
||||
if (team === null) continue;
|
||||
if (excludeBots && team === ColoredTeams.Bot) continue;
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
return teamToTiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the team with the highest tile count, or `null` if the map is empty.
|
||||
*/
|
||||
export function findCrownTeam(teamToTiles: Map<Team, number>): Team | null {
|
||||
let maxTiles = 0;
|
||||
let crown: Team | null = null;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crown = team;
|
||||
}
|
||||
}
|
||||
return crown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts raw crown-tick counts into display-friendly seconds.
|
||||
*
|
||||
* Non-crown-holder teams get `floor(ticks / 10)`.
|
||||
* The current crown holder receives the remainder so the total sums to
|
||||
* `totalElapsedSeconds`, keeping the sidebar timer and crown time in sync.
|
||||
*/
|
||||
export function normalizeCrownSeconds(
|
||||
allTeams: Team[],
|
||||
crownTicks: ReadonlyMap<Team, number>,
|
||||
crownHolder: Team | null,
|
||||
totalElapsedSeconds: number,
|
||||
): Map<Team, number> {
|
||||
const result = new Map<Team, number>();
|
||||
let othersSum = 0;
|
||||
for (const team of allTeams) {
|
||||
const ticks = crownTicks.get(team) ?? 0;
|
||||
if (team !== crownHolder) {
|
||||
const secs = Math.floor(ticks / 10);
|
||||
result.set(team, secs);
|
||||
othersSum += secs;
|
||||
}
|
||||
}
|
||||
if (crownHolder !== null) {
|
||||
result.set(crownHolder, Math.max(0, totalElapsedSeconds - othersSum));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user