mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-23 04:21:24 +01:00
Merge branch 'zone-types' into zoneFilter
This commit is contained in:
commit
be5fd52545
50 changed files with 5605 additions and 1374 deletions
|
|
@ -141,8 +141,8 @@ class Battle {
|
|||
const state = pack.states[regiment.state];
|
||||
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
|
||||
const color = state.color[0] === "#" ? state.color : "#999";
|
||||
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
|
||||
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
|
||||
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
|
||||
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ class Battle {
|
|||
dist = added ? "0 " + distanceUnitInput.value : distance(r);
|
||||
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
|
||||
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
|
||||
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
||||
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" ></svg>
|
||||
<div style="width:6em">${s.name.slice(0, 11)}</div>
|
||||
<div style="width:1.2em">${r.icon}</div>
|
||||
<div style="width:13em">${r.name.slice(0, 24)}</div>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ function editBiomes() {
|
|||
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
||||
|
||||
body.addEventListener("click", function (ev) {
|
||||
const el = ev.target,
|
||||
cl = el.classList;
|
||||
if (cl.contains("fillRect")) biomeChangeColor(el);
|
||||
const el = ev.target;
|
||||
const cl = el.classList;
|
||||
if (el.tagName === "FILL-BOX") biomeChangeColor(el);
|
||||
else if (cl.contains("icon-info-circled")) openWiki(el);
|
||||
else if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
|
||||
if (customization === 6) selectBiomeOnLineClick(el);
|
||||
|
|
@ -94,9 +94,7 @@ function editBiomes() {
|
|||
|
||||
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
|
||||
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
|
||||
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
|
||||
b.color[i]
|
||||
}" class="fillRect pointer"></svg>
|
||||
<fill-box fill="${b.color[i]}"></fill-box>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 class="biomeHabitability hide" value=${
|
||||
|
|
@ -158,15 +156,15 @@ function editBiomes() {
|
|||
|
||||
function biomeChangeColor(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const biome = +el.parentNode.parentNode.dataset.id;
|
||||
const biome = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
biomesData.color[biome] = fill;
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
biomesData.color[biome] = newFill;
|
||||
biomes
|
||||
.select("#biome" + biome)
|
||||
.attr("fill", fill)
|
||||
.attr("stroke", fill);
|
||||
.attr("fill", newFill)
|
||||
.attr("stroke", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
|
|
@ -270,7 +268,7 @@ function editBiomes() {
|
|||
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
|
||||
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
|
||||
<fill-box fill="${b.color[i]}"></fill-box>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ function editBurg(id) {
|
|||
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
|
||||
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
|
||||
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
|
||||
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
|
||||
|
||||
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
|
||||
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
|
||||
|
|
@ -51,6 +52,7 @@ function editBurg(id) {
|
|||
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
|
||||
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
|
||||
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
|
||||
document.getElementById("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
|
||||
|
||||
function updateBurgValues() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
|
|
@ -112,7 +114,13 @@ function editBurg(id) {
|
|||
if (options.showMFCGMap) {
|
||||
document.getElementById("mfcgPreviewSection").style.display = "block";
|
||||
updateMFCGFrame(b);
|
||||
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
|
||||
|
||||
if (b.link) {
|
||||
document.getElementById("mfcgBurgSeedSection").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("mfcgBurgSeedSection").style.display = "inline-block";
|
||||
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
|
||||
}
|
||||
} else {
|
||||
document.getElementById("mfcgPreviewSection").style.display = "none";
|
||||
}
|
||||
|
|
@ -347,22 +355,25 @@ function editBurg(id) {
|
|||
|
||||
function toggleFeature() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const b = pack.burgs[id];
|
||||
const burg = pack.burgs[id];
|
||||
const feature = this.dataset.feature;
|
||||
const turnOn = this.classList.contains("inactive");
|
||||
if (feature === "port") togglePort(id);
|
||||
else if (feature === "capital") toggleCapital(id);
|
||||
else b[feature] = +turnOn;
|
||||
if (b[feature]) this.classList.remove("inactive");
|
||||
else if (!b[feature]) this.classList.add("inactive");
|
||||
else burg[feature] = +turnOn;
|
||||
if (burg[feature]) this.classList.remove("inactive");
|
||||
else if (!burg[feature]) this.classList.add("inactive");
|
||||
|
||||
if (b.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
|
||||
if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
|
||||
else document.getElementById("burgEditAnchorStyle").style.display = "none";
|
||||
updateMFCGFrame(burg);
|
||||
}
|
||||
|
||||
function toggleBurgLockButton() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
toggleBurgLock(id);
|
||||
const burg = pack.burgs[id];
|
||||
burg.lock = !burg.lock;
|
||||
|
||||
updateBurgLockIcon();
|
||||
}
|
||||
|
||||
|
|
@ -426,6 +437,17 @@ function editBurg(id) {
|
|||
document.getElementById("mfcgBurgSeed").value = burgSeed;
|
||||
}
|
||||
|
||||
function addCustomMfcgLink() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
const message = "Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
|
||||
prompt(message, {default: burg.link || "", required: false}, link => {
|
||||
if (link) burg.link = link;
|
||||
else delete burg.link;
|
||||
updateMFCGFrame(burg);
|
||||
});
|
||||
}
|
||||
|
||||
function openEmblemEdit() {
|
||||
const id = +elSelected.attr("data-id"),
|
||||
burg = pack.burgs[id];
|
||||
|
|
@ -522,6 +544,11 @@ function editBurg(id) {
|
|||
editNotes("burg" + id, name);
|
||||
}
|
||||
|
||||
function showTemperatureGraph() {
|
||||
const id = elSelected.attr("data-id");
|
||||
showBurgTemperatureGraph(id);
|
||||
}
|
||||
|
||||
function removeSelectedBurg() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
if (pack.burgs[id].capital) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ function overviewBurgs() {
|
|||
|
||||
const body = document.getElementById("burgsBody");
|
||||
updateFilter();
|
||||
updateLockAllIcon();
|
||||
burgsOverviewAddLines();
|
||||
$("#burgsOverview").dialog();
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ function overviewBurgs() {
|
|||
document.getElementById("burgsListToLoad").addEventListener("change", function () {
|
||||
uploadFile(this, importBurgNames);
|
||||
});
|
||||
document.getElementById("burgsLockAll").addEventListener("click", toggleLockAll);
|
||||
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
|
||||
document.getElementById("burgsInvertLock").addEventListener("click", invertLock);
|
||||
|
||||
|
|
@ -87,7 +89,7 @@ function overviewBurgs() {
|
|||
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
|
||||
<input data-tip="Burg province" class="burgState" value="${province}" disabled>
|
||||
<input data-tip="Burg state" class="burgState" value="${state}" disabled>
|
||||
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(
|
||||
<select data-tip="Dominant culture. Click to change burg culture (to change cell culture use Cultures Editor)" class="stateCulture">${getCultureOptions(
|
||||
b.culture
|
||||
)}</select>
|
||||
<span data-tip="Burg population" class="icon-male"></span>
|
||||
|
|
@ -195,8 +197,11 @@ function overviewBurgs() {
|
|||
}
|
||||
|
||||
function toggleBurgLockStatus() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
toggleBurgLock(burg);
|
||||
const burgId = +this.parentNode.dataset.id;
|
||||
|
||||
const burg = pack.burgs[burgId];
|
||||
burg.lock = !burg.lock;
|
||||
|
||||
if (this.classList.contains("icon-lock")) {
|
||||
this.classList.remove("icon-lock");
|
||||
this.classList.add("icon-lock-open");
|
||||
|
|
@ -478,9 +483,9 @@ function overviewBurgs() {
|
|||
}
|
||||
|
||||
function renameBurgsInBulk() {
|
||||
const message = `Download burgs list as a text file, make changes and re-upload the file.
|
||||
alertMessage.innerHTML = `Download burgs list as a text file, make changes and re-upload the file.
|
||||
Make sure the file is a plain text document with each name on its own line (the dilimiter is CRLF).
|
||||
If you do not want to change the name, just leave it as is`;
|
||||
alertMessage.innerHTML = message;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Burgs bulk renaming",
|
||||
|
|
@ -562,4 +567,21 @@ function overviewBurgs() {
|
|||
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
|
||||
burgsOverviewAddLines();
|
||||
}
|
||||
|
||||
function toggleLockAll() {
|
||||
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
const allLocked = activeBurgs.every(burg => burg.lock);
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
burg.lock = !allLocked;
|
||||
});
|
||||
|
||||
burgsOverviewAddLines();
|
||||
document.getElementById("burgsLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
|
||||
}
|
||||
|
||||
function updateLockAllIcon() {
|
||||
const allLocked = pack.burgs.every(({lock, i, removed}) => lock || !i || removed);
|
||||
document.getElementById("burgsLockAll").className = allLocked ? "icon-lock-open" : "icon-lock";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,9 +108,7 @@ function editCultures() {
|
|||
|
||||
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
|
||||
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
|
||||
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
|
||||
</svg>
|
||||
<fill-box fill="${c.color}"></fill-box>
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
|
|
@ -148,7 +146,7 @@ function editCultures() {
|
|||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
|
||||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
|
||||
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", cultureChangeColor));
|
||||
body.querySelectorAll("fill-box").forEach(el => el.addEventListener("click", cultureChangeColor));
|
||||
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
|
||||
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
|
||||
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
|
||||
|
|
@ -248,16 +246,16 @@ function editCultures() {
|
|||
function cultureChangeColor() {
|
||||
const el = this;
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const culture = +el.parentNode.parentNode.dataset.id;
|
||||
const culture = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.cultures[culture].color = fill;
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
pack.cultures[culture].color = newFill;
|
||||
cults
|
||||
.select("#culture" + culture)
|
||||
.attr("fill", fill)
|
||||
.attr("stroke", fill);
|
||||
debug.select("#cultureCenter" + culture).attr("fill", fill);
|
||||
.attr("fill", newFill)
|
||||
.attr("stroke", newFill);
|
||||
debug.select("#cultureCenter" + culture).attr("fill", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
"use strict";
|
||||
function editDiplomacy() {
|
||||
if (customization) return;
|
||||
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
|
||||
tip("There should be at least 2 states to edit the diplomacy", false, "error");
|
||||
return;
|
||||
}
|
||||
if (pack.states.filter(s => s.i && !s.removed).length < 2) return tip("There should be at least 2 states to edit the diplomacy", false, "error");
|
||||
|
||||
const body = document.getElementById("diplomacyBodySection");
|
||||
|
||||
closeDialogs("#diplomacyEditor, .stable");
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
|
|
@ -14,21 +13,29 @@ function editDiplomacy() {
|
|||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleReligions")) toggleReligions();
|
||||
|
||||
const body = document.getElementById("diplomacyBodySection");
|
||||
const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
|
||||
const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ",
|
||||
" is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "];
|
||||
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
|
||||
refreshDiplomacyEditor();
|
||||
const relations = {
|
||||
Ally: {inText: "is an ally of", color: "#00b300", tip: "Allies formed a defensive pact and protect each other in case of third party aggression"},
|
||||
Friendly: {inText: "is friendly to", color: "#d4f8aa", tip: "State is friendly to anouther state when they share some common interests"},
|
||||
Neutral: {inText: "is neutral to", color: "#edeee8", tip: "Neutral means states relations are neither positive nor negative"},
|
||||
Suspicion: {inText: "is suspicious of", color: "#eeafaa", tip: "Suspicion means state has a cautious distrust of another state"},
|
||||
Enemy: {inText: "is at war with", color: "#e64b40", tip: "Enemies are states at war with each other"},
|
||||
Unknown: {inText: "does not know about", color: "#a9a9a9", tip: "Relations are unknown if states do not have enough information about each other"},
|
||||
Rival: {inText: "is a rival of", color: "#ad5a1f", tip: "Rivalry is a state of competing for dominance in the region"},
|
||||
Vassal: {inText: "is a vassal of", color: "#87CEFA", tip: "Vassal is a state having obligation to its suzerain"},
|
||||
Suzerain: {inText: "is suzerain to", color: "#00008B", tip: "Suzerain is a state having some control over its vassals"}
|
||||
};
|
||||
|
||||
tip("Click on a state to see its diplomatic relations", false, "warning");
|
||||
refreshDiplomacyEditor();
|
||||
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
|
||||
|
||||
if (modules.editDiplomacy) return;
|
||||
modules.editDiplomacy = true;
|
||||
|
||||
$("#diplomacyEditor").dialog({
|
||||
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
|
||||
title: "Diplomacy Editor",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
close: closeDiplomacyEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
|
|
@ -36,10 +43,30 @@ function editDiplomacy() {
|
|||
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
|
||||
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
|
||||
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
|
||||
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
|
||||
document.getElementById("diplomacyReset").addEventListener("click", resetRelations);
|
||||
document.getElementById("diplomacyShowMatrix").addEventListener("click", showRelationsMatrix);
|
||||
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
|
||||
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
|
||||
document.getElementById("diplomacySelect").addEventListener("mouseup", diplomacyChangeRelations);
|
||||
|
||||
body.addEventListener("click", function (ev) {
|
||||
const el = ev.target;
|
||||
if (el.parentElement.classList.contains("Self")) return;
|
||||
|
||||
if (el.classList.contains("changeRelations")) {
|
||||
const line = el.parentElement;
|
||||
const subjectId = +line.dataset.id;
|
||||
const objectId = +body.querySelector("div.Self").dataset.id;
|
||||
const currentRelation = line.dataset.relations;
|
||||
|
||||
selectRelation(subjectId, objectId, currentRelation);
|
||||
return;
|
||||
}
|
||||
|
||||
// select state of clicked line
|
||||
body.querySelector("div.Self").classList.remove("Self");
|
||||
el.parentElement.classList.add("Self");
|
||||
refreshDiplomacyEditor();
|
||||
});
|
||||
|
||||
function refreshDiplomacyEditor() {
|
||||
diplomacyEditorAddLines();
|
||||
|
|
@ -50,33 +77,34 @@ function editDiplomacy() {
|
|||
function diplomacyEditorAddLines() {
|
||||
const states = pack.states;
|
||||
const selectedLine = body.querySelector("div.Self");
|
||||
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
|
||||
const selName = states[sel].fullName;
|
||||
diplomacySelect.style.display = "none";
|
||||
const selectedId = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
|
||||
const selectedName = states[selectedId].name;
|
||||
|
||||
COArenderer.trigger("stateCOA"+sel, states[sel].coa);
|
||||
let lines = `<div class="states Self" data-id=${sel} data-tip="List below shows relations to ${selName}">
|
||||
<div style="width: max-content">${selName}</div>
|
||||
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${sel}"></use></svg>
|
||||
COArenderer.trigger("stateCOA" + selectedId, states[selectedId].coa);
|
||||
let lines = `<div class="states Self" data-id=${selectedId} data-tip="List below shows relations to ${selectedName}">
|
||||
<div style="width: max-content">${states[selectedId].fullName}</div>
|
||||
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${selectedId}"></use></svg>
|
||||
</div>`;
|
||||
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed || s.i === sel) continue;
|
||||
const relation = s.diplomacy[sel];
|
||||
const index = statuses.indexOf(relation);
|
||||
const color = colors[index];
|
||||
const tip = s.fullName + description[index] + selName;
|
||||
const tipSelect = `${tip}. Click to see relations to ${s.name}`;
|
||||
const tipChange = `${tip}. Click to change relations to ${selName}`;
|
||||
COArenderer.trigger("stateCOA"+s.i, s.coa);
|
||||
for (const state of states) {
|
||||
if (!state.i || state.removed || state.i === selectedId) continue;
|
||||
const relation = state.diplomacy[selectedId];
|
||||
const {color, inText} = relations[relation];
|
||||
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
|
||||
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
|
||||
<div data-tip="${tipSelect}" style="width:12em">${s.fullName}</div>
|
||||
<svg data-tip="${tipChange}" width=".9em" height=".9em" style="margin-bottom:-1px" class="changeRelations">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect pointer" style="pointer-events: none"></rect>
|
||||
</svg>
|
||||
<input data-tip="${tipChange}" class="changeRelations diplomacyRelations" value="${relation}" readonly/>
|
||||
const tip = `${state.name} ${inText} ${selectedName}`;
|
||||
const tipSelect = `${tip}. Click to see relations to ${state.name}`;
|
||||
const tipChange = `Click to change relations. ${tip}`;
|
||||
|
||||
const name = state.fullName.length < 23 ? state.fullName : state.name;
|
||||
COArenderer.trigger("stateCOA" + state.i, state.coa);
|
||||
|
||||
lines += `<div class="states" data-id=${state.i} data-name="${name}" data-relations="${relation}">
|
||||
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${state.i}"></use></svg>
|
||||
<div data-tip="${tipSelect}" style="width: 12em">${name}</div>
|
||||
<div data-tip="${tipChange}" class="changeRelations" style="width: 6em">
|
||||
<fill-box fill="${color}" size=".9em"></fill-box>
|
||||
${relation}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
|
@ -84,8 +112,6 @@ function editDiplomacy() {
|
|||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
|
||||
body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
|
||||
|
||||
applySorting(diplomacyHeader);
|
||||
$("#diplomacyEditor").dialog();
|
||||
|
|
@ -95,19 +121,31 @@ function editDiplomacy() {
|
|||
if (!layerIsOn("toggleStates")) return;
|
||||
const state = +event.target.dataset.id;
|
||||
if (customization || !state) return;
|
||||
const d = regions.select("#state"+state).attr("d");
|
||||
const d = regions.select("#state" + state).attr("d");
|
||||
|
||||
const path = debug.append("path").attr("class", "highlight").attr("d", d)
|
||||
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
|
||||
const path = debug
|
||||
.append("path")
|
||||
.attr("class", "highlight")
|
||||
.attr("d", d)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "red")
|
||||
.attr("stroke-width", 1)
|
||||
.attr("opacity", 1)
|
||||
.attr("filter", "url(#blur1)");
|
||||
|
||||
const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
|
||||
const l = path.node().getTotalLength(),
|
||||
dur = (l + 5000) / 2;
|
||||
const i = d3.interpolateString("0," + l, l + "," + l);
|
||||
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
|
||||
path
|
||||
.transition()
|
||||
.duration(dur)
|
||||
.attrTween("stroke-dasharray", function () {
|
||||
return t => i(t);
|
||||
});
|
||||
}
|
||||
|
||||
function stateHighlightOff(event) {
|
||||
debug.selectAll(".highlight").each(function() {
|
||||
debug.selectAll(".highlight").each(function () {
|
||||
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
|
||||
});
|
||||
}
|
||||
|
|
@ -118,22 +156,17 @@ function editDiplomacy() {
|
|||
if (!sel) return;
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
|
||||
statesBody.selectAll("path").each(function() {
|
||||
statesBody.selectAll("path").each(function () {
|
||||
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
|
||||
const id = +this.id.slice(5); // state id
|
||||
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
|
||||
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
|
||||
this.setAttribute("fill", clr);
|
||||
statesBody.select("#state-gap"+id).attr("stroke", clr);
|
||||
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
|
||||
});
|
||||
}
|
||||
|
||||
function selectStateOnLineClick() {
|
||||
if (this.classList.contains("Self")) return;
|
||||
body.querySelector("div.Self").classList.remove("Self");
|
||||
this.classList.add("Self");
|
||||
refreshDiplomacyEditor();
|
||||
const relation = pack.states[id].diplomacy[sel];
|
||||
const color = relations[relation]?.color || "#4682b4";
|
||||
|
||||
this.setAttribute("fill", color);
|
||||
statesBody.select("#state-gap" + id).attr("stroke", color);
|
||||
statesHalo.select("#state-border" + id).attr("stroke", d3.color(color).darker().hex());
|
||||
});
|
||||
}
|
||||
|
||||
function selectStateOnMapClick() {
|
||||
|
|
@ -145,42 +178,61 @@ function editDiplomacy() {
|
|||
if (+selectedLine.dataset.id === state) return;
|
||||
|
||||
selectedLine.classList.remove("Self");
|
||||
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
|
||||
body.querySelector("div[data-id='" + state + "']").classList.add("Self");
|
||||
refreshDiplomacyEditor();
|
||||
}
|
||||
|
||||
function toggleDiplomacySelect(event) {
|
||||
event.stopPropagation();
|
||||
const select = document.getElementById("diplomacySelect");
|
||||
const show = select.style.display === "none";
|
||||
if (!show) {select.style.display = "none"; return;}
|
||||
select.style.display = "block";
|
||||
const input = event.target.closest("div").querySelector("input");
|
||||
select.style.left = input.getBoundingClientRect().left + "px";
|
||||
select.style.top = input.getBoundingClientRect().bottom + "px";
|
||||
body.dataset.state = event.target.closest("div.states").dataset.id;
|
||||
function selectRelation(subjectId, objectId, currentRelation) {
|
||||
const states = pack.states;
|
||||
|
||||
const subject = states[subjectId];
|
||||
const header = `<div style="margin-bottom: 0.3em"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${subject.i}"></use></svg> <b>${subject.fullName}</b></div>`;
|
||||
|
||||
const options = Object.entries(relations)
|
||||
.map(
|
||||
([relation, {color, inText, tip}]) =>
|
||||
`<div style="margin-block: 0.2em" data-tip="${tip}"><label class="pointer">
|
||||
<input type="radio" name="relationSelect" value="${relation}" ${currentRelation === relation && "checked"} >
|
||||
<fill-box fill="${color}" size=".8em"></fill-box>
|
||||
${inText}
|
||||
</label></div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const object = states[objectId];
|
||||
const footer = `<div style="margin-top: 0.7em"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${object.i}"></use></svg> <b>${object.fullName}</b></div>`;
|
||||
|
||||
alertMessage.innerHTML = `<div style="overflow: hidden">${header} ${options} ${footer}</div>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
width: fitContent(),
|
||||
title: `Change relations`,
|
||||
buttons: {
|
||||
Apply: function () {
|
||||
const newRelation = document.querySelector('input[name="relationSelect"]:checked')?.value;
|
||||
changeRelation(subjectId, objectId, currentRelation, newRelation);
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function diplomacyChangeRelations(event) {
|
||||
event.stopPropagation();
|
||||
diplomacySelect.style.display = "none";
|
||||
const subject = +body.dataset.state;
|
||||
const rel = event.target.innerHTML;
|
||||
function changeRelation(subjectId, objectId, oldRelation, newRelation) {
|
||||
if (newRelation === oldRelation) return;
|
||||
const states = pack.states;
|
||||
const chronicle = states[0].diplomacy;
|
||||
|
||||
const states = pack.states, chronicle = states[0].diplomacy;
|
||||
const selectedLine = body.querySelector("div.Self");
|
||||
const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
|
||||
if (!object) return;
|
||||
const objectName = states[object].name; // object of relations change
|
||||
const subjectName = states[subject].name; // subject of relations change - actor
|
||||
const subjectName = states[subjectId].name;
|
||||
const objectName = states[objectId].name;
|
||||
|
||||
const oldRel = states[subject].diplomacy[object];
|
||||
if (rel === oldRel) return;
|
||||
states[subject].diplomacy[object] = rel;
|
||||
states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
|
||||
states[subjectId].diplomacy[objectId] = newRelation;
|
||||
states[objectId].diplomacy[subjectId] = newRelation === "Vassal" ? "Suzerain" : newRelation === "Suzerain" ? "Vassal" : newRelation;
|
||||
|
||||
// update relation history
|
||||
const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${rel.toLowerCase()}`];
|
||||
const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${newRelation.toLowerCase()}`];
|
||||
const ally = () => [`Defence pact`, `${subjectName} entered into defensive pact with ${objectName}`];
|
||||
const vassal = () => [`Vassalization`, `${subjectName} became a vassal of ${objectName}`];
|
||||
const suzerain = () => [`Vassalization`, `${subjectName} vassalized ${objectName}`];
|
||||
|
|
@ -189,24 +241,33 @@ function editDiplomacy() {
|
|||
const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`];
|
||||
const peace = () => {
|
||||
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
|
||||
const changed = rel === "Ally" ? ally()
|
||||
: rel === "Vassal" ? vassal()
|
||||
: rel === "Suzerain" ? suzerain()
|
||||
: rel === "Unknown" ? unknown()
|
||||
: change();
|
||||
const changed =
|
||||
newRelation === "Ally"
|
||||
? ally()
|
||||
: newRelation === "Vassal"
|
||||
? vassal()
|
||||
: newRelation === "Suzerain"
|
||||
? suzerain()
|
||||
: newRelation === "Unknown"
|
||||
? unknown()
|
||||
: change();
|
||||
return [`War termination`, treaty, changed[1]];
|
||||
}
|
||||
};
|
||||
|
||||
if (oldRel === "Enemy") chronicle.push(peace()); else
|
||||
if (rel === "Enemy") chronicle.push(war()); else
|
||||
if (rel === "Vassal") chronicle.push(vassal()); else
|
||||
if (rel === "Suzerain") chronicle.push(suzerain()); else
|
||||
if (rel === "Ally") chronicle.push(ally()); else
|
||||
if (rel === "Unknown") chronicle.push(unknown()); else
|
||||
if (rel === "Rival") chronicle.push(rival()); else
|
||||
chronicle.push(change());
|
||||
if (oldRelation === "Enemy") chronicle.push(peace());
|
||||
else if (newRelation === "Enemy") chronicle.push(war());
|
||||
else if (newRelation === "Vassal") chronicle.push(vassal());
|
||||
else if (newRelation === "Suzerain") chronicle.push(suzerain());
|
||||
else if (newRelation === "Ally") chronicle.push(ally());
|
||||
else if (newRelation === "Unknown") chronicle.push(unknown());
|
||||
else if (newRelation === "Rival") chronicle.push(rival());
|
||||
else chronicle.push(change());
|
||||
|
||||
refreshDiplomacyEditor();
|
||||
if (diplomacyMatrix.offsetParent) {
|
||||
document.getElementById("diplomacyMatrixBody").innerHTML = "";
|
||||
showRelationsMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
function regenerateRelations() {
|
||||
|
|
@ -214,28 +275,52 @@ function editDiplomacy() {
|
|||
refreshDiplomacyEditor();
|
||||
}
|
||||
|
||||
function resetRelations() {
|
||||
const selectedId = +body.querySelector("div.Self")?.dataset?.id;
|
||||
if (!selectedId) return;
|
||||
const states = pack.states;
|
||||
|
||||
states[selectedId].diplomacy.forEach((relations, index) => {
|
||||
if (relations !== "x") {
|
||||
states[selectedId].diplomacy[index] = "Neutral";
|
||||
states[index].diplomacy[selectedId] = "Neutral";
|
||||
}
|
||||
});
|
||||
|
||||
refreshDiplomacyEditor();
|
||||
}
|
||||
|
||||
function showRelationsHistory() {
|
||||
const chronicle = pack.states[0].diplomacy;
|
||||
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
|
||||
if (!chronicle.length) return tip("Relations history is blank", false, "error");
|
||||
|
||||
let message = `<div autocorrect="off" spellcheck="false">`;
|
||||
chronicle.forEach((e, d) => {
|
||||
chronicle.forEach((entry, d) => {
|
||||
message += `<div>`;
|
||||
e.forEach((l, i) => message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
|
||||
entry.forEach((l, i) => {
|
||||
message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`;
|
||||
});
|
||||
message += `‍</div>`;
|
||||
});
|
||||
alertMessage.innerHTML = message + `</div><i id="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</i>`;
|
||||
alertMessage.innerHTML = message + `</div><div class="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</div>`;
|
||||
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
|
||||
|
||||
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
|
||||
$("#alert").dialog({
|
||||
title: "Relations history",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Save: function() {
|
||||
Save: function () {
|
||||
const data = this.querySelector("div").innerText.split("\n").join("\r\n");
|
||||
const name = getFileName("Relations history") + ".txt";
|
||||
downloadFile(data, name);
|
||||
},
|
||||
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
|
||||
Close: function() {$(this).dialog("close");}
|
||||
Clear: function () {
|
||||
pack.states[0].diplomacy = [];
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Close: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -251,22 +336,48 @@ function editDiplomacy() {
|
|||
|
||||
function showRelationsMatrix() {
|
||||
const states = pack.states.filter(s => s.i && !s.removed);
|
||||
const valid = states.map(s => s.i);
|
||||
const valid = states.map(state => state.i);
|
||||
const diplomacyMatrixBody = document.getElementById("diplomacyMatrixBody");
|
||||
|
||||
let message = `<table class="matrix-table"><tr><th data-tip='‍'></th>`;
|
||||
message += states.map(s => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join("") + `</tr>`; // headers
|
||||
states.forEach(s => {
|
||||
message += `<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` + s.diplomacy
|
||||
.filter((v, i) => valid.includes(i)).map((r, i) => {
|
||||
const desc = description[statuses.indexOf(r)];
|
||||
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '‍';
|
||||
return `<td data-tip='${tip}' class='${r}'>${r}</td>`
|
||||
}).join("") + "</tr>";
|
||||
let table = `<table><thead><tr><th data-tip='‍'></th>`;
|
||||
table += states.map(state => `<th data-tip='Relations to ${state.fullName}'>${state.name}</th>`).join("") + `</tr>`;
|
||||
table += `<tbody>`;
|
||||
|
||||
states.forEach(state => {
|
||||
table +=
|
||||
`<tr data-id=${state.i}><th data-tip='Relations of ${state.fullName}'>${state.name}</th>` +
|
||||
state.diplomacy
|
||||
.filter((v, i) => valid.includes(i))
|
||||
.map((relation, index) => {
|
||||
const relationObj = relations[relation];
|
||||
if (!relationObj) return `<td class='${relation}'>${relation}</td>`;
|
||||
|
||||
const objectState = pack.states[valid[index]];
|
||||
const tip = `${state.fullName} ${relationObj.inText} ${objectState.fullName}`;
|
||||
return `<td data-id=${objectState.i} data-tip='${tip}' class='${relation}'>${relation}</td>`;
|
||||
})
|
||||
.join("") +
|
||||
"</tr>";
|
||||
});
|
||||
message += `</table>`;
|
||||
alertMessage.innerHTML = message;
|
||||
|
||||
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
|
||||
|
||||
table += `</tbody></table>`;
|
||||
diplomacyMatrixBody.innerHTML = table;
|
||||
|
||||
const tableEl = diplomacyMatrixBody.querySelector("table");
|
||||
tableEl.addEventListener("click", function (event) {
|
||||
const el = event.target;
|
||||
if (el.tagName !== "TD") return;
|
||||
|
||||
const currentRelation = el.innerText;
|
||||
if (!relations[currentRelation]) return;
|
||||
|
||||
const subjectId = +el.closest("tr")?.dataset?.id;
|
||||
const objectId = +el?.dataset?.id;
|
||||
|
||||
selectRelation(subjectId, objectId, currentRelation);
|
||||
});
|
||||
|
||||
$("#diplomacyMatrix").dialog({title: "Relations matrix", position: {my: "center", at: "center", of: "svg"}, buttons: {}});
|
||||
}
|
||||
|
||||
function downloadDiplomacyData() {
|
||||
|
|
@ -288,7 +399,8 @@ function editDiplomacy() {
|
|||
clearMainTip();
|
||||
const selected = body.querySelector("div.Self");
|
||||
if (selected) selected.classList.remove("Self");
|
||||
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
else toggleStates();
|
||||
debug.selectAll(".highlight").remove();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,41 +265,48 @@ function getBurgSeed(burg) {
|
|||
}
|
||||
|
||||
function getMFCGlink(burg) {
|
||||
if (burg.link) return burg.link;
|
||||
|
||||
const {cells} = pack;
|
||||
const {name, population, cell} = burg;
|
||||
const burgSeed = getBurgSeed(burg);
|
||||
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
|
||||
const {i, name, population: burgPopulation, cell} = burg;
|
||||
const seed = getBurgSeed(burg);
|
||||
|
||||
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
|
||||
const size = minmax(Math.ceil(sizeRaw), 6, 100);
|
||||
const people = rn(population * populationRate * urbanization);
|
||||
const population = rn(burgPopulation * populationRate * urbanization);
|
||||
|
||||
const river = cells.r[cell] ? 1 : 0;
|
||||
const coast = Number(burg.port > 0);
|
||||
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : null;
|
||||
|
||||
const biome = cells.biome[cell];
|
||||
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
|
||||
const farms = +arableBiomes.includes(biome);
|
||||
|
||||
const citadel = +burg.citadel;
|
||||
const urban_castle = +(citadel && each(2)(i));
|
||||
|
||||
const hub = +cells.road[cell] > 50;
|
||||
const river = cells.r[cell] ? 1 : 0;
|
||||
|
||||
const coast = +burg.port;
|
||||
const citadel = +burg.citadel;
|
||||
const walls = +burg.walls;
|
||||
const plaza = +burg.plaza;
|
||||
const temple = +burg.temple;
|
||||
const shanty = +burg.shanty;
|
||||
const shantytown = +burg.shanty;
|
||||
|
||||
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
|
||||
function getSeaDirections(i) {
|
||||
const p1 = cells.p[i];
|
||||
const p2 = cells.p[cells.haven[i]];
|
||||
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
|
||||
if (deg < 0) deg += 360;
|
||||
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
|
||||
return "&sea=" + norm;
|
||||
return rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
|
||||
}
|
||||
|
||||
const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0";
|
||||
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
|
||||
return url;
|
||||
}
|
||||
const parameters = {name, population, size, seed, river, coast, farms, citadel, urban_castle, hub, plaza, temple, walls, shantytown, gates: -1};
|
||||
const url = new URL("https://watabou.github.io/city-generator");
|
||||
url.search = new URLSearchParams(parameters);
|
||||
if (sea) url.searchParams.append("sea", sea);
|
||||
|
||||
function toggleBurgLock(burg) {
|
||||
const b = pack.burgs[burg];
|
||||
b.lock = b.lock ? 0 : 1;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// draw legend box
|
||||
|
|
@ -418,10 +425,10 @@ function clearLegend() {
|
|||
function createPicker() {
|
||||
const pos = () => tip("Drag to change the picker position");
|
||||
const cl = () => tip("Click to close the picker");
|
||||
const closePicker = () => contaiter.style("display", "none");
|
||||
const closePicker = () => container.style("display", "none");
|
||||
|
||||
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
|
||||
contaiter
|
||||
const container = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
|
||||
container
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
|
|
@ -430,7 +437,7 @@ function createPicker() {
|
|||
.attr("opacity", 0.2)
|
||||
.on("mousemove", cl)
|
||||
.on("click", closePicker);
|
||||
const picker = contaiter
|
||||
const picker = container
|
||||
.append("g")
|
||||
.attr("id", "picker")
|
||||
.call(
|
||||
|
|
@ -487,7 +494,7 @@ function createPicker() {
|
|||
|
||||
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
|
||||
const hatches = picker.append("g").attr("id", "pickerHatches").attr("stroke", "#333333");
|
||||
const hatching = d3.selectAll("g#hatching > pattern");
|
||||
const hatching = d3.selectAll("g#defs-hatching > pattern");
|
||||
const number = hatching.size();
|
||||
|
||||
const clr = d3.range(number).map(i => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
|
||||
|
|
@ -497,8 +504,8 @@ function createPicker() {
|
|||
.attr("id", "picker_" + d)
|
||||
.attr("fill", d)
|
||||
.attr("class", i ? "" : "selected")
|
||||
.attr("x", i * 22 + 4)
|
||||
.attr("y", 40)
|
||||
.attr("x", (i % 14) * 22 + 4)
|
||||
.attr("y", 40 + Math.floor(i / 14)*20)
|
||||
.attr("width", 16)
|
||||
.attr("height", 16);
|
||||
});
|
||||
|
|
@ -508,20 +515,20 @@ function createPicker() {
|
|||
.append("rect")
|
||||
.attr("id", "picker_" + this.id)
|
||||
.attr("fill", "url(#" + this.id + ")")
|
||||
.attr("x", i * 22 + 4)
|
||||
.attr("y", 61)
|
||||
.attr("x", (i % 14) * 22 + 4)
|
||||
.attr("y", Math.floor(i / 14)*20 + 20 + (number * 2))
|
||||
.attr("width", 16)
|
||||
.attr("height", 16);
|
||||
.attr("height", 16)
|
||||
});
|
||||
|
||||
colors
|
||||
.selectAll("rect")
|
||||
.on("click", pickerFillClicked)
|
||||
.on("mousemove", () => tip("Click to fill with the color"));
|
||||
.on("mouseover", () => tip("Click to fill with the color"));
|
||||
hatches
|
||||
.selectAll("rect")
|
||||
.on("click", pickerFillClicked)
|
||||
.on("mousemove", () => tip("Click to fill with the hatching"));
|
||||
.on("mouseover", function() { tip("Click to fill with the hatching " + this.id) });
|
||||
|
||||
// append box
|
||||
const bbox = picker.node().getBBox();
|
||||
|
|
@ -537,10 +544,10 @@ function createPicker() {
|
|||
.attr("fill", "#ffffff")
|
||||
.attr("stroke", "#5d4651")
|
||||
.on("mousemove", pos);
|
||||
picker.insert("text", ":first-child").attr("x", 291).attr("y", -10).attr("id", "pickerCloseText").text("✕");
|
||||
picker.insert("text", ":first-child").attr("x", width-20).attr("y", -10).attr("id", "pickerCloseText").text("✕");
|
||||
picker
|
||||
.insert("rect", ":first-child")
|
||||
.attr("x", 288)
|
||||
.attr("x", width-23)
|
||||
.attr("y", -21)
|
||||
.attr("id", "pickerCloseRect")
|
||||
.attr("width", 14)
|
||||
|
|
|
|||
|
|
@ -460,32 +460,31 @@ function showInfo() {
|
|||
const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord");
|
||||
const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit");
|
||||
const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
|
||||
const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello");
|
||||
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
|
||||
|
||||
const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial");
|
||||
const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page");
|
||||
const VideoTutorial = link("https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C", "Video tutorial");
|
||||
|
||||
alertMessage.innerHTML = `
|
||||
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
|
||||
In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them.
|
||||
<b>Fantasy Map Generator</b> (FMG) is a free open-source application.
|
||||
It means that you own all created maps and can use them as you wish.
|
||||
|
||||
<p>The development is supported by community, you can donate on ${Patreon}.
|
||||
<p>The development is community-backed, you can donate on ${Patreon}.
|
||||
You can also help creating overviews, tutorials and spreding the word about the Generator.</p>
|
||||
|
||||
<p>The best way to get help is to contact the community on ${Discord} and ${Reddit}.
|
||||
Before asking questions, please check out the ${QuickStart} and the ${QAA}.</p>
|
||||
<p>The best way to get help is to contact the community on ${Discord} and ${Reddit}.
|
||||
Before asking questions, please check out the ${QuickStart}, the ${QAA}, and ${VideoTutorial}.</p>
|
||||
|
||||
<p>Track the development process on ${Trello}.</p>
|
||||
<p>Check out our another project: ${Armoria} — heraldry generator and editor.</p>
|
||||
|
||||
<p>Check out our new project: ${Armoria}, heraldry generator and editor.</p>
|
||||
|
||||
<b>Links:</b>
|
||||
<ul style="columns:2">
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}</li>
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}</li>
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}</li>
|
||||
<li>${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}</li>
|
||||
<li>${link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Devboard")}</li>
|
||||
<li><a href="mailto:azgaar.fmg@yandex.by" target="_blank">Contact Azgaar</a></li>
|
||||
</ul>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
|
|
|
|||
|
|
@ -470,23 +470,26 @@ function togglePrec(event) {
|
|||
|
||||
function drawPrec() {
|
||||
prec.selectAll("circle").remove();
|
||||
const cells = grid.cells,
|
||||
p = grid.points;
|
||||
const {cells, points} = grid;
|
||||
|
||||
prec.style("display", "block");
|
||||
const show = d3.transition().duration(800).ease(d3.easeSinIn);
|
||||
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
|
||||
|
||||
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
|
||||
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
|
||||
const getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
|
||||
|
||||
prec
|
||||
.selectAll("circle")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("cx", d => p[d][0])
|
||||
.attr("cy", d => p[d][1])
|
||||
.attr("cx", d => points[d][0])
|
||||
.attr("cy", d => points[d][1])
|
||||
.attr("r", 0)
|
||||
.transition(show)
|
||||
.attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * 0.5), 0.8), 2));
|
||||
.attr("r", d => getRadius(cells.prec[d]));
|
||||
}
|
||||
|
||||
function togglePopulation(event) {
|
||||
|
|
|
|||
|
|
@ -75,12 +75,9 @@ function overviewMilitary() {
|
|||
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
|
||||
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
|
||||
|
||||
lines += `<div class="states" data-id=${s.i} data-state="${
|
||||
s.name
|
||||
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
|
||||
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
|
||||
s.color
|
||||
}" class="fillRect"></svg>
|
||||
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}"
|
||||
data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
|
||||
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
|
||||
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
|
||||
${lineData}
|
||||
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
|
||||
|
|
|
|||
|
|
@ -17,18 +17,24 @@ function editNamesbase() {
|
|||
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
|
||||
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
|
||||
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
|
||||
document.getElementById("namesbaseAnalize").addEventListener("click", analizeNamesbase);
|
||||
document.getElementById("namesbaseAnalyze").addEventListener("click", analyzeNamesbase);
|
||||
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
|
||||
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
|
||||
document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click());
|
||||
document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)});
|
||||
document.getElementById("namesbaseUpload").addEventListener("click", () => document.getElementById("namesbaseToLoad").click());
|
||||
document.getElementById("namesbaseToLoad").addEventListener("change", function () {
|
||||
uploadFile(this, namesbaseUpload);
|
||||
});
|
||||
document.getElementById("namesbaseCA").addEventListener("click", () => {
|
||||
openURL("https://cartographyassets.com/asset-category/specific-assets/azgaars-generator/namebases/");
|
||||
});
|
||||
document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent));
|
||||
|
||||
createBasesList();
|
||||
updateInputs();
|
||||
|
||||
$("#namesbaseEditor").dialog({
|
||||
title: "Namesbase Editor", width: "42.5em",
|
||||
title: "Namesbase Editor",
|
||||
width: "auto",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
|
|
@ -40,7 +46,10 @@ function editNamesbase() {
|
|||
|
||||
function updateInputs() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
|
||||
if (!nameBases[base]) {
|
||||
tip(`Namesbase ${base} is not defined`, false, "error");
|
||||
return;
|
||||
}
|
||||
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
|
||||
document.getElementById("namesbaseName").value = nameBases[base].name;
|
||||
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
||||
|
|
@ -52,7 +61,7 @@ function editNamesbase() {
|
|||
function updateExamples() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
let examples = "";
|
||||
for (let i=0; i < 10; i++) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const example = Names.getBase(base);
|
||||
if (example === undefined) {
|
||||
examples = "Cannot generate examples. Please verify the data";
|
||||
|
|
@ -84,13 +93,19 @@ function editNamesbase() {
|
|||
|
||||
function updateBaseMin() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;}
|
||||
if (+this.value > nameBases[base].max) {
|
||||
tip("Minimal length cannot be greater than maximal", false, "error");
|
||||
return;
|
||||
}
|
||||
nameBases[base].min = +this.value;
|
||||
}
|
||||
|
||||
function updateBaseMax() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;}
|
||||
if (+this.value < nameBases[base].min) {
|
||||
tip("Maximal length should be greater than minimal", false, "error");
|
||||
return;
|
||||
}
|
||||
nameBases[base].max = +this.value;
|
||||
}
|
||||
|
||||
|
|
@ -99,59 +114,70 @@ function editNamesbase() {
|
|||
nameBases[base].d = this.value;
|
||||
}
|
||||
|
||||
function analizeNamesbase() {
|
||||
const string = document.getElementById("namesbaseTextarea").value;
|
||||
if (!string) {tip("Names data field should not be empty", false, "error"); return;}
|
||||
const base = string.toLowerCase();
|
||||
const array = base.split(",");
|
||||
const l = array.length;
|
||||
if (!l) {tip("Names data should not be empty", false, "error"); return;}
|
||||
function analyzeNamesbase() {
|
||||
const namesSourceString = document.getElementById("namesbaseTextarea").value;
|
||||
const namesArray = namesSourceString.toLowerCase().split(",");
|
||||
const length = namesArray.length;
|
||||
if (!namesSourceString || !length) return tip("Names data should not be empty", false, "error");
|
||||
|
||||
const wordsLength = array.map(n => n.length);
|
||||
const multi = rn(d3.mean(array.map(n => (n.match(/ /i)||[]).length)) * 100, 2);
|
||||
const geminate = array.map(name => name.match(/[^\w\s]|(.)(?=\1)/g)||[]).flat();
|
||||
const doubled = ([...new Set(geminate)].filter(l => geminate.filter(d => d === l).length > 3)||["none"]).join("");
|
||||
const chain = Names.calculateChain(string);
|
||||
const depth = rn(d3.mean(Object.keys(chain).map(key => chain[key].filter(c => c !== " ").length)));
|
||||
const nonLatin = (string.match(/[^\u0000-\u007f]/g)||["none"]).join("");
|
||||
const chain = Names.calculateChain(namesSourceString);
|
||||
const variety = rn(d3.mean(Object.values(chain).map(keyValue => keyValue.length)));
|
||||
|
||||
const lengthStat =
|
||||
l < 30 ? "<span style='color:red'>[not enough]</span>" :
|
||||
l < 150 ? "<span style='color:darkred'>[low]</span>" :
|
||||
l < 150 ? "<span style='color:orange'>[low]</span>" :
|
||||
l < 400 ? "<span style='color:green'>[good]</span>" :
|
||||
l < 600 ? "<span style='color:orange'>[overmuch]</span>" :
|
||||
"<span style='color:darkred'>[overmuch]</span>";
|
||||
const wordsLength = namesArray.map(n => n.length);
|
||||
|
||||
const rangeStat =
|
||||
l < 10 ? "<span style='color:red'>[low]</span>" :
|
||||
l < 15 ? "<span style='color:darkred'>[low]</span>" :
|
||||
l < 20 ? "<span style='color:orange'>[low]</span>" :
|
||||
"<span style='color:green'>[good]</span>";
|
||||
const nonLatin = namesSourceString.match(/[^\u0000-\u007f]/g);
|
||||
const nonBasicLatinChars = nonLatin
|
||||
? unique(
|
||||
namesSourceString
|
||||
.match(/[^\u0000-\u007f]/g)
|
||||
.join("")
|
||||
.toLowerCase()
|
||||
).join("")
|
||||
: "none";
|
||||
|
||||
const depthStat =
|
||||
l < 15 ? "<span style='color:red'>[low]</span>" :
|
||||
l < 20 ? "<span style='color:darkred'>[low]</span>" :
|
||||
l < 25 ? "<span style='color:orange'>[low]</span>" :
|
||||
"<span style='color:green'>[good]</span>";
|
||||
const geminate = namesArray.map(name => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
|
||||
const doubled = unique(geminate).filter(char => geminate.filter(doudledChar => doudledChar === char).length > 3) || ["none"];
|
||||
|
||||
const duplicates = unique(namesArray.filter((e, i, a) => a.indexOf(e) !== i)).join(", ") || "none";
|
||||
const multiwordRate = d3.mean(namesArray.map(n => +n.includes(" ")));
|
||||
|
||||
const getLengthQuality = () => {
|
||||
if (length < 30) return "<span data-tip='Namesbase contains < 30 names - not enough to generate reasonable data' style='color:red'>[not enough]</span>";
|
||||
if (length < 100) return "<span data-tip='Namesbase contains < 100 names - not enough to generate good names' style='color:darkred'>[low]</span>";
|
||||
if (length <= 400) return "<span data-tip='Namesbase contains a reasonable number of samples' style='color:green'>[good]</span>";
|
||||
return "<span data-tip='Namesbase contains > 400 names. That is too much, try to reduce it to ~300 names' style='color:darkred'>[overmuch]</span>";
|
||||
};
|
||||
|
||||
const getVarietyLevel = () => {
|
||||
if (variety < 15) return "<span data-tip='Namesbase average variety < 15 - generated names will be too repetitive' style='color:red'>[low]</span>";
|
||||
if (variety < 30) return "<span data-tip='Namesbase average variety < 30 - names can be too repetitive' style='color:orange'>[mean]</span>";
|
||||
return "<span data-tip='Namesbase variety is good' style='color:green'>[good]</span>";
|
||||
};
|
||||
|
||||
alertMessage.innerHTML = `<div style="line-height: 1.6em; max-width: 20em">
|
||||
<div>Namesbase length: ${l} ${lengthStat}</div>
|
||||
<div>Namesbase range: ${Object.keys(chain).length-1} ${rangeStat}</div>
|
||||
<div>Namesbase depth: ${depth} ${depthStat}</div>
|
||||
<div>Non-basic chars: ${nonLatin}</div>
|
||||
<div data-tip="Number of names provided">Namesbase length: ${length} ${getLengthQuality()}</div>
|
||||
<div data-tip="Average number of generation variants for each key in the chain">Namesbase variety: ${variety} ${getVarietyLevel()}</div>
|
||||
<hr>
|
||||
<div>Min name length: ${d3.min(wordsLength)}</div>
|
||||
<div>Max name length: ${d3.max(wordsLength)}</div>
|
||||
<div>Mean name length: ${rn(d3.mean(wordsLength), 1)}</div>
|
||||
<div>Median name length: ${d3.median(wordsLength)}</div>
|
||||
<div>Doubled chars: ${doubled}</div>
|
||||
<div>Multi-word names: ${multi}%</div>
|
||||
<div data-tip="The shortest name length">Min name length: ${d3.min(wordsLength)}</div>
|
||||
<div data-tip="The longest name length">Max name length: ${d3.max(wordsLength)}</div>
|
||||
<div data-tip="Average name length">Mean name length: ${rn(d3.mean(wordsLength), 1)}</div>
|
||||
<div data-tip="Common name length">Median name length: ${d3.median(wordsLength)}</div>
|
||||
<hr>
|
||||
<div data-tip="Characters outside of Basic Latin have bad font support">Non-basic chars: ${nonBasicLatinChars}</div>
|
||||
<div data-tip="Characters that are frequently (more than 3 times) doubled">Doubled chars: ${doubled.join("")}</div>
|
||||
<div data-tip="Names used more than one time">Duplicates: ${duplicates}</div>
|
||||
<div data-tip="Percentage of names containing space character">Multi-word names: ${rn(multiwordRate * 100, 2)}%</div>
|
||||
</div>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false, title: "Data Analysis",
|
||||
resizable: false,
|
||||
title: "Data Analysis",
|
||||
position: {my: "left top-30", at: "right+10 top", of: "#namesbaseEditor"},
|
||||
buttons: {OK: function() {$(this).dialog("close");}}
|
||||
buttons: {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -171,35 +197,42 @@ function editNamesbase() {
|
|||
|
||||
function namesbaseRestoreDefault() {
|
||||
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
|
||||
$("#alert").dialog({resizable: false, title: "Restore default data",
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Restore default data",
|
||||
buttons: {
|
||||
Restore: function() {
|
||||
Restore: function () {
|
||||
$(this).dialog("close");
|
||||
Names.clearChains();
|
||||
nameBases = Names.getNameBases();
|
||||
createBasesList();
|
||||
updateInputs();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function namesbaseDownload() {
|
||||
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
|
||||
const data = nameBases.map((b, i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
|
||||
const name = getFileName("Namesbase") + ".txt";
|
||||
downloadFile(data, name);
|
||||
}
|
||||
|
||||
function namesbaseUpload(dataLoaded) {
|
||||
const data = dataLoaded.split("\r\n");
|
||||
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
|
||||
if (!data || !data[0]) {
|
||||
tip("Cannot load a namesbase. Please check the data format", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
Names.clearChains();
|
||||
nameBases = [];
|
||||
data.forEach(d => {
|
||||
const e = d.split("|");
|
||||
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4], b:e[5]});
|
||||
nameBases.push({name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b: e[5]});
|
||||
});
|
||||
|
||||
createBasesList();
|
||||
|
|
|
|||
|
|
@ -102,7 +102,9 @@ function showSupporters() {
|
|||
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
|
||||
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
|
||||
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
|
||||
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger`;
|
||||
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
|
||||
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce,
|
||||
Jordan,William Markus,Sidr Dim`;
|
||||
|
||||
const array = supporters
|
||||
.replace(/(?:\r\n|\r|\n)/g, "")
|
||||
|
|
@ -287,18 +289,16 @@ function generateMapWithSeed(source) {
|
|||
}
|
||||
|
||||
function showSeedHistoryDialog() {
|
||||
const alert = mapHistory
|
||||
.map(function (h, i) {
|
||||
const created = new Date(h.created).toLocaleTimeString();
|
||||
const button = `<i data-tip"Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
|
||||
return `<div>${i + 1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`;
|
||||
})
|
||||
.join("");
|
||||
alertMessage.innerHTML = alert;
|
||||
const lines = mapHistory.map((h, i) => {
|
||||
const created = new Date(h.created).toLocaleTimeString();
|
||||
const button = `<i data-tip="Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
|
||||
return `<li>Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</li>`;
|
||||
});
|
||||
alertMessage.innerHTML = `<ol style="margin: 0; padding-left: 1.5em">${lines.join("")}</ol>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Seed history",
|
||||
width: fitContent(),
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ function editProvinces() {
|
|||
cl = el.classList,
|
||||
line = el.parentNode,
|
||||
p = +line.dataset.id;
|
||||
if (cl.contains("fillRect")) changeFill(el);
|
||||
|
||||
if (el.tagName === "FILL-BOX") changeFill(el);
|
||||
else if (cl.contains("name")) editProvinceName(p);
|
||||
else if (cl.contains("coaIcon")) editEmblem("province", "provinceCOA" + p, pack.provinces[p]);
|
||||
else if (cl.contains("icon-star-empty")) capitalZoomIn(p);
|
||||
|
|
@ -133,11 +134,9 @@ function editProvinces() {
|
|||
lines += `<div class="states" data-id=${p.i} data-name="${p.name}" data-form="${p.formName}" data-color="${
|
||||
p.color
|
||||
}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
|
||||
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
|
||||
p.color
|
||||
}" class="fillRect pointer"></svg>
|
||||
<fill-box fill="${p.color}"></fill-box>
|
||||
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
|
||||
<svg data-tip="Click to show and edit province emblem" class="coaIcon hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
|
||||
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
|
||||
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly>
|
||||
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span>
|
||||
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${
|
||||
|
|
@ -215,14 +214,14 @@ function editProvinces() {
|
|||
|
||||
function changeFill(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const p = +el.parentNode.parentNode.dataset.id;
|
||||
const p = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.provinces[p].color = fill;
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
pack.provinces[p].color = newFill;
|
||||
const g = provs.select("#provincesBody");
|
||||
g.select("#province" + p).attr("fill", fill);
|
||||
g.select("#province-gap" + p).attr("stroke", fill);
|
||||
g.select("#province" + p).attr("fill", newFill);
|
||||
g.select("#province-gap" + p).attr("stroke", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ function overviewRegiments(state) {
|
|||
updateHeaders();
|
||||
|
||||
$("#regimentsOverview").dialog({
|
||||
title: "Regiments Overview", resizable: false, width: fitContent(),
|
||||
title: "Regiments Overview",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
|
|
@ -31,11 +33,13 @@ function overviewRegiments(state) {
|
|||
header.querySelectorAll(".removable").forEach(el => el.remove());
|
||||
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
|
||||
for (const u of options.military) {
|
||||
const label = capitalize(u.name.replace(/_/g, ' '));
|
||||
const label = capitalize(u.name.replace(/_/g, " "));
|
||||
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label} </div>`);
|
||||
}
|
||||
header.querySelectorAll(".removable").forEach(function(e) {
|
||||
e.addEventListener("click", function() {sortLines(this);});
|
||||
header.querySelectorAll(".removable").forEach(function (e) {
|
||||
e.addEventListener("click", function () {
|
||||
sortLines(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +55,13 @@ function overviewRegiments(state) {
|
|||
if (state !== -1 && s.i !== state) continue; // specific state is selected
|
||||
|
||||
for (const r of s.military) {
|
||||
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name]||0}`).join(" ");
|
||||
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name]||0}</div>`).join(" ");
|
||||
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" ");
|
||||
const lineData = options.military
|
||||
.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`)
|
||||
.join(" ");
|
||||
|
||||
lines += `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
|
||||
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
|
||||
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
|
||||
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
|
||||
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
|
||||
<input data-tip="Regiment's name" style="width:13em" value="${r.name}" readonly>
|
||||
|
|
@ -70,12 +76,15 @@ function overviewRegiments(state) {
|
|||
|
||||
lines += `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
|
||||
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
|
||||
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name]||0)))}</div>`).join(" ")}
|
||||
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`).join(" ")}
|
||||
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
|
||||
</div>`;
|
||||
|
||||
body.insertAdjacentHTML("beforeend", lines);
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
if (body.dataset.type === "percentage") {
|
||||
body.dataset.type = "absolute";
|
||||
togglePercentageMode();
|
||||
}
|
||||
applySorting(regimentsHeader);
|
||||
|
||||
// add listeners
|
||||
|
|
@ -87,7 +96,7 @@ function overviewRegiments(state) {
|
|||
const filter = document.getElementById("regimentsFilter");
|
||||
filter.options.length = 0; // remove all options
|
||||
filter.options.add(new Option(`all`, -1, false, state === -1));
|
||||
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
|
||||
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
statesSorted.forEach(s => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
|
||||
}
|
||||
|
||||
|
|
@ -108,19 +117,20 @@ function overviewRegiments(state) {
|
|||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
const lines = body.querySelectorAll(":scope > div:not(.totalLine)");
|
||||
const array = Array.from(lines), cache = [];
|
||||
const array = Array.from(lines),
|
||||
cache = [];
|
||||
|
||||
const total = function(type) {
|
||||
const total = function (type) {
|
||||
if (cache[type]) cache[type];
|
||||
cache[type] = d3.sum(array.map(el => +el.dataset[type]));
|
||||
return cache[type];
|
||||
}
|
||||
};
|
||||
|
||||
lines.forEach(function(el) {
|
||||
el.querySelectorAll("div").forEach(function(div) {
|
||||
lines.forEach(function (el) {
|
||||
el.querySelectorAll("div").forEach(function (div) {
|
||||
const type = div.dataset.type;
|
||||
if (type === "rate") return;
|
||||
div.textContent = total(type) ? rn(+el.dataset[type] / total(type) * 100) + "%" : "0%";
|
||||
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + "%" : "0%";
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
|
@ -145,15 +155,19 @@ function overviewRegiments(state) {
|
|||
|
||||
function addRegimentOnClick() {
|
||||
const state = +regimentsFilter.value;
|
||||
if (state === -1) {tip("Please select state from the list", false, "error"); return;}
|
||||
if (state === -1) {
|
||||
tip("Please select state from the list", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const point = d3.mouse(this);
|
||||
const cell = findCell(point[0], point[1]);
|
||||
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
|
||||
const x = pack.cells.p[cell][0],
|
||||
y = pack.cells.p[cell][1];
|
||||
const military = pack.states[state].military;
|
||||
const i = military.length ? last(military).i + 1 : 0;
|
||||
const n = +(pack.cells.h[cell] < 20); // naval or land
|
||||
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
|
||||
const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: "🛡️"};
|
||||
reg.name = Military.getName(reg, military);
|
||||
military.push(reg);
|
||||
Military.generateNote(reg, pack.states[state]); // add legend
|
||||
|
|
@ -163,9 +177,9 @@ function overviewRegiments(state) {
|
|||
|
||||
function downloadRegimentsData() {
|
||||
const units = options.military.map(u => u.name);
|
||||
let data = "State,Id,Name,"+units.map(u => capitalize(u)).join(",")+",Total\n"; // headers
|
||||
let data = "State,Id,Name," + units.map(u => capitalize(u)).join(",") + ",Total\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function(el) {
|
||||
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function (el) {
|
||||
data += el.dataset.state + ",";
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
|
|
@ -176,5 +190,4 @@ function overviewRegiments(state) {
|
|||
const name = getFileName("Regiments") + ".csv";
|
||||
downloadFile(data, name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function editReligions() {
|
|||
if (r.i) {
|
||||
lines += `<div class="states religions" data-id=${r.i} data-name="${r.name}" data-color="${r.color}" data-area=${area}
|
||||
data-population=${population} data-type=${r.type} data-form=${r.form} data-deity="${r.deity ? r.deity : ""}" data-expansionism=${r.expansionism}>
|
||||
<svg data-tip="Religion fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${r.color}" class="fillRect pointer"></svg>
|
||||
<fill-box fill="${r.color}"></fill-box>
|
||||
<input data-tip="Religion name. Click and type to change" class="religionName" value="${r.name}" autocorrect="off" spellcheck="false">
|
||||
<select data-tip="Religion type" class="religionType">${getTypeOptions(r.type)}</select>
|
||||
<input data-tip="Religion form" class="religionForm hide" value="${r.form}" autocorrect="off" spellcheck="false">
|
||||
|
|
@ -93,7 +93,9 @@ function editReligions() {
|
|||
</div>`;
|
||||
} else {
|
||||
// No religion (neutral) line
|
||||
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-color="" data-area=${area} data-population=${population} data-type="" data-form="" data-deity="" data-expansionism="">
|
||||
lines += `<div class="states" data-id=${r.i} data-name="${
|
||||
r.name
|
||||
}" data-color="" data-area=${area} data-population=${population} data-type="" data-form="" data-deity="" data-expansionism="">
|
||||
<svg width="9" height="9" class="placeholder"></svg>
|
||||
<input data-tip="Religion name. Click and type to change" class="religionName italic" value="${r.name}" autocorrect="off" spellcheck="false">
|
||||
<select data-tip="Religion type" class="religionType placeholder">${getTypeOptions(r.type)}</select>
|
||||
|
|
@ -124,7 +126,7 @@ function editReligions() {
|
|||
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev)));
|
||||
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick));
|
||||
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", religionChangeColor));
|
||||
body.querySelectorAll("fill-box").forEach(el => el.addEventListener("click", religionChangeColor));
|
||||
body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName));
|
||||
body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType));
|
||||
body.querySelectorAll("div > input.religionForm").forEach(el => el.addEventListener("input", religionChangeForm));
|
||||
|
|
@ -215,13 +217,13 @@ function editReligions() {
|
|||
function religionChangeColor() {
|
||||
const el = this;
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const religion = +el.parentNode.parentNode.dataset.id;
|
||||
const religion = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.religions[religion].color = fill;
|
||||
relig.select("#religion" + religion).attr("fill", fill);
|
||||
debug.select("#religionsCenter" + religion).attr("fill", fill);
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
pack.religions[religion].color = newFill;
|
||||
relig.select("#religion" + religion).attr("fill", newFill);
|
||||
debug.select("#religionsCenter" + religion).attr("fill", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
|
|
@ -459,7 +461,13 @@ function editReligions() {
|
|||
|
||||
// prepare svg
|
||||
alertMessage.innerHTML = "<div id='religionInfo' class='chartInfo'>‍</div>";
|
||||
const svg = d3.select("#alertMessage").insert("svg", "#religionInfo").attr("id", "hierarchy").attr("width", width).attr("height", height).style("text-anchor", "middle");
|
||||
const svg = d3
|
||||
.select("#alertMessage")
|
||||
.insert("svg", "#religionInfo")
|
||||
.attr("id", "hierarchy")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.style("text-anchor", "middle");
|
||||
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
|
||||
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
|
||||
const nodes = graph.append("g");
|
||||
|
|
@ -473,7 +481,24 @@ function editReligions() {
|
|||
.enter()
|
||||
.append("path")
|
||||
.attr("d", d => {
|
||||
return "M" + d.source.x + "," + d.source.y + "C" + d.source.x + "," + (d.source.y * 3 + d.target.y) / 4 + " " + d.target.x + "," + (d.source.y * 2 + d.target.y) / 3 + " " + d.target.x + "," + d.target.y;
|
||||
return (
|
||||
"M" +
|
||||
d.source.x +
|
||||
"," +
|
||||
d.source.y +
|
||||
"C" +
|
||||
d.source.x +
|
||||
"," +
|
||||
(d.source.y * 3 + d.target.y) / 4 +
|
||||
" " +
|
||||
d.target.x +
|
||||
"," +
|
||||
(d.source.y * 2 + d.target.y) / 3 +
|
||||
" " +
|
||||
d.target.x +
|
||||
"," +
|
||||
d.target.y
|
||||
);
|
||||
});
|
||||
|
||||
const node = nodes
|
||||
|
|
@ -578,7 +603,11 @@ function editReligions() {
|
|||
$("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
tip("Click on religion to select, drag the circle to change religion", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", selectReligionOnMapClick).call(d3.drag().on("start", dragReligionBrush)).on("touchmove mousemove", moveReligionBrush);
|
||||
viewbox
|
||||
.style("cursor", "crosshair")
|
||||
.on("click", selectReligionOnMapClick)
|
||||
.call(d3.drag().on("start", dragReligionBrush))
|
||||
.on("touchmove mousemove", moveReligionBrush);
|
||||
|
||||
body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ function createRiver() {
|
|||
const source = riverCells[0];
|
||||
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
|
||||
const sourceWidth = 0.05;
|
||||
const widthFactor = 1.2;
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor = 1.2 * defaultWidthFactor;
|
||||
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function editStates() {
|
|||
cl = el.classList,
|
||||
line = el.parentNode,
|
||||
state = +line.dataset.id;
|
||||
if (cl.contains("fillRect")) stateChangeFill(el);
|
||||
if (el.tagName === "FILL-BOX") stateChangeFill(el);
|
||||
else if (cl.contains("name")) editStateName(state);
|
||||
else if (cl.contains("coaIcon")) editEmblem("state", "stateCOA" + state, pack.states[state]);
|
||||
else if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state);
|
||||
|
|
@ -102,7 +102,7 @@ function editStates() {
|
|||
// Neutral line
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
|
||||
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
|
||||
<svg width="9" height="9" class="placeholder"></svg>
|
||||
<svg width="1em" height="1em" class="placeholder"></svg>
|
||||
<input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${s.name}" readonly>
|
||||
<svg class="coaIcon placeholder hide"></svg>
|
||||
<input class="stateForm placeholder" value="none">
|
||||
|
|
@ -126,17 +126,12 @@ function editStates() {
|
|||
|
||||
const capital = pack.burgs[s.capital].name;
|
||||
COArenderer.trigger("stateCOA" + s.i, s.coa);
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}" data-color="${
|
||||
s.color
|
||||
}" data-cells=${s.cells}
|
||||
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${
|
||||
s.type
|
||||
} data-expansionism=${s.expansionism}>
|
||||
<svg data-tip="State fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
|
||||
s.color
|
||||
}" class="fillRect pointer"></svg>
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}"
|
||||
data-color="${s.color}" data-cells=${s.cells} data-area=${area} data-population=${population} data-burgs=${s.burgs}
|
||||
data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
|
||||
<fill-box fill="${s.color}"></fill-box>
|
||||
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly>
|
||||
<svg data-tip="Click to show and edit state emblem" class="coaIcon hide" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
|
||||
<svg data-tip="Click to show and edit state emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
|
||||
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly>
|
||||
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
|
||||
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>
|
||||
|
|
@ -149,9 +144,8 @@ function editStates() {
|
|||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
<select data-tip="State type. Defines growth model. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
|
||||
<span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${
|
||||
s.expansionism
|
||||
}>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value"
|
||||
class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
|
||||
<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>
|
||||
|
|
@ -237,18 +231,18 @@ function editStates() {
|
|||
|
||||
function stateChangeFill(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const state = +el.parentNode.parentNode.dataset.id;
|
||||
const state = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.states[state].color = fill;
|
||||
statesBody.select("#state" + state).attr("fill", fill);
|
||||
statesBody.select("#state-gap" + state).attr("stroke", fill);
|
||||
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
|
||||
const callback = function (newFill) {
|
||||
el.fill = newFill;
|
||||
pack.states[state].color = newFill;
|
||||
statesBody.select("#state" + state).attr("fill", newFill);
|
||||
statesBody.select("#state-gap" + state).attr("stroke", newFill);
|
||||
const halo = d3.color(newFill) ? d3.color(newFill).darker().hex() : "#666666";
|
||||
statesHalo.select("#state-border" + state).attr("stroke", halo);
|
||||
|
||||
// recolor regiments
|
||||
const solidColor = fill[0] === "#" ? fill : "#999";
|
||||
const solidColor = newFill[0] === "#" ? newFill : "#999";
|
||||
const darkerColor = d3.color(solidColor).darker().hex();
|
||||
armies.select("#army" + state).attr("fill", solidColor);
|
||||
armies
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
311
modules/ui/stylePresets.js
Normal file
311
modules/ui/stylePresets.js
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
// UI module to control the style presets
|
||||
"use strict";
|
||||
|
||||
const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"];
|
||||
const customPresetPrefix = "fmgStyle_";
|
||||
|
||||
// add style presets to list
|
||||
{
|
||||
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
|
||||
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix));
|
||||
const customOptions = storedStyles.map(styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`);
|
||||
const options = systemOptions.join("") + customOptions.join("");
|
||||
document.getElementById("stylePreset").innerHTML = options;
|
||||
}
|
||||
|
||||
async function applyStyleOnLoad() {
|
||||
const desiredPreset = localStorage.getItem("presetStyle") || "default";
|
||||
const styleData = await getStylePreset(desiredPreset);
|
||||
const [appliedPreset, style] = styleData;
|
||||
|
||||
applyStyle(style);
|
||||
updateMapFilter();
|
||||
stylePreset.value = stylePreset.dataset.old = appliedPreset;
|
||||
setPresetRemoveButtonVisibiliy();
|
||||
}
|
||||
|
||||
async function getStylePreset(desiredPreset) {
|
||||
let presetToLoad = desiredPreset;
|
||||
|
||||
const isCustom = !systemPresets.includes(desiredPreset);
|
||||
if (isCustom) {
|
||||
const storedStyleJSON = localStorage.getItem(desiredPreset);
|
||||
if (!storedStyleJSON) {
|
||||
ERROR && console.error(`Custom style ${desiredPreset} in not found in localStorage. Applying default style`);
|
||||
presetToLoad = "default";
|
||||
} else {
|
||||
const isValid = JSON.isValid(storedStyleJSON);
|
||||
if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)];
|
||||
|
||||
ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
|
||||
presetToLoad = "default";
|
||||
}
|
||||
}
|
||||
|
||||
const style = await fetchSystemPreset(presetToLoad);
|
||||
return [presetToLoad, style];
|
||||
}
|
||||
|
||||
async function fetchSystemPreset(preset) {
|
||||
const style = await fetch(`./styles/${preset}.json`)
|
||||
.then(res => res.json())
|
||||
.catch(err => {
|
||||
ERROR && console.error("Error on loading style preset", preset, err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!style) throw new Error("Cannot fetch style preset", preset);
|
||||
return style;
|
||||
}
|
||||
|
||||
function applyStyle(style) {
|
||||
for (const selector in style) {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) continue;
|
||||
for (const attribute in style[selector]) {
|
||||
const value = style[selector][attribute];
|
||||
|
||||
if (value === "null" || value === null) {
|
||||
el.removeAttribute(attribute);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute === "text-shadow") {
|
||||
el.style[attribute] = value;
|
||||
} else {
|
||||
el.setAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestStylePresetChange(preset) {
|
||||
const isConfirmed = sessionStorage.getItem("styleChangeConfirmed");
|
||||
if (isConfirmed) {
|
||||
changeStyle(preset);
|
||||
return;
|
||||
}
|
||||
|
||||
confirmationDialog({
|
||||
title: "Change style preset",
|
||||
message: "Are you sure you want to change the style preset? All unsaved style changes will be lost",
|
||||
confirm: "Change",
|
||||
onConfirm: () => {
|
||||
sessionStorage.setItem("styleChangeConfirmed", true);
|
||||
changeStyle(preset);
|
||||
},
|
||||
onCancel: () => {
|
||||
stylePreset.value = stylePreset.dataset.old;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function changeStyle(desiredPreset) {
|
||||
const styleData = await getStylePreset(desiredPreset);
|
||||
const [appliedPreset, style] = styleData;
|
||||
localStorage.setItem("presetStyle", appliedPreset);
|
||||
applyStyleWithUiRefresh(style);
|
||||
}
|
||||
|
||||
function applyStyleWithUiRefresh(style) {
|
||||
applyStyle(style);
|
||||
updateElements();
|
||||
selectStyleElement(); // re-select element to trigger values update
|
||||
updateMapFilter();
|
||||
stylePreset.dataset.old = stylePreset.value;
|
||||
|
||||
invokeActiveZooming();
|
||||
setPresetRemoveButtonVisibiliy();
|
||||
}
|
||||
|
||||
function addStylePreset() {
|
||||
$("#styleSaver").dialog({title: "Style Saver", width: "26em", position: {my: "center", at: "center", of: "svg"}});
|
||||
|
||||
const styleName = stylePreset.value.replace(customPresetPrefix, "");
|
||||
document.getElementById("styleSaverName").value = styleName;
|
||||
styleSaverJSON.value = JSON.stringify(collectStyleData(), null, 2);
|
||||
checkName();
|
||||
|
||||
if (modules.saveStyle) return;
|
||||
modules.saveStyle = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("styleSaverName").addEventListener("input", checkName);
|
||||
document.getElementById("styleSaverSave").addEventListener("click", saveStyle);
|
||||
document.getElementById("styleSaverDownload").addEventListener("click", styleDownload);
|
||||
document.getElementById("styleSaverLoad").addEventListener("click", () => styleToLoad.click());
|
||||
document.getElementById("styleToLoad").addEventListener("change", loadStyleFile);
|
||||
|
||||
function collectStyleData() {
|
||||
const style = {};
|
||||
const attributes = {
|
||||
"#map": ["background-color", "filter", "data-filter"],
|
||||
"#armies": ["font-size", "box-size", "stroke", "stroke-width", "fill-opacity", "filter"],
|
||||
"#biomes": ["opacity", "filter", "mask"],
|
||||
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
|
||||
"#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
|
||||
"#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
|
||||
"#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"],
|
||||
"#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
|
||||
"#rose": ["transform"],
|
||||
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
|
||||
"#cults": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
|
||||
"#landmass": ["opacity", "fill", "filter"],
|
||||
"#markers": ["opacity", "rescale", "filter"],
|
||||
"#prec": ["opacity", "stroke", "stroke-width", "fill", "filter"],
|
||||
"#population": ["opacity", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
|
||||
"#rural": ["stroke"],
|
||||
"#urban": ["stroke"],
|
||||
"#freshwater": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#salt": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#sinkhole": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#frozen": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#lava": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#dry": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#sea_island": ["opacity", "stroke", "stroke-width", "filter", "auto-filter"],
|
||||
"#lake_island": ["opacity", "stroke", "stroke-width", "filter"],
|
||||
"#terrain": ["opacity", "set", "size", "density", "filter", "mask"],
|
||||
"#rivers": ["opacity", "filter", "fill"],
|
||||
"#ruler": ["opacity", "filter"],
|
||||
"#roads": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#trails": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#searoutes": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#statesBody": ["opacity", "filter"],
|
||||
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
|
||||
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
|
||||
"#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
|
||||
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
|
||||
"#emblems": ["opacity", "stroke-width", "filter"],
|
||||
"#texture": ["opacity", "filter", "mask"],
|
||||
"#textureImage": ["x", "y"],
|
||||
"#zones": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
|
||||
"#oceanLayers": ["filter", "layers"],
|
||||
"#oceanBase": ["fill"],
|
||||
"#oceanicPattern": ["href", "opacity"],
|
||||
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
|
||||
"#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"],
|
||||
"#legendBox": ["fill", "fill-opacity"],
|
||||
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
|
||||
"#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
|
||||
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
|
||||
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
|
||||
"#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
|
||||
"#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"],
|
||||
"#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
|
||||
"#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
|
||||
"#fogging": ["opacity", "fill", "filter"]
|
||||
};
|
||||
|
||||
for (const selector in attributes) {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) continue;
|
||||
|
||||
style[selector] = {};
|
||||
for (const attr of attributes[selector]) {
|
||||
let value = el.style[attr] || el.getAttribute(attr);
|
||||
if (attr === "font-size" && el.hasAttribute("data-size")) value = el.getAttribute("data-size");
|
||||
style[selector][attr] = parseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
function parseValue(value) {
|
||||
if (value === "null" || value === null) return null;
|
||||
if (value === "") return "";
|
||||
if (!isNaN(+value)) return +value;
|
||||
return value;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
function checkName() {
|
||||
const styleName = customPresetPrefix + styleSaverName.value;
|
||||
|
||||
const isSystem = systemPresets.includes(styleName) || systemPresets.includes(styleSaverName.value);
|
||||
if (isSystem) return (styleSaverTip.innerHTML = "default");
|
||||
|
||||
const isExisting = Array.from(stylePreset.options).some(option => option.value == styleName);
|
||||
if (isExisting) return (styleSaverTip.innerHTML = "existing");
|
||||
|
||||
styleSaverTip.innerHTML = "new";
|
||||
}
|
||||
|
||||
function saveStyle() {
|
||||
const styleJSON = styleSaverJSON.value;
|
||||
const desiredName = styleSaverName.value;
|
||||
|
||||
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
|
||||
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
|
||||
if (!desiredName) return tip("Please provide a preset name", false, "error");
|
||||
if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error");
|
||||
|
||||
const presetName = customPresetPrefix + desiredName;
|
||||
applyOption(stylePreset, presetName, desiredName + " [custom]");
|
||||
localStorage.setItem("presetStyle", presetName);
|
||||
localStorage.setItem(presetName, styleJSON);
|
||||
|
||||
applyStyleWithUiRefresh(JSON.parse(styleJSON));
|
||||
tip("Style preset is saved and applied", false, "success", 4000);
|
||||
$("#styleSaver").dialog("close");
|
||||
}
|
||||
|
||||
function styleDownload() {
|
||||
const styleJSON = styleSaverJSON.value;
|
||||
const styleName = styleSaverName.value;
|
||||
|
||||
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
|
||||
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
|
||||
if (!styleName) return tip("Please provide a preset name", false, "error");
|
||||
|
||||
downloadFile(styleJSON, styleName + ".json", "application/json");
|
||||
}
|
||||
|
||||
function loadStyleFile() {
|
||||
const fileName = this.files[0]?.name.replace(/\.[^.]*$/, "");
|
||||
uploadFile(this, styleUpload);
|
||||
|
||||
function styleUpload(dataLoaded) {
|
||||
if (!dataLoaded) return tip("Cannot load the file. Please check the data format", false, "error");
|
||||
const isValid = JSON.isValid(dataLoaded);
|
||||
if (!isValid) return tip("Loaded data is not a valid JSON, please check the format", false, "error");
|
||||
|
||||
styleSaverJSON.value = JSON.stringify(JSON.parse(dataLoaded), null, 2);
|
||||
styleSaverName.value = fileName;
|
||||
checkName();
|
||||
tip("Style preset is uploaded", false, "success", 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestRemoveStylePreset() {
|
||||
const isDefault = systemPresets.includes(stylePreset.value);
|
||||
if (isDefault) return tip("Cannot remove system preset", false, "error");
|
||||
|
||||
confirmationDialog({
|
||||
title: "Remove style preset",
|
||||
message: "Are you sure you want to remove the style preset? This action cannot be undone.",
|
||||
confirm: "Remove",
|
||||
onConfirm: removeStylePreset
|
||||
});
|
||||
}
|
||||
|
||||
function removeStylePreset() {
|
||||
localStorage.removeItem("presetStyle");
|
||||
localStorage.removeItem(stylePreset.value);
|
||||
stylePreset.selectedOptions[0].remove();
|
||||
|
||||
changeStyle("default");
|
||||
}
|
||||
|
||||
function updateMapFilter() {
|
||||
const filter = svg.attr("data-filter");
|
||||
mapFilters.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
if (!filter) return;
|
||||
mapFilters.querySelector("#" + filter).classList.add("pressed");
|
||||
}
|
||||
|
||||
function setPresetRemoveButtonVisibiliy() {
|
||||
const isDefault = systemPresets.includes(stylePreset.value);
|
||||
removeStyleButton.style.display = isDefault ? "none" : "inline-block";
|
||||
}
|
||||
161
modules/ui/temperature-graph.js
Normal file
161
modules/ui/temperature-graph.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"use strict";
|
||||
|
||||
function showBurgTemperatureGraph(id) {
|
||||
const b = pack.burgs[id];
|
||||
const lat = mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT;
|
||||
const burgTemp = grid.cells.temp[pack.cells.g[b.cell]];
|
||||
const prec = grid.cells.prec[pack.cells.g[b.cell]];
|
||||
|
||||
// prettier-ignore
|
||||
const weights = [
|
||||
[
|
||||
[10.782752257744338, 2.7100404240962126], [-2.8226802110591462, 51.62920138583541], [-6.6250956268643835, 4.427939197315455], [-59.64690518541339, 41.89084162654791], [-1.3302059550553835, -3.6964487738450913],
|
||||
[-2.5844898544535497, 0.09879268612455298], [-5.58528252533573, -0.23426224364501905], [26.94531337690372, 20.898158905988907], [3.816397481634785, -0.19045424064580757], [-4.835697931609101, -10.748232783636434]
|
||||
],
|
||||
[
|
||||
[-2.478952081870123, 0.6405800134306895, -7.136785640930911, -0.2186529024764509, 3.6568435212735424, 31.446026153530838, -19.91005187482281, 0.2543395274783306, -7.036924569659988, -0.7721371621651565],
|
||||
[-197.10583739743538, 6.889921141533474, 0.5058941504631129, 7.7667203434606416, -53.74180550086929, -15.717331715167001, -61.32068414155791, -2.259728220978728, 35.84049189540032, 94.6157364730977],
|
||||
[-5.312011591880851, -0.09923148954215096, -1.7132477487917586, -22.55559652066422, 0.4806107280554336, -26.5583974109492, 2.0558257347014863, 25.815645234787432, -18.569029876991156, -2.6792003366730035],
|
||||
[20.706518520569514, 18.344297403881875, 99.52244671131733, -58.53124969563653, -60.74384321042212, -80.57540534651835, 7.884792406540866, -144.33871131678563, 80.134199744324, 20.50745285622448],
|
||||
[-52.88299538575159, -15.782505343805528, 16.63316001054924, 88.09475330556671, -17.619552086641818, -19.943999528182427, -120.46286026828177, 19.354752020806302, 43.49422099308949, 28.733924806541363],
|
||||
[-2.4621368711159897, -1.2074759925679757, -1.5133898639835084, 2.173715352424188, -5.988707597991683, 3.0234147182203843, 3.3284199340000797, -1.8805161326360575, 5.151910934121654, -1.2540553911612116]
|
||||
],
|
||||
[
|
||||
[-0.3357437479474717, 0.01430651794222215, -0.7927524256670906, 0.2121636229648523, 1.0587803023358318, -3.759288325505095],
|
||||
[-1.1988028704442968, 1.3768997508052783, -3.8480086358278816, 0.5289387340947143, 0.5769459339961177, -1.2528318145750772],
|
||||
[1.0074966649240946, 1.155301164699459, -2.974254371052421, 0.47408176553219467, 0.5939042688615264, -0.7631976947131744]
|
||||
]
|
||||
];
|
||||
// From (-∞, ∞) to ~[-1, 1]
|
||||
const In1 = [(Math.abs(lat) - 26.950680212887473) / 48.378128506956, (prec - 12.229929140832644) / 29.94402033696607];
|
||||
|
||||
let lastIn = In1;
|
||||
let lstOut = [];
|
||||
|
||||
for (let levelN = 0; levelN < weights.length; levelN++) {
|
||||
const layerN = weights[levelN];
|
||||
for (let i = 0; i < layerN.length; i++) {
|
||||
lstOut[i] = 0;
|
||||
for (let j = 0; j < layerN[i].length; j++) {
|
||||
lstOut[i] = lstOut[i] + lastIn[j] * layerN[i][j];
|
||||
}
|
||||
// sigmoid
|
||||
lstOut[i] = 1 / (1 + Math.exp(-lstOut[i]));
|
||||
}
|
||||
lastIn = lstOut.slice(0);
|
||||
}
|
||||
|
||||
// Standard deviation for average temperature for the year from [0, 1] to [min, max]
|
||||
const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165;
|
||||
// Standard deviation for the difference between the minimum and maximum temperatures for the year
|
||||
const yearDelTmpSig = lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig ? yearSig : lstOut[1] * 13.541688670361175 + 0.1414213562373084;
|
||||
// Expected value for the difference between the minimum and maximum temperatures for the year
|
||||
const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663;
|
||||
|
||||
// Temperature change shape
|
||||
const delT = yearDelTmpMu / 2 + (0.5 * yearDelTmpSig) / 2;
|
||||
const minT = burgTemp - Math.max(yearSig + delT, 15);
|
||||
const maxT = burgTemp + (burgTemp - minT);
|
||||
|
||||
const chartWidth = Math.max(window.innerWidth / 2, 580);
|
||||
const chartHeight = 300;
|
||||
|
||||
// drawing starting point from top-left (y = 0) of SVG
|
||||
const xOffset = 60;
|
||||
const yOffset = 10;
|
||||
|
||||
const year = new Date().getFullYear(); // use current year
|
||||
const startDate = new Date(year, 0, 1);
|
||||
const endDate = new Date(year, 11, 31);
|
||||
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
|
||||
const xscale = d3.scaleTime().domain([startDate, endDate]).range([0, chartWidth]);
|
||||
const yscale = d3.scaleLinear().domain([minT, maxT]).range([chartHeight, 0]);
|
||||
|
||||
const tempMean = [];
|
||||
const tempMin = [];
|
||||
const tempMax = [];
|
||||
|
||||
months.forEach((month, index) => {
|
||||
const rate = index / 11;
|
||||
let formTmp = Math.cos(rate * 2 * Math.PI) / 2;
|
||||
if (lat > 0) formTmp = -formTmp;
|
||||
|
||||
const x = rate * chartWidth + xOffset;
|
||||
const tempAverage = formTmp * yearSig + burgTemp;
|
||||
const tempDelta = yearDelTmpMu / 2 + (formTmp * yearDelTmpSig) / 2;
|
||||
|
||||
tempMean.push([x, yscale(tempAverage) + yOffset]);
|
||||
tempMin.push([x, yscale(tempAverage - tempDelta) + yOffset]);
|
||||
tempMax.push([x, yscale(tempAverage + tempDelta) + yOffset]);
|
||||
});
|
||||
|
||||
drawGraph();
|
||||
$("#alert").dialog({title: "Annual temperature in " + b.name, width: "auto", position: {my: "center", at: "center", of: "svg"}});
|
||||
|
||||
function drawGraph() {
|
||||
alertMessage.innerHTML = "";
|
||||
const getCurve = data => round(d3.line().curve(d3.curveBasis)(data), 2);
|
||||
|
||||
const legendSize = 60;
|
||||
const chart = d3
|
||||
.select("#alertMessage")
|
||||
.append("svg")
|
||||
.attr("width", chartWidth + 120)
|
||||
.attr("height", chartHeight + yOffset + legendSize);
|
||||
|
||||
const legend = chart.append("g");
|
||||
const legendY = chartHeight + yOffset + legendSize * 0.8;
|
||||
const legendX = n => (chartWidth * n) / 4;
|
||||
const legendTextX = n => legendX(n) + 10;
|
||||
legend.append("circle").attr("cx", legendX(1)).attr("cy", legendY).attr("r", 4).style("fill", "red");
|
||||
legend.append("text").attr("x", legendTextX(1)).attr("y", legendY).attr("alignment-baseline", "central").text("Day temperature");
|
||||
legend.append("circle").attr("cx", legendX(2)).attr("cy", legendY).attr("r", 4).style("fill", "orange");
|
||||
legend.append("text").attr("x", legendTextX(2)).attr("y", legendY).attr("alignment-baseline", "central").text("Mean temperature");
|
||||
legend.append("circle").attr("cx", legendX(3)).attr("cy", legendY).attr("r", 4).style("fill", "blue");
|
||||
legend.append("text").attr("x", legendTextX(3)).attr("y", legendY).attr("alignment-baseline", "central").text("Night temperature");
|
||||
|
||||
const xGrid = d3.axisBottom(xscale).ticks().tickSize(-chartHeight);
|
||||
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth);
|
||||
|
||||
const grid = chart.append("g").attr("class", "epgrid").attr("stroke-dasharray", "4 1");
|
||||
grid.append("g").attr("transform", `translate(${xOffset}, ${chartHeight + yOffset})`).call(xGrid); // prettier-ignore
|
||||
grid.append("g").attr("transform", `translate(${xOffset}, ${yOffset})`).call(yGrid);
|
||||
grid.selectAll("text").remove();
|
||||
|
||||
// add zero degree line
|
||||
if (minT < 0 && maxT > 0) {
|
||||
grid
|
||||
.append("line")
|
||||
.attr("x1", xOffset)
|
||||
.attr("y1", yscale(0) + yOffset)
|
||||
.attr("x2", chartWidth + xOffset)
|
||||
.attr("y2", yscale(0) + yOffset)
|
||||
.attr("stroke", "gray");
|
||||
}
|
||||
|
||||
const xAxis = d3.axisBottom(xscale).ticks().tickFormat(d3.timeFormat("%B"));
|
||||
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(convertTemperature);
|
||||
|
||||
const axis = chart.append("g");
|
||||
axis
|
||||
.append("g")
|
||||
.attr("transform", `translate(${xOffset}, ${chartHeight + yOffset})`)
|
||||
.call(xAxis);
|
||||
axis.append("g").attr("transform", `translate(${xOffset}, ${yOffset})`).call(yAxis);
|
||||
axis.select("path.domain").attr("d", `M0.5,0.5 H${chartWidth + 0.5}`);
|
||||
|
||||
const curves = chart.append("g").attr("fill", "none").style("stroke-width", 2.5);
|
||||
curves.append("path").attr("d", getCurve(tempMean)).attr("data-type", "daily").attr("stroke", "orange").on("mousemove", printVal);
|
||||
curves.append("path").attr("d", getCurve(tempMin)).attr("data-type", "night").attr("stroke", "blue").on("mousemove", printVal);
|
||||
curves.append("path").attr("d", getCurve(tempMax)).attr("data-type", "day").attr("stroke", "red").on("mousemove", printVal);
|
||||
|
||||
function printVal() {
|
||||
const [x, y] = d3.mouse(this);
|
||||
const type = this.getAttribute("data-type");
|
||||
const temp = convertTemperature(yscale.invert(y - yOffset));
|
||||
const month = months[rn(((x - xOffset) / chartWidth) * 12)] || months[0];
|
||||
tip(`Average ${type} temperature in ${month}: ${temp}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ function regenerateStates() {
|
|||
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const statesCount = +regionsInput.value;
|
||||
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");
|
||||
|
|
@ -624,7 +624,9 @@ function addRiverOnClick() {
|
|||
|
||||
const source = riverCells[0];
|
||||
const mouth = riverCells[riverCells.length - 2];
|
||||
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
|
||||
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor = river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
|
|
|
|||
|
|
@ -37,26 +37,12 @@ function editZones() {
|
|||
const el = ev.target,
|
||||
cl = el.classList,
|
||||
zone = el.parentNode.dataset.id;
|
||||
if (cl.contains("culturePopulation")) {
|
||||
changePopulation(zone);
|
||||
return;
|
||||
}
|
||||
if (cl.contains("icon-trash-empty")) {
|
||||
zoneRemove(zone);
|
||||
return;
|
||||
}
|
||||
if (cl.contains("icon-eye")) {
|
||||
toggleVisibility(el);
|
||||
return;
|
||||
}
|
||||
if (cl.contains("icon-pin")) {
|
||||
toggleFog(zone, cl);
|
||||
return;
|
||||
}
|
||||
if (cl.contains("fillRect")) {
|
||||
changeFill(el);
|
||||
return;
|
||||
}
|
||||
if (el.tagName === "FILL-BOX") changeFill(el);
|
||||
else if (cl.contains("culturePopulation")) changePopulation(zone);
|
||||
else if (cl.contains("icon-trash-empty")) zoneRemove(zone);
|
||||
else if (cl.contains("icon-eye")) toggleVisibility(el);
|
||||
else if (cl.contains("icon-pin")) toggleFog(zone, cl);
|
||||
|
||||
if (customization) selectZone(el);
|
||||
});
|
||||
|
||||
|
|
@ -131,8 +117,9 @@ function editZones() {
|
|||
const inactive = this.style.display === "none";
|
||||
const focused = defs.select("#fog #focus" + this.id).size();
|
||||
|
||||
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
|
||||
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
|
||||
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}"
|
||||
data-cells=${c.length} data-area=${area} data-population=${population}>
|
||||
<fill-box fill="${fill}"></fill-box>
|
||||
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
|
||||
|
|
@ -337,9 +324,9 @@ function editZones() {
|
|||
|
||||
function changeFill(el) {
|
||||
const fill = el.getAttribute("fill");
|
||||
const callback = function (fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
document.getElementById(el.parentNode.dataset.id).setAttribute("fill", newFill);
|
||||
};
|
||||
|
||||
openPicker(fill, callback);
|
||||
|
|
@ -411,12 +398,12 @@ function editZones() {
|
|||
function addZonesLayer() {
|
||||
const id = getNextId("zone");
|
||||
const description = "Unknown zone";
|
||||
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
|
||||
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill).attr("data-type", zoneTypes[0]);
|
||||
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
|
||||
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
|
||||
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
|
||||
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
|
||||
<fill-box fill="${fill}"></fill-box>
|
||||
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">0</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue