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) {