Files
Battle-for-Tismo/shared/game_state/world.js
T

193 lines
6.4 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 description = this._cityDescriptionFor(civ, isCapital, coords);
const city = {
id: this._nextCityId,
civ: civIndex,
coords,
name: description.name,
isCapital,
population: description.population,
improvements: description.improvements.slice(),
buildings: {},
statuses: [],
};
this.cities.push(city);
this._cityById.set(this._nextCityId, city);
this._nextCityId += 1;
},
_cityDescriptionFor(civ, _isCapital, center) {
return this._generatedCityDescription(civ, center);
},
_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));
}
}
},
_generatedCityDescription(civ, center) {
const index = this.civilisations.indexOf(civ);
const name = this._nextCityName(civ, index);
const population = 500000 +
(Math.abs((center.x * 92837111) ^ (center.y * 689287499)) % 3500000);
return { name, population, improvements: [] };
},
// 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;
},
};