mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 02:01:22 +01:00
Moved burg-related functions like `removeBurg`, `changeGroup`, and `toggleCapital` into the Burgs module to improve code organization and maintainability. Updated all references to these functions across the codebase. This change reduces duplication and ensures consistent behavior for burg management operations.
988 lines
33 KiB
JavaScript
988 lines
33 KiB
JavaScript
"use strict";
|
|
|
|
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
|
|
|
toolsContent.addEventListener("click", function (event) {
|
|
if (customization) return tip("Please exit the customization mode first", false, "error");
|
|
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
|
|
const button = event.target.id;
|
|
|
|
// click on open Editor buttons
|
|
if (button === "editHeightmapButton") editHeightmap();
|
|
else if (button === "editBiomesButton") editBiomes();
|
|
else if (button === "editStatesButton") editStates();
|
|
else if (button === "editProvincesButton") editProvinces();
|
|
else if (button === "editDiplomacyButton") editDiplomacy();
|
|
else if (button === "editCulturesButton") editCultures();
|
|
else if (button === "editReligions") editReligions();
|
|
else if (button === "editEmblemButton") openEmblemEditor();
|
|
else if (button === "editNamesBaseButton") editNamesbase();
|
|
else if (button === "editUnitsButton") editUnits();
|
|
else if (button === "editNotesButton") editNotes();
|
|
else if (button === "editZonesButton") editZones();
|
|
else if (button === "overviewChartsButton") overviewCharts();
|
|
else if (button === "overviewBurgsButton") overviewBurgs();
|
|
else if (button === "overviewRoutesButton") overviewRoutes();
|
|
else if (button === "overviewRiversButton") overviewRivers();
|
|
else if (button === "overviewMilitaryButton") overviewMilitary();
|
|
else if (button === "overviewMarkersButton") overviewMarkers();
|
|
else if (button === "overviewCellsButton") viewCellDetails();
|
|
|
|
// click on Regenerate buttons
|
|
if (event.target.parentNode.id === "regenerateFeature") {
|
|
const dontAsk = sessionStorage.getItem("regenerateFeatureDontAsk");
|
|
if (dontAsk) return processFeatureRegeneration(event, button);
|
|
|
|
alertMessage.innerHTML = /* html */ `Regeneration will remove all the custom changes for the element.<br /><br />Are you sure you want to proceed?`;
|
|
$("#alert").dialog({
|
|
resizable: false,
|
|
title: "Regenerate element",
|
|
buttons: {
|
|
Proceed: function () {
|
|
processFeatureRegeneration(event, button);
|
|
$(this).dialog("close");
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
open: function () {
|
|
const checkbox =
|
|
'<span><input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label><span>';
|
|
const pane = this.parentElement.querySelector(".ui-dialog-buttonpane");
|
|
pane.insertAdjacentHTML("afterbegin", checkbox);
|
|
},
|
|
close: function () {
|
|
const box = this.parentElement.querySelector(".checkbox");
|
|
if (box?.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
|
|
$(this).dialog("destroy");
|
|
}
|
|
});
|
|
}
|
|
|
|
// click on Configure regenerate buttons
|
|
if (button === "configRegenerateMarkers") configMarkersGeneration();
|
|
|
|
// click on Add buttons
|
|
if (button === "addLabel") toggleAddLabel();
|
|
else if (button === "addBurgTool") toggleAddBurg();
|
|
else if (button === "addRiver") toggleAddRiver();
|
|
else if (button === "addRoute") createRoute();
|
|
else if (button === "addMarker") toggleAddMarker();
|
|
// click to create a new map buttons
|
|
else if (button === "openSubmapTool") openSubmapTool();
|
|
else if (button === "openTransformTool") openTransformTool();
|
|
});
|
|
|
|
function processFeatureRegeneration(event, button) {
|
|
if (button === "regenerateStateLabels") {
|
|
$("#labels").fadeIn();
|
|
drawStateLabels();
|
|
} else if (button === "regenerateReliefIcons") {
|
|
drawReliefIcons();
|
|
if (!layerIsOn("toggleRelief")) toggleRelief();
|
|
} else if (button === "regenerateRoutes") {
|
|
regenerateRoutes();
|
|
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
|
} else if (button === "regenerateRivers") regenerateRivers();
|
|
else if (button === "regeneratePopulation") recalculatePopulation();
|
|
else if (button === "regenerateStates") regenerateStates();
|
|
else if (button === "regenerateProvinces") regenerateProvinces();
|
|
else if (button === "regenerateBurgs") regenerateBurgs();
|
|
else if (button === "regenerateEmblems") regenerateEmblems();
|
|
else if (button === "regenerateReligions") regenerateReligions();
|
|
else if (button === "regenerateCultures") regenerateCultures();
|
|
else if (button === "regenerateMilitary") regenerateMilitary();
|
|
else if (button === "regenerateIce") regenerateIce();
|
|
else if (button === "regenerateMarkers") regenerateMarkers();
|
|
else if (button === "regenerateZones") regenerateZones(event);
|
|
}
|
|
|
|
async function openEmblemEditor() {
|
|
let type, id, el;
|
|
|
|
if (pack.states[1]?.coa) {
|
|
type = "state";
|
|
id = "stateCOA1";
|
|
el = pack.states[1];
|
|
} else if (pack.burgs[1]?.coa) {
|
|
type = "burg";
|
|
id = "burgCOA1";
|
|
el = pack.burgs[1];
|
|
} else {
|
|
tip("No emblems to edit, please generate states and burgs first", false, "error");
|
|
return;
|
|
}
|
|
|
|
await COArenderer.trigger(id, el.coa);
|
|
editEmblem(type, id, el);
|
|
}
|
|
|
|
function regenerateRoutes() {
|
|
const locked = pack.routes.filter(route => route.lock).map((route, index) => ({...route, i: index}));
|
|
Routes.generate(locked);
|
|
|
|
routes.selectAll("path").remove();
|
|
if (layerIsOn("toggleRoutes")) drawRoutes();
|
|
}
|
|
|
|
function regenerateRivers() {
|
|
Rivers.generate();
|
|
Rivers.specify();
|
|
Features.specify();
|
|
if (layerIsOn("toggleRivers")) drawRivers();
|
|
}
|
|
|
|
function recalculatePopulation() {
|
|
rankCells();
|
|
|
|
pack.burgs.forEach(b => {
|
|
if (!b.i || b.removed || b.lock) return;
|
|
const i = b.cell;
|
|
|
|
b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
|
|
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
|
if (b.port) b.population = b.population * 1.3; // increase port population
|
|
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
|
});
|
|
|
|
layerIsOn("togglePopulation") ? drawPopulation() : togglePopulation();
|
|
}
|
|
|
|
function regenerateStates() {
|
|
const newStates = recreateStates();
|
|
if (!newStates) return;
|
|
|
|
pack.states = newStates;
|
|
States.expandStates();
|
|
States.normalize();
|
|
States.getPoles();
|
|
States.findNeighbors();
|
|
States.collectStatistics();
|
|
States.assignColors();
|
|
States.generateCampaigns();
|
|
States.generateDiplomacy();
|
|
States.defineStateForms();
|
|
|
|
Provinces.generate(true);
|
|
Provinces.getPoles();
|
|
|
|
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
|
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
|
if (layerIsOn("toggleProvinces")) drawProvinces();
|
|
|
|
drawStateLabels();
|
|
Military.generate();
|
|
if (layerIsOn("toggleEmblems")) drawEmblems();
|
|
|
|
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
|
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
|
if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
|
|
}
|
|
|
|
function recreateStates() {
|
|
const localSeed = generateSeed();
|
|
Math.random = aleaPRNG(localSeed);
|
|
|
|
const statesCount = +byId("statesNumber").value;
|
|
if (!statesCount) {
|
|
tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error");
|
|
return null;
|
|
}
|
|
|
|
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
|
|
if (!validBurgs.length) {
|
|
tip("There are no any burgs to generate states. Please create burgs first", false, "error");
|
|
return null;
|
|
}
|
|
|
|
if (validBurgs.length < statesCount) {
|
|
const message = `Not enough burgs to generate ${statesCount} states. Will generate only ${validBurgs.length} states`;
|
|
tip(message, false, "warn");
|
|
}
|
|
|
|
const validStates = pack.states.filter(s => s.i && !s.removed);
|
|
const lockedStates = validStates.filter(s => s.lock);
|
|
const lockedStatesIds = lockedStates.map(s => s.i);
|
|
const lockedStatesCapitals = lockedStates.map(s => s.capital);
|
|
|
|
if (validStates.length && lockedStates.length === validStates.length) {
|
|
tip("Unable to regenerate as all states are locked", false, "error");
|
|
return null;
|
|
}
|
|
|
|
// turn all old capitals into town, except for the capitals of locked states
|
|
for (const burg of validBurgs) {
|
|
if (burg.capital) {
|
|
if (lockedStatesCapitals.includes(burg.i)) continue;
|
|
burg.capital = 0;
|
|
Burgs.changeGroup(burg);
|
|
}
|
|
}
|
|
|
|
// remove labels and emblems for non-locked states
|
|
for (const state of pack.states) {
|
|
if (!state.i || state.removed || state.lock) continue;
|
|
|
|
// remove state labels
|
|
byId(`stateLabel${state.i}`)?.remove();
|
|
byId(`textPath_stateLabel${state.i}`)?.remove();
|
|
|
|
// remove state emblems
|
|
byId(`stateCOA${state.i}`)?.remove();
|
|
document.querySelector(`#stateEmblems > use[data-i="${state.i}"]`)?.remove();
|
|
|
|
// remove province data and emblems
|
|
for (const provinceId of state.provinces) {
|
|
byId(`provinceCOA${provinceId}`)?.remove();
|
|
document.querySelector(`#provinceEmblems > use[data-i="${provinceId}"]`)?.remove();
|
|
pack.provinces[provinceId].removed = true;
|
|
}
|
|
}
|
|
|
|
unfog();
|
|
|
|
// burg local ids sorted by a bit randomized population. Also ignore burgs of a locked state
|
|
const sortedBurgs = validBurgs
|
|
.filter(b => !lockedStatesIds.includes(b.state))
|
|
.map(b => [b, b.population * Math.random()])
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(b => b[0]);
|
|
|
|
const count = Math.min(statesCount, validBurgs.length) + 1; // +1 for neutral
|
|
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
|
|
|
const capitalsTree = d3.quadtree();
|
|
const isTooClose = (x, y, spacing) => Boolean(capitalsTree.find(x, y, spacing));
|
|
|
|
const newStates = [{i: 0, name: pack.states[0].name}];
|
|
|
|
// restore locked states
|
|
lockedStates.forEach(state => {
|
|
const newId = newStates.length;
|
|
const {x, y} = pack.burgs[state.capital];
|
|
capitalsTree.add([x, y]);
|
|
|
|
// update label id reference
|
|
byId(`textPath_stateLabel${state.i}`)?.setAttribute("id", `textPath_stateLabel${newId}`);
|
|
const $label = byId(`stateLabel${state.i}`);
|
|
if ($label) {
|
|
$label.setAttribute("id", `stateLabel${newId}`);
|
|
const $textPath = $label.querySelector("textPath");
|
|
if ($textPath) {
|
|
$textPath.removeAttribute("href");
|
|
$textPath.setAttribute("href", `#textPath_stateLabel${newId}`);
|
|
}
|
|
}
|
|
|
|
// update emblem id reference
|
|
byId(`stateCOA${state.i}`)?.setAttribute("id", `stateCOA${newId}`);
|
|
document.querySelector(`#stateEmblems > use[data-i="${state.i}"]`)?.setAttribute("data-i", newId);
|
|
|
|
state.provinces.forEach(provinceId => {
|
|
if (!pack.provinces[provinceId]) return;
|
|
pack.provinces[provinceId].state = newId;
|
|
});
|
|
|
|
state.i = newId;
|
|
newStates.push(state);
|
|
});
|
|
|
|
for (const i of pack.cells.i) {
|
|
const stateId = pack.cells.state[i];
|
|
const lockedStateIndex = lockedStatesIds.indexOf(stateId) + 1;
|
|
// lockedStateIndex is an index of locked state or 0 if state is not locked
|
|
pack.cells.state[i] = lockedStateIndex;
|
|
}
|
|
|
|
for (let i = newStates.length; i < count; i++) {
|
|
let capital = null;
|
|
|
|
for (const burg of sortedBurgs) {
|
|
const {x, y} = burg;
|
|
if (!isTooClose(x, y, spacing)) {
|
|
burg.capital = 1;
|
|
capital = burg;
|
|
capitalsTree.add([x, y]);
|
|
Burgs.changeGroup(capital);
|
|
break;
|
|
}
|
|
|
|
spacing = Math.max(spacing - 1, 1);
|
|
}
|
|
|
|
// all burgs are too close, should not happen in normal conditions
|
|
if (!capital) break;
|
|
|
|
// create new state
|
|
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() * byId("sizeVariety").value + 1, 1);
|
|
|
|
const cultureType = pack.cultures[culture].type;
|
|
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
|
|
coa.shield = capital.coa.shield;
|
|
|
|
newStates.push({i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa});
|
|
}
|
|
|
|
return newStates;
|
|
}
|
|
|
|
function regenerateProvinces() {
|
|
unfog();
|
|
|
|
Provinces.generate(true, true);
|
|
Provinces.getPoles();
|
|
|
|
if (layerIsOn("toggleBorders")) drawBorders();
|
|
layerIsOn("toggleProvinces") ? drawProvinces() : toggleProvinces();
|
|
|
|
// remove emblems
|
|
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
|
emblems.selectAll("use").remove();
|
|
if (layerIsOn("toggleEmblems")) drawEmblems();
|
|
refreshAllEditors();
|
|
}
|
|
|
|
function regenerateBurgs() {
|
|
const {cells, features, burgs, states, provinces} = pack;
|
|
|
|
rankCells();
|
|
|
|
// remove notes for unlocked burgs
|
|
notes = notes.filter(note => {
|
|
if (note.id.startsWith("burg")) {
|
|
const burgId = +note.id.slice(4);
|
|
return burgs[burgId]?.lock;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const newBurgs = [0]; // new burgs array
|
|
const burgsTree = d3.quadtree();
|
|
|
|
cells.burg = new Uint16Array(cells.i.length); // clear cells burg data
|
|
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
|
|
provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
|
|
|
|
// readd locked burgs
|
|
const lockedburgs = burgs.filter(burg => burg.i && !burg.removed && burg.lock);
|
|
for (let j = 0; j < lockedburgs.length; j++) {
|
|
const lockedBurg = lockedburgs[j];
|
|
const newId = newBurgs.length;
|
|
|
|
const noteIndex = notes.findIndex(note => note.id === `burg${lockedBurg.i}`);
|
|
if (noteIndex !== -1) notes[noteIndex].id = `burg${newId}`;
|
|
|
|
lockedBurg.i = newId;
|
|
newBurgs.push(lockedBurg);
|
|
|
|
burgsTree.add([lockedBurg.x, lockedBurg.y]);
|
|
cells.burg[lockedBurg.cell] = newId;
|
|
|
|
if (lockedBurg.capital) {
|
|
const stateId = lockedBurg.state;
|
|
states[stateId].capital = newId;
|
|
states[stateId].center = lockedBurg.cell;
|
|
}
|
|
}
|
|
|
|
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
|
|
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
|
const existingStatesCount = states.filter(s => s.i && !s.removed).length;
|
|
const burgsCount =
|
|
(manorsInput.value === "1000" ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) : +manorsInput.value) +
|
|
existingStatesCount;
|
|
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between town
|
|
|
|
for (let i = 0; i < sorted.length && newBurgs.length < burgsCount; i++) {
|
|
const id = newBurgs.length;
|
|
const cell = sorted[i];
|
|
const [x, y] = cells.p[cell];
|
|
|
|
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
|
|
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
|
|
|
const stateId = cells.state[cell];
|
|
const capital = stateId && !states[stateId].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands
|
|
if (capital) {
|
|
states[stateId].capital = id;
|
|
states[stateId].center = cell;
|
|
}
|
|
|
|
const culture = cells.culture[cell];
|
|
const name = Names.getCulture(culture);
|
|
newBurgs.push({cell, x, y, state: stateId, i: id, culture, name, capital, feature: cells.f[cell]});
|
|
burgsTree.add([x, y]);
|
|
cells.burg[cell] = id;
|
|
}
|
|
|
|
pack.burgs = newBurgs; // assign new burgs array
|
|
|
|
// add a capital at former place for states without added capitals
|
|
states
|
|
.filter(s => s.i && !s.removed && !s.capital)
|
|
.forEach(s => {
|
|
const [x, y] = cells.p[s.center];
|
|
const burgId = Burgs.add([x, y]);
|
|
s.capital = burgId;
|
|
s.center = pack.burgs[burgId].cell;
|
|
|
|
const burg = pack.burgs[burgId];
|
|
burg.state = s.i;
|
|
burg.capital = 1;
|
|
Burgs.changeGroup(burg);
|
|
});
|
|
|
|
features.forEach(f => {
|
|
if (f.port) f.port = 0; // reset features ports counter
|
|
});
|
|
|
|
Burgs.specify();
|
|
regenerateRoutes();
|
|
|
|
drawBurgIcons();
|
|
drawBurgLabels();
|
|
|
|
// remove emblems
|
|
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
|
|
emblems.selectAll("use").remove();
|
|
if (layerIsOn("toggleEmblems")) drawEmblems();
|
|
|
|
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
|
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
|
}
|
|
|
|
function regenerateEmblems() {
|
|
// remove old emblems
|
|
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
|
|
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
|
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
|
|
emblems.selectAll("use").remove();
|
|
|
|
// generate new emblems
|
|
pack.states.forEach(state => {
|
|
if (!state.i || state.removed) return;
|
|
const cultureType = pack.cultures[state.culture].type;
|
|
state.coa = COA.generate(null, null, null, cultureType);
|
|
state.coa.shield = COA.getShield(state.culture, null);
|
|
});
|
|
|
|
pack.burgs.forEach(burg => {
|
|
if (!burg.i || burg.removed) return;
|
|
const state = pack.states[burg.state];
|
|
|
|
let kinship = state ? 0.25 : 0;
|
|
if (burg.capital) kinship += 0.1;
|
|
else if (burg.port) kinship -= 0.1;
|
|
if (state && burg.culture !== state.culture) kinship -= 0.25;
|
|
burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type);
|
|
burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0);
|
|
});
|
|
|
|
pack.provinces.forEach(province => {
|
|
if (!province.i || province.removed) return;
|
|
const parent = province.burg ? pack.burgs[province.burg] : pack.states[province.state];
|
|
|
|
let dominion = false;
|
|
if (!province.burg) {
|
|
dominion = P(0.2);
|
|
if (province.formName === "Colony") dominion = P(0.95);
|
|
else if (province.formName === "Island") dominion = P(0.6);
|
|
else if (province.formName === "Islands") dominion = P(0.5);
|
|
else if (province.formName === "Territory") dominion = P(0.4);
|
|
else if (province.formName === "Land") dominion = P(0.3);
|
|
}
|
|
|
|
const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3);
|
|
const kinship = dominion ? 0 : nameByBurg ? 0.8 : 0.4;
|
|
const culture = pack.cells.culture[province.center];
|
|
const type = Burgs.getType(province.center, parent.port);
|
|
province.coa = COA.generate(parent.coa, kinship, dominion, type);
|
|
province.coa.shield = COA.getShield(culture, province.state);
|
|
});
|
|
|
|
layerIsOn("toggleEmblems") ? drawEmblems() : toggleEmblems();
|
|
}
|
|
|
|
function regenerateReligions() {
|
|
Religions.generate();
|
|
|
|
layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
|
|
refreshAllEditors();
|
|
}
|
|
|
|
function regenerateCultures() {
|
|
Cultures.generate();
|
|
Cultures.expand();
|
|
|
|
// update culture for states
|
|
pack.states = pack.states.map(state => {
|
|
if (!state.i || state.removed) return state;
|
|
return {...state, culture: pack.cells.culture[state.center]};
|
|
});
|
|
|
|
// update culture for burgs
|
|
pack.burgs = pack.burgs.map(burg => {
|
|
if (!burg.i || burg.removed) return burg;
|
|
return {...burg, culture: pack.cells.culture[burg.cell]};
|
|
});
|
|
|
|
// update culture for religions
|
|
pack.religions = pack.religions.map(religion => {
|
|
if (!religion.i || religion.removed) return religion;
|
|
return {...religion, culture: pack.cells.culture[religion.center]};
|
|
});
|
|
|
|
layerIsOn("toggleCultures") ? drawCultures() : toggleCultures();
|
|
refreshAllEditors();
|
|
}
|
|
|
|
function regenerateMilitary() {
|
|
Military.generate();
|
|
if (layerIsOn("toggleMilitary")) drawMilitary();
|
|
else toggleMilitary();
|
|
|
|
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
|
}
|
|
|
|
function regenerateIce() {
|
|
if (!layerIsOn("toggleIce")) toggleIce();
|
|
ice.selectAll("*").remove();
|
|
drawIce();
|
|
}
|
|
|
|
function regenerateMarkers() {
|
|
Markers.regenerate();
|
|
turnButtonOn("toggleMarkers");
|
|
drawMarkers();
|
|
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
|
|
}
|
|
|
|
function regenerateZones(event) {
|
|
if (isCtrlClick(event))
|
|
prompt("Please provide zones number multiplier", {default: 1, step: 0.01, min: 0, max: 100}, v =>
|
|
addNumberOfZones(v)
|
|
);
|
|
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
|
|
|
|
function addNumberOfZones(number) {
|
|
Zones.generate(number);
|
|
if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
|
|
if (layerIsOn("toggleZones")) drawZones();
|
|
}
|
|
}
|
|
|
|
function unpressClickToAddButton() {
|
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
|
restoreDefaultEvents();
|
|
clearMainTip();
|
|
}
|
|
|
|
function toggleAddLabel() {
|
|
const pressed = byId("addLabel").classList.contains("pressed");
|
|
if (pressed) {
|
|
unpressClickToAddButton();
|
|
return;
|
|
}
|
|
|
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
|
addLabel.classList.add("pressed");
|
|
closeDialogs(".stable");
|
|
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
|
|
tip("Click on map to place label. Hold Shift to add multiple", true);
|
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
|
}
|
|
|
|
function addLabelOnClick() {
|
|
const point = d3.mouse(this);
|
|
|
|
// get culture in clicked point to generate a name
|
|
const cell = findCell(point[0], point[1]);
|
|
const culture = pack.cells.culture[cell];
|
|
const name = Names.getCulture(culture);
|
|
const id = getNextId("label");
|
|
|
|
// use most recently selected label group
|
|
const lastSelected = labelGroupSelect.value;
|
|
const groupId = ["", "states", "burgLabels"].includes(lastSelected) ? "#addedLabels" : "#" + lastSelected;
|
|
|
|
let group = labels.select(groupId);
|
|
if (!group.size())
|
|
group = labels
|
|
.append("g")
|
|
.attr("id", "addedLabels")
|
|
.attr("fill", "#3e3e4b")
|
|
.attr("opacity", 1)
|
|
.attr("stroke", "#3a3a3a")
|
|
.attr("stroke-width", 0)
|
|
.attr("font-family", "Almendra SC")
|
|
.attr("font-size", 18)
|
|
.attr("data-size", 18)
|
|
.attr("filter", null);
|
|
|
|
const example = group.append("text").attr("x", 0).attr("y", 0).text(name);
|
|
const width = example.node().getBBox().width;
|
|
example.remove();
|
|
|
|
group.classed("hidden", false);
|
|
group
|
|
.append("text")
|
|
.attr("text-rendering", "optimizeSpeed")
|
|
.attr("id", id)
|
|
.append("textPath")
|
|
.attr("text-rendering", "optimizeSpeed")
|
|
.attr("xlink:href", "#textPath_" + id)
|
|
.attr("startOffset", "50%")
|
|
.attr("font-size", "100%")
|
|
.append("tspan")
|
|
.attr("x", 0)
|
|
.text(name);
|
|
|
|
defs
|
|
.select("#textPaths")
|
|
.append("path")
|
|
.attr("id", "textPath_" + id)
|
|
.attr("d", `M${point[0] - width},${point[1]} h${width * 2}`);
|
|
|
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
|
}
|
|
|
|
function toggleAddBurg() {
|
|
unpressClickToAddButton();
|
|
byId("addBurgTool").classList.add("pressed");
|
|
overviewBurgs();
|
|
byId("addNewBurg").click();
|
|
}
|
|
|
|
function toggleAddRiver() {
|
|
const pressed = byId("addRiver").classList.contains("pressed");
|
|
if (pressed) {
|
|
unpressClickToAddButton();
|
|
byId("addNewRiver").classList.remove("pressed");
|
|
return;
|
|
}
|
|
|
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
|
addRiver.classList.add("pressed");
|
|
byId("addNewRiver").classList.add("pressed");
|
|
closeDialogs(".stable");
|
|
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
|
|
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
|
|
if (!layerIsOn("toggleRivers")) toggleRivers();
|
|
}
|
|
|
|
function addRiverOnClick() {
|
|
const {cells, rivers} = pack;
|
|
let i = findCell(...d3.mouse(this));
|
|
|
|
if (cells.r[i]) return tip("There is already a river here", false, "error");
|
|
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
|
|
if (cells.b[i]) return;
|
|
|
|
const riverCells = [];
|
|
let riverId = Rivers.getNextId(rivers);
|
|
let parent = riverId;
|
|
|
|
const initialFlux = grid.cells.prec[cells.g[i]];
|
|
cells.fl[i] = initialFlux;
|
|
|
|
const h = Rivers.alterHeights();
|
|
Rivers.resolveDepressions(h);
|
|
|
|
while (i) {
|
|
cells.r[i] = riverId;
|
|
riverCells.push(i);
|
|
|
|
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
|
|
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, "error");
|
|
|
|
// pour to water body
|
|
if (h[min] < 20) {
|
|
riverCells.push(min);
|
|
|
|
const feature = pack.features[cells.f[min]];
|
|
if (feature.type === "lake") {
|
|
if (feature.outlet) parent = feature.outlet;
|
|
feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// pour outside of map from border cell
|
|
if (cells.b[min]) {
|
|
cells.fl[min] += cells.fl[i];
|
|
riverCells.push(-1);
|
|
break;
|
|
}
|
|
|
|
// continue propagation if min cell has no river
|
|
if (!cells.r[min]) {
|
|
cells.fl[min] += cells.fl[i];
|
|
i = min;
|
|
continue;
|
|
}
|
|
|
|
// handle case when lowest cell already has a river
|
|
const oldRiverId = cells.r[min];
|
|
const oldRiver = rivers.find(river => river.i === oldRiverId);
|
|
const oldRiverCells = oldRiver?.cells || cells.i.filter(i => cells.r[i] === oldRiverId);
|
|
const oldRiverCellsUpper = oldRiverCells.filter(i => h[i] > h[min]);
|
|
|
|
// create new river as a tributary
|
|
if (riverCells.length <= oldRiverCellsUpper.length) {
|
|
cells.conf[min] += cells.fl[i];
|
|
riverCells.push(min);
|
|
parent = oldRiverId;
|
|
break;
|
|
}
|
|
|
|
// continue old river
|
|
byId("river" + oldRiverId)?.remove();
|
|
riverCells.forEach(i => (cells.r[i] = oldRiverId));
|
|
oldRiverCells.forEach(cell => {
|
|
if (h[cell] > h[min]) {
|
|
cells.r[cell] = 0;
|
|
cells.fl[cell] = grid.cells.prec[cells.g[cell]];
|
|
} else {
|
|
riverCells.push(cell);
|
|
cells.fl[cell] += cells.fl[i];
|
|
}
|
|
});
|
|
riverId = oldRiverId;
|
|
|
|
break;
|
|
}
|
|
|
|
const river = rivers.find(r => r.i === riverId);
|
|
|
|
const source = riverCells[0];
|
|
const mouth = riverCells[riverCells.length - 2];
|
|
|
|
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
|
const widthFactor =
|
|
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
|
const sourceWidth = river?.sourceWidth || Rivers.getSourceWidth(cells.fl[source]);
|
|
const meanderedPoints = Rivers.addMeandering(riverCells);
|
|
|
|
const discharge = cells.fl[mouth]; // m3 in second
|
|
const length = Rivers.getApproximateLength(meanderedPoints);
|
|
const width = Rivers.getWidth(
|
|
Rivers.getOffset({
|
|
flux: discharge,
|
|
pointIndex: meanderedPoints.length,
|
|
widthFactor,
|
|
startingWidth: sourceWidth
|
|
})
|
|
);
|
|
|
|
if (river) {
|
|
river.source = source;
|
|
river.length = length;
|
|
river.discharge = discharge;
|
|
river.width = width;
|
|
river.cells = riverCells;
|
|
} else {
|
|
const basin = Rivers.getBasin(parent);
|
|
const name = Rivers.getName(mouth);
|
|
const type = Rivers.getType({i: riverId, length, parent});
|
|
|
|
rivers.push({
|
|
i: riverId,
|
|
source,
|
|
mouth,
|
|
discharge,
|
|
length,
|
|
width,
|
|
widthFactor,
|
|
sourceWidth,
|
|
parent,
|
|
cells: riverCells,
|
|
basin,
|
|
name,
|
|
type
|
|
});
|
|
}
|
|
|
|
// render river
|
|
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
|
const id = "river" + riverId;
|
|
const riversG = viewbox.select("#rivers");
|
|
riversG.append("path").attr("id", id).attr("d", path);
|
|
|
|
if (d3.event.shiftKey === false) {
|
|
Lakes.cleanupLakeData();
|
|
unpressClickToAddButton();
|
|
byId("addNewRiver").classList.remove("pressed");
|
|
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
|
|
}
|
|
}
|
|
|
|
function toggleAddMarker() {
|
|
const pressed = byId("addMarker")?.classList.contains("pressed");
|
|
if (pressed) {
|
|
unpressClickToAddButton();
|
|
return;
|
|
}
|
|
|
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
|
addMarker.classList.add("pressed");
|
|
markersAddFromOverview.classList.add("pressed");
|
|
|
|
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
|
|
tip("Click on map to add a marker. Hold Shift to add multiple", true);
|
|
if (!layerIsOn("toggleMarkers")) toggleMarkers();
|
|
}
|
|
|
|
function addMarkerOnClick() {
|
|
const {markers} = pack;
|
|
const point = d3.mouse(this);
|
|
const x = rn(point[0], 2);
|
|
const y = rn(point[1], 2);
|
|
|
|
// Find the current cell
|
|
const cell = findCell(point[0], point[1]);
|
|
|
|
// Find the currently selected marker to use as a base
|
|
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
|
|
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
|
|
|
|
const selectedType = byId("addedMarkerType").value;
|
|
const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
|
|
|
|
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
|
|
const marker = Markers.add({...baseMarker, x, y, cell});
|
|
|
|
if (selectedConfig && selectedConfig.add) {
|
|
selectedConfig.add("marker" + marker.i, cell);
|
|
}
|
|
|
|
const markersElement = byId("markers");
|
|
const rescale = +markersElement.getAttribute("rescale");
|
|
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
|
|
|
|
if (d3.event.shiftKey === false) {
|
|
byId("markerAdd").classList.remove("pressed");
|
|
byId("markersAddFromOverview").classList.remove("pressed");
|
|
unpressClickToAddButton();
|
|
}
|
|
}
|
|
|
|
function configMarkersGeneration() {
|
|
drawConfigTable();
|
|
|
|
function drawConfigTable() {
|
|
const config = Markers.getConfig();
|
|
|
|
const headers = /* html */ `<thead style='font-weight:bold'><tr>
|
|
<td data-tip="Marker type name">Type</td>
|
|
<td data-tip="Marker icon">Icon</td>
|
|
<td data-tip="Marker number multiplier">Multiplier</td>
|
|
<td data-tip="Number of markers of that type on the current map">Number</td>
|
|
</tr></thead>`;
|
|
|
|
const lines = config.map(({type, icon, multiplier}) => {
|
|
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
|
|
|
|
return /* html */ `<tr>
|
|
<td><input class="type" value="${type}" /></td>
|
|
<td style="position: relative">
|
|
<img class="image" src="${isExternal ? icon : ""}" ${
|
|
isExternal ? "" : "hidden"
|
|
} style="width:1.2em; height:1.2em; vertical-align: middle;">
|
|
<span class="emoji" style="font-size:1.2em">${isExternal ? "" : icon}</span>
|
|
<button class="changeIcon icon-pencil"></button>
|
|
</td>
|
|
<td><input class="multiplier" type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
|
|
<td style="text-align:center">${pack.markers.filter(marker => marker.type === type).length}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
const table = `<table class="table">${headers}<tbody>${lines.join("")}</tbody></table>`;
|
|
alertMessage.innerHTML = table;
|
|
|
|
alertMessage.querySelectorAll("button.changeIcon").forEach(selectIconButton => {
|
|
selectIconButton.addEventListener("click", function () {
|
|
const image = this.parentElement.querySelector(".image");
|
|
const emoji = this.parentElement.querySelector(".emoji");
|
|
const icon = image.getAttribute("src") || emoji.textContent;
|
|
|
|
selectIcon(icon, value => {
|
|
const isExternal = value.startsWith("http") || value.startsWith("data:image");
|
|
image.setAttribute("src", isExternal ? value : "");
|
|
image.hidden = !isExternal;
|
|
emoji.textContent = isExternal ? "" : value;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const applyChanges = () => {
|
|
const rows = alertMessage.querySelectorAll("tbody > tr");
|
|
const rowsData = Array.from(rows).map(row => {
|
|
const type = row.querySelector(".type").value;
|
|
|
|
const image = row.querySelector(".image");
|
|
const emoji = row.querySelector(".emoji");
|
|
const icon = image.getAttribute("src") || emoji.textContent;
|
|
|
|
const multiplier = parseFloat(row.querySelector(".multiplier").value);
|
|
return {type, icon, multiplier};
|
|
});
|
|
|
|
const config = Markers.getConfig();
|
|
const newConfig = config.map((markerType, index) => {
|
|
const {type, icon, multiplier} = rowsData[index];
|
|
return {...markerType, type, icon, multiplier};
|
|
});
|
|
|
|
Markers.setConfig(newConfig);
|
|
};
|
|
|
|
$("#alert").dialog({
|
|
resizable: false,
|
|
title: "Markers generation settings",
|
|
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
|
|
buttons: {
|
|
Regenerate: () => {
|
|
applyChanges();
|
|
regenerateMarkers();
|
|
drawConfigTable();
|
|
},
|
|
Close: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
open: function () {
|
|
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
|
|
buttons[0].addEventListener("mousemove", () => tip("Apply changes and regenerate markers"));
|
|
buttons[1].addEventListener("mousemove", () => tip("Close the window"));
|
|
},
|
|
close: function () {
|
|
$(this).dialog("destroy");
|
|
}
|
|
});
|
|
}
|
|
|
|
function viewCellDetails() {
|
|
$("#cellInfo").dialog({
|
|
resizable: false,
|
|
width: "22em",
|
|
title: "Cell Details",
|
|
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
|
});
|
|
}
|
|
|
|
async function overviewCharts() {
|
|
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.99.00");
|
|
Overview.open();
|
|
}
|