Add the ability to lock states, provinces, cultures, and religions (#902)

* Add the basis for locking everything, code and test the culture locking

* Got the religion generator working, but not the tree. There are cycles being generated

* Religions work now, including the tree view

* Got the states and provinces working as well, all good and ready

* Refresh the province editor when regenerating

* Implement the versioning steps

* Fix the state naming and color changing even when locked

* The fix did not work with loaded maps, fix that too

* Fix a few more bugs and address the PR feedback

* Fix the state expanding event when they're locked bug

* Implement some logic to ignore state being locked when regenerating provinces directly.
This commit is contained in:
Guillaume St-Pierre 2022-12-15 15:32:49 -05:00 committed by GitHub
parent 3883933385
commit 80b8bc89a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 522 additions and 127 deletions

View file

@ -1437,7 +1437,9 @@ div.states .icon-trash-empty,
div.states .icon-eye,
div.states .icon-pin,
div.states .icon-flag-empty,
div.states .icon-cw {
div.states .icon-cw,
div.states .icon-lock,
div.states .icon-lock-open {
cursor: pointer;
}

View file

@ -108,7 +108,7 @@
}
</style>
<link rel="preload" href="index.css?v=1.88.03" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.89.00" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
@ -2017,11 +2017,11 @@
</button>
<button
id="regenerateStates"
data-tip="Click to select new capitals and regenerate states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
data-tip="Click to select new capitals and regenerate unlocked states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
>
States
</button>
<button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">
<button id="regenerateProvinces" data-tip="Click to regenerate unlocked provinces. States will remain as they are">
Provinces
</button>
<button
@ -2031,8 +2031,8 @@
Burgs
</button>
<button id="regenerateEmblems" data-tip="Click to regenerate all emblems">Emblems</button>
<button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button>
<button id="regenerateCultures" data-tip="Click to regenerate cultures">Cultures</button>
<button id="regenerateReligions" data-tip="Click to regenerate unlocked religions">Religions</button>
<button id="regenerateCultures" data-tip="Click to regenerate unlocked cultures">Cultures</button>
<button
id="regenerateMilitary"
data-tip="Click to recalculate military forces based on current military options"
@ -2040,7 +2040,7 @@
Military
</button>
<button id="regenerateIce" data-tip="Click to icebergs and glaciers">Ice</button>
<button id="regenerateMarkers" data-tip="Click to regenerate markers">
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers">
Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
</button>
<button
@ -7842,10 +7842,10 @@
<script src="modules/river-generator.js"></script>
<script src="modules/lakes.js"></script>
<script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.87.14"></script>
<script src="modules/burgs-and-states.js?v=1.87.10"></script>
<script src="modules/cultures-generator.js?v=1.89.00"></script>
<script src="modules/burgs-and-states.js?v=1.89.00"></script>
<script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js"></script>
<script src="modules/religions-generator.js?v=1.89.00"></script>
<script src="modules/military-generator.js"></script>
<script src="modules/markers-generator.js?v=1.87.13"></script>
<script src="modules/coa-generator.js"></script>
@ -7865,10 +7865,10 @@
<script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=1.87.07"></script>
<script defer src="modules/ui/tools.js?v=1.88.05"></script>
<script defer src="modules/ui/tools.js?v=1.89.00"></script>
<script defer src="modules/ui/world-configurator.js"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.88.03"></script>
<script defer src="modules/ui/provinces-editor.js"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.89.00"></script>
<script defer src="modules/ui/biomes-editor.js"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.87.10"></script>
<script defer src="modules/ui/elevation-profile.js"></script>

View file

@ -163,6 +163,144 @@ window.BurgsAndStates = (function () {
}
};
const regenerateStates = function() {
const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (burgs.length < statesCount)
tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
// turn all old capitals into towns, except for the capitals of locked states
burgs
.filter(b => b.capital && pack.states.find(s => s.lock && s.capital === b.i) === undefined)
.forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = 0;
});
// remove emblems
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
unfog();
if (!statesCount) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
pack.provinces = [0]; // remove all provinces
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
borders.selectAll("path").remove(); // remove borders
regions.selectAll("path").remove(); // remove states fill
labels.select("#states").selectAll("text"); // remove state labels
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
return;
}
// burg local ids sorted by a bit randomized population. Also ignore burgs of a locked state.
const sortedBurgs = burgs
.filter(b => !pack.states[b.state] || !pack.states[b.state].lock)
.map((b, i) => [b, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map(b => b[0]);
const capitalsTree = d3.quadtree();
const neutral = pack.states[0].name; // neutrals name
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
const states = [];
// Get all the states to restore
let statesToRestore = [];
if (pack.states) {
pack.states.forEach(state => {
if (!state.lock) return;
statesToRestore.push(state)
});
}
d3.range(count).forEach(i => {
if (!i) {
states.push({i, name: neutral});
return;
}
// If we still have states to restore from the locks, restore those first and assign them the right ids.
if (statesToRestore.length) {
const [toRestore, ...rest] = statesToRestore;
toRestore.old_i = toRestore.i;
toRestore.i = i;
states.push(toRestore);
// Also reassign the state id of all provinces of this state for locked provinces
toRestore.provinces.forEach(id => {
if (!pack.provinces[id]) return;
pack.provinces[id].state = toRestore.i;
pack.provinces[id].should_restore = true;
});
statesToRestore = rest;
const {x, y} = burgs[toRestore.capital];
capitalsTree.add([x, y]);
return;
}
let capital = null;
for (const burg of sortedBurgs) {
const {x, y} = burg;
if (capitalsTree.find(x, y, spacing) === undefined) {
burg.capital = 1;
capital = burg;
capitalsTree.add([x, y]);
moveBurgToGroup(burg.i, "cities");
break;
}
spacing = Math.max(spacing - 1, 1);
}
const culture = capital.culture;
const basename =
capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const name = Names.getState(basename, culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
const type = nomadic
? "Nomadic"
: pack.cultures[culture].type === "Nomadic"
? "Generic"
: pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
coa.shield = capital.coa.shield;
states.push({
i,
name,
type,
capital: capital.i,
center: capital.cell,
culture,
expansionism,
coa
});
});
pack.states = states;
}
// define burg coordinates, coa, port status and define details
const specifyBurgs = function () {
TIME && console.time("specifyBurgs");
@ -364,6 +502,12 @@ window.BurgsAndStates = (function () {
TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack;
const prevStates = {};
if (cells.state) {
cells.state.forEach(function (i, index) {
prevStates[index] = i;
})
}
cells.state = new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
@ -372,6 +516,17 @@ window.BurgsAndStates = (function () {
states
.filter(s => s.i && !s.removed)
.forEach(s => {
if (s.lock) {
Object.entries(prevStates).forEach(function ([index, stateId]) {
if (stateId === s.old_i) {
cells.state[index] = s.i;
cost[index] = neutral;
}
});
return;
}
const capitalCell = burgs[s.capital].cell;
cells.state[capitalCell] = s.i;
const cultureCenter = cultures[s.culture].center;
@ -387,6 +542,8 @@ window.BurgsAndStates = (function () {
cells.c[e].forEach(e => {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
// Do not overwrite cells from a locked state.
if (states[cells.state[e]].lock) return;
const cultureCost = culture === cells.culture[e] ? -9 : 100;
const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000;
@ -452,10 +609,15 @@ window.BurgsAndStates = (function () {
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
if (pack.states[cells.state[i]]?.lock) continue; // Do not overwrite cells of locks states
const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
const adversaries = neibs.filter(
c => !pack.states[cells.state[c]]?.lock && cells.state[c] !== cells.state[i]
);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
const buddies = neibs.filter(
c => !pack.states[cells.state[c]]?.lock && cells.state[c] === cells.state[i]
);
if (buddies.length > 2) continue;
if (adversaries.length <= buddies.length) continue;
cells.state[i] = cells.state[adversaries[0]];
@ -498,7 +660,8 @@ window.BurgsAndStates = (function () {
const mode = options.stateLabelsMode || "auto";
for (const s of states) {
if (!s.i || s.removed || !s.cells || (list && !list.includes(s.i))) continue;
if (!s.i || s.removed || s.lock || !s.cells || (list && !list.includes(s.i))) continue;
const used = [];
const visualCenter = findCell(s.pole[0], s.pole[1]);
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
@ -600,10 +763,37 @@ window.BurgsAndStates = (function () {
if (!list) {
// remove all labels and textpaths
g.selectAll("text").remove();
t.selectAll("path[id*='stateLabel']").remove();
g.selectAll("text").filter((_, i) => {
const id = g.select(`:nth-child(${i + 1})`).node()?.id;
if (!id) return true;
return !pack.states.some(s => s.lock && `${s.old_i}` === id.substring(10));
}).remove();
t.selectAll("path[id*='stateLabel']").filter((_, i) => {
const id = t.select(`:nth-child(${i + 1})`).node()?.id;
if (!id) return true;
return !pack.states.some(s => s.lock && `${s.old_i}` === id.substring(19));
}).remove();
}
pack.states.forEach(s => {
if (!s.lock) return;
// For locked states, get the name and update its index to keep it in place
const g = labels.select("#states");
const t = defs.select("#textPaths");
const labelNode = g.select(`#stateLabel${s.old_i}`);
const textNode = t.select(`#textPath_stateLabel${s.old_i}`);
labelNode
.attr('id', `stateLabel${s.i}`)
.select("textPath")
.attr("xlink:href", `#textPath_stateLabel${s.i}`);
textNode.attr('id', `textPath_stateLabel${s.i}`);
});
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
@ -752,7 +942,7 @@ window.BurgsAndStates = (function () {
// assign basic color using greedy coloring algorithm
pack.states.forEach(s => {
if (!s.i || s.removed) return;
if (!s.i || s.removed || s.lock) return;
const neibs = s.neighbors;
s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
if (!s.color) s.color = getRandomColor();
@ -761,7 +951,7 @@ window.BurgsAndStates = (function () {
// randomize each already used color a bit
colors.forEach(c => {
const sameColored = pack.states.filter(s => s.color === c);
const sameColored = pack.states.filter(s => s.color === c && !s.lock);
sameColored.forEach((s, d) => {
if (!d) return;
s.color = getMixedColor(s.color);
@ -990,7 +1180,7 @@ window.BurgsAndStates = (function () {
// select a forms for listed or all valid states
const defineStateForms = function (list) {
TIME && console.time("defineStateForms");
const states = pack.states.filter(s => s.i && !s.removed);
const states = pack.states.filter(s => s.i && !s.removed && !s.lock);
if (states.length < 1) return;
const generic = {Monarchy: 25, Republic: 2, Union: 1};
@ -1142,13 +1332,26 @@ window.BurgsAndStates = (function () {
return adjName ? `${getAdjective(s.name)} ${s.formName}` : `${s.formName} of ${s.name}`;
};
const generateProvinces = function (regenerate) {
const generateProvinces = function (
regenerate = false,
ignoreLockedStates = false
) {
TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack;
const provinces = (pack.provinces = [0]);
const provincesToRestore = pack.provinces ?
pack.provinces.filter(p => p.lock || p.should_restore)
: [];
const provinces = (pack.provinces = [0].concat(...provincesToRestore));
const prevProvinces = {};
if (cells.province) {
cells.province.forEach((i, index) => {
prevProvinces[index] = i;
})
}
cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value;
@ -1170,10 +1373,16 @@ window.BurgsAndStates = (function () {
// generate provinces for a selected burgs
states.forEach(s => {
s.provinces = [];
s.provinces = s.provinces ? s.provinces.filter(p => p.lock) : [];
// Don't regenerate provinces of a locked state
if (!ignoreLockedStates && s.lock) return;
if (!s.i || s.removed) return;
const stateBurgs = burgs
.filter(b => b.state === s.i && !b.removed)
// Filter for burgs of this state that haven't been removed and that are not in a locked province.
.filter(b =>
b.state === s.i && !b.removed && provincesToRestore.find(p => prevProvinces[b.cell] === p.i) === undefined
)
.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
@ -1200,11 +1409,32 @@ window.BurgsAndStates = (function () {
}
});
// Restore the indexes of locked and kept provinces
provincesToRestore.forEach((province, index) => {
delete province.should_restore;
province.old_i = province.i;
province.i = index + 1;
states[province.state].provinces.push(province.i);
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
provinces.forEach(function (p) {
if (!p.i || p.removed) return;
// Then restore the cells of locked or kept provinces
if (p.old_i) {
Object.entries(prevProvinces).forEach(function ([index, provId]) {
if (provId === p.old_i) {
cells.province[index] = p.i;
}
});
delete p.old_i;
return;
}
cells.province[p.center] = p.i;
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
cost[p.center] = 1;
@ -1217,6 +1447,20 @@ window.BurgsAndStates = (function () {
province = next.province,
state = next.state;
cells.c[n].forEach(function (e) {
// Do not overwrite cells from a locked state or province.
if (
(provinces[cells.province[e]] && provinces[cells.province[e]].lock) ||
(
// For finding if the state is locked, first make sure we care about that
// then find the province, the state for the province, and if both are defined,
// check the lock.
!ignoreLockedStates &&
provinces[cells.province[e]] &&
states[provinces[cells.province[e]].state] &&
states[provinces[cells.province[e]].state].lock
)
) return;
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
@ -1235,7 +1479,18 @@ window.BurgsAndStates = (function () {
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
// Do not process any locked provinces or states, if we care about the latter
if (
pack.provinces[cells.province[i]].lock ||
(!ignoreLockedStates && pack.states[cells.state[i]].lock)
) continue;
// Find neighbors, but ignore any cells from locked states or provinces
const neibs = cells.c[i].filter(
c =>
(ignoreLockedStates || !pack.states[cells.state[c]].lock) &&
!pack.provinces[cells.province[c]].lock &&
cells.state[c] === cells.state[i]
).map(c => cells.province[c]);
const adversaries = neibs.filter(c => c !== cells.province[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === cells.province[i]).length;
@ -1249,7 +1504,7 @@ window.BurgsAndStates = (function () {
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
states.forEach(s => {
if (!s.provinces.length) return;
if (!s.provinces.length || (!ignoreLockedStates && s.lock)) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
@ -1358,6 +1613,7 @@ window.BurgsAndStates = (function () {
return {
generate,
regenerateStates,
expandStates,
normalizeStates,
assignColors,

View file

@ -6,6 +6,12 @@ window.Cultures = (function () {
const generate = function () {
TIME && console.time("generateCultures");
cells = pack.cells;
const prevCultures = {};
if (cells.culture) {
cells.culture.forEach(function (cultureId, index) {
prevCultures[index] = cultureId;
})
}
cells.culture = new Uint16Array(cells.i.length); // cell cultures
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
@ -51,7 +57,29 @@ window.Cultures = (function () {
const emblemShape = document.getElementById("emblemShape").value;
const codes = [];
let unoccupied = [...populated];
cultures.forEach(function (c, i) {
if (c.lock) {
centers.add(c.center);
cells.culture[c.center] = i + 1;
codes.push(c.code);
const cultureCells = [];
Object.entries(prevCultures).forEach(function ([index, cultureId]) {
if (cultureId === c.i) {
cells.culture[index] = i + 1;
cultureCells.push(parseInt(index));
}
});
unoccupied = unoccupied.filter(function (cell) {
return !cultureCells.includes(cell);
});
c.i = i + 1;
return;
}
const cell = (c.center = placeCenter(c.sort ? c.sort : i => cells.s[i]));
centers.add(cells.p[cell]);
c.i = i + 1;
@ -70,12 +98,16 @@ window.Cultures = (function () {
function placeCenter(v) {
let c,
spacing = (graphWidth + graphHeight) / 2 / count;
const sorted = [...populated].sort((a, b) => v(b) - v(a)),
// Only use cells where there are no culture already on that cell, which may happen if a locked culture
// was restored. We can be sure that locked cultures will always be first in the list.
const sorted = [...unoccupied].sort((a, b) => v(b) - v(a)),
max = Math.floor(sorted.length / 2);
do {
c = sorted[biased(0, max, 5)];
spacing *= 0.9;
} while (centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined);
} while (
centers.find(cells.p[c][0], cells.p[c][1], spacing) !== undefined
);
return c;
}
@ -98,6 +130,12 @@ window.Cultures = (function () {
const count = Math.min(c, def.length);
const cultures = [];
if (pack.cultures) {
pack.cultures.forEach(function (culture) {
if (culture.lock) cultures.push(culture);
});
}
for (let culture, rnd, i = 0; cultures.length < count && i < 200; i++) {
do {
rnd = rand(def.length - 1);
@ -481,7 +519,7 @@ window.Cultures = (function () {
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return;
if (!c.i || c.removed || c.lock) return;
queue.queue({e: c.center, p: 0, c: c.i});
});
@ -494,6 +532,8 @@ window.Cultures = (function () {
c = next.c;
const type = pack.cultures[c].type;
cells.c[n].forEach(function (e) {
if (pack.cultures[cells.culture[e]]?.lock) return;
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change

View file

@ -228,6 +228,7 @@ function culturesEditorAddLines() {
style="width: 5em">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
${getShapeOptions(selectShape, c.shield)}
<span data-tip="Lock culture" class="icon-lock${c.lock ? '' : '-open'} hide"></span>
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
@ -257,6 +258,8 @@ function culturesEditorAddLines() {
$body.querySelectorAll("div > div.culturePopulation").forEach($el => $el.on("click", changePopulation));
$body.querySelectorAll("div > span.icon-arrows-cw").forEach($el => $el.on("click", cultureRegenerateBurgs));
$body.querySelectorAll("div > span.icon-trash-empty").forEach($el => $el.on("click", cultureRemovePrompt));
$body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus));
$body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus));
const $culturesHeader = byId("culturesHeader");
$culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? "inline-block" : "none";
@ -928,3 +931,15 @@ async function uploadCulturesData() {
drawCultures();
refreshCulturesEditor();
}
function updateLockStatus() {
if (customization) return;
const cultureId = +this.parentNode.dataset.id;
const classList = this.classList;
const c = pack.cultures[cultureId];
c.lock = !c.lock;
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}

View file

@ -212,6 +212,10 @@ function religionsEditorAddLines() {
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div>
<span
data-tip="Lock religion, will regenerate the origin folk and organized religion if they are not also locked"
class="icon-lock${r.lock ? '' : '-open'} hide"
></span>
<span data-tip="Remove religion" class="icon-trash-empty hide"></span>
</div>`;
}
@ -242,6 +246,8 @@ function religionsEditorAddLines() {
$body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity));
$body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation));
$body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt));
$body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus));
$body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus));
if ($body.dataset.type === "percentage") {
$body.dataset.type = "absolute";
@ -755,3 +761,15 @@ function closeReligionsEditor() {
exitReligionsManualAssignment("close");
exitAddReligionMode();
}
function updateLockStatus() {
if (customization) return;
const religionId = +this.parentNode.dataset.id;
const classList = this.classList;
const r = pack.religions[religionId];
r.lock = !r.lock;
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}

View file

@ -153,6 +153,7 @@ function addListeners() {
else if (classList.contains("statePopulation")) changePopulation(stateId);
else if (classList.contains("icon-pin")) toggleFog(stateId, classList);
else if (classList.contains("icon-trash-empty")) stateRemovePrompt(stateId);
else if (classList.contains("icon-lock") || classList.contains("icon-lock-open")) updateLockStatus(stateId, classList);
});
$body.on("input", function (ev) {
@ -288,6 +289,7 @@ function statesEditorAddLines() {
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Toggle state focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
<span data-tip="Lock the state" class="icon-lock${s.lock ? '' : '-open'} hide"></span>
<span data-tip="Remove the state" class="icon-trash-empty hide"></span>
</div>`;
}
@ -1362,3 +1364,11 @@ function closeStatesEditor() {
debug.selectAll(".highlight").remove();
$body.innerHTML = "";
}
function updateLockStatus(stateId, classList) {
const s = pack.states[stateId];
s.lock = !s.lock;
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}

View file

@ -347,11 +347,62 @@ window.Religions = (function () {
const cells = pack.cells,
states = pack.states,
cultures = pack.cultures;
const religions = (pack.religions = []);
// Keep a map of the previous religions per cell for referencing when we restore locked religions
const prevReligions = {};
if (cells.religion) {
cells.religion.forEach((rId, index) => {
prevReligions[index] = rId;
})
}
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
const religionsToRestore = [];
const folkToRestore = [];
const restoredCells = [];
const restoredHeresyCells = [];
// Restore locked religions to their existing cells
if (pack.religions) {
pack.religions.forEach( (religion) => {
// Keep any locked religions, we will reassign it to a folk or organized religion later
if (!religion.lock) return;
// Add all religions we restore after the base cults to keep the index correct
// Keep folk religion at the same index since we should restore them during the culture parsing
const id = religion.type === 'Folk' ? religion.i : pack.cultures.length + religionsToRestore.length;
Object.entries(prevReligions).forEach(([index, rId]) => {
if (rId !== religion.i) return;
if (religion.type === "Heresy") {
restoredHeresyCells.push(parseInt(index));
religion.old_i = religion.i;
} else {
restoredCells.push(parseInt(index));
cells.religion[index] = id;
}
});
if (religion.type === 'Folk') {
folkToRestore.push(religion);
} else {
religionsToRestore.push(religion);
religion.i = id;
}
});
}
const religions = (pack.religions = []);
// add folk religions
pack.cultures.forEach(c => {
// If a restored religion exists for this culture, move it to this position
const existingFolkReligion = folkToRestore.find(r => r.culture === c.i);
if (existingFolkReligion !== undefined) {
religions.push(existingFolkReligion);
return;
}
// We already preadded the "no religion" religion
if (!c.i) return religions.push({i: 0, name: "No religion"});
if (c.removed) {
@ -370,6 +421,7 @@ window.Religions = (function () {
const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type: "Folk", form, deity, center: c.center, origins: [0]});
});
religions.push(...religionsToRestore);
if (religionsInput.value == 0 || pack.cultures.length < 2)
return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name)));
@ -379,6 +431,7 @@ window.Religions = (function () {
burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
: cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
const available = [...sorted].filter(cellI => !restoredCells.includes(cellI));
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
@ -391,9 +444,35 @@ window.Religions = (function () {
return religions.length ? religions.slice(0, max) : [0];
}
// Restore the origins of any organized religion that was locked
pack.religions.forEach(religion => {
// Ignore if the religion is not locked or not organized
if (religion.type !== "Organized" || !religion.lock) return;
// Ignore if the religion already has a valid origin
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined)
return;
// Select a random folk religion for the religion
const culture = cells.culture[religion.center];
const [x, y] = cells.p[religion.center];
const isFolkBased = religion.expansion === "culture" || P(0.5);
const folk = isFolkBased && religions.find(r => r.i !== religion.i && r.culture === culture && r.type === "Folk");
if (folk && religion.expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name;
// have a counter here to adjust the search range and make sure we can find a religion
let runs = 1;
do {
// Run the search until we have at least one source religion that is not the current religion.
religion.origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: (150 * runs) / count, max: 2})
.filter(r => r !== religion.i);
runs++;
} while (!religion.origins.length)
religionsTree.add([x, y]);
});
// generate organized religions
for (let i = 0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
let center = available[biased(0, available.length - 1, 5)]; // religion center
const form = rw(forms.Organized);
const state = cells.state[center];
const culture = cells.culture[center];
@ -439,10 +518,30 @@ window.Religions = (function () {
religionsTree.add([x, y]);
}
// Restore the origins of any cult that was locked
pack.religions.forEach(religion => {
// Ignore if the religion is not locked or not organized
if (religion.type !== "Cult" || !religion.lock) return;
// Ignore if the religion already has a valid origin
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined)
return;
const [x, y] = cells.p[religion.center];
// have a counter here to adjust the search range and make sure we can find a religion
let runs = 1;
do {
// Run the search until we have at least one source religion that is not the current religion.
religion.origins = getReligionsInRadius({x, y, r: (300 * runs) / count, max: rand(0, 4)})
.filter(r => r !== religion.i);
runs++;
} while (!religion.origins.length)
religionsTree.add([x, y]);
});
// generate cults
for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult);
let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
let center = available[biased(0, available.length - 1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c]))
center = cells.c[center].find(c => cells.burg[c]);
const [x, y] = cells.p[center];
@ -473,7 +572,32 @@ window.Religions = (function () {
religionsTree.add([x, y]);
}
expandReligions();
expandReligions(restoredCells);
// Restore the origins of any heresy that was locked
pack.religions.forEach(religion => {
// Ignore if the religion is not locked or not organized
if (religion.type !== "Heresy" || !religion.lock) return;
const originReligion = cells.religion[religion.center];
// Restore the cells now that all other religions have been processed
Object.entries(prevReligions).forEach(([index, rId]) => {
if (rId !== religion.old_i) return;
cells.religion[parseInt(index)] = religion.i;
});
delete religion.old_i;
// Ignore if the religion already has a valid origin
if (pack.religions.find(r => r.i !== religion.i && religion.origins.includes(r.i) && r.lock) !== undefined)
return;
const [x, y] = cells.p[religion.center];
// Use the religion from the expanded cells, we'll restore the heresies later
religion.origins = [originReligion];
religionsTree.add([x, y]);
});
// generate heresies
religions
@ -512,7 +636,7 @@ window.Religions = (function () {
}
});
expandHeresies();
expandHeresies([...restoredCells, ...restoredHeresyCells]);
checkCenters();
TIME && console.timeEnd("generateReligions");
@ -567,14 +691,14 @@ window.Religions = (function () {
};
// growth algorithm to assign cells to religions
const expandReligions = function () {
const expandReligions = function (restoredCells) {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter(r => r.type === "Organized" || r.type === "Cult")
.filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult"))
.forEach(r => {
cells.religion[r.center] = r.i;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
@ -596,6 +720,7 @@ window.Religions = (function () {
cells.c[n].forEach(function (e) {
if (expansion === "culture" && c !== cells.culture[e]) return;
if (expansion === "state" && s !== cells.state[e]) return;
if (restoredCells.includes(e)) return;
const cultureCost = c !== cells.culture[e] ? 10 : 0;
const stateCost = s !== cells.state[e] ? 10 : 0;
@ -618,14 +743,14 @@ window.Religions = (function () {
};
// growth algorithm to assign cells to heresies
const expandHeresies = function () {
const expandHeresies = function (restoredCells) {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter(r => r.type === "Heresy")
.filter(r => !r.lock && r.type === "Heresy")
.forEach(r => {
const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id
@ -643,6 +768,8 @@ window.Religions = (function () {
b = next.b;
cells.c[n].forEach(function (e) {
if (restoredCells.includes(e)) return;
const religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;

View file

@ -53,6 +53,7 @@ function editProvinces() {
else if (cl.contains("culturePopulation")) changePopulation(p);
else if (cl.contains("icon-pin")) toggleFog(p, cl);
else if (cl.contains("icon-trash-empty")) removeProvince(p);
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
});
body.addEventListener("change", function (ev) {
@ -163,6 +164,7 @@ function editProvinces() {
class="icon-flag-empty ${separable ? "" : "placeholder"} hide"
></span>
<span data-tip="Toggle province focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
<span data-tip="Lock the province" class="icon-lock${p.lock ? '' : '-open'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`;
}
@ -1086,3 +1088,11 @@ function editProvinces() {
if (customization === 12) exitAddProvinceMode();
}
}
function updateLockStatus(provinceId, classList) {
const p = pack.provinces[provinceId];
p.lock = !p.lock;
classList.toggle("icon-lock-open");
classList.toggle("icon-lock");
}

View file

@ -138,93 +138,7 @@ function recalculatePopulation() {
}
function regenerateStates() {
const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
if (burgs.length < statesCount)
tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
// turn all old capitals into towns
burgs
.filter(b => b.capital)
.forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = 0;
});
// remove emblems
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
unfog();
if (!statesCount) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
pack.provinces = [0]; // remove all provinces
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
borders.selectAll("path").remove(); // remove borders
regions.selectAll("path").remove(); // remove states fill
labels.select("#states").selectAll("text"); // remove state labels
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
return;
}
// burg local ids sorted by a bit randomized population:
const sortedBurgs = burgs
.map((b, i) => [b, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map(b => b[0]);
const capitalsTree = d3.quadtree();
const neutral = pack.states[0].name; // neutrals name
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
pack.states = d3.range(count).map(i => {
if (!i) return {i, name: neutral};
let capital = null;
for (const burg of sortedBurgs) {
const {x, y} = burg;
if (capitalsTree.find(x, y, spacing) === undefined) {
burg.capital = 1;
capital = burg;
capitalsTree.add([x, y]);
moveBurgToGroup(burg.i, "cities");
break;
}
spacing = Math.max(spacing - 1, 1);
}
const culture = capital.culture;
const basename =
capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const name = Names.getState(basename, culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
const type = nomadic
? "Nomadic"
: pack.cultures[culture].type === "Nomadic"
? "Generic"
: pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
coa.shield = capital.coa.shield;
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
});
BurgsAndStates.regenerateStates();
BurgsAndStates.expandStates();
BurgsAndStates.normalizeStates();
BurgsAndStates.collectStatistics();
@ -249,7 +163,7 @@ function regenerateStates() {
function regenerateProvinces() {
unfog();
BurgsAndStates.generateProvinces(true);
BurgsAndStates.generateProvinces(true, true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
@ -257,6 +171,7 @@ function regenerateProvinces() {
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems();
refreshAllEditors();
}
function regenerateBurgs() {
@ -403,6 +318,7 @@ function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions();
else drawReligions();
refreshAllEditors();
}
function regenerateCultures() {

View file

@ -1,7 +1,7 @@
"use strict";
// version and caching control
const version = "1.88.05"; // generator version, update each time
const version = "1.89.00"; // generator version, update each time
{
document.title += " v" + version;
@ -28,6 +28,7 @@ const version = "1.88.05"; // generator version, update each time
<ul>
<strong>Latest changes:</strong>
<li>Can now lock states, provinces, cultures, and religions from being regenerated</li>
<li>Heightmap brushes: linear edit option</li>
<li>Data Charts screen</li>
<li>Сultures and religions can have multiple parents in hierarchy tree</li>