diff --git a/src/client/Transport.ts b/src/client/Transport.ts index cbef4bbba..7d85ae931 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -4,6 +4,7 @@ import { AllPlayers, Cell, GameType, + Gold, PlayerID, PlayerType, Tick, @@ -94,15 +95,13 @@ export class SendEmojiIntentEvent implements GameEvent { export class SendDonateGoldIntentEvent implements GameEvent { constructor( - public readonly sender: PlayerView, public readonly recipient: PlayerView, - public readonly gold: number | null, + public readonly gold: Gold | null, ) {} } export class SendDonateTroopsIntentEvent implements GameEvent { constructor( - public readonly sender: PlayerView, public readonly recipient: PlayerView, public readonly troops: number | null, ) {} @@ -110,7 +109,6 @@ export class SendDonateTroopsIntentEvent implements GameEvent { export class SendQuickChatEvent implements GameEvent { constructor( - public readonly sender: PlayerView, public readonly recipient: PlayerView, public readonly quickChatKey: string, public readonly variables: { [key: string]: string }, @@ -119,17 +117,13 @@ export class SendQuickChatEvent implements GameEvent { export class SendEmbargoIntentEvent implements GameEvent { constructor( - public readonly sender: PlayerView, public readonly target: PlayerView, public readonly action: "start" | "stop", ) {} } export class CancelAttackIntentEvent implements GameEvent { - constructor( - public readonly playerID: PlayerID, - public readonly attackID: string, - ) {} + constructor(public readonly attackID: string) {} } export class CancelBoatIntentEvent implements GameEvent { diff --git a/src/client/Utils.ts b/src/client/Utils.ts index a6c90191f..62fc2e80e 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -4,7 +4,8 @@ export function renderTroops(troops: number): string { return renderNumber(troops / 10); } -export function renderNumber(num: number): string { +export function renderNumber(num: number | bigint): string { + num = Number(num); num = Math.max(num, 0); if (num >= 10_000_000) { diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index f048c2271..70f28ea65 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -12,7 +12,7 @@ import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.s import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg"; import { translateText } from "../../../client/Utils"; import { EventBus } from "../../../core/EventBus"; -import { Cell, PlayerActions, UnitType } from "../../../core/game/Game"; +import { Cell, Gold, PlayerActions, UnitType } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameView } from "../../../core/game/GameView"; import { BuildUnitIntentEvent } from "../../Transport"; @@ -314,13 +314,13 @@ export class BuildMenu extends LitElement implements Layer { return unit[0].canBuild !== false; } - private cost(item: BuildItemDisplay): number { + private cost(item: BuildItemDisplay): Gold { for (const bu of this.playerActions?.buildableUnits ?? []) { if (bu.type === item.unitType) { return bu.cost; } } - return 0; + return 0n; } private count(item: BuildItemDisplay): string { diff --git a/src/client/graphics/layers/ChatModal.ts b/src/client/graphics/layers/ChatModal.ts index 40cb4e4a4..7bec6d3b4 100644 --- a/src/client/graphics/layers/ChatModal.ts +++ b/src/client/graphics/layers/ChatModal.ts @@ -236,7 +236,6 @@ export class ChatModal extends LitElement { this.eventBus.emit( new SendQuickChatEvent( - this.sender, this.recipient, this.selectedQuickChatKey, variables, diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 19d2bd02c..c01ae4bc9 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -2,6 +2,7 @@ import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "../../../client/Utils"; import { EventBus } from "../../../core/EventBus"; +import { Gold } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { AttackRatioEvent } from "../../InputHandler"; import { SendSetTargetTroopRatioEvent } from "../../Transport"; @@ -46,10 +47,10 @@ export class ControlPanel extends LitElement implements Layer { private _manpower: number = 0; @state() - private _gold: number; + private _gold: Gold; @state() - private _goldPerSecond: number; + private _goldPerSecond: Gold; private _lastPopulationIncreaseRate: number; @@ -124,7 +125,7 @@ export class ControlPanel extends LitElement implements Layer { this._troops = player.troops(); this._workers = player.workers(); this.popRate = this.game.config().populationIncreaseRate(player) * 10; - this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10; + this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n; this.currentTroopRatio = player.troops() / player.population(); this.requestUpdate(); diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 0ccba3800..ef236985e 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -380,7 +380,7 @@ export class EventsDisplay extends LitElement implements Layer { emitCancelAttackIntent(id: string) { const myPlayer = this.game.myPlayer(); if (!myPlayer) return; - this.eventBus.emit(new CancelAttackIntentEvent(myPlayer.id(), id)); + this.eventBus.emit(new CancelAttackIntentEvent(id)); } emitBoatCancelIntent(id: number) { diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index 4e82be5bb..859803e7c 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -90,7 +90,6 @@ export class PlayerPanel extends LitElement implements Layer { e.stopPropagation(); this.eventBus.emit( new SendDonateTroopsIntentEvent( - myPlayer, other, myPlayer.troops() * this.uiState.attackRatio, ), @@ -104,7 +103,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendDonateGoldIntentEvent(myPlayer, other, null)); + this.eventBus.emit(new SendDonateGoldIntentEvent(other, null)); this.hide(); } @@ -114,7 +113,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "start")); + this.eventBus.emit(new SendEmbargoIntentEvent(other, "start")); this.hide(); } @@ -124,7 +123,7 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "stop")); + this.eventBus.emit(new SendEmbargoIntentEvent(other, "stop")); this.hide(); } diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index e62152f13..dcbbd10b0 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -58,7 +58,7 @@ export class TeamStats extends LitElement implements Layer { this.teams = Object.entries(grouped) .map(([teamStr, teamPlayers]) => { - let totalGold = 0; + let totalGold = 0n; let totalTroops = 0; let totalScoreSort = 0; diff --git a/src/client/graphics/layers/TopBar.ts b/src/client/graphics/layers/TopBar.ts index 76e218ba5..991fda739 100644 --- a/src/client/graphics/layers/TopBar.ts +++ b/src/client/graphics/layers/TopBar.ts @@ -50,7 +50,7 @@ export class TopBar extends LitElement implements Layer { const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10; const maxPop = this.game.config().maxPopulation(myPlayer); - const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10; + const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10n; return html`
0, + cost: () => 0n, territoryBound: false, }; case UnitType.Warship: return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : Math.min( - 1_000_000, - (p.unitsIncludingConstruction(UnitType.Warship).length + 1) * - 250_000, + ? 0n + : BigInt( + Math.min( + 1_000_000, + (p.unitsIncludingConstruction(UnitType.Warship).length + + 1) * + 250_000, + ), ), territoryBound: false, maxHealth: 1000, }; case UnitType.Shell: return { - cost: () => 0, + cost: () => 0n, territoryBound: false, damage: 250, }; case UnitType.SAMMissile: return { - cost: () => 0, + cost: () => 0n, territoryBound: false, }; case UnitType.Port: return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : Math.min( - 1_000_000, - Math.pow( - 2, - p.unitsIncludingConstruction(UnitType.Port).length, - ) * 125_000, + ? 0n + : BigInt( + Math.min( + 1_000_000, + Math.pow( + 2, + p.unitsIncludingConstruction(UnitType.Port).length, + ) * 125_000, + ), ), territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 2 * 10, @@ -327,41 +332,43 @@ export class DefaultConfig implements Config { case UnitType.AtomBomb: return { cost: (p: Player) => - p.type() === PlayerType.Human && this.infiniteGold() ? 0 : 750_000, + p.type() === PlayerType.Human && this.infiniteGold() + ? 0n + : 750_000n, territoryBound: false, }; case UnitType.HydrogenBomb: return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : 5_000_000, + ? 0n + : 5_000_000n, territoryBound: false, }; case UnitType.MIRV: return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : 25_000_000, + ? 0n + : 25_000_000n, territoryBound: false, }; case UnitType.MIRVWarhead: return { - cost: () => 0, + cost: () => 0n, territoryBound: false, }; case UnitType.TradeShip: return { - cost: () => 0, + cost: () => 0n, territoryBound: false, }; case UnitType.MissileSilo: return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : 1_000_000, + ? 0n + : 1_000_000n, territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 10 * 10, }; @@ -369,12 +376,14 @@ export class DefaultConfig implements Config { return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : Math.min( - 250_000, - (p.unitsIncludingConstruction(UnitType.DefensePost).length + - 1) * - 50_000, + ? 0n + : BigInt( + Math.min( + 250_000, + (p.unitsIncludingConstruction(UnitType.DefensePost).length + + 1) * + 50_000, + ), ), territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 5 * 10, @@ -383,12 +392,14 @@ export class DefaultConfig implements Config { return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : Math.min( - 3_000_000, - (p.unitsIncludingConstruction(UnitType.SAMLauncher).length + - 1) * - 1_500_000, + ? 0n + : BigInt( + Math.min( + 3_000_000, + (p.unitsIncludingConstruction(UnitType.SAMLauncher).length + + 1) * + 1_500_000, + ), ), territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 30 * 10, @@ -397,20 +408,22 @@ export class DefaultConfig implements Config { return { cost: (p: Player) => p.type() === PlayerType.Human && this.infiniteGold() - ? 0 - : Math.min( - 1_000_000, - Math.pow( - 2, - p.unitsIncludingConstruction(UnitType.City).length, - ) * 125_000, + ? 0n + : BigInt( + Math.min( + 1_000_000, + Math.pow( + 2, + p.unitsIncludingConstruction(UnitType.City).length, + ) * 125_000, + ), ), territoryBound: true, constructionDuration: this.instantBuild() ? 0 : 2 * 10, }; case UnitType.Construction: return { - cost: () => 0, + cost: () => 0n, territoryBound: true, }; default: @@ -688,8 +701,8 @@ export class DefaultConfig implements Config { return Math.min(player.population() + toAdd, max) - player.population(); } - goldAdditionRate(player: Player): number { - return 0.045 * player.workers() ** 0.7; + goldAdditionRate(player: Player): Gold { + return BigInt(Math.floor(0.045 * player.workers() ** 0.7)); } troopAdjustmentRate(player: Player): number { diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 49ed2e89f..ba7a7c9c0 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -2,6 +2,7 @@ import { consolex } from "../Consolex"; import { Execution, Game, + Gold, Player, PlayerID, Tick, @@ -26,7 +27,7 @@ export class ConstructionExecution implements Execution { private ticksUntilComplete: Tick; - private cost: number; + private cost: Gold; constructor( private ownerId: PlayerID, diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index 166f34cde..20837d66e 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -1,5 +1,5 @@ import { consolex } from "../Consolex"; -import { Execution, Game, Player, PlayerID } from "../game/Game"; +import { Execution, Game, Gold, Player, PlayerID } from "../game/Game"; export class DonateGoldExecution implements Execution { private sender: Player; @@ -10,7 +10,7 @@ export class DonateGoldExecution implements Execution { constructor( private senderID: PlayerID, private recipientID: PlayerID, - private gold: number | null, + private gold: Gold | null, ) {} init(mg: Game, ticks: number): void { @@ -28,14 +28,16 @@ export class DonateGoldExecution implements Execution { this.sender = mg.player(this.senderID); this.recipient = mg.player(this.recipientID); if (this.gold === null) { - this.gold = Math.round(this.sender.gold() / 3); + this.gold = this.sender.gold() / 3n; } } tick(ticks: number): void { if (this.gold === null) throw new Error("not initialized"); - if (this.sender.canDonate(this.recipient)) { - this.sender.donateGold(this.recipient, this.gold); + if ( + this.sender.canDonate(this.recipient) && + this.sender.donateGold(this.recipient, this.gold) + ) { this.recipient.updateRelation(this.sender, 50); } else { consolex.warn( diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 99dc4ca66..40fde1781 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -34,8 +34,10 @@ export class DonateTroopsExecution implements Execution { tick(ticks: number): void { if (this.troops === null) throw new Error("not initialized"); - if (this.sender.canDonate(this.recipient)) { - this.sender.donateTroops(this.recipient, this.troops); + if ( + this.sender.canDonate(this.recipient) && + this.sender.donateTroops(this.recipient, this.troops) + ) { this.recipient.updateRelation(this.sender, 50); } else { consolex.warn( diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 4fa3ec7cd..7a657c176 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -4,6 +4,7 @@ import { Difficulty, Execution, Game, + Gold, Nation, Player, PlayerID, @@ -543,7 +544,7 @@ export class FakeHumanExecution implements Execution { return null; } - private cost(type: UnitType): number { + private cost(type: UnitType): Gold { if (this.player === null) throw new Error("not initialized"); return this.mg.unitInfo(type).cost(this.player); } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index f27821f50..d054a6b04 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -12,7 +12,7 @@ import { Stats } from "./Stats"; export type PlayerID = string; export type Tick = number; -export type Gold = number; +export type Gold = bigint; export const AllPlayers = "AllPlayers" as const; @@ -450,7 +450,7 @@ export interface Player { troops(): number; targetTroopRatio(): number; addGold(toAdd: Gold): void; - removeGold(toRemove: Gold): void; + removeGold(toRemove: Gold): Gold; addWorkers(toAdd: number): void; removeWorkers(toRemove: number): void; setTargetTroopRatio(target: number): void; @@ -506,8 +506,8 @@ export interface Player { // Donation canDonate(recipient: Player): boolean; - donateTroops(recipient: Player, troops: number): void; - donateGold(recipient: Player, gold: number): void; + donateTroops(recipient: Player, troops: number): boolean; + donateGold(recipient: Player, gold: Gold): boolean; // Embargo hasEmbargoAgainst(other: Player): boolean; @@ -619,7 +619,7 @@ export interface PlayerActions { export interface BuildableUnit { canBuild: TileRef | false; type: UnitType; - cost: number; + cost: Gold; } export interface PlayerProfile { diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 385739d8a..b238365d0 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -2,6 +2,7 @@ import { AllPlayersStats, ClientID } from "../Schemas"; import { EmojiMessage, GameUpdates, + Gold, MessageType, NameViewData, PlayerID, @@ -103,7 +104,7 @@ export interface PlayerUpdate { playerType: PlayerType; isAlive: boolean; tilesOwned: number; - gold: number; + gold: Gold; population: number; workers: number; troops: number; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index ccb35498d..8d8cbf949 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -137,7 +137,7 @@ export class PlayerImpl implements Player { playerType: this.type(), isAlive: this.isAlive(), tilesOwned: this.numTilesOwned(), - gold: Number(this._gold), + gold: this._gold, population: this.population(), workers: this.workers(), troops: this.troops(), @@ -539,9 +539,13 @@ export class PlayerImpl implements Player { return true; } - donateTroops(recipient: Player, troops: number): void { + donateTroops(recipient: Player, troops: number): boolean { + if (troops <= 0) return false; + const removed = this.removeTroops(troops); + if (removed === 0) return false; + recipient.addTroops(removed); + this.sentDonations.push(new Donation(recipient, this.mg.ticks())); - recipient.addTroops(this.removeTroops(troops)); this.mg.displayMessage( `Sent ${renderTroops(troops)} troops to ${recipient.name()}`, MessageType.INFO, @@ -552,10 +556,16 @@ export class PlayerImpl implements Player { MessageType.SUCCESS, recipient.id(), ); + return true; } - donateGold(recipient: Player, gold: number): void { + + donateGold(recipient: Player, gold: Gold): boolean { + if (gold <= 0n) return false; + const removed = this.removeGold(gold); + if (removed === 0n) return false; + recipient.addGold(removed); + this.sentDonations.push(new Donation(recipient, this.mg.ticks())); - recipient.addGold(this.removeGold(gold)); this.mg.displayMessage( `Sent ${renderNumber(gold)} gold to ${recipient.name()}`, MessageType.INFO, @@ -566,6 +576,7 @@ export class PlayerImpl implements Player { MessageType.SUCCESS, recipient.id(), ); + return true; } hasEmbargoAgainst(other: Player): boolean { @@ -632,20 +643,20 @@ export class PlayerImpl implements Player { } gold(): Gold { - return Number(this._gold); + return this._gold; } addGold(toAdd: Gold): void { - this._gold += toInt(toAdd); + this._gold += toAdd; } - removeGold(toRemove: Gold): number { - if (toRemove <= 1) { - return 0; + removeGold(toRemove: Gold): Gold { + if (toRemove <= 0n) { + return 0n; } - const actualRemoved = minInt(this._gold, toInt(toRemove)); + const actualRemoved = minInt(this._gold, toRemove); this._gold -= actualRemoved; - return Number(actualRemoved); + return actualRemoved; } population(): number {