feat: save stats to local storage

Victory lose, player stats, lobby config are stored to local storage
(for later use)
This commit is contained in:
ilan schemoul
2025-03-06 00:32:57 +01:00
parent d2208755c4
commit 398d354702
10 changed files with 130 additions and 14 deletions
+9 -5
View File
@@ -22,6 +22,7 @@ import { getConfig, getServerConfig } from "../core/configuration/Config";
import { GameView, PlayerView } from "../core/game/GameView";
import { GameUpdateViewData } from "../core/game/GameUpdates";
import { UserSettings } from "../core/game/UserSettings";
import { LocalPersistantStats } from "./LocalPersistantStats";
export interface LobbyConfig {
flag: () => string;
@@ -120,6 +121,7 @@ export async function createClientGame(
config,
gameMap.gameMap,
lobbyConfig.clientID,
lobbyConfig.gameID,
);
consolex.log("going to init path finder");
@@ -137,7 +139,7 @@ export async function createClientGame(
);
return new ClientGameRunner(
lobbyConfig.clientID,
lobbyConfig,
eventBus,
gameRenderer,
new InputHandler(canvas, eventBus),
@@ -148,6 +150,7 @@ export async function createClientGame(
}
export class ClientGameRunner {
private localPersistantsStats = new LocalPersistantStats();
private myPlayer: PlayerView;
private isActive = false;
@@ -155,7 +158,7 @@ export class ClientGameRunner {
private hasJoined = false;
constructor(
private clientID: ClientID,
private lobby: LobbyConfig,
private eventBus: EventBus,
private renderer: GameRenderer,
private input: InputHandler,
@@ -165,6 +168,7 @@ export class ClientGameRunner {
) {}
public start() {
this.localPersistantsStats.startGame(this.lobby);
consolex.log("starting client game");
this.isActive = true;
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e));
@@ -173,7 +177,7 @@ export class ClientGameRunner {
this.input.initialize();
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
if ("errMsg" in gu) {
showErrorModal(gu.errMsg, gu.stack, this.clientID);
showErrorModal(gu.errMsg, gu.stack, this.lobby.clientID);
return;
}
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
@@ -217,7 +221,7 @@ export class ClientGameRunner {
showErrorModal(
`desync from server: ${JSON.stringify(message)}`,
"",
this.clientID,
this.lobby.clientID,
);
}
if (message.type == "turn") {
@@ -269,7 +273,7 @@ export class ClientGameRunner {
return;
}
if (this.myPlayer == null) {
this.myPlayer = this.gameView.playerByClientID(this.clientID);
this.myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
if (this.myPlayer == null) {
return;
}
+88
View File
@@ -0,0 +1,88 @@
import { consolex } from "../core/Consolex";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { PlayerStats } from "../core/game/Stats";
import { ClientID, GameID } from "../core/Schemas";
import { LobbyConfig } from "./ClientGameRunner";
export interface GameStat {
lobby: {
clientID: ClientID;
persistentID: string;
map: GameMapType | null;
gameType: GameType;
difficulty: Difficulty | null;
infiniteGold: boolean | null;
infiniteTroops: boolean | null;
instantBuild: boolean | null;
bots: number | null;
disableNPCs: boolean | null;
};
playerStats?: PlayerStats;
outcome?: "victory" | "defeat";
}
export class PersistantStats {
// Can be used to handle breaking changes
version: "v0.0.1";
games: {
[key: GameID]: GameStat;
};
}
export class LocalPersistantStats {
private getStats() {
const statsStr = localStorage.getItem("stats");
let stats: PersistantStats;
if (!statsStr) {
stats = { version: "v0.0.1", games: {} };
} else {
stats = JSON.parse(statsStr);
}
return stats;
}
public startGame(lobby: LobbyConfig) {
if (typeof localStorage === "undefined") {
return;
}
const stats = this.getStats();
stats.games[lobby.gameID] = {
lobby: {
clientID: lobby.clientID,
persistentID: lobby.persistentID,
map: lobby.map,
gameType: lobby.gameType,
difficulty: lobby.difficulty,
infiniteGold: lobby.infiniteGold,
infiniteTroops: lobby.infiniteTroops,
instantBuild: lobby.instantBuild,
bots: lobby.bots,
disableNPCs: lobby.disableNPCs,
},
};
localStorage.setItem("stats", JSON.stringify(stats));
}
public endGame(
id: GameID,
playerStats: PlayerStats,
outcome: GameStat["outcome"],
) {
if (typeof localStorage === "undefined") {
return;
}
const stats = this.getStats();
const gameStat = stats.games[id];
if (!gameStat) {
consolex.log("game not found");
return;
}
gameStat.outcome = outcome;
gameStat.playerStats = playerStats;
localStorage.setItem("stats", JSON.stringify(stats));
}
}
+2
View File
@@ -22,12 +22,14 @@ import {
} from "../core/Schemas";
import { CreateGameRecord, generateID } from "../core/Util";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalPersistantStats } from "./LocalPersistantStats";
import { getPersistentIDFromCookie } from "./Main";
export class LocalServer {
private turns: Turn[] = [];
private intents: Intent[] = [];
private startedAt: number;
private localPersistantsStats = new LocalPersistantStats();
private endTurnIntervalID;
+7 -6
View File
@@ -8,6 +8,7 @@ import {
AllPlayers,
Player,
PlayerActions,
PlayerID,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -133,8 +134,8 @@ export class PlayerPanel extends LitElement implements Layer {
this.requestUpdate();
}
getTotalNukesSent(): number {
const stats = this.actions.interaction?.stats;
getTotalNukesSent(otherId: PlayerID): number {
const stats = this.g.player(otherId).stats();
if (!stats) {
return 0;
}
@@ -189,7 +190,7 @@ export class PlayerPanel extends LitElement implements Layer {
<button
@click=${this.handleClose}
class="absolute -top-2 -right-2 w-6 h-6 flex items-center justify-center
bg-red-500 hover:bg-red-600 text-white rounded-full
bg-red-500 hover:bg-red-600 text-white rounded-full
text-sm font-bold transition-colors"
>
@@ -199,7 +200,7 @@ export class PlayerPanel extends LitElement implements Layer {
<!-- Name section -->
<div class="flex items-center gap-1 lg:gap-2">
<div
class="px-4 h-8 lg:h-10 flex items-center justify-center
class="px-4 h-8 lg:h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
rounded text-sm lg:text-xl w-full"
>
@@ -251,7 +252,7 @@ export class PlayerPanel extends LitElement implements Layer {
Nukes sent by them to you
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${this.getTotalNukesSent()}
${this.getTotalNukesSent(other.id())}
</div>
</div>
@@ -261,7 +262,7 @@ export class PlayerPanel extends LitElement implements Layer {
? html`<button
@click=${(e) => this.handleTargetClick(e, other)}
class="w-10 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
<img src=${targetIcon} alt="Target" class="w-6 h-6" />
+12
View File
@@ -9,6 +9,7 @@ import { PseudoRandom } from "../../../core/PseudoRandom";
import { simpleHash } from "../../../core/Util";
import { EventBus } from "../../../core/EventBus";
import { SendWinnerEvent } from "../../Transport";
import { GameStat, LocalPersistantStats } from "../../LocalPersistantStats";
// Add this at the top of your file
declare global {
@@ -27,6 +28,7 @@ export class WinModal extends LitElement implements Layer {
private rand: PseudoRandom;
private hasShownDeathModal = false;
private localPersistantsStats = new LocalPersistantStats();
@state()
isVisible = false;
@@ -220,6 +222,14 @@ export class WinModal extends LitElement implements Layer {
this.rand = new PseudoRandom(simpleHash(this.game.myClientID()));
}
private updateGameStats(outcome: GameStat["outcome"]) {
this.localPersistantsStats.endGame(
this.game.gameID(),
this.game.myPlayer().stats(),
outcome,
);
}
tick() {
const myPlayer = this.game.myPlayer();
if (!this.hasShownDeathModal && myPlayer && !myPlayer.isAlive()) {
@@ -235,9 +245,11 @@ export class WinModal extends LitElement implements Layer {
if (winner == this.game.myPlayer()) {
this._title = "You Won!";
this.won = true;
this.updateGameStats("victory");
} else {
this._title = `${winner.name()} has won!`;
this.won = false;
this.updateGameStats("defeat");
}
this.show();
});
-1
View File
@@ -168,7 +168,6 @@ export class GameRunner {
canBreakAlliance: player.isAlliedWith(other),
canDonate: player.canDonate(other),
canEmbargo: !player.hasEmbargoAgainst(other),
stats: this.game.stats().getPlayerStats(other.id()),
};
}
-1
View File
@@ -421,7 +421,6 @@ export interface PlayerInteraction {
canTarget: boolean;
canDonate: boolean;
canEmbargo: boolean;
stats: PlayerStats;
}
export interface EmojiMessage {
+2
View File
@@ -12,6 +12,7 @@ import {
UnitType,
} from "./Game";
import { TileRef, TileUpdate } from "./GameMap";
import { PlayerStats } from "./Stats";
export interface GameUpdateViewData {
tick: number;
@@ -105,6 +106,7 @@ export interface PlayerUpdate {
outgoingAttacks: AttackUpdate[];
incomingAttacks: AttackUpdate[];
outgoingAllianceRequests: PlayerID[];
stats: PlayerStats;
}
export interface AllianceRequestUpdate {
+9 -1
View File
@@ -24,12 +24,13 @@ import {
UnitInfo,
UnitType,
} from "./Game";
import { ClientID } from "../Schemas";
import { ClientID, GameID } from "../Schemas";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { WorkerClient } from "../worker/WorkerClient";
import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap";
import { GameUpdateViewData } from "./GameUpdates";
import { DefenseGrid } from "./DefensePostGrid";
import { PlayerStats } from "./Stats";
export class UnitView {
public _wasUpdated = true;
@@ -220,6 +221,9 @@ export class PlayerView {
this.id(),
);
}
stats(): PlayerStats {
return this.data.stats;
}
}
export class GameView implements GameMap {
@@ -240,6 +244,7 @@ export class GameView implements GameMap {
private _config: Config,
private _map: GameMap,
private _myClientID: ClientID,
private _gameID: GameID,
) {
this.lastUpdate = {
tick: 0,
@@ -487,4 +492,7 @@ export class GameView implements GameMap {
numTilesWithFallout(): number {
return this._map.numTilesWithFallout();
}
gameID(): GameID {
return this._gameID;
}
}
+1
View File
@@ -150,6 +150,7 @@ export class PlayerImpl implements Player {
}) as AttackUpdate,
),
outgoingAllianceRequests: outgoingAllianceRequests,
stats: this.mg.stats().getPlayerStats(this.id()),
};
}