diff --git a/index.html b/index.html
index 8e958451..57d05266 100644
--- a/index.html
+++ b/index.html
@@ -5237,7 +5237,7 @@
-
+
diff --git a/main.js b/main.js
index 629bef98..0bb23cef 100644
--- a/main.js
+++ b/main.js
@@ -108,15 +108,7 @@ terrs.append("g").attr("id", "landHeights");
labels.append("g").attr("id", "states");
labels.append("g").attr("id", "addedLabels");
-
let burgLabels = labels.append("g").attr("id", "burgLabels");
-burgIcons.append("g").attr("id", "cities");
-burgLabels.append("g").attr("id", "cities");
-anchors.append("g").attr("id", "cities");
-
-burgIcons.append("g").attr("id", "towns");
-burgLabels.append("g").attr("id", "towns");
-anchors.append("g").attr("id", "towns");
// population groups
population.append("g").attr("id", "rural");
@@ -162,6 +154,29 @@ let customization = 0;
let biomesData = Biomes.getDefault();
let nameBases = Names.getNameBases(); // cultures-related data
+// default options, based on Earth data
+let options = {
+ pinNotes: false,
+ winds: [225, 45, 225, 315, 135, 315],
+ temperatureEquator: 27,
+ temperatureNorthPole: -30,
+ temperatureSouthPole: -15,
+ stateLabelsMode: "auto",
+ showBurgPreview: true,
+ villageMaxPopulation: 2000,
+ burgs: {
+ groups: Burgs.getDefaultGroups()
+ }
+};
+
+// create groups for each burg type
+{
+ for (const {name} of options.burgs.groups) {
+ burgIcons.append("g").attr("id", name);
+ burgLabels.append("g").attr("id", name);
+ }
+}
+
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
@@ -185,21 +200,6 @@ const onZoom = debounce(function () {
}, 50);
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoom);
-// default options, based on Earth data
-let options = {
- pinNotes: false,
- winds: [225, 45, 225, 315, 135, 315],
- temperatureEquator: 27,
- temperatureNorthPole: -30,
- temperatureSouthPole: -15,
- stateLabelsMode: "auto",
- showBurgPreview: true,
- villageMaxPopulation: 2000,
- burgs: {
- groups: Burgs.getDefaultGroups()
- }
-};
-
let mapCoordinates = {}; // map coordinates on globe
let populationRate = +byId("populationRateInput").value;
let distanceScale = +byId("distanceScaleInput").value;
@@ -668,7 +668,7 @@ async function generate(options) {
States.defineStateForms();
Provinces.generate();
Provinces.getPoles();
- Burgs.specifyBurgs();
+ Burgs.specify();
Rivers.specify();
Features.specify();
@@ -1180,36 +1180,44 @@ function rankCells() {
cells.s = new Int16Array(cells.i.length); // cell suitability array
cells.pop = new Float32Array(cells.i.length); // cell population array
- const flMean = d3.median(cells.fl.filter(f => f)) || 0,
- flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
- const areaMean = d3.mean(cells.area); // to adjust population by cell area
+ const meanFlux = d3.median(cells.fl.filter(f => f)) || 0;
+ const maxFlux = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
+ const meanArea = d3.mean(cells.area); // to adjust population by cell area
+
+ const scoreMap = {
+ estuary: 15,
+ ocean_coast: 5,
+ save_harbor: 20,
+ freshwater: 30,
+ salt: 10,
+ frozen: 1,
+ dry: -5,
+ sinkhole: -5,
+ lava: -30
+ };
for (const i of cells.i) {
if (cells.h[i] < 20) continue; // no population in water
- let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
- if (!s) continue; // uninhabitable biomes has 0 suitability
- if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued
- s -= (cells.h[i] - 50) / 5; // low elevation is valued, high is not;
+ let score = biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
+ if (!score) continue; // uninhabitable biomes has 0 suitability
+
+ if (meanFlux) score += normalize(cells.fl[i] + cells.conf[i], meanFlux, maxFlux) * 250; // big rivers and confluences are valued
+ score -= (cells.h[i] - 50) / 5; // low elevation is valued, high is not;
if (cells.t[i] === 1) {
- if (cells.r[i]) s += 15; // estuary is valued
+ if (cells.r[i]) score += scoreMap.estuary;
const feature = features[cells.f[cells.haven[i]]];
if (feature.type === "lake") {
- if (feature.group === "freshwater") s += 30;
- else if (feature.group == "salt") s += 10;
- else if (feature.group == "frozen") s += 1;
- else if (feature.group == "dry") s -= 5;
- else if (feature.group == "sinkhole") s -= 5;
- else if (feature.group == "lava") s -= 30;
+ score += scoreMap[feature.water] || 0;
} else {
- s += 5; // ocean coast is valued
- if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued
+ score += scoreMap.ocean_coast;
+ if (cells.harbor[i] === 1) score += scoreMap.save_harbor;
}
}
- cells.s[i] = s / 5; // general population rate
+ cells.s[i] = score / 5; // general population rate
// cell rural population is suitability adjusted by cell area
- cells.pop[i] = cells.s[i] > 0 ? (cells.s[i] * cells.area[i]) / areaMean : 0;
+ cells.pop[i] = cells.s[i] > 0 ? (cells.s[i] * cells.area[i]) / meanArea : 0;
}
TIME && console.timeEnd("rankCells");
diff --git a/modules/burgs-generator.js b/modules/burgs-generator.js
index 6baa9072..0b777957 100644
--- a/modules/burgs-generator.js
+++ b/modules/burgs-generator.js
@@ -17,6 +17,7 @@ window.Burgs = (() => {
let quadtree = d3.quadtree();
generateCapitals();
generateTowns();
+ shiftBurgs();
pack.burgs = burgs;
TIME && console.timeEnd("generateBurgs");
@@ -108,137 +109,93 @@ window.Burgs = (() => {
spacing *= 0.5;
}
}
+
+ // define port status and shift ports and burgs on rivers
+ function shiftBurgs() {
+ const {cells, features} = pack;
+ const temp = grid.cells.temp;
+
+ // port is a capital with any harbor OR any burg with a safe harbor
+ const featurePorts = {};
+ for (const burg of burgs) {
+ if (!burg.i || burg.lock) continue;
+ const i = burg.cell;
+
+ const haven = cells.haven[i];
+ const harbor = cells.harbor[i];
+
+ if (haven !== undefined && temp[cells.g[i]] > 0) {
+ const featureId = cells.f[haven];
+ const canBePort = features[featureId].cells > 1 && ((burg.capital && harbor) || harbor === 1);
+ if (canBePort) {
+ if (!featurePorts[featureId]) featurePorts[featureId] = [];
+ featurePorts[featureId].push(burg);
+ }
+ }
+ }
+
+ // shift ports to the edge of the water body. Only bodies with 2+ ports are considered
+ Object.entries(featurePorts).forEach(([featureId, burgs]) => {
+ if (burgs.length < 2) return;
+ burgs.forEach(burg => {
+ burg.port = featureId;
+ const haven = cells.haven[burg.cell];
+ const [x, y] = getCloseToEdgePoint(burg.cell, haven);
+ burg.x = x;
+ burg.y = y;
+ });
+ });
+
+ // shift non-port river burgs a bit
+ for (const burg of burgs) {
+ if (!burg.i || burg.lock || burg.port || !cells.r[burg.cell]) continue;
+ const cellId = burg.cell;
+ const shift = Math.min(cells.fl[cellId] / 150, 1);
+ burg.x = cellId % 2 ? rn(burg.x + shift, 2) : rn(burg.x - shift, 2);
+ burg.y = cells.r[cellId] % 2 ? rn(burg.y + shift, 2) : rn(burg.y - shift, 2);
+ }
+
+ function getCloseToEdgePoint(cell1, cell2) {
+ const {cells, vertices} = pack;
+
+ const [x0, y0] = cells.p[cell1];
+ const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
+ const [x1, y1] = vertices.p[commonVertices[0]];
+ const [x2, y2] = vertices.p[commonVertices[1]];
+ const xEdge = (x1 + x2) / 2;
+ const yEdge = (y1 + y2) / 2;
+
+ const x = rn(x0 + 0.95 * (xEdge - x0), 2);
+ const y = rn(y0 + 0.95 * (yEdge - y0), 2);
+
+ return [x, y];
+ }
+ }
};
- const getDefaultGroups = () => [
- {name: "capitals", active: true, features: {capital: true}, preview: "watabou-city-generator"},
- {name: "cities", active: true, percentile: 90, preview: "watabou-city-generator"},
- {
- name: "forts",
- active: true,
- features: {citadel: true, walls: false, plaza: false, port: false},
- population: [0, 1],
- preview: null
- },
- {
- name: "monasteries",
- active: true,
- features: {temple: true, walls: false, plaza: false, port: false},
- population: [0, 1],
- preview: null
- },
- {
- name: "caravanserais",
- active: true,
- features: {port: false},
- population: [0, 1],
- biomes: [1, 2, 3],
- preview: null
- },
- {
- name: "trading posts",
- active: true,
- features: {plaza: true},
- population: [0, 1],
- biomes: [1, 2, 3],
- preview: null
- },
- {name: "villages", active: true, population: [0.1, 2], preview: "watabou-village-generator"},
- {
- name: "hamlets",
- active: true,
- features: {plaza: true, walls: false, plaza: false},
- population: [0, 0.1],
- preview: "watabou-village-generator"
- },
- {name: "towns", active: true, isDefault: true, preview: "watabou-city-generator"}
- ];
-
- // define burg coordinates, coa, port status and define details
- const specifyBurgs = () => {
+ const specify = () => {
TIME && console.time("specifyBurgs");
- const {cells, features} = pack;
- const temp = grid.cells.temp;
- for (const burg of pack.burgs) {
- if (!burg.i || burg.lock) continue;
- const i = burg.cell;
+ pack.burgs.forEach(burg => {
+ if (!burg.i || burg.removed || burg.lock) return;
+ definePopulation(burg);
+ defineEmblem(burg);
+ defineFeatures(burg);
+ });
- // asign port status to some coastline burgs with temp > 0 °C
- const haven = cells.haven[i];
- if (haven && temp[cells.g[i]] > 0) {
- const f = cells.f[haven]; // water body id
- // port is a capital with any harbor OR town with good harbor
- const port = features[f].cells > 1 && ((burg.capital && cells.harbor[i]) || cells.harbor[i] === 1);
- burg.port = port ? f : 0; // port is defined by water body id it lays on
- } else burg.port = 0;
+ const populations = pack.burgs
+ .filter(b => b.i && !b.removed)
+ .map(b => b.population)
+ .sort((a, b) => a - b); // ascending
- // define burg population (keep urbanization at about 10% rate)
- burg.population = rn(Math.max(cells.s[i] / 8 + burg.i / 1000 + (i % 100) / 1000, 0.1), 3);
- if (burg.capital) burg.population = rn(burg.population * 1.3, 3); // increase capital population
-
- if (burg.port) {
- burg.population = burg.population * 1.3; // increase port population
- const [x, y] = getCloseToEdgePoint(i, haven);
- burg.x = x;
- burg.y = y;
- }
-
- // add random factor
- burg.population = rn(burg.population * gauss(2, 3, 0.6, 20, 3), 3);
-
- // shift burgs on rivers semi-randomly and just a bit
- if (!burg.port && cells.r[i]) {
- const shift = Math.min(cells.fl[i] / 150, 1);
- if (i % 2) burg.x = rn(burg.x + shift, 2);
- else burg.x = rn(burg.x - shift, 2);
- if (cells.r[i] % 2) burg.y = rn(burg.y + shift, 2);
- else burg.y = rn(burg.y - shift, 2);
- }
-
- // define emblem
- const state = pack.states[burg.state];
- const stateCOA = state.coa;
- let kinship = 0.25;
- if (burg.capital) kinship += 0.1;
- else if (burg.port) kinship -= 0.1;
- if (burg.culture !== state.culture) kinship -= 0.25;
- burg.type = getType(i, burg.port);
- const type = burg.capital && P(0.2) ? "Capital" : burg.type === "Generic" ? "City" : burg.type;
- burg.coa = COA.generate(stateCOA, kinship, null, type);
- burg.coa.shield = COA.getShield(burg.culture, burg.state);
- }
-
- // de-assign port status if it's the only one on feature
- const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
- for (const f of features) {
- if (!f.i || f.land || f.border) continue;
- const featurePorts = ports.filter(b => b.port === f.i);
- if (featurePorts.length === 1) featurePorts[0].port = 0;
- }
-
- pack.burgs.filter(b => b.i && !b.removed && !b.lock).forEach(defineBurgFeatures);
+ pack.burgs.forEach(burg => {
+ if (!burg.i || burg.removed || burg.lock) return;
+ defineGroup(burg, populations);
+ });
TIME && console.timeEnd("specifyBurgs");
};
- function getCloseToEdgePoint(cell1, cell2) {
- const {cells, vertices} = pack;
-
- const [x0, y0] = cells.p[cell1];
-
- const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
- const [x1, y1] = vertices.p[commonVertices[0]];
- const [x2, y2] = vertices.p[commonVertices[1]];
- const xEdge = (x1 + x2) / 2;
- const yEdge = (y1 + y2) / 2;
-
- const x = rn(x0 + 0.95 * (xEdge - x0), 2);
- const y = rn(y0 + 0.95 * (yEdge - y0), 2);
-
- return [x, y];
- }
-
const getType = (cellId, port) => {
const {cells, features} = pack;
@@ -261,19 +218,180 @@ window.Burgs = (() => {
return "Generic";
};
- const defineBurgFeatures = burg => {
- const {cells, states} = pack;
+ function definePopulation(burg) {
+ const cellId = burg.cell;
+ let population = pack.cells.s[cellId] / 5;
+ if (burg.capital) population *= 1.5;
+ const connectivityRate = Routes.getConnectivityRate(cellId);
+ if (connectivityRate) population *= connectivityRate;
+ population *= gauss(1, 1, 0.25, 4, 5); // randomize
+ population += ((burg.i % 100) - (cellId % 100)) / 1000; // unround
+ burg.population = rn(Math.max(population, 0.01), 3);
+ }
+
+ function defineEmblem(burg) {
+ burg.type = getType(burg.cell, burg.port);
+
+ const state = pack.states[burg.state];
+ const stateCOA = state.coa;
+
+ let kinship = 0.25;
+ if (burg.capital) kinship += 0.1;
+ else if (burg.port) kinship -= 0.1;
+ if (burg.culture !== state.culture) kinship -= 0.25;
+
+ const type = burg.capital && P(0.2) ? "Capital" : burg.type === "Generic" ? "City" : burg.type;
+ burg.coa = COA.generate(stateCOA, kinship, null, type);
+ burg.coa.shield = COA.getShield(burg.culture, burg.state);
+ }
+
+ function defineFeatures(burg) {
const pop = burg.population;
burg.citadel = Number(burg.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
- burg.plaza = Number(pop > 20 || (pop > 10 && P(0.8)) || (pop > 4 && P(0.7)) || P(0.6));
+ burg.plaza = Number(
+ Routes.isCrossroad(burg.cell) || (Routes.hasRoad(burg.cell) && P(0.7)) || pop > 20 || (pop > 10 && P(0.8))
+ );
burg.walls = Number(burg.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
burg.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && burg.walls && P(0.4)));
- const religion = cells.religion[burg.cell];
- const theocracy = states[burg.state].form === "Theocracy";
+ const religion = pack.cells.religion[burg.cell];
+ const theocracy = pack.states[burg.state].form === "Theocracy";
burg.temple = Number(
(religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
);
- };
+ }
- return {generate, getDefaultGroups, specifyBurgs, getType, defineBurgFeatures};
+ const getDefaultGroups = () => [
+ {name: "capitals", active: true, features: {capital: true}, preview: "watabou-city-generator"},
+ {name: "cities", active: true, percentile: 90, population: [5, Infinity], preview: "watabou-city-generator"},
+ {
+ name: "forts",
+ active: true,
+ features: {citadel: true, walls: false, plaza: false, port: false},
+ population: [0, 1],
+ preview: null
+ },
+ {
+ name: "monasteries",
+ active: true,
+ features: {temple: true, walls: false, plaza: false, port: false},
+ population: [0, 0.8],
+ preview: null
+ },
+ {
+ name: "caravanserais",
+ active: true,
+ features: {port: false, plaza: true},
+ population: [0, 0.8],
+ biomes: [1, 2, 3],
+ preview: null
+ },
+ {
+ name: "trading_posts",
+ active: true,
+ features: {plaza: true},
+ population: [0, 0.8],
+ biomes: [5, 6, 7, 8, 9, 10, 11, 12],
+ preview: null
+ },
+ {
+ name: "villages",
+ active: true,
+ population: [0.1, 2],
+ features: {walls: false},
+ preview: "watabou-village-generator"
+ },
+ {
+ name: "hamlets",
+ active: true,
+ features: {walls: false, plaza: false},
+ population: [0, 0.1],
+ preview: "watabou-village-generator"
+ },
+ {name: "towns", active: true, isDefault: true, preview: "watabou-city-generator"}
+ ];
+
+ function defineGroup(burg, populations) {
+ for (const group of options.burgs.groups) {
+ if (!group.active) continue;
+
+ if (group.population) {
+ const [min, max] = group.population;
+ const isFit = burg.population >= min && burg.population <= max;
+ if (!isFit) continue;
+ }
+
+ if (group.features) {
+ const isFit = Object.entries(group.features).every(([feature, value]) => Boolean(burg[feature]) === value);
+ if (!isFit) continue;
+ }
+
+ if (group.biomes) {
+ const isFit = group.biomes.includes(pack.cells.biome[burg.cell]);
+ if (!isFit) continue;
+ }
+
+ if (group.percentile) {
+ const index = populations.indexOf(burg.population);
+ const isFit = index >= Math.floor((populations.length * group.percentile) / 100);
+ if (!isFit) continue;
+ }
+
+ // apply fitting or default group
+ burg.group = group.name;
+ return;
+ }
+ }
+
+ function add([x, y]) {
+ const {cells} = pack;
+
+ const burgId = pack.burgs.length;
+ const cellId = findCell(x, y);
+ const culture = cells.culture[cellId];
+ const name = Names.getCulture(culture);
+ const state = cells.state[cellId];
+ const feature = cells.f[cellId];
+
+ const burg = {
+ cell: cellId,
+ x,
+ y,
+ i: burgId,
+ state,
+ culture,
+ name,
+ feature,
+ capital: 0,
+ port: 0
+ };
+ definePopulation(burg);
+ defineEmblem(burg);
+ defineFeatures(burg);
+
+ const populations = pack.burgs
+ .filter(b => b.i && !b.removed)
+ .map(b => b.population)
+ .sort((a, b) => a - b); // ascending
+ defineGroup(burg, populations);
+
+ pack.burgs.push(burg);
+ cells.burg[cellId] = burgId;
+
+ const newRoute = Routes.connect(cellId);
+ if (newRoute && layerIsOn("toggleRoutes")) {
+ const path = Routes.getPath(newRoute);
+ routes
+ .select("#" + newRoute.group)
+ .append("path")
+ .attr("d", path)
+ .attr("id", "route" + newRoute.i);
+ }
+
+ drawBurgIcon(burg);
+ drawBurgLabel(burg);
+
+ return burgId;
+ }
+
+ return {generate, getDefaultGroups, specify, getType, add};
})();
diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js
index 7f4e5e8e..cb4ca6d9 100644
--- a/modules/dynamic/auto-update.js
+++ b/modules/dynamic/auto-update.js
@@ -968,8 +968,9 @@ export function resolveVersionConflicts(mapVersion) {
// v1.106.0 change burg groups and added customizable icons
icons.selectAll("circle, use").remove();
+ const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id);
options.burgs = {
- groups: Burgs.getDefaultGroups()
+ groups: groups.map(name => ({name, active: true, preview: null}))
};
}
}
diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js
index 6640e2d7..8118fc1b 100644
--- a/modules/dynamic/editors/states-editor.js
+++ b/modules/dynamic/editors/states-editor.js
@@ -1184,7 +1184,7 @@ function addState() {
if (burg && burgs[burg].capital)
return tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error");
- if (!burg) burg = addBurg(point); // add new burg
+ if (!burg) burg = Burgs.add(point);
const oldState = cells.state[center];
const newState = states.length;
diff --git a/modules/renderers/draw-burg-icons.js b/modules/renderers/draw-burg-icons.js
index 54f72e34..fc9c4a70 100644
--- a/modules/renderers/draw-burg-icons.js
+++ b/modules/renderers/draw-burg-icons.js
@@ -5,65 +5,47 @@ function drawBurgIcons() {
icons.selectAll("circle, use").remove(); // cleanup
- // capitals
- const capitals = pack.burgs.filter(b => b.capital && !b.removed);
- const capitalIcons = burgIcons.select("#cities");
- const capitalIcon = capitalIcons.attr("data-icon") || "#icon-circle";
- const capitalAnchors = anchors.selectAll("#cities");
- const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
+ for (const {name} of options.burgs.groups) {
+ const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
+ if (!burgsInGroup.length) continue;
- capitalIcons
- .selectAll("use")
- .data(capitals)
- .enter()
- .append("use")
- .attr("id", d => "burg" + d.i)
- .attr("href", capitalIcon)
- .attr("data-id", d => d.i)
- .attr("x", d => d.x)
- .attr("y", d => d.y);
+ const g = burgIcons.select("#" + name);
+ if (g.empty()) continue;
- capitalAnchors
- .selectAll("use")
- .data(capitals.filter(c => c.port))
- .enter()
- .append("use")
- .attr("xlink:href", "#icon-anchor")
- .attr("data-id", d => d.i)
- .attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
- .attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
- .attr("width", capitalAnchorsSize)
- .attr("height", capitalAnchorsSize);
+ const icon = g.attr("data-icon") || "#icon-circle";
+ g.selectAll("use")
+ .data(burgsInGroup)
+ .enter()
+ .append("use")
+ .attr("href", icon)
+ .attr("id", d => "burg" + d.i)
+ .attr("data-id", d => d.i)
+ .attr("x", d => d.x)
+ .attr("y", d => d.y);
- // towns
- const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
- const townIcons = burgIcons.select("#towns");
- const townIcon = townIcons.attr("data-icon") || "#icon-circle";
- const townsAnchors = anchors.selectAll("#towns");
- const townsAnchorsSize = townsAnchors.attr("size") || 1;
-
- townIcons
- .selectAll("use")
- .data(towns)
- .enter()
- .append("use")
- .attr("id", d => "burg" + d.i)
- .attr("href", townIcon)
- .attr("data-id", d => d.i)
- .attr("x", d => d.x)
- .attr("y", d => d.y);
-
- townsAnchors
- .selectAll("use")
- .data(towns.filter(c => c.port))
- .enter()
- .append("use")
- .attr("xlink:href", "#icon-anchor")
- .attr("data-id", d => d.i)
- .attr("x", d => rn(d.x - townsAnchorsSize * 0.47, 2))
- .attr("y", d => rn(d.y - townsAnchorsSize * 0.47, 2))
- .attr("width", townsAnchorsSize)
- .attr("height", townsAnchorsSize);
+ // capitalAnchors
+ // .selectAll("use")
+ // .data(capitals.filter(c => c.port))
+ // .enter()
+ // .append("use")
+ // .attr("xlink:href", "#icon-anchor")
+ // .attr("data-id", d => d.i)
+ // .attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
+ // .attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
+ // .attr("width", capitalAnchorsSize)
+ // .attr("height", capitalAnchorsSize);
+ }
TIME && console.timeEnd("drawBurgIcons");
}
+
+function drawBurgIcon(burg) {
+ burgIcons
+ .select("#" + burg.group)
+ .append("use")
+ .attr("href", "#icon-circle")
+ .attr("id", "burg" + burg.i)
+ .attr("data-id", burg.i)
+ .attr("x", burg.x)
+ .attr("y", burg.y);
+}
diff --git a/modules/renderers/draw-burg-labels.js b/modules/renderers/draw-burg-labels.js
index d887b9ca..4f5b004f 100644
--- a/modules/renderers/draw-burg-labels.js
+++ b/modules/renderers/draw-burg-labels.js
@@ -5,35 +5,37 @@ function drawBurgLabels() {
burgLabels.selectAll("text").remove(); // cleanup
- const capitals = pack.burgs.filter(b => b.capital && !b.removed);
- const capitalSize = burgIcons.select("#cities").attr("size") || 1;
- burgLabels
- .select("#cities")
- .selectAll("text")
- .data(capitals)
- .enter()
- .append("text")
- .attr("id", d => "burgLabel" + d.i)
- .attr("data-id", d => d.i)
- .attr("x", d => d.x)
- .attr("y", d => d.y)
- .attr("dy", `${capitalSize * -1.5}px`)
- .text(d => d.name);
+ for (const {name} of options.burgs.groups) {
+ const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
+ if (!burgsInGroup.length) continue;
- const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
- const townSize = burgIcons.select("#towns").attr("size") || 0.5;
- burgLabels
- .select("#towns")
- .selectAll("text")
- .data(towns)
- .enter()
- .append("text")
- .attr("id", d => "burgLabel" + d.i)
- .attr("data-id", d => d.i)
- .attr("x", d => d.x)
- .attr("y", d => d.y)
- .attr("dy", `${townSize * -2}px`)
- .text(d => d.name);
+ const labelGroup = burgLabels.select("#" + name);
+ if (labelGroup.empty()) continue;
+
+ labelGroup
+ .selectAll("text")
+ .data(burgsInGroup)
+ .enter()
+ .append("text")
+ .attr("id", d => "burgLabel" + d.i)
+ .attr("data-id", d => d.i)
+ .attr("x", d => d.x)
+ .attr("y", d => d.y)
+ .attr("dy", "-0.4em")
+ .text(d => d.name);
+ }
TIME && console.timeEnd("drawBurgLabels");
}
+
+function drawBurgLabel(burg) {
+ burgLabels
+ .select("#" + burg.group)
+ .append("text")
+ .attr("id", "burgLabel" + burg.i)
+ .attr("data-id", burg.i)
+ .attr("x", burg.x)
+ .attr("y", burg.y)
+ .attr("dy", "-0.4em")
+ .text(burg.name);
+}
diff --git a/modules/routes-generator.js b/modules/routes-generator.js
index 9f7eaa0f..bd119d06 100644
--- a/modules/routes-generator.js
+++ b/modules/routes-generator.js
@@ -537,6 +537,26 @@ window.Routes = (function () {
return roadConnections.length > 2;
}
+ const connectivityRates = {
+ roads: 0.2,
+ trails: 0.1,
+ searoutes: 0.2,
+ default: 0.1
+ };
+
+ function getConnectivityRate(cellId) {
+ const connections = pack.cells.routes[cellId];
+ if (!connections) return 0;
+
+ const connectivity = Object.values(connections).reduce((acc, routeId) => {
+ const route = pack.routes.find(route => route.i === routeId);
+ const rate = connectivityRates[route.group] || connectivityRates.default;
+ return acc + rate;
+ }, 0.8);
+
+ return connectivity;
+ }
+
// name generator data
const models = {
roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1},
@@ -749,6 +769,7 @@ window.Routes = (function () {
getRoute,
hasRoad,
isCrossroad,
+ getConnectivityRate,
generateName,
getPath,
getLength,
diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js
index e2457042..a13bcc02 100644
--- a/modules/ui/burgs-overview.js
+++ b/modules/ui/burgs-overview.js
@@ -287,7 +287,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (pack.cells.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error");
- addBurg(point); // add new burg
+ Burgs.add(point); // add new burg
if (d3.event.shiftKey === false) {
exitAddBurgMode();
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index 0e8e2bdc..1fcdffe2 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -128,78 +128,6 @@ function applySorting(headers) {
.forEach(line => list.appendChild(line));
}
-function addBurg(point) {
- const {cells, states} = pack;
- const x = rn(point[0], 2);
- const y = rn(point[1], 2);
-
- const cellId = findCell(x, y);
- const i = pack.burgs.length;
- const culture = cells.culture[cellId];
- const name = Names.getCulture(culture);
- const state = cells.state[cellId];
- const feature = cells.f[cellId];
-
- const population = Math.max(cells.s[cellId] / 3 + i / 1000 + (cellId % 100) / 1000, 0.1);
- const type = Burgs.getType(cellId, false);
-
- // generate emblem
- const coa = COA.generate(states[state].coa, 0.25, null, type);
- coa.shield = COA.getShield(culture, state);
- COArenderer.add("burg", i, coa, x, y);
-
- const burg = {
- name,
- cell: cellId,
- x,
- y,
- state,
- i,
- culture,
- feature,
- capital: 0,
- port: 0,
- temple: 0,
- population,
- coa,
- type
- };
- pack.burgs.push(burg);
- cells.burg[cellId] = i;
-
- const townSize = burgIcons.select("#towns").attr("size") || 0.5;
- burgIcons
- .select("#towns")
- .append("circle")
- .attr("id", "burg" + i)
- .attr("data-id", i)
- .attr("cx", x)
- .attr("cy", y)
- .attr("r", townSize);
- burgLabels
- .select("#towns")
- .append("text")
- .attr("id", "burgLabel" + i)
- .attr("data-id", i)
- .attr("x", x)
- .attr("y", y)
- .attr("dy", `${townSize * -1.5}px`)
- .text(name);
-
- Burgs.defineBurgFeatures(burg);
-
- const newRoute = Routes.connect(cellId);
- if (newRoute && layerIsOn("toggleRoutes")) {
- routes
- .select("#" + newRoute.group)
- .append("path")
- .attr("d", Routes.getPath(newRoute))
- .attr("id", "route" + newRoute.i);
- }
-
- return i;
-}
-
function moveBurgToGroup(id, g) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 1aa79494..2c3689cb 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -250,7 +250,7 @@ function editHeightmap(options) {
States.defineStateForms();
Provinces.generate();
Provinces.getPoles();
- Burgs.specifyBurgs();
+ Burgs.specify();
Rivers.specify();
Features.specify();
diff --git a/modules/ui/style-presets.js b/modules/ui/style-presets.js
index 1045a5c7..08c1f118 100644
--- a/modules/ui/style-presets.js
+++ b/modules/ui/style-presets.js
@@ -342,8 +342,8 @@ function addStylePreset() {
"stroke-dasharray",
"stroke-linecap"
];
- options.burgs.groups.forEach(group => {
- attributes[`#burgIcons > g[data-name='${group}']`] = burgIconsAttributes;
+ options.burgs.groups.forEach(({name}) => {
+ attributes[`#burgIcons > g.${name}`] = burgIconsAttributes;
});
for (const selector in attributes) {
diff --git a/modules/ui/style.js b/modules/ui/style.js
index 49d1d8fb..10c9658f 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -1125,39 +1125,6 @@ styleScaleBar.on("input", function (event) {
});
function updateElements() {
- // burgIcons to desired size
- burgIcons.selectAll("g").each(function () {
- const size = +this.getAttribute("size");
- d3.select(this)
- .selectAll("circle")
- .each(function () {
- this.setAttribute("r", size);
- });
- burgLabels
- .select("g#" + this.id)
- .selectAll("text")
- .each(function () {
- this.setAttribute("dy", `${size * -1.5}px`);
- });
- });
-
- // anchor icons to desired size
- anchors.selectAll("g").each(function (d) {
- const size = +this.getAttribute("size");
- d3.select(this)
- .selectAll("use")
- .each(function () {
- const id = +this.dataset.id;
- const x = pack.burgs[id].x,
- y = pack.burgs[id].y;
- this.setAttribute("x", rn(x - size * 0.47, 2));
- this.setAttribute("y", rn(y - size * 0.47, 2));
- this.setAttribute("width", size);
- this.setAttribute("height", size);
- });
- });
-
- // redraw elements
if (layerIsOn("toggleHeight")) drawHeightmap();
if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend();
oceanLayers.selectAll("path").remove();
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 3d0cc3cf..d7ee828e 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -431,7 +431,7 @@ function regenerateBurgs() {
.filter(s => s.i && !s.removed && !s.capital)
.forEach(s => {
const [x, y] = cells.p[s.center];
- const burgId = addBurg([x, y]);
+ const burgId = Burgs.add([x, y]);
s.capital = burgId;
s.center = pack.burgs[burgId].cell;
pack.burgs[burgId].capital = 1;
@@ -443,7 +443,7 @@ function regenerateBurgs() {
if (f.port) f.port = 0; // reset features ports counter
});
- Burgs.specifyBurgs();
+ Burgs.specify();
regenerateRoutes();
drawBurgIcons();
diff --git a/styles/default.json b/styles/default.json
index fe9dfd0f..e1f644f7 100644
--- a/styles/default.json
+++ b/styles/default.json
@@ -328,7 +328,7 @@
"data-y": 93,
"data-columns": 8
},
- "#burgLabels > #cities": {
+ "#burgLabels > g#capitals": {
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
@@ -337,7 +337,7 @@
"font-size": 7,
"font-family": "Almendra SC"
},
- "#burgIcons > #cities": {
+ "#burgIcons > g#capitals": {
"data-icon": "#icon-square",
"opacity": 1,
"fill": "#ffffff",
@@ -349,14 +349,154 @@
"stroke-linecap": "butt",
"stroke-linejoin": "round"
},
- "#anchors > #cities": {
+ "#burgLabels > g#cities": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 5,
+ "font-size": 5,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#cities": {
+ "data-icon": "#icon-circle",
"opacity": 1,
"fill": "#ffffff",
- "size": 2,
+ "fill-opacity": 0.7,
+ "font-size": 1.5,
"stroke": "#3e3e4b",
- "stroke-width": 1.2
+ "stroke-width": 8,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
},
- "#burgLabels > #towns": {
+ "#burgLabels > g#forts": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 2,
+ "font-size": 2,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#forts": {
+ "data-icon": "#icon-triangle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.7,
+ "stroke": "#3e3e4b",
+ "stroke-width": 10,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#monasteries": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 2,
+ "font-size": 2,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#monasteries": {
+ "data-icon": "#icon-triangle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.7,
+ "stroke": "#3e3e4b",
+ "stroke-width": 10,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#caravanserais": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 2,
+ "font-size": 2,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#caravanserais": {
+ "data-icon": "#icon-triangle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.7,
+ "stroke": "#3e3e4b",
+ "stroke-width": 10,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#trading_posts": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 2,
+ "font-size": 2,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#trading_posts": {
+ "data-icon": "#icon-triangle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.7,
+ "stroke": "#3e3e4b",
+ "stroke-width": 10,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#villages": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 3,
+ "font-size": 3,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#villages": {
+ "data-icon": "#icon-circle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.7,
+ "stroke": "#3e3e4b",
+ "stroke-width": 12,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#hamlets": {
+ "opacity": 1,
+ "fill": "#3e3e4b",
+ "text-shadow": "white 0px 0px 4px",
+ "letter-spacing": 0,
+ "data-size": 2,
+ "font-size": 2,
+ "font-family": "Almendra SC"
+ },
+ "#burgIcons > g#hamlets": {
+ "data-icon": "#icon-circle",
+ "opacity": 1,
+ "fill": "#ffffff",
+ "fill-opacity": 0.7,
+ "font-size": 0.5,
+ "stroke": "#3e3e4b",
+ "stroke-width": 12,
+ "stroke-dasharray": null,
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "round"
+ },
+ "#burgLabels > g#towns": {
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
@@ -365,25 +505,18 @@
"font-size": 4,
"font-family": "Almendra SC"
},
- "#burgIcons > #towns": {
+ "#burgIcons > g#towns": {
"data-icon": "#icon-circle",
"opacity": 1,
"fill": "#ffffff",
"fill-opacity": 0.7,
"font-size": 1,
"stroke": "#3e3e4b",
- "stroke-width": 10,
+ "stroke-width": 12,
"stroke-dasharray": null,
"stroke-linecap": "butt",
"stroke-linejoin": "round"
},
- "#anchors > #towns": {
- "opacity": 1,
- "fill": "#ffffff",
- "size": 1,
- "stroke": "#3e3e4b",
- "stroke-width": 1.2
- },
"#labels > #states": {
"opacity": 1,
"fill": "#3e3e4b",