feat: split burgs to groups

This commit is contained in:
Azgaar 2024-09-30 20:38:52 +02:00
parent f51f78a7a6
commit 63898d8fd8
15 changed files with 543 additions and 383 deletions

View file

@ -5237,7 +5237,7 @@
</slider-input> </slider-input>
</div> </div>
<div data-tip="Set urbanization rate: burgs population relative to all population"> <div data-tip="Set urban population modifier. Change to increase or descrese burgs population">
<slider-input id="urbanizationInput" data-stored="urbanization" min=".01" max="5" step=".01" value="1"> <slider-input id="urbanizationInput" data-stored="urbanization" min=".01" max="5" step=".01" value="1">
<label>Urbanization rate:</label> <label>Urbanization rate:</label>
</slider-input> </slider-input>

92
main.js
View file

@ -108,15 +108,7 @@ terrs.append("g").attr("id", "landHeights");
labels.append("g").attr("id", "states"); labels.append("g").attr("id", "states");
labels.append("g").attr("id", "addedLabels"); labels.append("g").attr("id", "addedLabels");
let burgLabels = labels.append("g").attr("id", "burgLabels"); 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 groups
population.append("g").attr("id", "rural"); population.append("g").attr("id", "rural");
@ -162,6 +154,29 @@ let customization = 0;
let biomesData = Biomes.getDefault(); let biomesData = Biomes.getDefault();
let nameBases = Names.getNameBases(); // cultures-related data 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 let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
@ -185,21 +200,6 @@ const onZoom = debounce(function () {
}, 50); }, 50);
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoom); 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 mapCoordinates = {}; // map coordinates on globe
let populationRate = +byId("populationRateInput").value; let populationRate = +byId("populationRateInput").value;
let distanceScale = +byId("distanceScaleInput").value; let distanceScale = +byId("distanceScaleInput").value;
@ -668,7 +668,7 @@ async function generate(options) {
States.defineStateForms(); States.defineStateForms();
Provinces.generate(); Provinces.generate();
Provinces.getPoles(); Provinces.getPoles();
Burgs.specifyBurgs(); Burgs.specify();
Rivers.specify(); Rivers.specify();
Features.specify(); Features.specify();
@ -1180,36 +1180,44 @@ function rankCells() {
cells.s = new Int16Array(cells.i.length); // cell suitability array cells.s = new Int16Array(cells.i.length); // cell suitability array
cells.pop = new Float32Array(cells.i.length); // cell population array cells.pop = new Float32Array(cells.i.length); // cell population array
const flMean = d3.median(cells.fl.filter(f => f)) || 0, const meanFlux = d3.median(cells.fl.filter(f => f)) || 0;
flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux const maxFlux = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
const areaMean = d3.mean(cells.area); // to adjust population by cell area 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) { for (const i of cells.i) {
if (cells.h[i] < 20) continue; // no population in water if (cells.h[i] < 20) continue; // no population in water
let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability let score = biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
if (!s) continue; // uninhabitable biomes has 0 suitability if (!score) 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; 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.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]]]; const feature = features[cells.f[cells.haven[i]]];
if (feature.type === "lake") { if (feature.type === "lake") {
if (feature.group === "freshwater") s += 30; score += scoreMap[feature.water] || 0;
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;
} else { } else {
s += 5; // ocean coast is valued score += scoreMap.ocean_coast;
if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued 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 // 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"); TIME && console.timeEnd("rankCells");

View file

@ -17,6 +17,7 @@ window.Burgs = (() => {
let quadtree = d3.quadtree(); let quadtree = d3.quadtree();
generateCapitals(); generateCapitals();
generateTowns(); generateTowns();
shiftBurgs();
pack.burgs = burgs; pack.burgs = burgs;
TIME && console.timeEnd("generateBurgs"); TIME && console.timeEnd("generateBurgs");
@ -108,137 +109,93 @@ window.Burgs = (() => {
spacing *= 0.5; 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 = () => [ const specify = () => {
{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 = () => {
TIME && console.time("specifyBurgs"); TIME && console.time("specifyBurgs");
const {cells, features} = pack;
const temp = grid.cells.temp;
for (const burg of pack.burgs) { pack.burgs.forEach(burg => {
if (!burg.i || burg.lock) continue; if (!burg.i || burg.removed || burg.lock) return;
const i = burg.cell; definePopulation(burg);
defineEmblem(burg);
defineFeatures(burg);
});
// asign port status to some coastline burgs with temp > 0 °C const populations = pack.burgs
const haven = cells.haven[i]; .filter(b => b.i && !b.removed)
if (haven && temp[cells.g[i]] > 0) { .map(b => b.population)
const f = cells.f[haven]; // water body id .sort((a, b) => a - b); // ascending
// 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;
// define burg population (keep urbanization at about 10% rate) pack.burgs.forEach(burg => {
burg.population = rn(Math.max(cells.s[i] / 8 + burg.i / 1000 + (i % 100) / 1000, 0.1), 3); if (!burg.i || burg.removed || burg.lock) return;
if (burg.capital) burg.population = rn(burg.population * 1.3, 3); // increase capital population defineGroup(burg, populations);
});
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);
TIME && console.timeEnd("specifyBurgs"); 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 getType = (cellId, port) => {
const {cells, features} = pack; const {cells, features} = pack;
@ -261,19 +218,180 @@ window.Burgs = (() => {
return "Generic"; return "Generic";
}; };
const defineBurgFeatures = burg => { function definePopulation(burg) {
const {cells, states} = pack; 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; const pop = burg.population;
burg.citadel = Number(burg.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1)); 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.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))); burg.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && burg.walls && P(0.4)));
const religion = cells.religion[burg.cell]; const religion = pack.cells.religion[burg.cell];
const theocracy = states[burg.state].form === "Theocracy"; const theocracy = pack.states[burg.state].form === "Theocracy";
burg.temple = Number( burg.temple = Number(
(religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5)) (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};
})(); })();

View file

@ -968,8 +968,9 @@ export function resolveVersionConflicts(mapVersion) {
// v1.106.0 change burg groups and added customizable icons // v1.106.0 change burg groups and added customizable icons
icons.selectAll("circle, use").remove(); icons.selectAll("circle, use").remove();
const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id);
options.burgs = { options.burgs = {
groups: Burgs.getDefaultGroups() groups: groups.map(name => ({name, active: true, preview: null}))
}; };
} }
} }

View file

@ -1184,7 +1184,7 @@ function addState() {
if (burg && burgs[burg].capital) if (burg && burgs[burg].capital)
return tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); 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 oldState = cells.state[center];
const newState = states.length; const newState = states.length;

View file

@ -5,65 +5,47 @@ function drawBurgIcons() {
icons.selectAll("circle, use").remove(); // cleanup icons.selectAll("circle, use").remove(); // cleanup
// capitals for (const {name} of options.burgs.groups) {
const capitals = pack.burgs.filter(b => b.capital && !b.removed); const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
const capitalIcons = burgIcons.select("#cities"); if (!burgsInGroup.length) continue;
const capitalIcon = capitalIcons.attr("data-icon") || "#icon-circle";
const capitalAnchors = anchors.selectAll("#cities");
const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
capitalIcons const g = burgIcons.select("#" + name);
.selectAll("use") if (g.empty()) continue;
.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);
capitalAnchors const icon = g.attr("data-icon") || "#icon-circle";
.selectAll("use") g.selectAll("use")
.data(capitals.filter(c => c.port)) .data(burgsInGroup)
.enter() .enter()
.append("use") .append("use")
.attr("xlink:href", "#icon-anchor") .attr("href", icon)
.attr("data-id", d => d.i) .attr("id", d => "burg" + d.i)
.attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2)) .attr("data-id", d => d.i)
.attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2)) .attr("x", d => d.x)
.attr("width", capitalAnchorsSize) .attr("y", d => d.y);
.attr("height", capitalAnchorsSize);
// towns // capitalAnchors
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed); // .selectAll("use")
const townIcons = burgIcons.select("#towns"); // .data(capitals.filter(c => c.port))
const townIcon = townIcons.attr("data-icon") || "#icon-circle"; // .enter()
const townsAnchors = anchors.selectAll("#towns"); // .append("use")
const townsAnchorsSize = townsAnchors.attr("size") || 1; // .attr("xlink:href", "#icon-anchor")
// .attr("data-id", d => d.i)
townIcons // .attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
.selectAll("use") // .attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
.data(towns) // .attr("width", capitalAnchorsSize)
.enter() // .attr("height", capitalAnchorsSize);
.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);
TIME && console.timeEnd("drawBurgIcons"); 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);
}

