From 6b3df6c4d86622bd9cac737a10cc4c5f6186d4e4 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 2 Sep 2024 13:47:11 +0200 Subject: [PATCH] feat: render provinces --- index.css | 13 +- index.html | 3 +- main.js | 3 +- modules/burgs-and-states.js | 259 +---------------------- modules/dynamic/auto-update.js | 9 +- modules/dynamic/editors/states-editor.js | 3 +- modules/provinces-generator.js | 257 ++++++++++++++++++++++ modules/ui/heightmap-editor.js | 3 +- modules/ui/layers.js | 145 +++---------- modules/ui/provinces-editor.js | 97 +++++---- modules/ui/tools.js | 6 +- versioning.js | 2 +- 12 files changed, 362 insertions(+), 438 deletions(-) create mode 100644 modules/provinces-generator.js diff --git a/index.css b/index.css index eb1e82fc..af2ec591 100644 --- a/index.css +++ b/index.css @@ -190,20 +190,12 @@ t, font-size: 0.8em; } -#statesBody { - stroke-width: 3; -} - #statesHalo { fill: none; stroke-linecap: round; stroke-linejoin: round; } -#provincesBody { - stroke-width: 0.2; -} - #statesBody, #provincesBody, #relig, @@ -220,6 +212,11 @@ t, mask: url(#land); } +#statesBody, +#provincesBody { + stroke-width: 3; +} + #borders { stroke-linejoin: round; fill: none; diff --git a/index.html b/index.html index ef4d1660..096a3e7b 100644 --- a/index.html +++ b/index.html @@ -8030,7 +8030,8 @@ - + + diff --git a/main.js b/main.js index 4263318e..c2aba7a0 100644 --- a/main.js +++ b/main.js @@ -650,7 +650,8 @@ async function generate(options) { Routes.generate(); Religions.generate(); BurgsAndStates.defineStateForms(); - BurgsAndStates.generateProvinces(); + Provinces.generate(); + Provinces.getPoles(); BurgsAndStates.defineBurgFeatures(); drawStates(); diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 86eca6d7..b08e1564 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -498,26 +498,19 @@ window.BurgsAndStates = (() => { }); }; - // Resets the cultures of all burgs and states to their - // cell or center cell's (respectively) culture. + // Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture const updateCultures = () => { TIME && console.time("updateCulturesForBurgsAndStates"); - // Assign the culture associated with the burgs cell. + // Assign the culture associated with the burgs cell pack.burgs = pack.burgs.map((burg, index) => { - // Ignore metadata burg - if (index === 0) { - return burg; - } + if (index === 0) return burg; return {...burg, culture: pack.cells.culture[burg.cell]}; }); - // Assign the culture associated with the states' center cell. + // Assign the culture associated with the states' center cell pack.states = pack.states.map((state, index) => { - // Ignore neutrals state - if (index === 0) { - return state; - } + if (index === 0) return state; return {...state, culture: pack.cells.culture[state.center]}; }); @@ -961,247 +954,6 @@ window.BurgsAndStates = (() => { return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`; }; - const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => { - TIME && console.time("generateProvinces"); - const localSeed = regenerate ? generateSeed() : seed; - Math.random = aleaPRNG(localSeed); - - const {cells, states, burgs} = pack; - const provinces = [0]; - const provinceIds = new Uint16Array(cells.i.length); - - const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock); - const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]); - - if (regenerate) { - pack.provinces.forEach(province => { - if (!province.i || province.removed || !isProvinceLocked(province)) return; - - const newId = provinces.length; - for (const i of cells.i) { - if (cells.province[i] === province.i) provinceIds[i] = newId; - } - - province.i = newId; - provinces.push(province); - }); - } - - const provincesRatio = +byId("provincesRatio").value; - const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth - - const forms = { - Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1}, - Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1}, - Theocracy: {Parish: 3, Deanery: 1}, - Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, - Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1}, - Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1} - }; - - // generate provinces for selected burgs - states.forEach(s => { - s.provinces = []; - if (!s.i || s.removed) return; - if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids - if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state - - const stateBurgs = burgs - .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell]) - .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population) - .sort((a, b) => b.capital - a.capital); - if (stateBurgs.length < 2) return; // at least 2 provinces are required - const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2); - - const form = Object.assign({}, forms[s.form]); - - for (let i = 0; i < provincesNumber; i++) { - const provinceId = provinces.length; - const center = stateBurgs[i].cell; - const burg = stateBurgs[i].i; - const c = stateBurgs[i].culture; - const nameByBurg = P(0.5); - const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); - const formName = rw(form); - form[formName] += 10; - const fullName = name + " " + formName; - const color = getMixedColor(s.color); - const kinship = nameByBurg ? 0.8 : 0.4; - const type = getType(center, burg.port); - const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); - coa.shield = COA.getShield(c, s.i); - - s.provinces.push(provinceId); - provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); - } - }); - - // expand generated provinces - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = []; - - provinces.forEach(p => { - if (!p.i || p.removed || isProvinceLocked(p)) return; - provinceIds[p.center] = p.i; - queue.queue({e: p.center, p: 0, province: p.i, state: p.state}); - cost[p.center] = 1; - }); - - while (queue.length) { - const {e, p, province, state} = queue.dequeue(); - - cells.c[e].forEach(e => { - if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces - - const land = cells.h[e] >= 20; - if (!land && !cells.t[e]) return; // cannot pass deep ocean - if (land && cells.state[e] !== state) return; - const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100; - const totalCost = p + evevation; - - if (totalCost > max) return; - if (!cost[e] || totalCost < cost[e]) { - if (land) provinceIds[e] = province; // assign province to a cell - cost[e] = totalCost; - queue.queue({e, p: totalCost, province, state}); - } - }); - } - - // justify provinces shapes a bit - for (const i of cells.i) { - if (cells.burg[i]) continue; // do not overwrite burgs - if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces - - const neibs = cells.c[i] - .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c)) - .map(c => provinceIds[c]); - const adversaries = neibs.filter(c => c !== provinceIds[i]); - if (adversaries.length < 2) continue; - - const buddies = neibs.filter(c => c === provinceIds[i]).length; - if (buddies.length > 2) continue; - - const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0)); - const max = d3.max(competitors); - if (buddies >= max) continue; - - provinceIds[i] = adversaries[competitors.indexOf(max)]; - } - - // add "wild" provinces if some cells don't have a province assigned - const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned - states.forEach(s => { - if (!s.i || s.removed) return; - if (s.lock && !regenerateInLockedStates) return; - if (!s.provinces.length) return; - - const coreProvinceNames = s.provinces.map(p => provinces[p]?.name); - const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)); - const getColonyName = () => { - if (colonyNamePool.length < 1) return null; - - const index = rand(colonyNamePool.length - 1); - const spliced = colonyNamePool.splice(index, 1); - return spliced[0] ? `New ${spliced[0]}` : null; - }; - - let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - while (stateNoProvince.length) { - // add new province - const provinceId = provinces.length; - const burgCell = stateNoProvince.find(i => cells.burg[i]); - const center = burgCell ? burgCell : stateNoProvince[0]; - const burg = burgCell ? cells.burg[burgCell] : 0; - provinceIds[center] = provinceId; - - // expand province - const cost = []; - cost[center] = 1; - queue.queue({e: center, p: 0}); - while (queue.length) { - const {e, p} = queue.dequeue(); - - cells.c[e].forEach(nextCellId => { - if (provinceIds[nextCellId]) return; - const land = cells.h[nextCellId] >= 20; - if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return; - const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30; - const totalCost = p + ter; - - if (totalCost > max) return; - if (!cost[nextCellId] || totalCost < cost[nextCellId]) { - if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell - cost[nextCellId] = totalCost; - queue.queue({e: nextCellId, p: totalCost}); - } - }); - } - - // generate "wild" province name - const c = cells.culture[center]; - const f = pack.features[cells.f[center]]; - const color = getMixedColor(s.color); - - const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId); - const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); - const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); - const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center); - - const name = (() => { - const colonyName = colony && P(0.8) && getColonyName(); - if (colonyName) return colonyName; - if (burgCell && P(0.5)) return burgs[burg].name; - return Names.getState(Names.getCultureShort(c), c); - })(); - - const formName = (() => { - if (singleIsle) return "Island"; - if (isleGroup) return "Islands"; - if (colony) return "Colony"; - return rw(forms["Wild"]); - })(); - - const fullName = name + " " + formName; - - const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3); - const kinship = dominion ? 0 : 0.4; - const type = getType(center, burgs[burg]?.port); - const coa = COA.generate(s.coa, kinship, dominion, type); - coa.shield = COA.getShield(c, s.i); - - provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); - s.provinces.push(provinceId); - - // check if there is a land way within the same state between two cells - function isPassable(from, to) { - if (cells.f[from] !== cells.f[to]) return false; // on different islands - const queue = [from], - used = new Uint8Array(cells.i.length), - state = cells.state[from]; - while (queue.length) { - const current = queue.pop(); - if (current === to) return true; // way is found - cells.c[current].forEach(c => { - if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return; - queue.push(c); - used[c] = 1; - }); - } - return false; // way is not found - } - - // re-check - stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - } - }); - - cells.province = provinceIds; - pack.provinces = provinces; - - TIME && console.timeEnd("generateProvinces"); - }; - return { generate, expandStates, @@ -1218,7 +970,6 @@ window.BurgsAndStates = (() => { generateDiplomacy, defineStateForms, getFullName, - generateProvinces, updateCultures }; })(); diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index a11faa31..de93a8a1 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -52,7 +52,8 @@ export function resolveVersionConflicts(mapVersion) { BurgsAndStates.generateDiplomacy(); BurgsAndStates.defineStateForms(); drawStates(); - BurgsAndStates.generateProvinces(); + Provinces.generate(); + Provinces.getPoles(); drawBorders(); if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); @@ -940,4 +941,10 @@ export function resolveVersionConflicts(mapVersion) { zones.style("display", null).selectAll("*").remove(); if (layerIsOn("toggleZones")) drawZones(); } + + if (isOlderThan("1.103.0")) { + // v1.103.00 separated pole of inaccessibility detection from layer rendering + BurgsAndStates.getPoles(); + Provinces.getPoles(); + } } diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js index 2e6c6ea6..d6a14cef 100644 --- a/modules/dynamic/editors/states-editor.js +++ b/modules/dynamic/editors/states-editor.js @@ -839,7 +839,8 @@ function recalculateStates(must) { if (!must && !statesAutoChange.checked) return; BurgsAndStates.expandStates(); - BurgsAndStates.generateProvinces(); + Provinces.generate(); + Provinces.getPoles(); if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); if (!layerIsOn("toggleBorders")) toggleBorders(); diff --git a/modules/provinces-generator.js b/modules/provinces-generator.js new file mode 100644 index 00000000..340562e9 --- /dev/null +++ b/modules/provinces-generator.js @@ -0,0 +1,257 @@ +"use strict"; + +window.Provinces = (function () { + const forms = { + Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1}, + Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1}, + Theocracy: {Parish: 3, Deanery: 1}, + Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, + Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1}, + Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1} + }; + + const generate = (regenerate = false, regenerateLockedStates = false) => { + TIME && console.time("generateProvinces"); + const localSeed = regenerate ? generateSeed() : seed; + Math.random = aleaPRNG(localSeed); + + const {cells, states, burgs} = pack; + const provinces = [0]; // 0 index is reserved for "no province" + const provinceIds = new Uint16Array(cells.i.length); + + const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock); + const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]); + + if (regenerate) { + pack.provinces.forEach(province => { + if (!province.i || province.removed || !isProvinceLocked(province)) return; + + const newId = provinces.length; + for (const i of cells.i) { + if (cells.province[i] === province.i) provinceIds[i] = newId; + } + + province.i = newId; + provinces.push(province); + }); + } + + const provincesRatio = +byId("provincesRatio").value; + const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth + + // generate provinces for selected burgs + states.forEach(s => { + s.provinces = []; + if (!s.i || s.removed) return; + if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids + if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state + + const stateBurgs = burgs + .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell]) + .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population) + .sort((a, b) => b.capital - a.capital); + if (stateBurgs.length < 2) return; // at least 2 provinces are required + const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2); + + const form = Object.assign({}, forms[s.form]); + + for (let i = 0; i < provincesNumber; i++) { + const provinceId = provinces.length; + const center = stateBurgs[i].cell; + const burg = stateBurgs[i].i; + const c = stateBurgs[i].culture; + const nameByBurg = P(0.5); + const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); + const formName = rw(form); + form[formName] += 10; + const fullName = name + " " + formName; + const color = getMixedColor(s.color); + const kinship = nameByBurg ? 0.8 : 0.4; + const type = BurgsAndStates.getType(center, burg.port); + const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); + coa.shield = COA.getShield(c, s.i); + + s.provinces.push(provinceId); + provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); + } + }); + + // expand generated provinces + const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); + const cost = []; + + provinces.forEach(p => { + if (!p.i || p.removed || isProvinceLocked(p)) return; + provinceIds[p.center] = p.i; + queue.queue({e: p.center, p: 0, province: p.i, state: p.state}); + cost[p.center] = 1; + }); + + while (queue.length) { + const {e, p, province, state} = queue.dequeue(); + + cells.c[e].forEach(e => { + if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces + + const land = cells.h[e] >= 20; + if (!land && !cells.t[e]) return; // cannot pass deep ocean + if (land && cells.state[e] !== state) return; + const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100; + const totalCost = p + evevation; + + if (totalCost > max) return; + if (!cost[e] || totalCost < cost[e]) { + if (land) provinceIds[e] = province; // assign province to a cell + cost[e] = totalCost; + queue.queue({e, p: totalCost, province, state}); + } + }); + } + + // justify provinces shapes a bit + for (const i of cells.i) { + if (cells.burg[i]) continue; // do not overwrite burgs + if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces + + const neibs = cells.c[i] + .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c)) + .map(c => provinceIds[c]); + const adversaries = neibs.filter(c => c !== provinceIds[i]); + if (adversaries.length < 2) continue; + + const buddies = neibs.filter(c => c === provinceIds[i]).length; + if (buddies.length > 2) continue; + + const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0)); + const max = d3.max(competitors); + if (buddies >= max) continue; + + provinceIds[i] = adversaries[competitors.indexOf(max)]; + } + + // add "wild" provinces if some cells don't have a province assigned + const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned + states.forEach(s => { + if (!s.i || s.removed) return; + if (s.lock && !regenerateLockedStates) return; + if (!s.provinces.length) return; + + const coreProvinceNames = s.provinces.map(p => provinces[p]?.name); + const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)); + const getColonyName = () => { + if (colonyNamePool.length < 1) return null; + + const index = rand(colonyNamePool.length - 1); + const spliced = colonyNamePool.splice(index, 1); + return spliced[0] ? `New ${spliced[0]}` : null; + }; + + let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); + while (stateNoProvince.length) { + // add new province + const provinceId = provinces.length; + const burgCell = stateNoProvince.find(i => cells.burg[i]); + const center = burgCell ? burgCell : stateNoProvince[0]; + const burg = burgCell ? cells.burg[burgCell] : 0; + provinceIds[center] = provinceId; + + // expand province + const cost = []; + cost[center] = 1; + queue.queue({e: center, p: 0}); + while (queue.length) { + const {e, p} = queue.dequeue(); + + cells.c[e].forEach(nextCellId => { + if (provinceIds[nextCellId]) return; + const land = cells.h[nextCellId] >= 20; + if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return; + const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30; + const totalCost = p + ter; + + if (totalCost > max) return; + if (!cost[nextCellId] || totalCost < cost[nextCellId]) { + if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell + cost[nextCellId] = totalCost; + queue.queue({e: nextCellId, p: totalCost}); + } + }); + } + + // generate "wild" province name + const c = cells.culture[center]; + const f = pack.features[cells.f[center]]; + const color = getMixedColor(s.color); + + const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId); + const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); + const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); + const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center); + + const name = (() => { + const colonyName = colony && P(0.8) && getColonyName(); + if (colonyName) return colonyName; + if (burgCell && P(0.5)) return burgs[burg].name; + return Names.getState(Names.getCultureShort(c), c); + })(); + + const formName = (() => { + if (singleIsle) return "Island"; + if (isleGroup) return "Islands"; + if (colony) return "Colony"; + return rw(forms["Wild"]); + })(); + + const fullName = name + " " + formName; + + const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3); + const kinship = dominion ? 0 : 0.4; + const type = BurgsAndStates.getType(center, burgs[burg]?.port); + const coa = COA.generate(s.coa, kinship, dominion, type); + coa.shield = COA.getShield(c, s.i); + + provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); + s.provinces.push(provinceId); + + // check if there is a land way within the same state between two cells + function isPassable(from, to) { + if (cells.f[from] !== cells.f[to]) return false; // on different islands + const queue = [from], + used = new Uint8Array(cells.i.length), + state = cells.state[from]; + while (queue.length) { + const current = queue.pop(); + if (current === to) return true; // way is found + cells.c[current].forEach(c => { + if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return; + queue.push(c); + used[c] = 1; + }); + } + return false; // way is not found + } + + // re-check + stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); + } + }); + + cells.province = provinceIds; + pack.provinces = provinces; + + TIME && console.timeEnd("generateProvinces"); + }; + + // calculate pole of inaccessibility for each province + const getPoles = () => { + const getType = cellId => pack.cells.province[cellId]; + const poles = getPolesOfInaccessibility(getType); + + pack.provinces.forEach(province => { + if (!province.i || province.removed) return; + province.pole = poles[province.i] || [0, 0]; + }); + }; + + return {generate, getPoles}; +})(); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index a88c698c..cd49f52c 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -249,7 +249,8 @@ function editHeightmap(options) { Routes.generate(); Religions.generate(); BurgsAndStates.defineStateForms(); - BurgsAndStates.generateProvinces(); + Provinces.generate(); + Provinces.getPoles(); BurgsAndStates.defineBurgFeatures(); drawStates(); diff --git a/modules/ui/layers.js b/modules/ui/layers.js index fb2b73ed..efe85b89 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1002,7 +1002,8 @@ function drawStates() { const color = states[index].color; bodyPaths.push( - /* html */ `` + /* html */ ``, + /* html */ `` ); if (renderHalo) { @@ -1150,10 +1151,7 @@ function toggleProvinces(event) { drawProvinces(); if (event && isCtrlClick(event)) editStyle("provs"); } else { - if (event && isCtrlClick(event)) { - editStyle("provs"); - return; - } + if (event && isCtrlClick(event)) return editStyle("provs"); provs.selectAll("*").remove(); turnButtonOff("toggleProvinces"); } @@ -1161,128 +1159,34 @@ function toggleProvinces(event) { function drawProvinces() { TIME && console.time("drawProvinces"); - const labelsOn = provs.attr("data-labels") == 1; - provs.selectAll("*").remove(); + const {cells, provinces} = pack; - const provinces = pack.provinces; - const {body, gap} = getProvincesVertices(); + const bodyPaths = new Array(provinces.length - 1); + const isolines = getIsolines(cellId => cells.province[cellId], {fill: true, waterGap: true}); + for (const [index, {fill, waterGap}] of isolines) { + const color = provinces[index].color; + bodyPaths.push( + /* html */ ``, + /* html */ `` + ); + } - const g = provs.append("g").attr("id", "provincesBody"); - const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); - g.selectAll("path") - .data(bodyData) - .enter() - .append("path") - .attr("d", d => d[0]) - .attr("fill", d => d[2]) - .attr("stroke", "none") - .attr("id", d => "province" + d[1]); - const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]); - g.selectAll(".path") - .data(gapData) - .enter() - .append("path") - .attr("d", d => d[0]) - .attr("fill", "none") - .attr("stroke", d => d[2]) - .attr("id", d => "province-gap" + d[1]); + const labels = provinces + .filter(p => p.i && !p.removed) + .map(p => { + const [x, y] = p.pole || cells.p[p.center]; + return /* html */ `${p.name}`; + }); - const labels = provs.append("g").attr("id", "provinceLabels"); - labels.style("display", `${labelsOn ? "block" : "none"}`); - const labelData = provinces.filter(p => p.i && !p.removed && p.pole); - labels - .selectAll(".path") - .data(labelData) - .enter() - .append("text") - .attr("x", d => d.pole[0]) - .attr("y", d => d.pole[1]) - .attr("id", d => "provinceLabel" + d.i) - .text(d => d.name); + byId("provs").innerHTML = /* html */ ` + ${bodyPaths.join("")} + ${labels.join("")} + `; + byId("provinceLabels").style.display = byId("provs").dataset.labels === "1" ? "block" : "none"; TIME && console.timeEnd("drawProvinces"); } -function getProvincesVertices() { - const cells = pack.cells, - vertices = pack.vertices, - provinces = pack.provinces, - n = cells.i.length; - const used = new Uint8Array(cells.i.length); - const vArray = new Array(provinces.length); // store vertices array - const body = new Array(provinces.length).fill(""); // store path around each province - const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps - - for (const i of cells.i) { - if (!cells.province[i] || used[i]) continue; - const p = cells.province[i]; - const onborder = cells.c[i].some(n => cells.province[n] !== p); - if (!onborder) continue; - - const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p); - const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith)); - const chain = connectVertices(vertex, p, borderWith); - if (chain.length < 3) continue; - const points = chain.map(v => vertices.p[v[0]]); - if (!vArray[p]) vArray[p] = []; - vArray[p].push(points); - body[p] += "M" + points.join("L"); - gap[p] += - "M" + - vertices.p[chain[0][0]] + - chain.reduce( - (r, v, i, d) => - !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r, - "" - ); - } - - // find province visual center - vArray.forEach((ar, i) => { - const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number - provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility - }); - - return {body, gap}; - - // connect vertices to chain - function connectVertices(start, t, province) { - const chain = []; // vertices chain to form a path - let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t); - function check(i) { - province = cells.province[i]; - land = cells.h[i] >= 20; - } - - for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) { - const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain - chain.push([current, province, land]); // add current vertex to sequence - const c = vertices.c[current]; // cells adjacent to vertex - c.filter(c => cells.province[c] === t).forEach(c => (used[c] = 1)); - const c0 = c[0] >= n || cells.province[c[0]] !== t; - const c1 = c[1] >= n || cells.province[c[1]] !== t; - const c2 = c[2] >= n || cells.province[c[2]] !== t; - const v = vertices.v[current]; // neighboring vertices - if (v[0] !== prev && c0 !== c1) { - current = v[0]; - check(c0 ? c[0] : c[1]); - } else if (v[1] !== prev && c1 !== c2) { - current = v[1]; - check(c1 ? c[1] : c[2]); - } else if (v[2] !== prev && c0 !== c2) { - current = v[2]; - check(c2 ? c[2] : c[0]); - } - if (current === chain[chain.length - 1][0]) { - ERROR && console.error("Next vertex is not found"); - break; - } - } - chain.push([start, province, land]); // add starting vertex to sequence to close the path - return chain; - } -} - function toggleGrid(event) { if (!gridOverlay.selectAll("*").size()) { turnButtonOn("toggleGrid"); @@ -1832,7 +1736,6 @@ function drawEmblems() { const sizeProvinces = getProvinceEmblemsSize(); const provinceCOAs = validProvinces.map(province => { - if (!province.pole) getProvincesVertices(); const [x, y] = province.pole || pack.cells.p[province.center]; const size = province.coa.size || 1; const shift = (sizeProvinces * size) / 2; diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js index f8a88cfb..2846a054 100644 --- a/modules/ui/provinces-editor.js +++ b/modules/ui/provinces-editor.js @@ -8,7 +8,7 @@ function editProvinces() { if (layerIsOn("toggleCultures")) toggleCultures(); provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true); - const body = document.getElementById("provincesBodySection"); + const body = byId("provincesBodySection"); refreshProvincesEditor(); if (modules.editProvinces) return; @@ -23,22 +23,22 @@ function editProvinces() { }); // add listeners - document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor); - document.getElementById("provincesEditStyle").addEventListener("click", () => editStyle("provs")); - document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines); - document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode); - document.getElementById("provincesChart").addEventListener("click", showChart); - document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels); - document.getElementById("provincesExport").addEventListener("click", downloadProvincesData); - document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces); - document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent); - document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent); - document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment()); - document.getElementById("provincesRelease").addEventListener("click", triggerProvincesRelease); - document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode); - document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces); + byId("provincesEditorRefresh").on("click", refreshProvincesEditor); + byId("provincesEditStyle").on("click", () => editStyle("provs")); + byId("provincesFilterState").on("change", provincesEditorAddLines); + byId("provincesPercentage").on("click", togglePercentageMode); + byId("provincesChart").on("click", showChart); + byId("provincesToggleLabels").on("click", toggleLabels); + byId("provincesExport").on("click", downloadProvincesData); + byId("provincesRemoveAll").on("click", removeAllProvinces); + byId("provincesManually").on("click", enterProvincesManualAssignent); + byId("provincesManuallyApply").on("click", applyProvincesManualAssignent); + byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment()); + byId("provincesRelease").on("click", triggerProvincesRelease); + byId("provincesAdd").on("click", enterAddProvinceMode); + byId("provincesRecolor").on("click", recolorProvinces); - body.addEventListener("click", function (ev) { + body.on("click", function (ev) { if (customization) return; const el = ev.target, cl = el.classList, @@ -58,7 +58,7 @@ function editProvinces() { else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl); }); - body.addEventListener("change", function (ev) { + body.on("change", function (ev) { const el = ev.target, cl = el.classList, line = el.parentNode, @@ -100,7 +100,7 @@ function editProvinces() { } function updateFilter() { - const stateFilter = document.getElementById("provincesFilterState"); + const stateFilter = byId("provincesFilterState"); const selectedState = stateFilter.value || 1; stateFilter.options.length = 0; // remove all options stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1)); @@ -111,7 +111,7 @@ function editProvinces() { // add line for each province function provincesEditorAddLines() { const unit = " " + getAreaUnit(); - const selectedState = +document.getElementById("provincesFilterState").value; + const selectedState = +byId("provincesFilterState").value; let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state body.innerHTML = ""; @@ -194,9 +194,9 @@ function editProvinces() { byId("provincesFooterPopulation").dataset.population = totalPopulation; body.querySelectorAll("div.states").forEach(el => { - el.addEventListener("click", selectProvinceOnLineClick); - el.addEventListener("mouseenter", ev => provinceHighlightOn(ev)); - el.addEventListener("mouseleave", ev => provinceHighlightOff(ev)); + el.on("click", selectProvinceOnLineClick); + el.on("mouseenter", ev => provinceHighlightOn(ev)); + el.on("mouseleave", ev => provinceHighlightOff(ev)); }); if (body.dataset.type === "percentage") { @@ -306,7 +306,7 @@ function editProvinces() { const {cell: center, culture} = burgs[burgId]; const color = getRandomColor(); const coa = province.coa; - const coaEl = document.getElementById("provinceCOA" + provinceId); + const coaEl = byId("provinceCOA" + provinceId); if (coaEl) coaEl.id = "stateCOA" + newStateId; emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove(); @@ -482,7 +482,7 @@ function editProvinces() { unfog("focusProvince" + p); const coaId = "provinceCOA" + p; - if (document.getElementById(coaId)) document.getElementById(coaId).remove(); + if (byId(coaId)) byId(coaId).remove(); emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove(); pack.provinces[p] = {i: p, removed: true}; @@ -504,13 +504,13 @@ function editProvinces() { function editProvinceName(province) { const p = pack.provinces[province]; - document.getElementById("provinceNameEditor").dataset.province = province; - document.getElementById("provinceNameEditorShort").value = p.name; + byId("provinceNameEditor").dataset.province = province; + byId("provinceNameEditorShort").value = p.name; applyOption(provinceNameEditorSelectForm, p.formName); - document.getElementById("provinceNameEditorFull").value = p.fullName; + byId("provinceNameEditorFull").value = p.fullName; const cultureId = pack.cells.culture[p.center]; - document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name; + byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name; $("#provinceNameEditor").dialog({ resizable: false, @@ -531,22 +531,22 @@ function editProvinces() { modules.editProvinceName = true; // add listeners - document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture); - document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom); - document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm); - document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName); + byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture); + byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom); + byId("provinceNameEditorAddForm").on("click", addCustomForm); + byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName); function regenerateShortNameCulture() { const province = +provinceNameEditor.dataset.province; const culture = pack.cells.culture[pack.provinces[province].center]; const name = Names.getState(Names.getCultureShort(culture), culture); - document.getElementById("provinceNameEditorShort").value = name; + byId("provinceNameEditorShort").value = name; } function regenerateShortNameRandom() { const base = rand(nameBases.length - 1); const name = Names.getState(Names.getBase(base), undefined, base); - document.getElementById("provinceNameEditorShort").value = name; + byId("provinceNameEditorShort").value = name; } function addCustomForm() { @@ -558,9 +558,9 @@ function editProvinces() { } function regenerateFullName() { - const short = document.getElementById("provinceNameEditorShort").value; - const form = document.getElementById("provinceNameEditorSelectForm").value; - document.getElementById("provinceNameEditorFull").value = getFullName(); + const short = byId("provinceNameEditorShort").value; + const form = byId("provinceNameEditorSelectForm").value; + byId("provinceNameEditorFull").value = getFullName(); function getFullName() { if (!form) return short; @@ -570,9 +570,9 @@ function editProvinces() { } function applyNameChange(p) { - p.name = document.getElementById("provinceNameEditorShort").value; - p.formName = document.getElementById("provinceNameEditorSelectForm").value; - p.fullName = document.getElementById("provinceNameEditorFull").value; + p.name = byId("provinceNameEditorShort").value; + p.formName = byId("provinceNameEditorSelectForm").value; + p.fullName = byId("provinceNameEditorFull").value; provs.select("#provinceLabel" + p.i).text(p.name); refreshProvincesEditor(); } @@ -651,7 +651,7 @@ function editProvinces() { .attr("height", height) .attr("font-size", "10px"); const graph = svg.append("g").attr("transform", `translate(10, 0)`); - document.getElementById("provincesTreeType").addEventListener("change", updateChart); + byId("provincesTreeType").on("change", updateChart); treeLayout(root); @@ -688,7 +688,7 @@ function editProvinces() { function hideInfo(ev) { provinceHighlightOff(ev); - if (!document.getElementById("provinceInfo")) return; + if (!byId("provinceInfo")) return; provinceInfo.innerHTML = "‍"; d3.select(ev.target).select("rect").classed("selected", 0); } @@ -816,7 +816,7 @@ function editProvinces() { stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2); customization = 11; - provs.select("g#provincesBody").append("g").attr("id", "temp"); + provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3); provs .select("g#provincesBody") .append("g") @@ -826,7 +826,7 @@ function editProvinces() { .attr("stroke-width", 1); document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none")); - document.getElementById("provincesManuallyButtons").style.display = "inline-block"; + byId("provincesManuallyButtons").style.display = "inline-block"; provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em"; @@ -952,8 +952,11 @@ function editProvinces() { if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); + + Provinces.getPoles(); if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces(); + exitProvincesManualAssignment(); refreshProvincesEditor(); } @@ -970,7 +973,7 @@ function editProvinces() { debug.selectAll("path.selected").remove(); document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block")); - document.getElementById("provincesManuallyButtons").style.display = "none"; + byId("provincesManuallyButtons").style.display = "none"; provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden")); provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em"; @@ -1049,7 +1052,7 @@ function editProvinces() { if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces(); collectStatistics(); - document.getElementById("provincesFilterState").value = state; + byId("provincesFilterState").value = state; provincesEditorAddLines(); } @@ -1062,7 +1065,7 @@ function editProvinces() { } function recolorProvinces() { - const state = +document.getElementById("provincesFilterState").value; + const state = +byId("provincesFilterState").value; pack.provinces.forEach(p => { if (!p || p.removed) return; diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 9b3a8e9d..aa3b831a 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -158,7 +158,8 @@ function regenerateStates() { BurgsAndStates.generateCampaigns(); BurgsAndStates.generateDiplomacy(); BurgsAndStates.defineStateForms(); - BurgsAndStates.generateProvinces(true); + Provinces.generate(true); + Provinces.getPoles(); layerIsOn("toggleStates") ? drawStates() : toggleStates(); layerIsOn("toggleBorders") ? drawBorders() : toggleBorders(); @@ -333,7 +334,8 @@ function recreateStates() { function regenerateProvinces() { unfog(); - BurgsAndStates.generateProvinces(true, true); + Provinces.generate(true, true); + Provinces.getPoles(); drawBorders(); if (layerIsOn("toggleProvinces")) drawProvinces(); diff --git a/versioning.js b/versioning.js index 403ae70c..c5f2a404 100644 --- a/versioning.js +++ b/versioning.js @@ -12,7 +12,7 @@ * * Example: 1.102.0 -> Major version 1, Minor version 102, Patch version 0 */ -const VERSION = "1.101.01"; +const VERSION = "1.103.00"; { document.title += " v" + VERSION;