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",