diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index efaa2a896..3265946d7 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -492,7 +492,11 @@ export class PlayerInfoOverlay extends LitElement implements Layer { const isCity = unit.type() === UnitType.City; const area = Math.PI * Math.pow(unit.areaRadius(), 2); - const population = Math.floor(area * 1500 * (1 + unit.density())); + const terrainMag = this.game.magnitude(unit.tile()); + const terrainFactor = Math.max(0.4, 1.0 - terrainMag / 40.0); + const population = Math.floor( + area * 1500 * (1 + unit.density()) * terrainFactor, + ); return html`
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 8595b3a21..31a715a9f 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -170,6 +170,7 @@ export interface Config { structureMinDist(): number; isReplay(): boolean; allianceExtensionPromptOffset(): number; + setMap(map: GameMap): void; } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 8800b0cf9..240254889 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -15,7 +15,7 @@ import { UnitInfo, UnitType, } from "../game/Game"; -import { TileRef } from "../game/GameMap"; +import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { UserSettings } from "../game/UserSettings"; import { GameConfig, GameID, TeamCountConfig } from "../Schemas"; @@ -139,6 +139,8 @@ export abstract class DefaultServerConfig implements ServerConfig { export class DefaultConfig implements Config { private pastelTheme: PastelTheme = new PastelTheme(); private pastelThemeDark: PastelThemeDark = new PastelThemeDark(); + private _map: GameMap | null = null; + constructor( private _serverConfig: ServerConfig, private _gameConfig: GameConfig, @@ -146,6 +148,10 @@ export class DefaultConfig implements Config { private _isReplay: boolean, ) {} + setMap(map: GameMap): void { + this._map = map; + } + stripePublishableKey(): string { return Env.STRIPE_PUBLISHABLE_KEY ?? ""; } @@ -778,7 +784,10 @@ export class DefaultConfig implements Config { .reduce((acc, city) => { const area = Math.PI * Math.pow(city.areaRadius() || 1, 2); const density = city.density() || 0; - const population = area * 1500 * (1 + density); + // Population/power is based on area, density, and terrain resistance + const terrainMag = this._map?.magnitude(city.tile()) ?? 0; + const terrainFactor = Math.max(0.4, 1.0 - terrainMag / 40.0); + const population = area * 1500 * (1 + density) * terrainFactor; return acc + (isNaN(population) ? 0 : population); }, 0); @@ -853,7 +862,9 @@ export class DefaultConfig implements Config { if (!city.isUnderConstruction()) { const area = Math.PI * Math.pow(city.areaRadius() || 1, 2); const density = city.density() || 0; - cityIncome += area * 0.1 * (1 + density); + const terrainMag = this._map?.magnitude(city.tile()) ?? 0; + const terrainFactor = Math.max(0.4, 1.0 - terrainMag / 40.0); + cityIncome += area * 0.1 * (1 + density) * terrainFactor; } } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 417128f57..f0b27ee7f 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -107,6 +107,7 @@ export class GameImpl implements Game { ) { const constructorStart = performance.now(); + this._config.setMap(this._map); this._terraNullius = new TerraNulliusImpl(); this._width = _map.width(); this._height = _map.height(); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index b2db29268..5244befbd 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -674,6 +674,7 @@ export class GameView implements GameMap { private humans: Player[], ) { this._map = this._mapData.gameMap; + this._config.setMap(this._map); this.lastUpdate = null; this.unitGrid = new UnitGrid(this._map); // Replace the local player's username with their own stored username. @@ -1039,7 +1040,7 @@ export class GameView implements GameMap { const cached = this.urbanizationCache.get(tile); if (cached) return cached; - const nearby = this.nearbyUnits(tile, 60, UnitType.City); + const nearby = this.nearbyUnits(tile, 120, UnitType.City); if (nearby.length === 0) { const result = { density: 0 }; this.urbanizationCache.set(tile, result); @@ -1071,22 +1072,26 @@ export class GameView implements GameMap { const angle = Math.atan2(dy, dx); const uid = unit.id(); + const age = unit.age(); - // More complex noise to ensure irregular shapes even at high city levels + // Dynamic organic noise: shifts slowly over time (age) const noise = 1 + - 0.22 * Math.sin(angle * 3 + uid) + - 0.15 * Math.sin(angle * 7 - uid * 1.3) + + 0.25 * Math.sin(angle * 3 + uid + age / 500) + + 0.15 * Math.sin(angle * 7 - uid * 1.3 + age / 800) + 0.08 * Math.sin(angle * 13 + uid * 0.7); - // Terrain expansion logic: cities expand easily in plains, poorly in mountains. - // Magnitude 0-30 scale. We penalize distance based on terrain difficulty. - const terrainFactor = Math.max(0.2, 1.0 - Math.min(terrainMag, 25) / 35); - const irregularRadius = radius * noise * terrainFactor; + // "Expand to least resistant (flat) land 1st" + // Mountains (high magnitude) act as resistance, making the "effective distance" greater. + // Magnitude is typically 0-30. + const resistance = 1.0 + terrainMag / 12.0; + const effectiveDist = dist * resistance; - if (dist <= irregularRadius) { - // Linear fade is faster than Math.pow - const fade = 1 - dist / irregularRadius; + const irregularMaxRadius = radius * noise; + + if (effectiveDist <= irregularMaxRadius) { + // Density fades out based on effective distance (terrain-aware) + const fade = 1 - effectiveDist / irregularMaxRadius; const d = unit.density() * fade; if (d > maxDensity) {