183 lines
6.0 KiB
JavaScript
183 lines
6.0 KiB
JavaScript
// World generation and settlement placement: laying out capitals and cities,
|
|
// spawning their starting units, seeding population and budgets, and the small
|
|
// terrain predicates the rest of the model leans on.
|
|
|
|
import { ECONOMY } from "../data/economy.js";
|
|
import { Random } from "../rng.js";
|
|
import { key, parseKey } from "../hex.js";
|
|
|
|
export const worldMethods = {
|
|
_placeSettlements() {
|
|
const capitals = this._placeCapitals(this.civilisations.length);
|
|
for (const site of this._placeCities(capitals)) this._spawnCity(site);
|
|
this._rebuildCityIndex();
|
|
for (let i = 0; i < Math.min(this.civilisations.length, capitals.length); i++) {
|
|
this._spawnUnit(capitals[i], i, this.protoUnits[0]);
|
|
}
|
|
this._recomputeTerritory();
|
|
this._generatePopulation();
|
|
this._initBudgets();
|
|
this._balanceStartingEconomy();
|
|
},
|
|
|
|
_placeCapitals(count) {
|
|
const rng = new Random(this.seed);
|
|
return this._generator.pickCapitals(count, rng);
|
|
},
|
|
|
|
_placeCities(capitals) {
|
|
return this._generator.pickCities(capitals, this.mapConfig.citiesPerCiv, this.mapConfig.citySpacing);
|
|
},
|
|
|
|
_spawnCity(site) {
|
|
const civIndex = site.civIndex;
|
|
if (civIndex < 0 || civIndex >= this.civilisations.length) return;
|
|
const civ = this.civilisations[civIndex];
|
|
const coords = site.coords;
|
|
const isCapital = site.isCapital;
|
|
const name = this._nextCityName(civ, civIndex);
|
|
const population = 500000 +
|
|
(Math.abs((coords.x * 92837111) ^ (coords.y * 689287499)) % 3500000);
|
|
const city = {
|
|
id: this._nextCityId,
|
|
civ: civIndex,
|
|
coords,
|
|
name,
|
|
isCapital,
|
|
population,
|
|
improvements: [],
|
|
buildings: {},
|
|
statuses: [],
|
|
};
|
|
this.cities.push(city);
|
|
this._cityById.set(this._nextCityId, city);
|
|
this._nextCityId += 1;
|
|
},
|
|
|
|
_rebuildCityIndex() {
|
|
this._cityByCoords = new Map();
|
|
this._cityAdjacent = new Map();
|
|
for (const city of this.cities) {
|
|
this._cityByCoords.set(key(city.coords.x, city.coords.y), city);
|
|
const owner = city.civ;
|
|
if (!this._cityAdjacent.has(owner)) this._cityAdjacent.set(owner, new Set());
|
|
const adjacent = this._cityAdjacent.get(owner);
|
|
for (const neighbour of this._neighbours(city.coords)) {
|
|
adjacent.add(key(neighbour.x, neighbour.y));
|
|
}
|
|
}
|
|
},
|
|
|
|
// Picks the civilisation's next city name, skipping any a city elsewhere has
|
|
// already taken, and falls back to a numbered name so the whole world never
|
|
// has two cities called the same thing.
|
|
_nextCityName(civ, index) {
|
|
const start = this._cityNamesUsed.get(index) || 0;
|
|
for (let i = start; i < civ.cityNames.length; i++) {
|
|
const candidate = civ.cityNames[i];
|
|
if (this._usedCityNames.has(candidate)) continue;
|
|
this._cityNamesUsed.set(index, i + 1);
|
|
this._usedCityNames.add(candidate);
|
|
return candidate;
|
|
}
|
|
let suffix = Math.max(start, civ.cityNames.length) + 1;
|
|
let candidate = `${civ.name} ${suffix}`;
|
|
while (this._usedCityNames.has(candidate)) {
|
|
suffix += 1;
|
|
candidate = `${civ.name} ${suffix}`;
|
|
}
|
|
this._cityNamesUsed.set(index, suffix);
|
|
this._usedCityNames.add(candidate);
|
|
return candidate;
|
|
},
|
|
|
|
_spawnUnit(center, civIndex, proto) {
|
|
let index = this.protoUnits.indexOf(proto);
|
|
if (index < 0) {
|
|
this.protoUnits.push(proto);
|
|
index = this.protoUnits.length - 1;
|
|
}
|
|
const unit = {
|
|
id: this._nextUnitId,
|
|
civ: civIndex,
|
|
proto: index,
|
|
coords: center,
|
|
hp: proto.maxHp,
|
|
maxHp: proto.maxHp,
|
|
path: [],
|
|
pathIndex: 0,
|
|
progressHours: 0,
|
|
moveSpeed: null,
|
|
waypoints: [],
|
|
invader: false,
|
|
statuses: [],
|
|
};
|
|
this.units.push(unit);
|
|
this._unitById.set(this._nextUnitId, unit);
|
|
this._nextUnitId += 1;
|
|
return unit;
|
|
},
|
|
|
|
_generatePopulation() {
|
|
this.tilePopulation = new Map();
|
|
for (const k in this.tiles) {
|
|
if (this._isLand(parseKey(k))) this.tilePopulation.set(k, this._basePopulation(parseKey(k)));
|
|
}
|
|
for (const city of this.cities) {
|
|
this.tilePopulation.set(key(city.coords.x, city.coords.y), city.population);
|
|
}
|
|
},
|
|
|
|
_basePopulation(coords) {
|
|
return Math.round(ECONOMY.basePopulation * this.terrainStats.populationMultiplier(coords));
|
|
},
|
|
|
|
_initBudgets() {
|
|
for (let i = 0; i < this.civilisations.length; i++) {
|
|
this.budgets.set(i, ECONOMY.startingBudget);
|
|
}
|
|
},
|
|
|
|
// Map generation should not hand one civilisation a richer start than
|
|
// another. Terrain makes the raw territories unequal, so after the world is
|
|
// populated we scale each civ's population to the common mean and then its
|
|
// GDP per capita to the mean of the raw per-capita figures. Total population
|
|
// and total GDP are preserved; from here on the factors are constants and
|
|
// terrain, buildings, technology and war move the numbers normally.
|
|
_balanceStartingEconomy() {
|
|
const count = this.civilisations.length;
|
|
this._populationBalance = new Map();
|
|
this._perCapitaBalance = new Map();
|
|
if (count <= 1) return;
|
|
const bases = [];
|
|
let totalPopulation = 0;
|
|
let totalGdp = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
const base = this._basePlayerAggregates(i);
|
|
bases.push(base);
|
|
totalPopulation += base.population;
|
|
totalGdp += base.gdp;
|
|
}
|
|
if (totalPopulation <= 0) return;
|
|
const targetPopulation = totalPopulation / count;
|
|
const targetPerCapita = totalGdp / totalPopulation;
|
|
for (let i = 0; i < count; i++) {
|
|
const base = bases[i];
|
|
if (base.population <= 0) continue;
|
|
const perCapita = base.gdp / base.population;
|
|
this._populationBalance.set(i, targetPopulation / base.population);
|
|
this._perCapitaBalance.set(i, perCapita > 0 ? targetPerCapita / perCapita : 1);
|
|
}
|
|
},
|
|
|
|
_isLand(coords) {
|
|
const tile = this.tiles[key(coords.x, coords.y)];
|
|
return !!tile && tile.terrainClass === "Land";
|
|
},
|
|
|
|
_isNextToCity(coords, owner) {
|
|
const adjacent = this._cityAdjacent.get(owner);
|
|
return adjacent ? adjacent.has(key(coords.x, coords.y)) : false;
|
|
},
|
|
};
|