Fixed: Creative mode no longer applies to bots and npcs

This commit is contained in:
NewHappyRabbit
2025-02-12 16:46:04 +02:00
parent e67a07a894
commit 2cceacea4f
+417 -384
View File
@@ -1,417 +1,450 @@
import { import {
Difficulty, Difficulty,
Game, Game,
GameType, GameType,
Gold, Gold,
Player, Player,
PlayerInfo, PlayerInfo,
PlayerType, PlayerType,
TerrainType, TerrainType,
TerraNullius, TerraNullius,
Tick, Tick,
UnitInfo, UnitInfo,
UnitType, UnitType,
} from "../game/Game"; } from '../game/Game';
import { GameMap, TileRef } from "../game/GameMap"; import { GameMap, TileRef } from '../game/GameMap';
import { PlayerView } from "../game/GameView"; import { PlayerView } from '../game/GameView';
import { GameConfig } from "../Schemas"; import { GameConfig } from '../Schemas';
import { assertNever, within } from "../Util"; import { assertNever, within } from '../Util';
import { Config, ServerConfig, Theme } from "./Config"; import { Config, ServerConfig, Theme } from './Config';
import { pastelTheme } from "./PastelTheme"; import { pastelTheme } from './PastelTheme';
export abstract class DefaultServerConfig implements ServerConfig { export abstract class DefaultServerConfig implements ServerConfig {
turnIntervalMs(): number { turnIntervalMs(): number {
return 100; return 100;
} }
gameCreationRate(): number { gameCreationRate(): number {
return 1 * 60 * 1000; return 1 * 60 * 1000;
} }
lobbyLifetime(): number { lobbyLifetime(): number {
return 2 * 60 * 1000; return 2 * 60 * 1000;
} }
} }
export class DefaultConfig implements Config { export class DefaultConfig implements Config {
constructor( constructor(
private _serverConfig: ServerConfig, private _serverConfig: ServerConfig,
private _gameConfig: GameConfig private _gameConfig: GameConfig
) {} ) {}
spawnImmunityDuration(): Tick { spawnImmunityDuration(): Tick {
return 5 * 10; return 5 * 10;
} }
gameConfig(): GameConfig { gameConfig(): GameConfig {
return this._gameConfig; return this._gameConfig;
} }
serverConfig(): ServerConfig { serverConfig(): ServerConfig {
return this._serverConfig; return this._serverConfig;
} }
difficultyModifier(difficulty: Difficulty): number { difficultyModifier(difficulty: Difficulty): number {
switch (difficulty) { switch (difficulty) {
case Difficulty.Easy: case Difficulty.Easy:
return 1; return 1;
case Difficulty.Medium: case Difficulty.Medium:
return 3; return 3;
case Difficulty.Hard: case Difficulty.Hard:
return 9; return 9;
case Difficulty.Impossible: case Difficulty.Impossible:
return 18; return 18;
} }
} }
cityPopulationIncrease(): number { cityPopulationIncrease(): number {
return 250_000; return 250_000;
} }
falloutDefenseModifier(): number { falloutDefenseModifier(): number {
return 5; return 5;
} }
defensePostRange(): number { defensePostRange(): number {
return 30; return 30;
} }
defensePostDefenseBonus(): number { defensePostDefenseBonus(): number {
return 5; return 5;
} }
spawnNPCs(): boolean { spawnNPCs(): boolean {
return !this._gameConfig.disableNPCs; return !this._gameConfig.disableNPCs;
} }
spawnBots(): boolean { spawnBots(): boolean {
return !this._gameConfig.disableBots; return !this._gameConfig.disableBots;
} }
creativeMode(): boolean { creativeMode(): boolean {
return this._gameConfig.creativeMode; return this._gameConfig.creativeMode;
} }
tradeShipGold(dist: number): Gold { tradeShipGold(dist: number): Gold {
return 10000 + 100 * Math.pow(dist, 1.1); return 10000 + 100 * Math.pow(dist, 1.1);
} }
tradeShipSpawnRate(): number { tradeShipSpawnRate(): number {
return 500; return 500;
} }
unitInfo(type: UnitType): UnitInfo { unitInfo(type: UnitType): UnitInfo {
switch (type) { switch (type) {
case UnitType.TransportShip: case UnitType.TransportShip:
return { return {
cost: () => 0, cost: () => 0,
territoryBound: false, territoryBound: false,
}; };
case UnitType.Warship: case UnitType.Warship:
return { return {
cost: (p: Player) => this.creativeMode() ? 0 : (p.units(UnitType.Warship).length + 1) * 250_000, cost: (p: Player) =>
territoryBound: false, p.type() == PlayerType.Human && this.creativeMode()
maxHealth: 1000, ? 0
}; : (p.units(UnitType.Warship).length + 1) * 250_000,
case UnitType.Shell: territoryBound: false,
return { maxHealth: 1000,
cost: () => 0, };
territoryBound: false, case UnitType.Shell:
damage: 250, return {
}; cost: () => 0,
case UnitType.Port: territoryBound: false,
return { damage: 250,
cost: (p: Player) => };
this.creativeMode() ? 0 : case UnitType.Port:
Math.min( return {
1_000_000, cost: (p: Player) =>
Math.pow(2, p.units(UnitType.Port).length) * 250_000 p.type() == PlayerType.Human && this.creativeMode()
), ? 0
territoryBound: true, : Math.min(
constructionDuration: this.creativeMode() ? 0 : 2 * 10, 1_000_000,
}; Math.pow(2, p.units(UnitType.Port).length) *
case UnitType.AtomBomb: 250_000
return { ),
cost: () => this.creativeMode() ? 0 : 750_000, territoryBound: true,
territoryBound: false, constructionDuration: this.creativeMode() ? 0 : 2 * 10,
}; };
case UnitType.HydrogenBomb: case UnitType.AtomBomb:
return { return {
cost: () => this.creativeMode() ? 0 : 5_000_000, cost: (p: Player) =>
territoryBound: false, p.type() == PlayerType.Human && this.creativeMode()
}; ? 0
case UnitType.MIRV: : 750_000,
return { territoryBound: false,
cost: () => this.creativeMode() ? 0 : 10_000_000, };
territoryBound: false, case UnitType.HydrogenBomb:
}; return {
case UnitType.MIRVWarhead: cost: (p: Player) =>
return { p.type() == PlayerType.Human && this.creativeMode()
cost: () => 0, ? 0
territoryBound: false, : 5_000_000,
}; territoryBound: false,
case UnitType.TradeShip: };
return { case UnitType.MIRV:
cost: () => 0, return {
territoryBound: false, cost: (p: Player) =>
}; p.type() == PlayerType.Human && this.creativeMode()
case UnitType.MissileSilo: ? 0
return { : 10_000_000,
cost: () => this.creativeMode() ? 0 : 1_000_000, territoryBound: false,
territoryBound: true, };
constructionDuration: this.creativeMode() ? 0 : 10 * 10, case UnitType.MIRVWarhead:
}; return {
case UnitType.DefensePost: cost: () => 0,
return { territoryBound: false,
cost: (p: Player) => };
this.creativeMode() ? 0 : case UnitType.TradeShip:
Math.min( return {
250_000, cost: () => 0,
(p.units(UnitType.DefensePost).length + 1) * 50_000 territoryBound: false,
), };
territoryBound: true, case UnitType.MissileSilo:
constructionDuration: this.creativeMode() ? 0 : 5 * 10, return {
}; cost: (p: Player) =>
case UnitType.City: p.type() == PlayerType.Human && this.creativeMode()
return { ? 0
cost: (p: Player) => : 1_000_000,
this.creativeMode() ? 0 : territoryBound: true,
Math.min( constructionDuration: this.creativeMode() ? 0 : 10 * 10,
1_000_000, };
Math.pow(2, p.units(UnitType.City).length) * 125_000 case UnitType.DefensePost:
), return {
territoryBound: true, cost: (p: Player) =>
constructionDuration: this.creativeMode() ? 0 : 2 * 10, p.type() == PlayerType.Human && this.creativeMode()
}; ? 0
case UnitType.Construction: : Math.min(
return { 250_000,
cost: () => 0, (p.units(UnitType.DefensePost).length + 1) *
territoryBound: true, 50_000
}; ),
default: territoryBound: true,
assertNever(type); constructionDuration: this.creativeMode() ? 0 : 5 * 10,
} };
} case UnitType.City:
defaultDonationAmount(sender: Player): number { return {
return Math.floor(sender.troops() / 3); cost: (p: Player) =>
} p.type() == PlayerType.Human && this.creativeMode()
donateCooldown(): Tick { ? 0
return 10 * 10; : Math.min(
} 1_000_000,
emojiMessageDuration(): Tick { Math.pow(2, p.units(UnitType.City).length) *
return 5 * 10; 125_000
} ),
emojiMessageCooldown(): Tick { territoryBound: true,
return 15 * 10; constructionDuration: this.creativeMode() ? 0 : 2 * 10,
} };
targetDuration(): Tick { case UnitType.Construction:
return 10 * 10; return {
} cost: () => 0,
targetCooldown(): Tick { territoryBound: true,
return 15 * 10; };
} default:
allianceRequestCooldown(): Tick { assertNever(type);
return 30 * 10; }
} }
allianceDuration(): Tick { defaultDonationAmount(sender: Player): number {
return 600 * 10; return Math.floor(sender.troops() / 3);
} }
percentageTilesOwnedToWin(): number { donateCooldown(): Tick {
return 80; return 10 * 10;
} }
boatMaxNumber(): number { emojiMessageDuration(): Tick {
return 3; return 5 * 10;
} }
boatMaxDistance(): number { emojiMessageCooldown(): Tick {
return 500; return 15 * 10;
} }
numSpawnPhaseTurns(): number { targetDuration(): Tick {
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300; return 10 * 10;
} }
numBots(): number { targetCooldown(): Tick {
return 400; return 15 * 10;
} }
theme(): Theme { allianceRequestCooldown(): Tick {
return pastelTheme; return 30 * 10;
} }
allianceDuration(): Tick {
return 600 * 10;
}
percentageTilesOwnedToWin(): number {
return 80;
}
boatMaxNumber(): number {
return 3;
}
boatMaxDistance(): number {
return 500;
}
numSpawnPhaseTurns(): number {
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300;
}
numBots(): number {
return 400;
}
theme(): Theme {
return pastelTheme;
}
attackLogic( attackLogic(
gm: Game, gm: Game,
attackTroops: number, attackTroops: number,
attacker: Player, attacker: Player,
defender: Player | TerraNullius, defender: Player | TerraNullius,
tileToConquer: TileRef tileToConquer: TileRef
): { ): {
attackerTroopLoss: number; attackerTroopLoss: number;
defenderTroopLoss: number; defenderTroopLoss: number;
tilesPerTickUsed: number; tilesPerTickUsed: number;
} { } {
let mag = 0; let mag = 0;
let speed = 0; let speed = 0;
const type = gm.terrainType(tileToConquer); const type = gm.terrainType(tileToConquer);
switch (type) { switch (type) {
case TerrainType.Plains: case TerrainType.Plains:
mag = 80; mag = 80;
speed = 15; speed = 15;
break; break;
case TerrainType.Highland: case TerrainType.Highland:
mag = 100; mag = 100;
speed = 20; speed = 20;
break; break;
case TerrainType.Mountain: case TerrainType.Mountain:
mag = 120; mag = 120;
speed = 25; speed = 25;
break; break;
default: default:
throw new Error(`terrain type ${type} not supported`); throw new Error(`terrain type ${type} not supported`);
} }
if (defender.isPlayer()) { if (defender.isPlayer()) {
for (const dp of gm.nearbyDefensePosts(tileToConquer)) { for (const dp of gm.nearbyDefensePosts(tileToConquer)) {
if (dp.owner() == defender) { if (dp.owner() == defender) {
mag *= this.defensePostDefenseBonus(); mag *= this.defensePostDefenseBonus();
speed *= this.defensePostDefenseBonus(); speed *= this.defensePostDefenseBonus();
break; break;
} }
} }
} }
if (gm.hasFallout(tileToConquer)) { if (gm.hasFallout(tileToConquer)) {
mag *= this.falloutDefenseModifier(); mag *= this.falloutDefenseModifier();
speed *= this.falloutDefenseModifier(); speed *= this.falloutDefenseModifier();
} }
if (attacker.isPlayer() && defender.isPlayer()) { if (attacker.isPlayer() && defender.isPlayer()) {
if ( if (
attacker.type() == PlayerType.Human && attacker.type() == PlayerType.Human &&
defender.type() == PlayerType.Bot defender.type() == PlayerType.Bot
) { ) {
mag *= 0.8; mag *= 0.8;
} }
if ( if (
attacker.type() == PlayerType.FakeHuman && attacker.type() == PlayerType.FakeHuman &&
defender.type() == PlayerType.Bot defender.type() == PlayerType.Bot
) { ) {
mag *= 0.8; mag *= 0.8;
} }
} }
if (defender.isPlayer()) { if (defender.isPlayer()) {
return { return {
attackerTroopLoss: attackerTroopLoss:
within(defender.troops() / (2.5 * attackTroops), 0.1, 10) * mag, within(defender.troops() / (2.5 * attackTroops), 0.1, 10) *
defenderTroopLoss: defender.troops() / defender.numTilesOwned(), mag,
tilesPerTickUsed: defenderTroopLoss: defender.troops() / defender.numTilesOwned(),
within(defender.troops() / (5 * attackTroops), 0.2, 1.5) * speed, tilesPerTickUsed:
}; within(defender.troops() / (5 * attackTroops), 0.2, 1.5) *
} else { speed,
return { };
attackerTroopLoss: } else {
attacker.type() == PlayerType.Bot ? mag / 10 : mag / 5, return {
defenderTroopLoss: 0, attackerTroopLoss:
tilesPerTickUsed: within( attacker.type() == PlayerType.Bot ? mag / 10 : mag / 5,
(2000 * Math.max(10, speed)) / attackTroops, defenderTroopLoss: 0,
5, tilesPerTickUsed: within(
100 (2000 * Math.max(10, speed)) / attackTroops,
), 5,
}; 100
} ),
} };
}
}
attackTilesPerTick( attackTilesPerTick(
attackTroops: number, attackTroops: number,
attacker: Player, attacker: Player,
defender: Player | TerraNullius, defender: Player | TerraNullius,
numAdjacentTilesWithEnemy: number numAdjacentTilesWithEnemy: number
): number { ): number {
if (defender.isPlayer()) { if (defender.isPlayer()) {
return ( return (
within(((5 * attackTroops) / defender.troops()) * 2, 0.01, 0.5) * within(
numAdjacentTilesWithEnemy * ((5 * attackTroops) / defender.troops()) * 2,
3 0.01,
); 0.5
} else { ) *
return numAdjacentTilesWithEnemy * 2; numAdjacentTilesWithEnemy *
} 3
} );
} else {
return numAdjacentTilesWithEnemy * 2;
}
}
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number { boatAttackAmount(
return Math.floor(attacker.troops() / 5); attacker: Player,
} defender: Player | TerraNullius
): number {
return Math.floor(attacker.troops() / 5);
}
attackAmount(attacker: Player, defender: Player | TerraNullius) { attackAmount(attacker: Player, defender: Player | TerraNullius) {
if (attacker.type() == PlayerType.Bot) { if (attacker.type() == PlayerType.Bot) {
return attacker.troops() / 20; return attacker.troops() / 20;
} else { } else {
return attacker.troops() / 5; return attacker.troops() / 5;
} }
} }
startManpower(playerInfo: PlayerInfo): number { startManpower(playerInfo: PlayerInfo): number {
if (playerInfo.playerType == PlayerType.Bot) { if (playerInfo.playerType == PlayerType.Bot) {
return 10_000; return 10_000;
} }
if (playerInfo.playerType == PlayerType.FakeHuman) { if (playerInfo.playerType == PlayerType.FakeHuman) {
switch (this._gameConfig.difficulty) { switch (this._gameConfig.difficulty) {
case Difficulty.Easy: case Difficulty.Easy:
return 2_500 * (playerInfo?.nation?.strength ?? 1); return 2_500 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Medium: case Difficulty.Medium:
return 5_000 * (playerInfo?.nation?.strength ?? 1); return 5_000 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Hard: case Difficulty.Hard:
return 15_000 * (playerInfo?.nation?.strength ?? 1); return 15_000 * (playerInfo?.nation?.strength ?? 1);
case Difficulty.Impossible: case Difficulty.Impossible:
return 20_000 * (playerInfo?.nation?.strength ?? 1); return 20_000 * (playerInfo?.nation?.strength ?? 1);
} }
} }
return this.creativeMode() ? 1_000_000 : 25_000; return this.creativeMode() ? 1_000_000 : 25_000;
} }
maxPopulation(player: Player | PlayerView): number { maxPopulation(player: Player | PlayerView): number {
let maxPop = this.creativeMode() ? 999_999_999_999 : let maxPop =
2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) + player.type() == PlayerType.Human && this.creativeMode()
player.units(UnitType.City).length * this.cityPopulationIncrease(); ? 999_999_999_999
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
player.units(UnitType.City).length *
this.cityPopulationIncrease();
if (player.type() == PlayerType.Bot) { if (player.type() == PlayerType.Bot) {
return maxPop / 2; return maxPop / 2;
} }
if (player.type() == PlayerType.Human) { if (player.type() == PlayerType.Human) {
return maxPop; return maxPop;
} }
switch (this._gameConfig.difficulty) { switch (this._gameConfig.difficulty) {
case Difficulty.Easy: case Difficulty.Easy:
return maxPop * 0.5; return maxPop * 0.5;
case Difficulty.Medium: case Difficulty.Medium:
return maxPop * 0.7; return maxPop * 0.7;
case Difficulty.Hard: case Difficulty.Hard:
return maxPop * 1; return maxPop * 1;
case Difficulty.Impossible: case Difficulty.Impossible:
return maxPop * 1.5; return maxPop * 1.5;
} }
} }
populationIncreaseRate(player: Player): number { populationIncreaseRate(player: Player): number {
let max = this.maxPopulation(player); let max = this.maxPopulation(player);
let toAdd = 10 + Math.pow(player.population(), 0.73) / 4; let toAdd = 10 + Math.pow(player.population(), 0.73) / 4;
const ratio = 1 - player.population() / max; const ratio = 1 - player.population() / max;
toAdd *= ratio; toAdd *= ratio;
if (player.type() == PlayerType.Bot) { if (player.type() == PlayerType.Bot) {
toAdd *= 0.7; toAdd *= 0.7;
} }
return Math.min(player.population() + toAdd, max) - player.population(); return Math.min(player.population() + toAdd, max) - player.population();
} }
goldAdditionRate(player: Player): number { goldAdditionRate(player: Player): number {
return Math.sqrt(player.workers() * player.numTilesOwned()) / 200; return Math.sqrt(player.workers() * player.numTilesOwned()) / 200;
} }
troopAdjustmentRate(player: Player): number { troopAdjustmentRate(player: Player): number {
const maxDiff = this.maxPopulation(player) / 1000; const maxDiff = this.maxPopulation(player) / 1000;
const target = player.population() * player.targetTroopRatio(); const target = player.population() * player.targetTroopRatio();
const diff = target - player.troops(); const diff = target - player.troops();
if (Math.abs(diff) < maxDiff) { if (Math.abs(diff) < maxDiff) {
return diff; return diff;
} }
const adjustment = maxDiff * Math.sign(diff); const adjustment = maxDiff * Math.sign(diff);
// Can ramp down troops much faster // Can ramp down troops much faster
if (adjustment < 0) { if (adjustment < 0) {
return adjustment * 5; return adjustment * 5;
} }
return adjustment; return adjustment;
} }
} }