View file

@ -5,35 +5,37 @@ function drawBurgLabels() {
burgLabels.selectAll("text").remove(); // cleanup burgLabels.selectAll("text").remove(); // cleanup
const capitals = pack.burgs.filter(b => b.capital && !b.removed); for (const {name} of options.burgs.groups) {
const capitalSize = burgIcons.select("#cities").attr("size") || 1; const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
burgLabels if (!burgsInGroup.length) continue;
.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);
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed); const labelGroup = burgLabels.select("#" + name);
const townSize = burgIcons.select("#towns").attr("size") || 0.5; if (labelGroup.empty()) continue;
burgLabels
.select("#towns") labelGroup
.selectAll("text") .selectAll("text")
.data(towns) .data(burgsInGroup)
.enter() .enter()
.append("text") .append("text")
.attr("id", d => "burgLabel" + d.i) .attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i) .attr("data-id", d => d.i)
.attr("x", d => d.x) .attr("x", d => d.x)
.attr("y", d => d.y) .attr("y", d => d.y)
.attr("dy", `${townSize * -2}px`) .attr("dy", "-0.4em")
.text(d => d.name); .text(d => d.name);
}
TIME && console.timeEnd("drawBurgLabels"); 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);
}

View file

@ -537,6 +537,26 @@ window.Routes = (function () {
return roadConnections.length > 2; 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 // name generator data
const models = { const models = {
roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1}, 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, getRoute,
hasRoad, hasRoad,
isCrossroad, isCrossroad,
getConnectivityRate,
generateName, generateName,
getPath, getPath,
getLength, getLength,

View file

@ -287,7 +287,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (pack.cells.burg[cell]) if (pack.cells.burg[cell])
return tip("There is already a burg in this cell. Please select a free cell", false, "error"); 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) { if (d3.event.shiftKey === false) {
exitAddBurgMode(); exitAddBurgMode();

View file

@ -128,78 +128,6 @@ function applySorting(headers) {
.forEach(line => list.appendChild(line)); .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) { function moveBurgToGroup(id, g) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']"); const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']"); const icon = document.querySelector("#burgIcons [data-id='" + id + "']");

View file

@ -250,7 +250,7 @@ function editHeightmap(options) {
States.defineStateForms(); States.defineStateForms();
Provinces.generate(); Provinces.generate();
Provinces.getPoles(); Provinces.getPoles();
Burgs.specifyBurgs(); Burgs.specify();
Rivers.specify(); Rivers.specify();
Features.specify(); Features.specify();

View file

@ -342,8 +342,8 @@ function addStylePreset() {
"stroke-dasharray", "stroke-dasharray",
"stroke-linecap" "stroke-linecap"
]; ];
options.burgs.groups.forEach(group => { options.burgs.groups.forEach(({name}) => {
attributes[`#burgIcons > g[data-name='${group}']`] = burgIconsAttributes; attributes[`#burgIcons > g.${name}`] = burgIconsAttributes;
}); });
for (const selector in attributes) { for (const selector in attributes) {

View file

@ -1125,39 +1125,6 @@ styleScaleBar.on("input", function (event) {
}); });
function updateElements() { 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 (layerIsOn("toggleHeight")) drawHeightmap();
if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend(); if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend();
oceanLayers.selectAll("path").remove(); oceanLayers.selectAll("path").remove();

View file

@ -431,7 +431,7 @@ function regenerateBurgs() {
.filter(s => s.i && !s.removed && !s.capital) .filter(s => s.i && !s.removed && !s.capital)
.forEach(s => { .forEach(s => {
const [x, y] = cells.p[s.center]; const [x, y] = cells.p[s.center];
const burgId = addBurg([x, y]); const burgId = Burgs.add([x, y]);
s.capital = burgId; s.capital = burgId;
s.center = pack.burgs[burgId].cell; s.center = pack.burgs[burgId].cell;
pack.burgs[burgId].capital = 1; pack.burgs[burgId].capital = 1;
@ -443,7 +443,7 @@ function regenerateBurgs() {
if (f.port) f.port = 0; // reset features ports counter if (f.port) f.port = 0; // reset features ports counter
}); });
Burgs.specifyBurgs(); Burgs.specify();
regenerateRoutes(); regenerateRoutes();
drawBurgIcons(); drawBurgIcons();

View file

@ -328,7 +328,7 @@
"data-y": 93, "data-y": 93,
"data-columns": 8 "data-columns": 8
}, },
"#burgLabels > #cities": { "#burgLabels > g#capitals": {
"opacity": 1, "opacity": 1,
"fill": "#3e3e4b", "fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px", "text-shadow": "white 0px 0px 4px",
@ -337,7 +337,7 @@
"font-size": 7, "font-size": 7,
"font-family": "Almendra SC" "font-family": "Almendra SC"
}, },
"#burgIcons > #cities": { "#burgIcons > g#capitals": {
"data-icon": "#icon-square", "data-icon": "#icon-square",
"opacity": 1, "opacity": 1,
"fill": "#ffffff", "fill": "#ffffff",
@ -349,14 +349,154 @@
"stroke-linecap": "butt", "stroke-linecap": "butt",
"stroke-linejoin": "round" "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, "opacity": 1,
"fill": "#ffffff", "fill": "#ffffff",
"size": 2, "fill-opacity": 0.7,
"font-size": 1.5,
"stroke": "#3e3e4b", "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, "opacity": 1,
"fill": "#3e3e4b", "fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px", "text-shadow": "white 0px 0px 4px",
@ -365,25 +505,18 @@
"font-size": 4, "font-size": 4,
"font-family": "Almendra SC" "font-family": "Almendra SC"
}, },
"#burgIcons > #towns": { "#burgIcons > g#towns": {
"data-icon": "#icon-circle", "data-icon": "#icon-circle",
"opacity": 1, "opacity": 1,
"fill": "#ffffff", "fill": "#ffffff",
"fill-opacity": 0.7, "fill-opacity": 0.7,
"font-size": 1, "font-size": 1,
"stroke": "#3e3e4b", "stroke": "#3e3e4b",
"stroke-width": 10, "stroke-width": 12,
"stroke-dasharray": null, "stroke-dasharray": null,
"stroke-linecap": "butt", "stroke-linecap": "butt",
"stroke-linejoin": "round" "stroke-linejoin": "round"
}, },
"#anchors > #towns": {
"opacity": 1,
"fill": "#ffffff",
"size": 1,
"stroke": "#3e3e4b",
"stroke-width": 1.2
},
"#labels > #states": { "#labels > #states": {
"opacity": 1, "opacity": 1,
"fill": "#3e3e4b", "fill": "#3e3e4b",