diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index cc8627bb2..7da156e3b 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -125,7 +125,8 @@ export interface Config { defensePostRange(): number; SAMCooldown(): number; SiloCooldown(): number; - defensePostDefenseBonus(): number; + defensePostLossMultiplier(): number; + defensePostSpeedMultiplier(): number; falloutDefenseModifier(percentOfFallout: number): number; difficultyModifier(difficulty: Difficulty): number; warshipPatrolRange(): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index eca34f10b..2d9a891cd 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -38,6 +38,12 @@ const JwksSchema = z.object({ .min(1), }); +const TERRAIN_EFFECTS = { + [TerrainType.Plains]: { mag: 0.85, speed: 0.8 }, + [TerrainType.Highland]: { mag: 1, speed: 1 }, + [TerrainType.Mountain]: { mag: 1.2, speed: 1.3 }, +} as const; + export abstract class DefaultServerConfig implements ServerConfig { private publicKey: JWK; abstract jwtAudience(): string; @@ -249,8 +255,8 @@ export class DefaultConfig implements Config { falloutDefenseModifier(falloutRatio: number): number { // falloutRatio is between 0 and 1 - // So defense modifier is between [5, 2.5] - return 5 - falloutRatio * 2; + // So defense modifier is between [3, 1] + return 3 - falloutRatio * 2; } SAMCooldown(): number { return 75; @@ -260,10 +266,13 @@ export class DefaultConfig implements Config { } defensePostRange(): number { - return 30; + return 40; } - defensePostDefenseBonus(): number { - return 5; + defensePostLossMultiplier(): number { + return 6; + } + defensePostSpeedMultiplier(): number { + return 3; } playerTeams(): number | typeof Duos { return this._gameConfig.playerTeams ?? 0; @@ -493,34 +502,27 @@ export class DefaultConfig implements Config { defenderTroopLoss: number; tilesPerTickUsed: number; } { - let mag = 0; - let speed = 0; const type = gm.terrainType(tileToConquer); - switch (type) { - case TerrainType.Plains: - mag = 85; - speed = 16.5; - break; - case TerrainType.Highland: - mag = 100; - speed = 20; - break; - case TerrainType.Mountain: - mag = 120; - speed = 25; - break; - default: - throw new Error(`terrain type ${type} not supported`); + const mod = TERRAIN_EFFECTS[type]; + if (!mod) { + throw new Error(`terrain type ${type} not supported`); } - if (defender.isPlayer()) { + let mag = mod.mag; + let speed = mod.speed; + + const attackerType = attacker.type(); + const defenderIsPlayer = defender.isPlayer(); + const defenderType = defenderIsPlayer ? defender.type() : null; + + if (defenderIsPlayer) { for (const dp of gm.nearbyUnits( tileToConquer, gm.config().defensePostRange(), UnitType.DefensePost, )) { if (dp.unit.owner() === defender) { - mag *= this.defensePostDefenseBonus(); - speed *= this.defensePostDefenseBonus(); + mag *= this.defensePostLossMultiplier(); + speed *= this.defensePostSpeedMultiplier(); break; } } @@ -532,55 +534,50 @@ export class DefaultConfig implements Config { speed *= this.falloutDefenseModifier(falloutRatio); } - if (attacker.isPlayer() && defender.isPlayer()) { + if (attacker.isPlayer() && defenderIsPlayer) { if ( - attacker.type() === PlayerType.Human && - defender.type() === PlayerType.Bot + attackerType === PlayerType.Human && + defenderType === PlayerType.Bot ) { mag *= 0.8; } if ( - attacker.type() === PlayerType.FakeHuman && - defender.type() === PlayerType.Bot + attackerType === PlayerType.FakeHuman && + defenderType === PlayerType.Bot ) { mag *= 0.8; } } - - let largeLossModifier = 1; - if (attacker.numTilesOwned() > 100_000) { - largeLossModifier = Math.sqrt(100_000 / attacker.numTilesOwned()); + if (attackerType === PlayerType.Bot) { + speed *= 4; // slow bot attacks } - let largeSpeedMalus = 1; - if (attacker.numTilesOwned() > 75_000) { - // sqrt is only exponent 1/2 which doesn't slow enough huge players - largeSpeedMalus = (75_000 / attacker.numTilesOwned()) ** 0.6; - } - - if (defender.isPlayer()) { + if (defenderIsPlayer) { + const defenderTroops = defender.troops(); + const defenderTiles = defender.numTilesOwned(); + const defenderDensity = defenderTroops / defenderTiles; + const attackRatio = defenderTroops / attackTroops; + const traitorDebuff = defender.isTraitor() + ? this.traitorDefenseDebuff() + : 1; + const baseTroopLoss = 16; + const baseTileCost = 23; + const attackStandardSize = 10_000; return { attackerTroopLoss: - within(defender.troops() / attackTroops, 0.6, 2) * - mag * - 0.8 * - largeLossModifier * - (defender.isTraitor() ? this.traitorDefenseDebuff() : 1), - defenderTroopLoss: defender.troops() / defender.numTilesOwned(), + mag * (baseTroopLoss + defenderDensity * traitorDebuff), + defenderTroopLoss: defenderDensity, tilesPerTickUsed: - within(defender.troops() / (5 * attackTroops), 0.2, 1.5) * + baseTileCost * + within(defenderDensity, 3, 100) ** 0.2 * + (attackStandardSize / attackTroops) ** 0.1 * speed * - largeSpeedMalus, + within(attackRatio, 0.1, 20) ** 0.4, }; } else { return { - attackerTroopLoss: - attacker.type() === PlayerType.Bot ? mag / 10 : mag / 5, + attackerTroopLoss: 16 * mag, defenderTroopLoss: 0, - tilesPerTickUsed: within( - (2000 * Math.max(10, speed)) / attackTroops, - 5, - 100, - ), + tilesPerTickUsed: 31 * speed, }; } } @@ -592,13 +589,9 @@ export class DefaultConfig implements Config { numAdjacentTilesWithEnemy: number, ): number { if (defender.isPlayer()) { - return ( - within(((5 * attackTroops) / defender.troops()) * 2, 0.01, 0.5) * - numAdjacentTilesWithEnemy * - 3 - ); + return 10 * numAdjacentTilesWithEnemy; } else { - return numAdjacentTilesWithEnemy * 2; + return 12 * numAdjacentTilesWithEnemy; } } @@ -628,28 +621,28 @@ export class DefaultConfig implements Config { startManpower(playerInfo: PlayerInfo): number { if (playerInfo.playerType === PlayerType.Bot) { - return 10_000; + return 6_000; } if (playerInfo.playerType === PlayerType.FakeHuman) { switch (this._gameConfig.difficulty) { case Difficulty.Easy: - return 2_500 * (playerInfo?.nation?.strength ?? 1); + return 2_500 + 1000 * (playerInfo?.nation?.strength ?? 1); case Difficulty.Medium: - return 5_000 * (playerInfo?.nation?.strength ?? 1); + return 6_000 + 2000 * (playerInfo?.nation?.strength ?? 1); case Difficulty.Hard: - return 20_000 * (playerInfo?.nation?.strength ?? 1); + return 20_000 + 4000 * (playerInfo?.nation?.strength ?? 1); case Difficulty.Impossible: - return 50_000 * (playerInfo?.nation?.strength ?? 1); + return 50_000 + 8000 * (playerInfo?.nation?.strength ?? 1); } } - return this.infiniteTroops() ? 1_000_000 : 25_000; + return this.infiniteTroops() ? 1_000_000 : 20_000; } maxPopulation(player: Player | PlayerView): number { const maxPop = player.type() === PlayerType.Human && this.infiniteTroops() ? 1_000_000_000 - : 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + + : 1 * (player.numTilesOwned() * 30 + 50000) + player.units(UnitType.City).length * this.cityPopulationIncrease(); if (player.type() === PlayerType.Bot) { @@ -662,22 +655,26 @@ export class DefaultConfig implements Config { switch (this._gameConfig.difficulty) { case Difficulty.Easy: - return maxPop * 0.5; + return maxPop * 0.4; case Difficulty.Medium: - return maxPop * 1; + return maxPop * 0.8; case Difficulty.Hard: - return maxPop * 1.5; + return maxPop * 1.4; case Difficulty.Impossible: - return maxPop * 2; + return maxPop * 1.8; } } populationIncreaseRate(player: Player): number { const max = this.maxPopulation(player); - - let toAdd = 10 + Math.pow(player.population(), 0.73) / 4; - - const ratio = 1 - player.population() / max; + //population grows proportional to current population with growth decreasing as it approaches max + // smaller countries recieve a boost to pop growth to speed up early game + const baseAdditionRate = 10; + const basePopGrowthRate = 1300 / max + 1 / 140; + const reproductionPop = 0.8 * player.troops() + 1.2 * player.workers(); + let toAdd = baseAdditionRate + basePopGrowthRate * reproductionPop; + const totalPop = player.totalPopulation(); + const ratio = 1 - totalPop / max; toAdd *= ratio; if (player.type() === PlayerType.Bot) { @@ -701,7 +698,7 @@ export class DefaultConfig implements Config { } } - return Math.min(player.population() + toAdd, max) - player.population(); + return Math.min(totalPop + toAdd, max) - totalPop; } goldAdditionRate(player: Player): number { @@ -709,7 +706,7 @@ export class DefaultConfig implements Config { } troopAdjustmentRate(player: Player): number { - const maxDiff = this.maxPopulation(player) / 1000; + const maxDiff = this.maxPopulation(player) / 500; const target = player.population() * player.targetTroopRatio(); const diff = target - player.troops(); if (Math.abs(diff) < maxDiff) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 1eace413d..fda7d78c4 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -422,6 +422,7 @@ export interface Player { // Resources & Population gold(): Gold; population(): number; + totalPopulation(): number; workers(): number; troops(): number; targetTroopRatio(): number; diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index bb080a694..426c8c598 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -104,6 +104,7 @@ export interface PlayerUpdate { tilesOwned: number; gold: number; population: number; + totalPopulation: number; workers: number; troops: number; targetTroopRatio: number; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index d1396ff7a..71aad4f1f 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -219,6 +219,9 @@ export class PlayerView { population(): number { return this.data.population; } + totalPopulation(): number { + return this.data.totalPopulation; + } workers(): number { return this.data.workers; } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 0178bbe02..989afc109 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -138,6 +138,7 @@ export class PlayerImpl implements Player { tilesOwned: this.numTilesOwned(), gold: Number(this._gold), population: this.population(), + totalPopulation: this.totalPopulation(), workers: this.workers(), troops: this.troops(), targetTroopRatio: this.targetTroopRatio(), @@ -648,6 +649,21 @@ export class PlayerImpl implements Player { population(): number { return Number(this._troops + this._workers); } + totalPopulation(): number { + return this.population() + this.attackingTroops(); + } + private attackingTroops(): number { + const landAttackTroops = this._outgoingAttacks + .filter((a) => a.isActive()) + .reduce((sum, a) => sum + a.troops(), 0); + + const boatTroops = this.units(UnitType.TransportShip) + .map((u) => u.troops()) + .reduce((sum, n) => sum + n, 0); + + return landAttackTroops + boatTroops; + } + workers(): number { return Math.max(1, Number(this._workers)); }