mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 18:52:03 +00:00
attack meta changes (#742)
## Description: Full changes for the attack meta for v23. These include changes to attack losses and speed both in general and in relation with defense posts and fallout. Max population, population growth, and gold generation are also adjusted. Troops and attack boats sent now count within max pop. Deployed at attackmeta.openfront.dev ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: 1brucben
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -422,6 +422,7 @@ export interface Player {
|
||||
// Resources & Population
|
||||
gold(): Gold;
|
||||
population(): number;
|
||||
totalPopulation(): number;
|
||||
workers(): number;
|
||||
troops(): number;
|
||||
targetTroopRatio(): number;
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface PlayerUpdate {
|
||||
tilesOwned: number;
|
||||
gold: number;
|
||||
population: number;
|
||||
totalPopulation: number;
|
||||
workers: number;
|
||||
troops: number;
|
||||
targetTroopRatio: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user