mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-03 20:20:03 +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 { GameView, PlayerView } from "../../core/game/GameView";
|
||||||
|
import { computeTeamTiles, findCrownTeam } from "../../core/game/TeamUtils";
|
||||||
import allianceIcon from "/images/AllianceIcon.svg?url";
|
import allianceIcon from "/images/AllianceIcon.svg?url";
|
||||||
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
|
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
|
||||||
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.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. */
|
/** Returns the team with the most total tiles, or null if no team leads. */
|
||||||
export function getCrownTeam(game: GameView): string | null {
|
export function getCrownTeam(game: GameView): string | null {
|
||||||
const teamToTiles = new Map<string, number>();
|
return findCrownTeam(computeTeamTiles(game.playerViews()));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlayerIcons(
|
export function getPlayerIcons(
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
UnitType,
|
UnitType,
|
||||||
} from "../../../core/game/Game";
|
} from "../../../core/game/Game";
|
||||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||||
|
import {
|
||||||
|
computeTeamTiles,
|
||||||
|
findCrownTeam,
|
||||||
|
normalizeCrownSeconds,
|
||||||
|
} from "../../../core/game/TeamUtils";
|
||||||
import { SendWinnerEvent } from "../../Transport";
|
import { SendWinnerEvent } from "../../Transport";
|
||||||
import {
|
import {
|
||||||
formatPercentage,
|
formatPercentage,
|
||||||
@@ -98,15 +103,9 @@ export class TeamStats extends LitElement implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private trackMetrics() {
|
private trackMetrics() {
|
||||||
const teamToTiles = new Map<Team, number>();
|
const teamToTiles = computeTeamTiles(this.game.playerViews());
|
||||||
for (const player of this.game.playerViews()) {
|
for (const team of teamToTiles.keys()) {
|
||||||
const team = player.team();
|
|
||||||
if (team === null || team === ColoredTeams.Bot) continue;
|
|
||||||
this._knownTeams.add(team);
|
this._knownTeams.add(team);
|
||||||
teamToTiles.set(
|
|
||||||
team,
|
|
||||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
for (const [team, tiles] of teamToTiles) {
|
for (const [team, tiles] of teamToTiles) {
|
||||||
const prev = this._peakTiles.get(team) ?? 0;
|
const prev = this._peakTiles.get(team) ?? 0;
|
||||||
@@ -119,14 +118,9 @@ export class TeamStats extends LitElement implements Layer {
|
|||||||
private freezeScores() {
|
private freezeScores() {
|
||||||
const numTilesWithoutFallout =
|
const numTilesWithoutFallout =
|
||||||
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
||||||
for (const player of this.game.playerViews()) {
|
const teamToTiles = computeTeamTiles(this.game.playerViews());
|
||||||
const team = player.team();
|
for (const [team, tiles] of teamToTiles) {
|
||||||
if (team === null || team === ColoredTeams.Bot) continue;
|
this._frozenCurrentPercent.set(team, tiles / numTilesWithoutFallout);
|
||||||
this._frozenCurrentPercent.set(
|
|
||||||
team,
|
|
||||||
(this._frozenCurrentPercent.get(team) ?? 0) +
|
|
||||||
player.numTilesOwned() / numTilesWithoutFallout,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
for (const [team, peakTiles] of this._peakTiles) {
|
for (const [team, peakTiles] of this._peakTiles) {
|
||||||
this._frozenPeakPercent.set(team, peakTiles / numTilesWithoutFallout);
|
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.min(Math.floor(elapsedGameTicks / 10), maxTimerValue * 60)
|
||||||
: Math.floor(elapsedGameTicks / 10);
|
: Math.floor(elapsedGameTicks / 10);
|
||||||
const serverCrownTicks = this.game.teamCrownTicks() ?? {};
|
const serverCrownTicks = this.game.teamCrownTicks() ?? {};
|
||||||
// Crown holder = team with most tiles (same logic as WinCheckExecution)
|
const allTeamKeys = Object.keys(grouped);
|
||||||
let crownHolder: string | null = null;
|
const crownHolder = findCrownTeam(
|
||||||
let maxTilesNow = 0;
|
computeTeamTiles(this.game.playerViews()),
|
||||||
for (const [team, players] of Object.entries(grouped)) {
|
);
|
||||||
const teamTotal = players.reduce((sum, p) => sum + p.numTilesOwned(), 0);
|
const serverTicksMap = new Map<Team, number>(
|
||||||
if (teamTotal > maxTilesNow) {
|
Object.entries(serverCrownTicks),
|
||||||
maxTilesNow = teamTotal;
|
);
|
||||||
crownHolder = team;
|
const crownSecondsMap = normalizeCrownSeconds(
|
||||||
}
|
allTeamKeys,
|
||||||
}
|
serverTicksMap,
|
||||||
// Non-holder teams get floor(ticks/10), holder gets remainder
|
crownHolder,
|
||||||
const normalizedCrownTicks = new Map<string, number>();
|
elapsedSeconds,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.teams = Object.entries(grouped)
|
this.teams = Object.entries(grouped)
|
||||||
.map(([teamStr, teamPlayers]) => {
|
.map(([teamStr, teamPlayers]) => {
|
||||||
@@ -245,7 +226,7 @@ export class TeamStats extends LitElement implements Layer {
|
|||||||
this._gameOver && this.game.competitiveScores()
|
this._gameOver && this.game.competitiveScores()
|
||||||
? (this.game.competitiveScores()!.find((s) => s.team === teamStr)
|
? (this.game.competitiveScores()!.find((s) => s.team === teamStr)
|
||||||
?.crownTimeSeconds ?? 0)
|
?.crownTimeSeconds ?? 0)
|
||||||
: Math.floor((normalizedCrownTicks.get(teamStr) ?? 0) / 10),
|
: (crownSecondsMap.get(teamStr) ?? 0),
|
||||||
|
|
||||||
totalLaunchers: renderNumber(totalLaunchers),
|
totalLaunchers: renderNumber(totalLaunchers),
|
||||||
totalSAMs: renderNumber(totalSAMs),
|
totalSAMs: renderNumber(totalSAMs),
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export class AttackExecution implements Execution {
|
|||||||
|
|
||||||
// CrownBreakPoint: 25% troop bonus when attacking the crown-holding team
|
// CrownBreakPoint: 25% troop bonus when attacking the crown-holding team
|
||||||
if (
|
if (
|
||||||
|
this.mg.config().gameConfig().competitiveScoring &&
|
||||||
this.mg.config().gameConfig().gameMode === GameMode.Team &&
|
this.mg.config().gameConfig().gameMode === GameMode.Team &&
|
||||||
this.target.isPlayer()
|
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:
|
* Tracks team-level competitive metrics every 10 ticks:
|
||||||
@@ -30,30 +31,13 @@ export class TeamMetricsExecution implements Execution {
|
|||||||
if (elapsedSeconds >= maxTimerValue * 60) return;
|
if (elapsedSeconds >= maxTimerValue * 60) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamToTiles = new Map<Team, number>();
|
const teamToTiles = computeTeamTiles(this.mg.players());
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track peak tiles for each team
|
|
||||||
for (const [team, tiles] of teamToTiles) {
|
for (const [team, tiles] of teamToTiles) {
|
||||||
this.mg.updateTeamPeakTiles(team, tiles);
|
this.mg.updateTeamPeakTiles(team, tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track crown (team with most tiles)
|
const crownTeam = findCrownTeam(teamToTiles);
|
||||||
let maxTiles = 0;
|
|
||||||
let crownTeam: Team | null = null;
|
|
||||||
for (const [team, tiles] of teamToTiles) {
|
|
||||||
if (tiles > maxTiles) {
|
|
||||||
maxTiles = tiles;
|
|
||||||
crownTeam = team;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (crownTeam !== null) {
|
if (crownTeam !== null) {
|
||||||
this.mg.addCrownTick(crownTeam, 10);
|
this.mg.addCrownTick(crownTeam, 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
RankedType,
|
RankedType,
|
||||||
Team,
|
Team,
|
||||||
} from "../game/Game";
|
} from "../game/Game";
|
||||||
|
import {
|
||||||
|
computeTeamTiles,
|
||||||
|
findCrownTeam,
|
||||||
|
normalizeCrownSeconds,
|
||||||
|
} from "../game/TeamUtils";
|
||||||
|
|
||||||
export class WinEvent implements GameEvent {
|
export class WinEvent implements GameEvent {
|
||||||
constructor(public readonly winner: Player) {}
|
constructor(public readonly winner: Player) {}
|
||||||
@@ -148,7 +153,7 @@ export class WinCheckExecution implements Execution {
|
|||||||
if (winner === undefined) return;
|
if (winner === undefined) return;
|
||||||
|
|
||||||
const scores = this.mg.config().gameConfig().competitiveScoring
|
const scores = this.mg.config().gameConfig().competitiveScoring
|
||||||
? this.computeScores(teamToTiles, numTilesWithoutFallout)
|
? this.computeScores(this.mg, teamToTiles, numTilesWithoutFallout)
|
||||||
: undefined;
|
: undefined;
|
||||||
this.mg.setWinner(winner[0], this.mg.stats().stats(), scores);
|
this.mg.setWinner(winner[0], this.mg.stats().stats(), scores);
|
||||||
console.log(`${winner[0]} has won the game`);
|
console.log(`${winner[0]} has won the game`);
|
||||||
@@ -157,17 +162,15 @@ export class WinCheckExecution implements Execution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private computeScores(
|
private computeScores(
|
||||||
|
mg: Game,
|
||||||
teamToTiles: Map<Team, number>,
|
teamToTiles: Map<Team, number>,
|
||||||
numTilesWithoutFallout: number,
|
numTilesWithoutFallout: number,
|
||||||
) {
|
) {
|
||||||
if (this.mg === null) return undefined;
|
const eliminationOrder = mg.teamEliminationOrder();
|
||||||
|
|
||||||
const eliminationOrder = this.mg.teamEliminationOrder();
|
|
||||||
const allTeams = Array.from(teamToTiles.keys()).filter(
|
const allTeams = Array.from(teamToTiles.keys()).filter(
|
||||||
(t) => t !== ColoredTeams.Bot,
|
(t) => t !== ColoredTeams.Bot,
|
||||||
);
|
);
|
||||||
const totalGameTicks =
|
const totalGameTicks = mg.ticks() - mg.config().numSpawnPhaseTurns();
|
||||||
this.mg.ticks() - this.mg.config().numSpawnPhaseTurns();
|
|
||||||
|
|
||||||
// Rank surviving teams by current tiles (more tiles = better placement)
|
// Rank surviving teams by current tiles (more tiles = better placement)
|
||||||
const survivingTeams = allTeams.filter(
|
const survivingTeams = allTeams.filter(
|
||||||
@@ -177,37 +180,19 @@ export class WinCheckExecution implements Execution {
|
|||||||
(a, b) => (teamToTiles.get(b) ?? 0) - (teamToTiles.get(a) ?? 0),
|
(a, b) => (teamToTiles.get(b) ?? 0) - (teamToTiles.get(a) ?? 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Normalize crown seconds: non-crown teams get floor(ticks/10),
|
const nonBotTiles = computeTeamTiles(mg.allPlayers());
|
||||||
// crown holder gets the remainder so the sum matches the game timer.
|
const crownHolder = findCrownTeam(nonBotTiles);
|
||||||
const elapsedSeconds = Math.floor(Math.max(0, totalGameTicks) / 10);
|
const elapsedSeconds = Math.floor(Math.max(0, totalGameTicks) / 10);
|
||||||
let crownHolder: Team | null = null;
|
const allCrownTicks = mg.allTeamCrownTicks();
|
||||||
let maxTiles = 0;
|
const teamCrownSeconds = normalizeCrownSeconds(
|
||||||
for (const [team, tiles] of teamToTiles) {
|
allTeams,
|
||||||
if (team !== ColoredTeams.Bot && tiles > maxTiles) {
|
allCrownTicks,
|
||||||
maxTiles = tiles;
|
crownHolder,
|
||||||
crownHolder = team;
|
elapsedSeconds,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
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 metrics: TeamRawMetrics[] = allTeams.map((team) => {
|
const metrics: TeamRawMetrics[] = allTeams.map((team) => {
|
||||||
const peakTiles = this.mg!.teamPeakTiles(team);
|
const peakTiles = mg.teamPeakTiles(team);
|
||||||
const peakTilePercentage = (peakTiles / numTilesWithoutFallout) * 100;
|
const peakTilePercentage = (peakTiles / numTilesWithoutFallout) * 100;
|
||||||
const crownTicks = allCrownTicks.get(team) ?? 0;
|
const crownTicks = allCrownTicks.get(team) ?? 0;
|
||||||
const crownRatio = totalGameTicks > 0 ? crownTicks / totalGameTicks : 0;
|
const crownRatio = totalGameTicks > 0 ? crownTicks / totalGameTicks : 0;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { createRailNetwork } from "./RailNetworkImpl";
|
|||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { StatsImpl } from "./StatsImpl";
|
import { StatsImpl } from "./StatsImpl";
|
||||||
import { assignTeams } from "./TeamAssignment";
|
import { assignTeams } from "./TeamAssignment";
|
||||||
|
import { computeTeamTiles, findCrownTeam } from "./TeamUtils";
|
||||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||||
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
||||||
|
|
||||||
@@ -1256,28 +1257,11 @@ export class GameImpl implements Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
crownTeam(): Team | null {
|
crownTeam(): Team | null {
|
||||||
const teamToTiles = new Map<Team, number>();
|
return findCrownTeam(computeTeamTiles(this.players()));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allTeamCrownTicks(): ReadonlyMap<Team, number> {
|
allTeamCrownTicks(): ReadonlyMap<Team, number> {
|
||||||
return new Map(this._teamCrownTicks);
|
return this._teamCrownTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCrownTick(team: Team, amount: number): void {
|
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