mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
Record player stats during the game (#784)
## Description: Record player stats for the analytics worker to import in to to postgres. This changes defines a new Analytics schema version, `v0.0.2`, containing additional metadata about each player. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
This commit is contained in:
@@ -193,6 +193,7 @@ export class ClientGameRunner {
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobby.playerName,
|
||||
clientID: this.lobby.clientID,
|
||||
stats: update.allPlayersStats[this.lobby.clientID],
|
||||
},
|
||||
];
|
||||
let winner: ClientID | Team | null = null;
|
||||
@@ -217,7 +218,6 @@ export class ClientGameRunner {
|
||||
Date.now(),
|
||||
winner,
|
||||
update.winnerType,
|
||||
update.allPlayersStats,
|
||||
);
|
||||
endGame(record);
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ export class LocalServer {
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobbyConfig.playerName,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
stats: this.allPlayersStats[this.lobbyConfig.clientID],
|
||||
},
|
||||
];
|
||||
if (this.lobbyConfig.gameStartInfo === undefined) {
|
||||
@@ -194,7 +195,6 @@ export class LocalServer {
|
||||
Date.now(),
|
||||
this.winner?.winner ?? null,
|
||||
this.winner?.winnerType ?? null,
|
||||
this.allPlayersStats,
|
||||
);
|
||||
if (!saveFullGame) {
|
||||
// Clear turns because beacon only supports up to 64kb
|
||||
|
||||
@@ -10,7 +10,7 @@ import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
|
||||
import { AllPlayers, Cell, nukeTypes, UnitType } from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
@@ -450,7 +450,7 @@ export class NameLayer implements Layer {
|
||||
const isSendingNuke = render.player.id() === unit.owner().id();
|
||||
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
|
||||
return (
|
||||
nukeTypes.includes(unit.type()) &&
|
||||
(nukeTypes as UnitType[]).includes(unit.type()) &&
|
||||
isSendingNuke &&
|
||||
notMyPlayer &&
|
||||
unit.isActive()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
|
||||
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
|
||||
@@ -9,12 +9,7 @@ import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import {
|
||||
AllPlayers,
|
||||
PlayerActions,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { AllPlayers, PlayerActions } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
@@ -205,28 +200,6 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return time.trim();
|
||||
}
|
||||
|
||||
getTotalNukesSent(otherId: PlayerID): number {
|
||||
const stats = this.g.player(otherId).stats();
|
||||
if (!stats) {
|
||||
return 0;
|
||||
}
|
||||
let sum = 0;
|
||||
const player = this.g.myPlayer();
|
||||
if (player === null) {
|
||||
return 0;
|
||||
}
|
||||
const nukes = stats.sentNukes[player.id()];
|
||||
if (!nukes) {
|
||||
return 0;
|
||||
}
|
||||
for (const nukeType in nukes) {
|
||||
if (nukeType !== UnitType.MIRVWarhead) {
|
||||
sum += nukes[nukeType];
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
@@ -353,15 +326,6 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<!-- Stats -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-white text-opacity-80 text-sm px-2">
|
||||
${translateText("player_panel.nuke")}
|
||||
</div>
|
||||
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
|
||||
${this.getTotalNukesSent(other.id())}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
@@ -44,5 +44,9 @@ export const UserMeResponseSchema = z.object({
|
||||
discriminator: z.string(),
|
||||
locale: z.string(),
|
||||
}),
|
||||
player: z.object({
|
||||
publicId: z.string(),
|
||||
roles: z.string().array(),
|
||||
}),
|
||||
});
|
||||
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { z } from "zod";
|
||||
import { UnitType } from "./game/Game";
|
||||
|
||||
export const BombUnitSchema = z.union([
|
||||
z.literal("abomb"),
|
||||
z.literal("hbomb"),
|
||||
z.literal("mirv"),
|
||||
z.literal("mirvw"),
|
||||
]);
|
||||
export type BombUnit = z.infer<typeof BombUnitSchema>;
|
||||
export type NukeType =
|
||||
| UnitType.AtomBomb
|
||||
| UnitType.HydrogenBomb
|
||||
| UnitType.MIRV
|
||||
| UnitType.MIRVWarhead;
|
||||
|
||||
export const unitTypeToBombUnit = {
|
||||
[UnitType.AtomBomb]: "abomb",
|
||||
[UnitType.HydrogenBomb]: "hbomb",
|
||||
[UnitType.MIRV]: "mirv",
|
||||
[UnitType.MIRVWarhead]: "mirvw",
|
||||
} as const satisfies Record<NukeType, BombUnit>;
|
||||
|
||||
export const BoatUnitSchema = z.union([z.literal("trade"), z.literal("trans")]);
|
||||
export type BoatUnit = z.infer<typeof BoatUnitSchema>;
|
||||
export type BoatUnitType = UnitType.TradeShip | UnitType.TransportShip;
|
||||
|
||||
// export const unitTypeToBoatUnit = {
|
||||
// [UnitType.TradeShip]: "trade",
|
||||
// [UnitType.TransportShip]: "trans",
|
||||
// } as const satisfies Record<BoatUnitType, BoatUnit>;
|
||||
|
||||
export const OtherUnitSchema = z.union([
|
||||
z.literal("city"),
|
||||
z.literal("defp"),
|
||||
z.literal("port"),
|
||||
z.literal("wshp"),
|
||||
z.literal("silo"),
|
||||
z.literal("saml"),
|
||||
]);
|
||||
export type OtherUnit = z.infer<typeof OtherUnitSchema>;
|
||||
export type OtherUnitType =
|
||||
| UnitType.City
|
||||
| UnitType.DefensePost
|
||||
| UnitType.MissileSilo
|
||||
| UnitType.Port
|
||||
| UnitType.SAMLauncher
|
||||
| UnitType.Warship;
|
||||
|
||||
export const unitTypeToOtherUnit = {
|
||||
[UnitType.City]: "city",
|
||||
[UnitType.DefensePost]: "defp",
|
||||
[UnitType.MissileSilo]: "silo",
|
||||
[UnitType.Port]: "port",
|
||||
[UnitType.SAMLauncher]: "saml",
|
||||
[UnitType.Warship]: "wshp",
|
||||
} as const satisfies Record<OtherUnitType, OtherUnit>;
|
||||
|
||||
// Attacks
|
||||
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
|
||||
|
||||
// Boats
|
||||
export const BOAT_INDEX_SENT = 0; // Boats launched
|
||||
export const BOAT_INDEX_ARRIVE = 1; // Boats arrived
|
||||
export const BOAT_INDEX_CAPTURE = 2; // Boats captured
|
||||
export const BOAT_INDEX_DESTROY = 3; // Boats destroyed
|
||||
|
||||
// Bombs
|
||||
export const BOMB_INDEX_LAUNCH = 0; // Bombs launched
|
||||
export const BOMB_INDEX_LAND = 1; // Bombs landed
|
||||
export const BOMB_INDEX_INTERCEPT = 2; // Bombs intercepted
|
||||
|
||||
// Gold
|
||||
export const GOLD_INDEX_WORK = 0; // Gold earned by workers
|
||||
export const GOLD_INDEX_WAR = 1; // Gold earned by conquering players
|
||||
export const GOLD_INDEX_TRADE = 2; // Gold earned by trade ships
|
||||
export const GOLD_INDEX_STEAL = 3; // Gold earned by capturing trade ships
|
||||
|
||||
// Other Units
|
||||
export const OTHER_INDEX_BUILT = 0; // Structures and warships built
|
||||
export const OTHER_INDEX_DESTROY = 1; // Structures and warships destroyed
|
||||
export const OTHER_INDEX_CAPTURE = 2; // Structures captured
|
||||
export const OTHER_INDEX_LOST = 3; // Structures/warships destroyed/captured by others
|
||||
|
||||
const AtLeastOneNumberSchema = z.number().array().min(1);
|
||||
export type AtLeastOneNumber = z.infer<typeof AtLeastOneNumberSchema>;
|
||||
|
||||
export const PlayerStatsSchema = z
|
||||
.object({
|
||||
attacks: AtLeastOneNumberSchema.optional(),
|
||||
betrayals: z.number().positive().optional(),
|
||||
boats: z.record(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
bombs: z.record(BombUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
gold: AtLeastOneNumberSchema.optional(),
|
||||
units: z.record(OtherUnitSchema, AtLeastOneNumberSchema).optional(),
|
||||
})
|
||||
.optional();
|
||||
export type PlayerStats = z.infer<typeof PlayerStatsSchema>;
|
||||
+4
-17
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
|
||||
import { PlayerStatsSchema } from "./ArchiveSchemas";
|
||||
import {
|
||||
AllPlayers,
|
||||
Difficulty,
|
||||
@@ -91,7 +92,6 @@ export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>;
|
||||
|
||||
export type AllPlayersStats = z.infer<typeof AllPlayersStatsSchema>;
|
||||
export type PlayerStats = z.infer<typeof PlayerStatsSchema>;
|
||||
export type Player = z.infer<typeof PlayerSchema>;
|
||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||
const PlayerTypeSchema = z.nativeEnum(PlayerType);
|
||||
@@ -176,19 +176,6 @@ const ID = z
|
||||
.regex(/^[a-zA-Z0-9]+$/)
|
||||
.length(8);
|
||||
|
||||
const NukesEnum = z.enum([
|
||||
"Atom Bomb",
|
||||
"Hydrogen Bomb",
|
||||
"MIRV",
|
||||
"MIRV Warhead",
|
||||
]);
|
||||
|
||||
const NukeStatsSchema = z.record(NukesEnum, z.number());
|
||||
|
||||
export const PlayerStatsSchema = z.object({
|
||||
sentNukes: z.record(ID, NukeStatsSchema),
|
||||
});
|
||||
|
||||
export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
|
||||
|
||||
// Zod schemas
|
||||
@@ -460,6 +447,7 @@ export const PlayerRecordSchema = z.object({
|
||||
username: SafeString,
|
||||
ip: SafeString.nullable(), // WARNING: PII
|
||||
persistentID: PersistentIdSchema, // WARNING: PII
|
||||
stats: PlayerStatsSchema,
|
||||
});
|
||||
|
||||
export const GameRecordSchema = z.object({
|
||||
@@ -474,7 +462,6 @@ export const GameRecordSchema = z.object({
|
||||
turns: z.array(TurnSchema),
|
||||
winner: z.union([ID, SafeString]).nullable().optional(),
|
||||
winnerType: z.enum(["player", "team"]).nullable().optional(),
|
||||
allPlayersStats: z.record(ID, PlayerStatsSchema),
|
||||
version: z.enum(["v0.0.1"]),
|
||||
gitCommit: z.string().nullable().optional(),
|
||||
version: z.literal("v0.0.2"),
|
||||
gitCommit: z.string(),
|
||||
});
|
||||
|
||||
+17
-13
@@ -4,7 +4,6 @@ import twemoji from "twemoji";
|
||||
import { Cell, Team, Unit } from "./game/Game";
|
||||
import { GameMap, TileRef } from "./game/GameMap";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
ClientID,
|
||||
GameID,
|
||||
GameRecord,
|
||||
@@ -186,28 +185,33 @@ export function onlyImages(html: string) {
|
||||
|
||||
export function createGameRecord(
|
||||
id: GameID,
|
||||
gameStart: GameStartInfo,
|
||||
gameStartInfo: GameStartInfo,
|
||||
// username does not need to be set.
|
||||
players: PlayerRecord[],
|
||||
turns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
startTimestampMS: number,
|
||||
endTimestampMS: number,
|
||||
winner: ClientID | Team | null,
|
||||
winnerType: "player" | "team" | null,
|
||||
allPlayersStats: AllPlayersStats,
|
||||
): GameRecord {
|
||||
const durationSeconds = Math.floor(
|
||||
(endTimestampMS - startTimestampMS) / 1000,
|
||||
);
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
const version = "v0.0.2";
|
||||
const gitCommit = "";
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
gameStartInfo: gameStart,
|
||||
gitCommit,
|
||||
id,
|
||||
gameStartInfo,
|
||||
players,
|
||||
startTimestampMS: start,
|
||||
endTimestampMS: end,
|
||||
durationSeconds: Math.floor((end - start) / 1000),
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
startTimestampMS,
|
||||
endTimestampMS,
|
||||
durationSeconds,
|
||||
date,
|
||||
num_turns: 0,
|
||||
turns: [],
|
||||
allPlayersStats,
|
||||
version: "v0.0.1",
|
||||
version,
|
||||
winner,
|
||||
winnerType,
|
||||
};
|
||||
|
||||
@@ -116,6 +116,9 @@ export class AttackExecution implements Execution {
|
||||
this.sourceTile,
|
||||
);
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().attack(this._owner, this.target, this.startTroops);
|
||||
|
||||
for (const incoming of this._owner.incomingAttacks()) {
|
||||
if (incoming.attacker() === this.target) {
|
||||
// Target has opposing attack, cancel them out
|
||||
@@ -180,9 +183,13 @@ export class AttackExecution implements Execution {
|
||||
this._owner.id(),
|
||||
);
|
||||
}
|
||||
this._owner.addTroops(this.attack.troops() - deaths);
|
||||
const survivors = this.attack.troops() - deaths;
|
||||
this._owner.addTroops(survivors);
|
||||
this.attack.delete();
|
||||
this.active = false;
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().attackCancel(this._owner, this.target, survivors);
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
|
||||
@@ -56,13 +56,8 @@ export class MirvExecution implements Execution {
|
||||
this.targetPlayer = this.mg.owner(this.dst);
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
|
||||
this.mg
|
||||
.stats()
|
||||
.increaseNukeCount(
|
||||
this.player.id(),
|
||||
this.targetPlayer.id(),
|
||||
UnitType.MIRV,
|
||||
);
|
||||
// Record stats
|
||||
this.mg.stats().bombLaunch(this.player, this.targetPlayer, UnitType.MIRV);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
@@ -93,6 +88,8 @@ export class MirvExecution implements Execution {
|
||||
if (result === true) {
|
||||
this.separate();
|
||||
this.active = false;
|
||||
// Record stats
|
||||
this.mg.stats().bombLand(this.player, this.targetPlayer, UnitType.MIRV);
|
||||
return;
|
||||
} else {
|
||||
this.nuke.move(result);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NukeType } from "../ArchiveSchemas";
|
||||
import { consolex } from "../Consolex";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
MessageType,
|
||||
NukeType,
|
||||
Player,
|
||||
PlayerID,
|
||||
TerraNullius,
|
||||
@@ -122,8 +122,10 @@ export class NukeExecution implements Execution {
|
||||
detonationDst: this.dst,
|
||||
});
|
||||
if (this.mg.hasOwner(this.dst)) {
|
||||
const target = this.mg.owner(this.dst) as Player;
|
||||
if (this.type === UnitType.AtomBomb) {
|
||||
const target = this.mg.owner(this.dst);
|
||||
if (!target.isPlayer()) {
|
||||
// Ignore terra nullius
|
||||
} else if (this.type === UnitType.AtomBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
`${this.player.name()} - atom bomb inbound`,
|
||||
@@ -131,8 +133,7 @@ export class NukeExecution implements Execution {
|
||||
target.id(),
|
||||
);
|
||||
this.breakAlliances(this.tilesToDestroy());
|
||||
}
|
||||
if (this.type === UnitType.HydrogenBomb) {
|
||||
} else if (this.type === UnitType.HydrogenBomb) {
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
`${this.player.name()} - hydrogen bomb inbound`,
|
||||
@@ -142,13 +143,10 @@ export class NukeExecution implements Execution {
|
||||
this.breakAlliances(this.tilesToDestroy());
|
||||
}
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.increaseNukeCount(
|
||||
this.senderID,
|
||||
target.id(),
|
||||
this.nuke.type() as NukeType,
|
||||
);
|
||||
.bombLaunch(this.player, target, this.nuke.type() as NukeType);
|
||||
}
|
||||
|
||||
// after sending a nuke set the missilesilo on cooldown
|
||||
@@ -184,9 +182,10 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
private detonate() {
|
||||
if (this.mg === null || this.nuke === null) {
|
||||
if (this.mg === null || this.nuke === null || this.player === null) {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
this.breakAlliances(toDestroy);
|
||||
@@ -235,12 +234,17 @@ export class NukeExecution implements Execution {
|
||||
unit.type() !== UnitType.MIRV
|
||||
) {
|
||||
if (this.mg.euclideanDistSquared(this.dst, unit.tile()) < outer2) {
|
||||
unit.delete();
|
||||
unit.delete(true, this.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
this.nuke.delete(false);
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.bombLand(this.player, this.target(), this.nuke.type() as NukeType);
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
|
||||
@@ -48,10 +48,6 @@ export class PlayerExecution implements Execution {
|
||||
this.player.decayRelations();
|
||||
const hasPort = this.player.units(UnitType.Port).length > 0;
|
||||
this.player.units().forEach((u) => {
|
||||
if (u.health() <= 0) {
|
||||
u.delete();
|
||||
return;
|
||||
}
|
||||
if (hasPort && u.type() === UnitType.Warship) {
|
||||
u.modifyHealth(1);
|
||||
}
|
||||
@@ -69,6 +65,7 @@ export class PlayerExecution implements Execution {
|
||||
});
|
||||
|
||||
if (!this.player.isAlive()) {
|
||||
// Player has no tiles, delete any remaining units
|
||||
this.player.units().forEach((u) => {
|
||||
if (
|
||||
u.type() !== UnitType.AtomBomb &&
|
||||
@@ -86,7 +83,12 @@ export class PlayerExecution implements Execution {
|
||||
const popInc = this.config.populationIncreaseRate(this.player);
|
||||
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()));
|
||||
this.player.addTroops(popInc * this.player.targetTroopRatio());
|
||||
this.player.addGold(this.config.goldAdditionRate(this.player));
|
||||
const goldFromWorkers = this.config.goldAdditionRate(this.player);
|
||||
this.player.addGold(goldFromWorkers);
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().goldWork(this.player, goldFromWorkers);
|
||||
|
||||
const adjustRate = this.config.troopAdjustmentRate(this.player);
|
||||
this.player.addTroops(adjustRate);
|
||||
this.player.removeWorkers(adjustRate);
|
||||
@@ -245,6 +247,9 @@ export class PlayerExecution implements Execution {
|
||||
);
|
||||
capturing.addGold(gold);
|
||||
this.player.removeGold(gold);
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().goldWar(capturing, this.player, gold);
|
||||
}
|
||||
|
||||
for (const tile of tiles) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NukeType } from "../ArchiveSchemas";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
@@ -65,8 +66,17 @@ export class SAMMissileExecution implements Execution {
|
||||
this._owner.id(),
|
||||
);
|
||||
this.active = false;
|
||||
this.target.delete();
|
||||
this.target.delete(true, this._owner);
|
||||
this.SAMMissile.delete(false);
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.bombIntercept(
|
||||
this._owner,
|
||||
this.target.owner(),
|
||||
this.target.type() as NukeType,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
this.SAMMissile.move(result);
|
||||
|
||||
@@ -51,7 +51,7 @@ export class ShellExecution implements Execution {
|
||||
);
|
||||
if (result === true) {
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.effectOnTarget());
|
||||
this.target.modifyHealth(-this.effectOnTarget(), this._owner);
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
} else {
|
||||
|
||||
@@ -53,6 +53,9 @@ export class TradeShipExecution implements Execution {
|
||||
dstPort: this._dstPort,
|
||||
lastSetSafeFromPirates: ticks,
|
||||
});
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().boatSendTrade(this.origOwner, this._dstPort.owner());
|
||||
}
|
||||
|
||||
if (!this.tradeShip.isActive()) {
|
||||
@@ -153,12 +156,16 @@ export class TradeShipExecution implements Execution {
|
||||
const gold = this.mg.config().tradeShipGold(this.tilesTraveled);
|
||||
|
||||
if (this.wasCaptured) {
|
||||
this.tradeShip.owner().addGold(gold);
|
||||
const player = this.tradeShip.owner();
|
||||
player.addGold(gold);
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.tradeShip.owner().id(),
|
||||
);
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().boatCapturedTrade(player, this.origOwner, gold);
|
||||
} else {
|
||||
this.srcPort.owner().addGold(gold);
|
||||
this._dstPort.owner().addGold(gold);
|
||||
@@ -172,6 +179,11 @@ export class TradeShipExecution implements Execution {
|
||||
MessageType.SUCCESS,
|
||||
this.srcPort.owner().id(),
|
||||
);
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.boatArriveTrade(this.srcPort.owner(), this._dstPort.owner(), gold);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,6 +146,9 @@ export class TransportShipExecution implements Execution {
|
||||
this.targetID,
|
||||
);
|
||||
}
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().boatSendTroops(this.attacker, this.target, this.troops);
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
@@ -176,6 +179,11 @@ export class TransportShipExecution implements Execution {
|
||||
this.attacker.addTroops(this.troops);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.boatArriveTroops(this.attacker, this.target, this.troops);
|
||||
return;
|
||||
}
|
||||
this.attacker.conquer(this.dst);
|
||||
@@ -194,6 +202,11 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
|
||||
// Record stats
|
||||
this.mg
|
||||
.stats()
|
||||
.boatArriveTroops(this.attacker, this.target, this.troops);
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
this.boat.move(result.tile);
|
||||
|
||||
@@ -202,8 +202,6 @@ export const nukeTypes = [
|
||||
UnitType.MIRV,
|
||||
] as UnitType[];
|
||||
|
||||
export type NukeType = (typeof nukeTypes)[number];
|
||||
|
||||
export enum Relation {
|
||||
Hostile = 0,
|
||||
Distrustful = 1,
|
||||
@@ -331,7 +329,7 @@ export interface Unit {
|
||||
type(): UnitType;
|
||||
owner(): Player;
|
||||
info(): UnitInfo;
|
||||
delete(displayerMessage?: boolean): void;
|
||||
delete(displayMessage?: boolean, destroyer?: Player): void;
|
||||
tile(): TileRef;
|
||||
lastTile(): TileRef;
|
||||
move(tile: TileRef): void;
|
||||
@@ -353,7 +351,7 @@ export interface Unit {
|
||||
retreating(): boolean;
|
||||
orderBoatRetreat(): void;
|
||||
health(): number;
|
||||
modifyHealth(delta: number): void;
|
||||
modifyHealth(delta: number, attacker?: Player): void;
|
||||
|
||||
// Troops
|
||||
setTroops(troops: number): void;
|
||||
|
||||
@@ -44,7 +44,8 @@ export function createGame(
|
||||
miniGameMap: GameMap,
|
||||
config: Config,
|
||||
): Game {
|
||||
return new GameImpl(humans, nations, gameMap, miniGameMap, config);
|
||||
const stats = new StatsImpl();
|
||||
return new GameImpl(humans, nations, gameMap, miniGameMap, config, stats);
|
||||
}
|
||||
|
||||
export type CellString = string;
|
||||
@@ -71,8 +72,6 @@ export class GameImpl implements Game {
|
||||
private updates: GameUpdates = createGameUpdatesMap();
|
||||
private unitGrid: UnitGrid;
|
||||
|
||||
private _stats: StatsImpl = new StatsImpl();
|
||||
|
||||
private playerTeams: Team[] = [ColoredTeams.Red, ColoredTeams.Blue];
|
||||
private botTeam: Team = ColoredTeams.Bot;
|
||||
|
||||
@@ -82,6 +81,7 @@ export class GameImpl implements Game {
|
||||
private _map: GameMap,
|
||||
private miniGameMap: GameMap,
|
||||
private _config: Config,
|
||||
private _stats: Stats,
|
||||
) {
|
||||
this._terraNullius = new TerraNulliusImpl();
|
||||
this._width = _map.width();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AllPlayersStats, ClientID, PlayerStats } from "../Schemas";
|
||||
import { AllPlayersStats, ClientID } from "../Schemas";
|
||||
import {
|
||||
EmojiMessage,
|
||||
GameUpdates,
|
||||
@@ -116,7 +116,6 @@ export interface PlayerUpdate {
|
||||
outgoingAttacks: AttackUpdate[];
|
||||
incomingAttacks: AttackUpdate[];
|
||||
outgoingAllianceRequests: PlayerID[];
|
||||
stats: PlayerStats;
|
||||
hasSpawned: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config } from "../configuration/Config";
|
||||
import { ClientID, GameID, PlayerStats } from "../Schemas";
|
||||
import { ClientID, GameID } from "../Schemas";
|
||||
import { createRandomName } from "../Util";
|
||||
import { WorkerClient } from "../worker/WorkerClient";
|
||||
import {
|
||||
@@ -279,9 +279,6 @@ export class PlayerView {
|
||||
this.id(),
|
||||
);
|
||||
}
|
||||
stats(): PlayerStats {
|
||||
return this.data.stats;
|
||||
}
|
||||
hasSpawned(): boolean {
|
||||
return this.data.hasSpawned;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,6 @@ export class PlayerImpl implements Player {
|
||||
}) as AttackUpdate,
|
||||
),
|
||||
outgoingAllianceRequests: outgoingAllianceRequests,
|
||||
stats: this.mg.stats().getPlayerStats(this.id()),
|
||||
hasSpawned: this.hasSpawned(),
|
||||
};
|
||||
}
|
||||
@@ -381,8 +380,12 @@ export class PlayerImpl implements Player {
|
||||
this.mg.config().traitorDuration()
|
||||
);
|
||||
}
|
||||
|
||||
markTraitor(): void {
|
||||
this.markedTraitorTick = this.mg.ticks();
|
||||
|
||||
// Record stats
|
||||
this.mg.stats().betray(this);
|
||||
}
|
||||
|
||||
createAllianceRequest(recipient: Player): AllianceRequest | null {
|
||||
|
||||
+76
-7
@@ -1,12 +1,81 @@
|
||||
import { AllPlayersStats, PlayerStats } from "../Schemas";
|
||||
import { NukeType, PlayerID } from "./Game";
|
||||
import { NukeType, OtherUnitType, PlayerStats } from "../ArchiveSchemas";
|
||||
import { AllPlayersStats } from "../Schemas";
|
||||
import { Player, TerraNullius } from "./Game";
|
||||
|
||||
export interface Stats {
|
||||
increaseNukeCount(
|
||||
sender: PlayerID,
|
||||
target: PlayerID | null,
|
||||
getPlayerStats(player: Player): PlayerStats | null;
|
||||
stats(): AllPlayersStats;
|
||||
|
||||
// Player attacks target
|
||||
attack(player: Player, target: Player | TerraNullius, troops: number): void;
|
||||
|
||||
// Player cancels attack on target
|
||||
attackCancel(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void;
|
||||
|
||||
// Player betrays another player
|
||||
betray(player: Player): void;
|
||||
|
||||
// Player sends a trade ship to target
|
||||
boatSendTrade(player: Player, target: Player): void;
|
||||
|
||||
// Player's trade ship arrives at target, both players earn gold
|
||||
boatArriveTrade(player: Player, target: Player, gold: number): void;
|
||||
|
||||
// Player's trade ship, captured from target, arrives. Player earns gold.
|
||||
boatCapturedTrade(player: Player, target: Player, gold: number): void;
|
||||
|
||||
// Player destroys target's trade ship
|
||||
boatDestroyTrade(player: Player, target: Player): void;
|
||||
|
||||
// Player sends a transport ship to target with troops
|
||||
boatSendTroops(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void;
|
||||
|
||||
// Player's transport ship arrives at target with troops
|
||||
boatArriveTroops(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void;
|
||||
|
||||
// Player destroys target's transport ship with troops
|
||||
boatDestroyTroops(player: Player, target: Player, troops: number): void;
|
||||
|
||||
// Player launches bomb at target
|
||||
bombLaunch(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
type: NukeType,
|
||||
): void;
|
||||
getPlayerStats(player: PlayerID): PlayerStats;
|
||||
stats(): AllPlayersStats;
|
||||
|
||||
// Player's bomb lands at target
|
||||
bombLand(player: Player, target: Player | TerraNullius, type: NukeType): void;
|
||||
|
||||
// Player's SAM intercepts a bomb from attacker
|
||||
bombIntercept(player: Player, attacker: Player, type: NukeType): void;
|
||||
|
||||
// Player earns gold from conquering tiles or trade ships from captured
|
||||
goldWar(player: Player, captured: Player, gold: number): void;
|
||||
|
||||
// Player earns gold from workers
|
||||
goldWork(player: Player, gold: number): void;
|
||||
|
||||
// Player builds a unit of type
|
||||
unitBuild(player: Player, type: OtherUnitType): void;
|
||||
|
||||
// Player captures a unit of type
|
||||
unitCapture(player: Player, type: OtherUnitType): void;
|
||||
|
||||
// Player destroys a unit of type
|
||||
unitDestroy(player: Player, type: OtherUnitType): void;
|
||||
|
||||
// Player loses a unit of type
|
||||
unitLose(player: Player, type: OtherUnitType): void;
|
||||
}
|
||||
|
||||
+220
-24
@@ -1,34 +1,230 @@
|
||||
import { AllPlayersStats, PlayerStats } from "../Schemas";
|
||||
import { NukeType, PlayerID, UnitType } from "./Game";
|
||||
import {
|
||||
ATTACK_INDEX_CANCEL,
|
||||
ATTACK_INDEX_RECV,
|
||||
ATTACK_INDEX_SENT,
|
||||
BOAT_INDEX_ARRIVE,
|
||||
BOAT_INDEX_CAPTURE,
|
||||
BOAT_INDEX_DESTROY,
|
||||
BOAT_INDEX_SENT,
|
||||
BoatUnit,
|
||||
BOMB_INDEX_INTERCEPT,
|
||||
BOMB_INDEX_LAND,
|
||||
BOMB_INDEX_LAUNCH,
|
||||
GOLD_INDEX_STEAL,
|
||||
GOLD_INDEX_TRADE,
|
||||
GOLD_INDEX_WAR,
|
||||
GOLD_INDEX_WORK,
|
||||
NukeType,
|
||||
OTHER_INDEX_BUILT,
|
||||
OTHER_INDEX_CAPTURE,
|
||||
OTHER_INDEX_DESTROY,
|
||||
OTHER_INDEX_LOST,
|
||||
OtherUnitType,
|
||||
PlayerStats,
|
||||
unitTypeToBombUnit,
|
||||
unitTypeToOtherUnit,
|
||||
} from "../ArchiveSchemas";
|
||||
import { AllPlayersStats } from "../Schemas";
|
||||
import { Player, TerraNullius } from "./Game";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
export class StatsImpl implements Stats {
|
||||
data: AllPlayersStats = {};
|
||||
private readonly data: AllPlayersStats = {};
|
||||
|
||||
_createUserData(sender: PlayerID, target: PlayerID): void {
|
||||
if (!this.data[sender]) {
|
||||
this.data[sender] = { sentNukes: {} };
|
||||
}
|
||||
if (!this.data[sender].sentNukes[target]) {
|
||||
this.data[sender].sentNukes[target] = {
|
||||
[UnitType.MIRV]: 0,
|
||||
[UnitType.MIRVWarhead]: 0,
|
||||
[UnitType.AtomBomb]: 0,
|
||||
[UnitType.HydrogenBomb]: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
increaseNukeCount(sender: PlayerID, target: PlayerID, type: NukeType): void {
|
||||
this._createUserData(sender, target);
|
||||
this.data[sender].sentNukes[target][type]++;
|
||||
}
|
||||
|
||||
getPlayerStats(player: PlayerID): PlayerStats {
|
||||
return this.data[player];
|
||||
getPlayerStats(player: Player): PlayerStats {
|
||||
const clientID = player.clientID();
|
||||
if (clientID === null) return undefined;
|
||||
return this.data[clientID];
|
||||
}
|
||||
|
||||
stats() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
private _makePlayerStats(player: Player): PlayerStats {
|
||||
const clientID = player.clientID();
|
||||
if (clientID === null) return undefined;
|
||||
if (clientID in this.data) {
|
||||
return this.data[clientID];
|
||||
}
|
||||
const data = {} satisfies PlayerStats;
|
||||
this.data[clientID] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
private _addAttack(player: Player, index: number, value: number) {
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.attacks === undefined) p.attacks = [0];
|
||||
while (p.attacks.length < index) p.attacks.push(0);
|
||||
p.attacks[index] += value;
|
||||
}
|
||||
|
||||
private _addBetrayal(player: Player, value: number) {
|
||||
const data = this._makePlayerStats(player);
|
||||
if (data === undefined) return;
|
||||
if (data.betrayals === undefined) {
|
||||
data.betrayals = value;
|
||||
} else {
|
||||
data.betrayals += value;
|
||||
}
|
||||
}
|
||||
|
||||
private _addBoat(
|
||||
player: Player,
|
||||
type: BoatUnit,
|
||||
index: number,
|
||||
value: number,
|
||||
) {
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.boats === undefined) p.boats = { [type]: [0] };
|
||||
if (p.boats[type] === undefined) p.boats[type] = [0];
|
||||
while (p.boats[type].length < index) p.boats[type].push(0);
|
||||
p.boats[type][index] += value;
|
||||
}
|
||||
|
||||
private _addBomb(
|
||||
player: Player,
|
||||
nukeType: NukeType,
|
||||
index: number,
|
||||
value: number,
|
||||
): void {
|
||||
const type = unitTypeToBombUnit[nukeType];
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.bombs === undefined) p.bombs = { [type]: [0] };
|
||||
if (p.bombs[type] === undefined) p.bombs[type] = [0];
|
||||
while (p.bombs[type].length < index) p.bombs[type].push(0);
|
||||
p.bombs[type][index] += value;
|
||||
}
|
||||
|
||||
private _addGold(player: Player, index: number, value: number) {
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.gold === undefined) p.gold = [0];
|
||||
while (p.gold.length < index) p.gold.push(0);
|
||||
p.gold[index] += value;
|
||||
}
|
||||
|
||||
private _addOtherUnit(
|
||||
player: Player,
|
||||
otherUnitType: OtherUnitType,
|
||||
index: number,
|
||||
value: number,
|
||||
) {
|
||||
const type = unitTypeToOtherUnit[otherUnitType];
|
||||
const p = this._makePlayerStats(player);
|
||||
if (p === undefined) return;
|
||||
if (p.units === undefined) p.units = { [type]: [0] };
|
||||
if (p.units[type] === undefined) p.units[type] = [0];
|
||||
while (p.units[type].length < index) p.units[type].push(0);
|
||||
p.units[type][index] += value;
|
||||
}
|
||||
|
||||
attack(player: Player, target: Player | TerraNullius, troops: number): void {
|
||||
this._addAttack(player, ATTACK_INDEX_SENT, troops);
|
||||
if (target.isPlayer()) {
|
||||
this._addAttack(target, ATTACK_INDEX_RECV, troops);
|
||||
}
|
||||
}
|
||||
|
||||
attackCancel(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void {
|
||||
this._addAttack(player, ATTACK_INDEX_CANCEL, troops);
|
||||
this._addAttack(player, ATTACK_INDEX_SENT, -troops);
|
||||
if (target.isPlayer()) {
|
||||
this._addAttack(target, ATTACK_INDEX_RECV, -troops);
|
||||
}
|
||||
}
|
||||
|
||||
betray(player: Player): void {
|
||||
this._addBetrayal(player, 1);
|
||||
}
|
||||
|
||||
boatSendTrade(player: Player, target: Player): void {
|
||||
this._addBoat(player, "trade", BOAT_INDEX_SENT, 1);
|
||||
}
|
||||
|
||||
boatArriveTrade(player: Player, target: Player, gold: number): void {
|
||||
this._addBoat(player, "trade", BOAT_INDEX_ARRIVE, 1);
|
||||
this._addGold(player, GOLD_INDEX_TRADE, gold);
|
||||
this._addGold(target, GOLD_INDEX_TRADE, gold);
|
||||
}
|
||||
|
||||
boatCapturedTrade(player: Player, target: Player, gold: number): void {
|
||||
this._addBoat(player, "trade", BOAT_INDEX_CAPTURE, 1);
|
||||
this._addGold(player, GOLD_INDEX_STEAL, gold);
|
||||
}
|
||||
|
||||
boatDestroyTrade(player: Player, target: Player): void {
|
||||
this._addBoat(player, "trade", BOAT_INDEX_DESTROY, 1);
|
||||
}
|
||||
|
||||
boatSendTroops(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void {
|
||||
this._addBoat(player, "trans", BOAT_INDEX_SENT, 1);
|
||||
}
|
||||
|
||||
boatArriveTroops(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
): void {
|
||||
this._addBoat(player, "trans", BOAT_INDEX_ARRIVE, 1);
|
||||
}
|
||||
|
||||
boatDestroyTroops(player: Player, target: Player, troops: number): void {
|
||||
this._addBoat(player, "trans", BOAT_INDEX_DESTROY, 1);
|
||||
}
|
||||
|
||||
bombLaunch(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
type: NukeType,
|
||||
): void {
|
||||
this._addBomb(player, type, BOMB_INDEX_LAUNCH, 1);
|
||||
}
|
||||
|
||||
bombLand(
|
||||
player: Player,
|
||||
target: Player | TerraNullius,
|
||||
type: NukeType,
|
||||
): void {
|
||||
this._addBomb(player, type, BOMB_INDEX_LAND, 1);
|
||||
}
|
||||
|
||||
bombIntercept(player: Player, attacker: Player, type: NukeType): void {
|
||||
this._addBomb(player, type, BOMB_INDEX_INTERCEPT, 1);
|
||||
}
|
||||
|
||||
goldWork(player: Player, gold: number): void {
|
||||
this._addGold(player, GOLD_INDEX_WORK, gold);
|
||||
}
|
||||
|
||||
goldWar(player: Player, captured: Player, gold: number): void {
|
||||
this._addGold(player, GOLD_INDEX_WAR, gold);
|
||||
}
|
||||
|
||||
unitBuild(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_BUILT, 1);
|
||||
}
|
||||
|
||||
unitCapture(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_CAPTURE, 1);
|
||||
}
|
||||
|
||||
unitDestroy(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_DESTROY, 1);
|
||||
}
|
||||
|
||||
unitLose(player: Player, type: OtherUnitType): void {
|
||||
this._addOtherUnit(player, type, OTHER_INDEX_LOST, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { simpleHash, toInt, withinInt } from "../Util";
|
||||
import {
|
||||
AllUnitParams,
|
||||
MessageType,
|
||||
Player,
|
||||
Tick,
|
||||
Unit,
|
||||
UnitInfo,
|
||||
@@ -43,6 +44,16 @@ export class UnitImpl implements Unit {
|
||||
"lastSetSafeFromPirates" in params
|
||||
? (params.lastSetSafeFromPirates ?? 0)
|
||||
: 0;
|
||||
|
||||
switch (this._type) {
|
||||
case UnitType.Warship:
|
||||
case UnitType.Port:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.DefensePost:
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.City:
|
||||
this.mg.stats().unitBuild(_owner, this._type);
|
||||
}
|
||||
}
|
||||
touch(): void {
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
@@ -128,6 +139,16 @@ export class UnitImpl implements Unit {
|
||||
}
|
||||
|
||||
setOwner(newOwner: PlayerImpl): void {
|
||||
switch (this._type) {
|
||||
case UnitType.Warship:
|
||||
case UnitType.Port:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.DefensePost:
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.City:
|
||||
this.mg.stats().unitCapture(newOwner, this._type);
|
||||
this.mg.stats().unitLose(this._owner, this._type);
|
||||
}
|
||||
this._lastOwner = this._owner;
|
||||
this._lastOwner._units = this._lastOwner._units.filter((u) => u !== this);
|
||||
this._owner = newOwner;
|
||||
@@ -145,15 +166,18 @@ export class UnitImpl implements Unit {
|
||||
);
|
||||
}
|
||||
|
||||
modifyHealth(delta: number): void {
|
||||
modifyHealth(delta: number, attacker?: Player): void {
|
||||
this._health = withinInt(
|
||||
this._health + toInt(delta),
|
||||
0n,
|
||||
toInt(this.info().maxHealth ?? 1),
|
||||
);
|
||||
if (this._health === 0n) {
|
||||
this.delete(true, attacker);
|
||||
}
|
||||
}
|
||||
|
||||
delete(displayMessage: boolean = true): void {
|
||||
delete(displayMessage?: boolean, destroyer?: Player): void {
|
||||
if (!this.isActive()) {
|
||||
throw new Error(`cannot delete ${this} not active`);
|
||||
}
|
||||
@@ -161,14 +185,36 @@ export class UnitImpl implements Unit {
|
||||
this._active = false;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
this.mg.removeUnit(this);
|
||||
if (displayMessage && this.type() !== UnitType.MIRVWarhead) {
|
||||
if (displayMessage !== false && this._type !== UnitType.MIRVWarhead) {
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was destroyed`,
|
||||
`Your ${this._type} was destroyed`,
|
||||
MessageType.ERROR,
|
||||
this.owner().id(),
|
||||
);
|
||||
}
|
||||
if (destroyer !== undefined) {
|
||||
switch (this._type) {
|
||||
case UnitType.TransportShip:
|
||||
this.mg
|
||||
.stats()
|
||||
.boatDestroyTroops(destroyer, this._owner, this._troops);
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
this.mg.stats().boatDestroyTrade(destroyer, this._owner);
|
||||
break;
|
||||
case UnitType.City:
|
||||
case UnitType.DefensePost:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.Port:
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.Warship:
|
||||
this.mg.stats().unitDestroy(destroyer, this._type);
|
||||
this.mg.stats().unitLose(this.owner(), this._type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) {
|
||||
ip: p.ip,
|
||||
persistentID: p.persistentID,
|
||||
clientID: p.clientID,
|
||||
stats: p.stats,
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
+15
-11
@@ -2,7 +2,6 @@ import ipAnonymize from "ip-anonymize";
|
||||
import { Logger } from "winston";
|
||||
import WebSocket from "ws";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
ClientID,
|
||||
ClientMessage,
|
||||
ClientMessageSchema,
|
||||
@@ -49,8 +48,6 @@ export class GameServer {
|
||||
private lastPingUpdate = 0;
|
||||
|
||||
private winner: ClientSendWinnerMessage | null = null;
|
||||
// This field is currently only filled at victory
|
||||
private allPlayersStats: AllPlayersStats = {};
|
||||
|
||||
private gameStartInfo: GameStartInfo;
|
||||
|
||||
@@ -204,7 +201,6 @@ export class GameServer {
|
||||
}
|
||||
if (clientMsg.type === "winner") {
|
||||
this.winner = clientMsg;
|
||||
this.allPlayersStats = clientMsg.allPlayersStats;
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.info(
|
||||
@@ -389,12 +385,21 @@ export class GameServer {
|
||||
if (this.allClients.size > 0) {
|
||||
const playerRecords: PlayerRecord[] = Array.from(
|
||||
this.allClients.values(),
|
||||
).map((client) => ({
|
||||
ip: ipAnonymize(client.ip),
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
}));
|
||||
).map((client) => {
|
||||
const stats = this.winner?.allPlayersStats[client.clientID];
|
||||
if (stats === undefined) {
|
||||
this.log.warn(
|
||||
`Unable to find stats for clientID ${client.clientID}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
ip: ipAnonymize(client.ip),
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
stats,
|
||||
} satisfies PlayerRecord;
|
||||
});
|
||||
archive(
|
||||
createGameRecord(
|
||||
this.id,
|
||||
@@ -405,7 +410,6 @@ export class GameServer {
|
||||
Date.now(),
|
||||
this.winner?.winner ?? null,
|
||||
this.winner?.winnerType ?? null,
|
||||
this.allPlayersStats,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -70,7 +70,7 @@ const logger = winston.createLogger({
|
||||
),
|
||||
defaultMeta: {
|
||||
service: "openfront",
|
||||
environment: process.env.NODE_ENV,
|
||||
environment: process.env.GAME_ENV ?? "prod",
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
|
||||
Reference in New Issue
Block a user