Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into dev-economics

This commit is contained in:
Azgaar 2021-07-05 21:11:33 +03:00
commit 7dc71a5616
33 changed files with 5797 additions and 2941 deletions

View file

@ -1,6 +1,5 @@
"use strict";
class Battle {
constructor(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
@ -11,8 +10,8 @@ class Battle {
this.x = defender.x;
this.y = defender.y;
this.cell = findCell(this.x, this.y);
this.attackers = {regiments:[], distances:[], morale:100, casualties:0, power:0};
this.defenders = {regiments:[], distances:[], morale:100, casualties:0, power:0};
this.attackers = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0};
this.defenders = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0};
this.addHeaders();
this.addRegiment("attackers", attacker);
@ -26,7 +25,9 @@ class Battle {
this.getInitialMorale();
$("#battleScreen").dialog({
title: this.name, resizable: false, width: fitContent(),
title: this.name,
resizable: false,
width: fitContent(),
position: {my: "center", at: "center", of: "#map"},
close: () => Battle.prototype.context.cancelResults()
});
@ -38,7 +39,7 @@ class Battle {
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
document.getElementById("battleNamePlace").addEventListener("change", ev => Battle.prototype.context.place = ev.target.value);
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
@ -69,9 +70,9 @@ class Battle {
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attackers and defender have only aviation units
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
if (P(.1) && [5,6,7,8,9,12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
return "field";
}
};
this.type = getType();
this.setType();
@ -80,9 +81,9 @@ class Battle {
setType() {
document.getElementById("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_"+this.type+"_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_"+this.type).content;
const defenders = sideSpecific ? document.getElementById("battlePhases_"+this.type+"_defenders").content : attackers;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
@ -91,9 +92,13 @@ class Battle {
}
definePlace() {
const cells = pack.cells, i = this.cell;
const cells = pack.cells,
i = this.cell;
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
const getRiver = i => {const river = pack.rivers.find(r => r.i === i); return river.name + " " + river.type};
const getRiver = i => {
const river = pack.rivers.find(r => r.i === i);
return river.name + " " + river.type;
};
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
return burg ? burg : river ? river : proper;
@ -102,10 +107,10 @@ class Battle {
defineName() {
if (this.type === "field") return "Battle of " + this.place;
if (this.type === "naval") return "Naval Battle of " + this.place;
if (this.type === "siege") return "Siege of "+ this.place;
if (this.type === "siege") return "Siege of " + this.place;
if (this.type === "ambush") return this.place + " Ambush";
if (this.type === "landing") return this.place + " Landing";
if (this.type === "air") return `${this.place} ${P(.8) ? "Air Battle" : "Dogfight"}`;
if (this.type === "air") return `${this.place} ${P(0.8) ? "Air Battle" : "Dogfight"}`;
}
getTypeName() {
@ -121,7 +126,7 @@ class Battle {
let headers = "<thead><tr><th></th><th></th>";
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
const label = capitalize(u.name.replace(/_/g, " "));
headers += `<th data-tip="${label}">${u.icon}</th>`;
}
@ -130,11 +135,11 @@ class Battle {
}
addRegiment(side, regiment) {
regiment.casualties = Object.keys(regiment.u).reduce((a,b) => (a[b]=0,a), {});
regiment.casualties = Object.keys(regiment.u).reduce((a, b) => ((a[b] = 0), a), {});
regiment.survivors = Object.assign({}, regiment.u);
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 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>
@ -146,14 +151,14 @@ class Battle {
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) {
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name]||0}</td>`;
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name] || 0}</td>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name]||0}</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name] || 0}</td>`;
}
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a||0}</td></tr>`;
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a||0}</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a || 0}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
@ -164,13 +169,19 @@ class Battle {
addSide() {
const body = document.getElementById("regimentSelectorBody");
const context = Battle.prototype.context;
const regiments = pack.states.filter(s => s.military && !s.removed).map(s => s.military).flat();
const distance = reg => rn(Math.hypot(context.y-reg.y, context.x-reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const regiments = pack.states
.filter(s => s.military && !s.removed)
.map(s => s.military)
.flat();
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
body.innerHTML = regiments.map(r => {
const s = pack.states[r.state], added = isAdded(r), 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}
body.innerHTML = regiments
.map(r => {
const s = pack.states[r.state],
added = isAdded(r),
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>
<div style="width:6em">${s.name.slice(0, 11)}</div>
@ -179,11 +190,15 @@ class Battle {
<div style="width:4em">${r.a}</div>
<div style="width:4em">${dist}</div>
</div>`;
}).join("");
})
.join("");
$("#regimentSelectorScreen").dialog({
resizable: false, width: fitContent(), title: "Add regiment to the battle",
position: {my: "left center", at: "right+10 center", of: "#battleScreen"}, close: addSideClosed,
resizable: false,
width: fitContent(),
title: "Add regiment to the battle",
position: {my: "left center", at: "right+10 center", of: "#battleScreen"},
close: addSideClosed,
buttons: {
"Add to attackers": () => addSideClicked("attackers"),
"Add to defenders": () => addSideClicked("defenders"),
@ -195,13 +210,19 @@ class Battle {
body.addEventListener("click", selectLine);
function selectLine(ev) {
if (ev.target.className === "inactive") {tip("Regiment is already in the battle", false, "error"); return};
if (ev.target.className === "inactive") {
tip("Regiment is already in the battle", false, "error");
return;
}
ev.target.classList.toggle("selected");
}
function addSideClicked(side) {
const selected = body.querySelectorAll(".selected");
if (!selected.length) {tip("Please select a regiment first", false, "error"); return}
if (!selected.length) {
tip("Please select a regiment first", false, "error");
return;
}
$("#regimentSelectorScreen").dialog("close");
selected.forEach(line => {
@ -212,8 +233,9 @@ class Battle {
Battle.prototype.getInitialMorale.call(context);
// move regiment
const defenders = context.defenders.regiments, attackers = context.attackers.regiments;
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length-1) * 8;
const defenders = context.defenders.regiments,
attackers = context.attackers.regiments;
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x;
regiment.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
@ -227,7 +249,7 @@ class Battle {
}
showNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("battleNameSection").style.display = "inline-block";
document.getElementById("battleNamePlace").value = this.place;
@ -235,22 +257,20 @@ class Battle {
}
hideNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("battleNameSection").style.display = "none";
}
changeName(ev) {
this.name = ev.target.value;
$("#battleScreen").dialog({"title":this.name});
$("#battleScreen").dialog({title: this.name});
}
generateName(type) {
const place = type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length-1));
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({"title":this.name});
$("#battleScreen").dialog({title: this.name});
}
getJoinedForces(regiments) {
@ -266,47 +286,47 @@ class Battle {
calculateStrength(side) {
const scheme = {
// field battle phases
"skirmish": {"melee":.2, "ranged":2.4, "mounted":.1, "machinery":3, "naval":1, "armored":.2, "aviation":1.8, "magical":1.8}, // ranged excel
"melee": {"melee":2, "ranged":1.2, "mounted":1.5, "machinery":.5, "naval":.2, "armored":2, "aviation":.8, "magical":.8}, // melee excel
"pursue": {"melee":1, "ranged":1, "mounted":4, "machinery":.05, "naval":1, "armored":1, "aviation":1.5, "magical":.6}, // mounted excel
"retreat": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.2, "armored":.1, "aviation":.8, "magical":.05}, // reduced
skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel
melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel
pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel
retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced
// naval battle phases
"shelling": {"melee":0, "ranged":.2, "mounted":0, "machinery":2, "naval":2, "armored":0, "aviation":.1, "magical":.5}, // naval and machinery excel
"boarding": {"melee":1, "ranged":.5, "mounted":.5, "machinery":0, "naval":.5, "armored":.4, "aviation":0, "magical":.2}, // melee excel
"chase": {"melee":0, "ranged":.15, "mounted":0, "machinery":1, "naval":1, "armored":0, "aviation":.15, "magical":.5}, // reduced
"withdrawal": {"melee":0, "ranged":.02, "mounted":0, "machinery":.5, "naval":.1, "armored":0, "aviation":.1, "magical":.3}, // reduced
shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel
boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel
chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced
withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced
// siege phases
"blockade": {"melee":.25, "ranged":.25, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
"sheltering": {"melee":.3, "ranged":.5, "mounted":.2, "machinery":.5, "naval":.2, "armored":.1, "aviation":.25, "magical":.25}, // no active actions
"sortie": {"melee":2, "ranged":.5, "mounted":1.2, "machinery":.2, "naval":.1, "armored":.5, "aviation":1, "magical":1}, // melee excel
"bombardment": {"melee":.2, "ranged":.5, "mounted":.2, "machinery":3, "naval":1, "armored":.5, "aviation":1, "magical":1}, // machinery excel
"storming": {"melee":1, "ranged":.6, "mounted":.5, "machinery":1, "naval":.1, "armored":.1, "aviation":.5, "magical":.5}, // melee excel
"defense": {"melee":2, "ranged":3, "mounted":1, "machinery":1, "naval":.1, "armored":1, "aviation":.5, "magical":1}, // ranged excel
"looting": {"melee":1.6, "ranged":1.6, "mounted":.5, "machinery":.2, "naval":.02, "armored":.2, "aviation":.1, "magical":.3}, // melee excel
"surrendering": {"melee":.1, "ranged":.1, "mounted":.05, "machinery":.01, "naval":.01, "armored":.02, "aviation":.01, "magical":.03}, // reduced
blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel
bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel
storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel
defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel
looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel
surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced
// ambush phases
"surprise": {"melee":2, "ranged":2.4, "mounted":1, "machinery":1, "naval":1, "armored":1, "aviation":.8, "magical":1.2}, // increased
"shock": {"melee":.5, "ranged":.5, "mounted":.5, "machinery":.4, "naval":.3, "armored":.1, "aviation":.4, "magical":.5}, // reduced
surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased
shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced
// langing phases
"landing": {"melee":.8, "ranged":.6, "mounted":.6, "machinery":.5, "naval":.5, "armored":.5, "aviation":.5, "magical":.6}, // reduced
"flee": {"melee":.1, "ranged":.01, "mounted":.5, "machinery":.01, "naval":.5, "armored":.1, "aviation":.2, "magical":.05}, // reduced
"waiting": {"melee":.05, "ranged":.5, "mounted":.05, "machinery":.5, "naval":2, "armored":.05, "aviation":.5, "magical":.5}, // reduced
landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced
flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced
waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced
// air battle phases
"maneuvering": {"melee":0, "ranged":.1, "mounted":0, "machinery":.2, "naval":0, "armored":0, "aviation":1, "magical":.2}, // aviation
"dogfight": {"melee":0, "ranged":.1, "mounted":0, "machinery":.1, "naval":0, "armored":0, "aviation":2, "magical":.1} // aviation
maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation
dogfight: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.1, naval: 0, armored: 0, aviation: 2, magical: 0.1} // aviation
};
const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase;
const adjuster = Math.max(populationRate.value / 10, 10); // population adjuster, by default 100
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power|0, 1) : 0;
document.getElementById("battlePower_"+side).innerHTML = UIvalue;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
}
getInitialMorale() {
@ -320,7 +340,7 @@ class Battle {
}
updateMorale(side) {
const morale = document.getElementById("battleMorale_"+side);
const morale = document.getElementById("battleMorale_" + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value;
@ -335,9 +355,11 @@ class Battle {
}
rollDie(side) {
const el = document.getElementById("battleDie_"+side);
const el = document.getElementById("battleDie_" + side);
const prev = +el.innerHTML;
do {el.innerHTML = rand(1, 6)} while (el.innerHTML == prev)
do {
el.innerHTML = rand(1, 6);
} while (el.innerHTML == prev);
this[side].die = +el.innerHTML;
}
@ -357,12 +379,18 @@ class Battle {
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
const total = d3.sum(Object.values(forces)); // total forces
const ranged = d3.sum(options.military.filter(u => u.type === "ranged").map(u => u.name).map(u => forces[u])) / total; // ranged units
if (P(ranged) || P(.8-i/10)) return ["skirmish", "skirmish"];
const ranged =
d3.sum(
options.military
.filter(u => u.type === "ranged")
.map(u => u.name)
.map(u => forces[u])
) / total; // ranged units
if (P(ranged) || P(0.8 - i / 10)) return ["skirmish", "skirmish"];
}
return ["melee", "melee"]; // default option
}
};
const getNavalBattlePhase = () => {
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
@ -372,66 +400,66 @@ class Battle {
// withdrawal phase when power imbalanced
if (!prev[0] === "boarding") {
if (powerRatio < .5 || P(this.attackers.casualties) && powerRatio < 1) return ["withdrawal", "chase"];
if (powerRatio > 2 || P(this.defenders.casualties) && powerRatio > 1) return ["chase", "withdrawal"];
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ["withdrawal", "chase"];
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ["chase", "withdrawal"];
}
// boarding phase can start from 2nd iteration
if (prev[0] === "boarding" || P(i/10 - .1)) return ["boarding", "boarding"];
if (prev[0] === "boarding" || P(i / 10 - 0.1)) return ["boarding", "boarding"];
return ["shelling", "shelling"]; // default option
}
};
const getSiegePhase = () => {
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
let phase = ["blockade", "sheltering"] // default phase
let phase = ["blockade", "sheltering"]; // default phase
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
if (P(1 - morale[0] / 30) && powerRatio < 1) return ["retreat", "pursue"]; // attackers retreat chance if moral < 30
if (P(1 - morale[1] / 15)) return ["looting", "surrendering"]; // defenders surrendering chance if moral < 15
if (P((powerRatio-1) / 2)) return ["storming", "defense"]; // start storm
if (P((powerRatio - 1) / 2)) return ["storming", "defense"]; // start storm
if (prev[0] !== "storming") {
const machinery = options.military.filter(u => u.type === "machinery").map(u => u.name); // machinery units
const attackers = this.getJoinedForces(this.attackers.regiments);
const machineryA = d3.sum(machinery.map(u => attackers[u]));
if (i && machineryA && P(.9)) phase[0] = "bombardment";
if (i && machineryA && P(0.9)) phase[0] = "bombardment";
const defenders = this.getJoinedForces(this.defenders.regiments);
const machineryD = d3.sum(machinery.map(u => defenders[u]));
if (machineryD && P(.9)) phase[1] = "bombardment";
if (machineryD && P(0.9)) phase[1] = "bombardment";
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(.25) && P(morale[1]/70)) phase[1] = "sortie"; // defenders sortie
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = "sortie"; // defenders sortie
}
return phase;
}
};
const getAmbushPhase = () => {
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
if (prev[1] === "surprise" && P(1-powerRatio*i/5)) return ["shock", "surprise"];
if (prev[1] === "surprise" && P(1 - (powerRatio * i) / 5)) return ["shock", "surprise"];
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
return ["melee", "melee"]; // default option
}
};
const getLandingPhase = () => {
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
if (prev[1] === "waiting") return ["flee", "waiting"];
if (prev[1] === "pursue") return ["flee", P(.3) ? "pursue" : "waiting"];
if (prev[1] === "pursue") return ["flee", P(0.3) ? "pursue" : "waiting"];
if (prev[1] === "retreat") return ["pursue", "retreat"];
if (prev[0] === "landing") {
const attackers = P(i/2) ? "melee" : "landing";
const defenders = i ? prev[1] : P(.5) ? "defense" : "shock";
const attackers = P(i / 2) ? "melee" : "landing";
const defenders = i ? prev[1] : P(0.5) ? "defense" : "shock";
return [attackers, defenders];
}
@ -439,7 +467,7 @@ class Battle {
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
return ["melee", "melee"]; // default option
}
};
const getAirBattlePhase = () => {
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
@ -448,53 +476,87 @@ class Battle {
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (prev[0] === "maneuvering" && P(1-i/10)) return ["maneuvering", "maneuvering"];
if (prev[0] === "maneuvering" && P(1 - i / 10)) return ["maneuvering", "maneuvering"];
return ["dogfight", "dogfight"]; // default option
}
};
const phase = function(type) {
const phase = (function (type) {
switch (type) {
case "field": return getFieldBattlePhase();
case "naval": return getNavalBattlePhase();
case "siege": return getSiegePhase();
case "ambush": return getAmbushPhase();
case "landing": return getLandingPhase();
case "air": return getAirBattlePhase();
default: getFieldBattlePhase();
case "field":
return getFieldBattlePhase();
case "naval":
return getNavalBattlePhase();
case "siege":
return getSiegePhase();
case "ambush":
return getAmbushPhase();
case "landing":
return getLandingPhase();
case "air":
return getAirBattlePhase();
default:
getFieldBattlePhase();
}
}(this.type);
})(this.type);
this.attackers.phase = phase[0];
this.defenders.phase = phase[1];
const buttonA = document.getElementById("battlePhase_attackers");
buttonA.className = "icon-button-" + this.attackers.phase;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='"+phase[0]+"']").dataset.tip;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip;
const buttonD = document.getElementById("battlePhase_defenders");
buttonD.className = "icon-button-" + this.defenders.phase;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='"+phase[1]+"']").dataset.tip;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip;
}
run() {
// validations
if (!this.attackers.power) {tip("Attackers army destroyed", false, "warn"); return}
if (!this.defenders.power) {tip("Defenders army destroyed", false, "warn"); return}
if (!this.attackers.power) {
tip("Attackers army destroyed", false, "warn");
return;
}
if (!this.defenders.power) {
tip("Defenders army destroyed", false, "warn");
return;
}
// calculate casualties
const attack = this.attackers.power * (this.attackers.die / 10 + .4);
const defense = this.defenders.power * (this.defenders.die / 10 + .4);
const attack = this.attackers.power * (this.attackers.die / 10 + 0.4);
const defense = this.defenders.power * (this.defenders.die / 10 + 0.4);
// casualties modifier for phase
const phase = {
"skirmish":.1, "melee":.2, "pursue":.3, "retreat":.3, "boarding":.2, "shelling":.1, "chase":.03, "withdrawal": .03,
"blockade":0, "sheltering":0, "sortie":.1, "bombardment":.05, "storming":.2, "defense":.2, "looting":.5, "surrendering":.5,
"surprise":.3, "shock":.3, "landing":.3, "flee":0, "waiting":0, "maneuvering":.1, "dogfight":.2};
skirmish: 0.1,
melee: 0.2,
pursue: 0.3,
retreat: 0.3,
boarding: 0.2,
shelling: 0.1,
chase: 0.03,
withdrawal: 0.03,
blockade: 0,
sheltering: 0,
sortie: 0.1,
bombardment: 0.05,
storming: 0.2,
defense: 0.2,
looting: 0.5,
surrendering: 0.5,
surprise: 0.3,
shock: 0.3,
landing: 0.3,
flee: 0,
waiting: 0,
maneuvering: 0.1,
dogfight: 0.2
};
const casualties = Math.random() * (Math.max(phase[this.attackers.phase], phase[this.defenders.phase])); // total casualties, ~10% per iteration
const casualtiesA = casualties * defense / (attack + defense); // attackers casualties, ~5% per iteration
const casualtiesD = casualties * attack / (attack + defense); // defenders casualties, ~5% per iteration
const casualties = Math.random() * Math.max(phase[this.attackers.phase], phase[this.defenders.phase]); // total casualties, ~10% per iteration
const casualtiesA = (casualties * defense) / (attack + defense); // attackers casualties, ~5% per iteration
const casualtiesD = (casualties * attack) / (attack + defense); // defenders casualties, ~5% per iteration
this.calculateCasualties("attackers", casualtiesA);
this.calculateCasualties("defenders", casualtiesD);
@ -519,7 +581,7 @@ class Battle {
calculateCasualties(side, casualties) {
for (const r of this[side].regiments) {
for (const unit in r.u) {
const rand = .8 + Math.random() * .4;
const rand = 0.8 + Math.random() * 0.4;
const died = Math.min(Pint(r.u[unit] * casualties * rand), r.survivors[unit]);
r.casualties[unit] -= died;
r.survivors[unit] -= died;
@ -551,10 +613,16 @@ class Battle {
const button = ev.target;
const div = button.nextElementSibling;
const hideSection = function() {button.style.opacity = 1; div.style.display = "none"}
if (div.style.display === "block") {hideSection(); return}
const hideSection = function () {
button.style.opacity = 1;
div.style.display = "none";
};
if (div.style.display === "block") {
hideSection();
return;
}
button.style.opacity = .5;
button.style.opacity = 0.5;
div.style.display = "block";
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
@ -568,13 +636,13 @@ class Battle {
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.name = this.defineName();
$("#battleScreen").dialog({"title":this.name});
$("#battleScreen").dialog({title: this.name});
}
changePhase(ev, side) {
if (ev.target.tagName !== "BUTTON") return;
const phase = this[side].phase = ev.target.dataset.phase;
const button = document.getElementById("battlePhase_"+side);
const phase = (this[side].phase = ev.target.dataset.phase);
const button = document.getElementById("battlePhase_" + side);
button.className = "icon-button-" + phase;
button.dataset.tip = ev.target.dataset.tip;
this.calculateStrength(side);
@ -587,12 +655,12 @@ class Battle {
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
function getBattleStatus(relative, max) {
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
if (max < .05) return ["minor skirmishes", "minor skirmishes"];
if (max < 0.05) return ["minor skirmishes", "minor skirmishes"];
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
if (relative > .7) return ["attackers decisive victory", "defenders disastrous defeat"];
if (relative > .6) return ["attackers victory", "defenders defeat"];
if (relative > .4) return ["stalemate", "stalemate"];
if (relative > .3) return ["attackers defeat", "defenders victory"];
if (relative > 0.7) return ["attackers decisive victory", "defenders disastrous defeat"];
if (relative > 0.6) return ["attackers victory", "defenders defeat"];
if (relative > 0.4) return ["stalemate", "stalemate"];
if (relative > 0.3) return ["attackers defeat", "defenders victory"];
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
return ["stalemate", "stalemate"]; // exception
@ -609,16 +677,10 @@ class Battle {
if (note) {
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
const regStatus =
losses === 1 ? "is destroyed" :
losses > .8 ? "is almost completely destroyed" :
losses > .5 ? "suffered terrible losses" :
losses > .3 ? "suffered severe losses" :
losses > .2 ? "suffered heavy losses" :
losses > .05 ? "suffered significant losses" :
losses > 0 ? "suffered unsignificant losses" :
"left the battle without loss";
const casualties = Object.keys(r.casualties).map(t => r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null).filter(c => c);
const regStatus = losses === 1 ? "is destroyed" : losses > 0.8 ? "is almost completely destroyed" : losses > 0.5 ? "suffered terrible losses" : losses > 0.3 ? "suffered severe losses" : losses > 0.2 ? "suffered heavy losses" : losses > 0.05 ? "suffered significant losses" : losses > 0 ? "suffered unsignificant losses" : "left the battle without loss";
const casualties = Object.keys(r.casualties)
.map(t => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
.filter(c => c);
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
note.legend += legend;
@ -630,33 +692,38 @@ class Battle {
}
// append battlefield marker
void function addMarkerSymbol() {
void (function addMarkerSymbol() {
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0)
.attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
}()
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0).attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
})();
const getSide = (regs, n) => regs.length > 1
? `${n ? "regiments" : "forces"} of ${list([... new Set(regs.map(r => pack.states[r.state].name))])}`
: getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name;
const getSide = (regs, n) => (regs.length > 1 ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name);
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
const status = battleStatus[+P(.7)];
const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
const id = getNextId("markerElement");
notes.push({id, name:this.name, legend});
notes.push({id, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000);
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
.attr("data-x", this.x).attr("data-y", this.y).attr("x", this.x - 15).attr("y", this.y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
markers
.append("use")
.attr("id", id)
.attr("xlink:href", "#marker_battlefield")
.attr("data-id", "#marker_battlefield")
.attr("data-x", this.x)
.attr("data-y", this.y)
.attr("x", this.x - 15)
.attr("y", this.y - 30)
.attr("data-size", 1)
.attr("width", 30)
.attr("height", 30);
$("#battleScreen").dialog("destroy");
this.cleanData();
@ -682,5 +749,4 @@ class Battle {
});
delete Battle.prototype.context;
}
}
}

View file

@ -16,7 +16,10 @@ function editBiomes() {
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor,
title: "Biomes Editor",
resizable: false,
width: fitContent(),
close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
@ -33,18 +36,20 @@ function editBiomes() {
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList;
if (cl.contains("fillRect")) biomeChangeColor(el); else
if (cl.contains("icon-info-circled")) openWiki(el); else
if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
body.addEventListener("click", function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("fillRect")) 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);
});
body.addEventListener("change", function(ev) {
const el = ev.target, cl = el.classList;
if (cl.contains("biomeName")) biomeChangeName(el); else
if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
body.addEventListener("change", function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("biomeName")) biomeChangeName(el);
else if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
});
function refreshBiomesEditor() {
@ -73,13 +78,15 @@ function editBiomes() {
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const b = biomesData;
let lines = "", totalArea = 0, totalPopulation = 0;;
let lines = "",
totalArea = 0,
totalPopulation = 0;
for (const i of b.i) {
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate.value;
const urban = b.urban[i] * populationRate.value * urbanization.value;
const rural = b.rural[i] * populationRate;
const urban = b.urban[i] * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
@ -98,7 +105,7 @@ function editBiomes() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
${i>12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
</div>`;
}
body.innerHTML = lines;
@ -115,7 +122,10 @@ function editBiomes() {
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
togglePercentageMode();
}
applySorting(biomesHeader);
$("#biomesEditor").dialog({width: fitContent()});
}
@ -123,25 +133,37 @@ function editBiomes() {
function biomeHighlightOn(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11");
biomes
.select("#biome" + biome)
.raise()
.transition(animate)
.attr("stroke-width", 2)
.attr("stroke", "#cd4c11");
}
function biomeHighlightOff(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
const color = biomesData.color[biome];
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
biomes
.select("#biome" + biome)
.transition()
.attr("stroke-width", 0.7)
.attr("stroke", color);
}
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const biome = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
const callback = function (fill) {
el.setAttribute("fill", fill);
biomesData.color[biome] = fill;
biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill);
}
biomes
.select("#biome" + biome)
.attr("fill", fill)
.attr("stroke", fill);
};
openPicker(currentFill, callback);
}
@ -168,30 +190,52 @@ function editBiomes() {
function openWiki(el) {
const name = el.parentNode.dataset.name;
if (name === "Custom" || !name) {tip("Please provide a biome name", false, "error"); return;}
if (name === "Custom" || !name) {
tip("Please provide a biome name", false, "error");
return;
}
const wiki = "https://en.wikipedia.org/wiki/";
switch (name) {
case "Hot desert": openURL(wiki + "Desert_climate#Hot_desert_climates");
case "Cold desert": openURL(wiki + "Desert_climate#Cold_desert_climates");
case "Savanna": openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
case "Grassland": openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
case "Tropical seasonal forest": openURL(wiki + "Seasonal_tropical_forest");
case "Temperate deciduous forest": openURL(wiki + "Temperate_deciduous_forest");
case "Tropical rainforest": openURL(wiki + "Tropical_rainforest");
case "Temperate rainforest": openURL(wiki + "Temperate_rainforest");
case "Taiga": openURL(wiki + "Taiga");
case "Tundra": openURL(wiki + "Tundra");
case "Glacier": openURL(wiki + "Glacier");
case "Wetland": openURL(wiki + "Wetland");
default: openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
case "Hot desert":
openURL(wiki + "Desert_climate#Hot_desert_climates");
case "Cold desert":
openURL(wiki + "Desert_climate#Cold_desert_climates");
case "Savanna":
openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
case "Grassland":
openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
case "Tropical seasonal forest":
openURL(wiki + "Seasonal_tropical_forest");
case "Temperate deciduous forest":
openURL(wiki + "Temperate_deciduous_forest");
case "Tropical rainforest":
openURL(wiki + "Tropical_rainforest");
case "Temperate rainforest":
openURL(wiki + "Temperate_rainforest");
case "Taiga":
openURL(wiki + "Taiga");
case "Tundra":
openURL(wiki + "Tundra");
case "Glacier":
openURL(wiki + "Glacier");
case "Wetland":
openURL(wiki + "Wetland");
default:
openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
}
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
if (legend.selectAll("*").size()) {
clearLegend();
return;
} // hide legend
const d = biomesData;
const data = Array.from(d.i).filter(i => d.cells[i]).sort((a, b) => d.area[b] - d.area[a]).map(i => [i, d.color[i], d.name[i]]);
const data = Array.from(d.i)
.filter(i => d.cells[i])
.sort((a, b) => d.area[b] - d.area[a])
.map(i => [i, d.color[i], d.name[i]]);
drawLegend("Biomes", data);
}
@ -202,10 +246,10 @@ function editBiomes() {
const totalArea = +biomesFooterArea.dataset.area;
const totalPopulation = +biomesFooterPopulation.dataset.population;
body.querySelectorAll(":scope> div").forEach(function(el) {
el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
body.querySelectorAll(":scope> div").forEach(function (el) {
el.querySelector(".biomeCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%";
});
} else {
body.dataset.type = "absolute";
@ -214,8 +258,12 @@ function editBiomes() {
}
function addCustomBiome() {
const b = biomesData, i = biomesData.i.length;
if (i > 254) {tip("Maximum number of biomes reached (255), data cleansing is required", false, "error"); return;}
const b = biomesData,
i = biomesData.i.length;
if (i > 254) {
tip("Maximum number of biomes reached (255), data cleansing is required", false, "error");
return;
}
b.i.push(i);
b.color.push(getRandomColor());
@ -264,9 +312,9 @@ function editBiomes() {
function downloadBiomesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
let data = "Id,Biome,Color,Habitability,Cells,Area " + unit + ",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
@ -285,20 +333,17 @@ function editBiomes() {
customization = 6;
biomes.append("g").attr("id", "temp");
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "none"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "block"));
body.querySelector("div.biomes").classList.add("selected");
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
biomesFooter.style.display = "none";
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair")
.on("click", selectBiomeOnMapClick)
.call(d3.drag().on("start", dragBiomeBrush))
.on("touchmove mousemove", moveBiomeBrush);
viewbox.style("cursor", "crosshair").on("click", selectBiomeOnMapClick).call(d3.drag().on("start", dragBiomeBrush)).on("touchmove mousemove", moveBiomeBrush);
}
function selectBiomeOnLineClick(line) {
@ -310,13 +355,16 @@ function editBiomes() {
function selectBiomeOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;}
if (pack.cells.h[i] < 20) {
tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error");
return;
}
const assigned = biomes.select("#temp").select("polygon[data-cell='"+i+"']");
const assigned = biomes.select("#temp").select("polygon[data-cell='" + i + "']");
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+biome+"']").classList.add("selected");
body.querySelector("div[data-id='" + biome + "']").classList.add("selected");
}
function dragBiomeBrush() {
@ -341,8 +389,8 @@ function editBiomes() {
const biomeNew = selected.dataset.id;
const color = biomesData.color[biomeNew];
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
if (biomeNew === biomeOld) return;
@ -361,7 +409,7 @@ function editBiomes() {
function applyBiomesChange() {
const changed = biomes.select("#temp").selectAll("polygon");
changed.each(function() {
changed.each(function () {
const i = +this.dataset.cell;
const b = +this.dataset.biome;
pack.cells.biome[i] = b;
@ -379,10 +427,10 @@ function editBiomes() {
biomes.select("#temp").remove();
removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "none"));
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
biomesFooter.style.display = "block";
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});

View file

@ -64,7 +64,7 @@ function editBurg(id) {
document.getElementById('burgName').value = b.name;
document.getElementById('burgType').value = b.type || 'Generic';
document.getElementById('burgPopulation').value = rn(b.population * populationRate.value * urbanization.value);
document.getElementById('burgPopulation').value = rn(b.population * populationRate * urbanization);
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
// update list and select culture
@ -123,7 +123,7 @@ function editBurg(id) {
'Okhotsk (Russia)',
'Fairbanks (Alaska)',
'Nuuk (Greenland)',
'Murmansk',
'Murmansk', // -5 - 0
'Arkhangelsk',
'Anchorage',
'Tromsø',
@ -133,7 +133,7 @@ function editBurg(id) {
'Halifax',
'Prague',
'Copenhagen',
'London',
'London', // 1 - 10
'Antwerp',
'Paris',
'Milan',
@ -143,7 +143,7 @@ function editBurg(id) {
'Lisbon',
'Barcelona',
'Marrakesh',
'Alexandria',
'Alexandria', // 11 - 20
'Tegucigalpa',
'Guangzhou',
'Rio de Janeiro',
@ -153,11 +153,10 @@ function editBurg(id) {
'Mogadishu',
'Bangkok',
'Aden',
'Khartoum',
'Mecca'
];
const city = cities[temperature + 5];
return city ? 'in ' + city : null;
'Khartoum'
]; // 21 - 30
if (temperature > 30) return 'Mecca';
return cities[temperature + 5] || null;
}
function dragBurgLabel() {
@ -332,7 +331,7 @@ function editBurg(id) {
function changePopulation() {
const id = +elSelected.attr('data-id');
pack.burgs[id].population = rn(burgPopulation.value / populationRate.value / urbanization.value, 4);
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
}
function toggleFeature() {
@ -423,7 +422,7 @@ function editBurg(id) {
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
const population = rn(burg.population * populationRate.value * urbanization.value);
const population = rn(burg.population * populationRate * urbanization);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
@ -547,7 +546,7 @@ function editBurg(id) {
const id = +elSelected.attr('data-id');
if (pack.burgs[id].capital) {
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
Please change state capital first. You can do it using Burgs Editor (shift + T)`;
You can change the capital using Burgs Editor (shift + T)`;
$('#alert').dialog({
resizable: false,
title: 'Remove burg',

View file

@ -71,7 +71,7 @@ function overviewBurgs() {
totalPopulation = 0;
for (const b of filtered) {
const population = b.population * populationRate.value * urbanization.value;
const population = b.population * populationRate * urbanization;
totalPopulation += population;
const type = b.capital && b.port ? 'a-capital-port' : b.capital ? 'c-capital' : b.port ? 'p-port' : 'z-burg';
const state = pack.states[b.state].name;
@ -91,8 +91,8 @@ function overviewBurgs() {
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
</div>
<span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
@ -163,10 +163,10 @@ function overviewBurgs() {
const burg = +this.parentNode.dataset.id;
if (this.value == '' || isNaN(+this.value)) {
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
this.value = si(pack.burgs[burg].population * populationRate * urbanization);
return;
}
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
pack.burgs[burg].population = this.value / populationRate / urbanization;
this.parentNode.dataset.population = this.value;
this.value = si(this.value);
@ -255,14 +255,9 @@ function overviewBurgs() {
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
if (pack.cells.h[cell] < 20) {
tip('You cannot place state into the water. Please click on a land cell', false, 'error');
return;
}
if (pack.cells.burg[cell]) {
tip('There is already a burg in this cell. Please select a free cell', false, 'error');
return;
}
if (pack.cells.h[cell] < 20) return tip('You cannot place state into the water. Please click on a land cell', false, 'error');
if (pack.cells.burg[cell]) return tip('There is already a burg in this cell. Please select a free cell', false, 'error');
addBurg(point); // add new burg
if (d3.event.shiftKey === false) {
@ -347,7 +342,7 @@ function overviewBurgs() {
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
const name = d.data.name;
const parent = d.parent.data.name;
const population = si(d.value * populationRate.value * urbanization.value);
const population = si(d.value * populationRate * urbanization);
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
burgHighlightOn(ev);
@ -449,7 +444,7 @@ function overviewBurgs() {
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
data += pack.cultures[b.culture].name + ',';
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
data += rn(b.population * populationRate.value * urbanization.value) + ',';
data += rn(b.population * populationRate * urbanization) + ',';
// add geography data
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
@ -497,15 +492,9 @@ function overviewBurgs() {
}
function importBurgNames(dataLoaded) {
if (!dataLoaded) {
tip('Cannot load the file, please check the format', false, 'error');
return;
}
if (!dataLoaded) return tip('Cannot load the file, please check the format', false, 'error');
const data = dataLoaded.split('\r\n');
if (!data.length) {
tip('Cannot parse the list, please check the file format', false, 'error');
return;
}
if (!data.length) return tip('Cannot parse the list, please check the file format', false, 'error');
let change = [],
message = `Burgs will be renamed as below. Please confirm`;

View file

@ -72,8 +72,8 @@ function editCultures() {
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * distanceScaleInput.value ** 2;
const rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.value;
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`;
totalArea += area;
@ -186,8 +186,8 @@ function editCultures() {
.select("g[data-id='" + culture + "'] > path")
.classed('selected', 1);
const c = pack.cultures[culture];
const rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.value;
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rural + urban > 0 ? si(rn(rural + urban)) + ' people' : 'Extinct';
info.innerHTML = `${c.name} culture. ${c.type}. ${population}`;
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
@ -323,8 +323,8 @@ function editCultures() {
tip('Culture does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(c.rural * populationRate.value);
const urban = rn(c.urban * populationRate.value * urbanization.value);
const rural = rn(c.rural * populationRate);
const urban = rn(c.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
const burgs = pack.burgs.filter((b) => !b.removed && b.culture === culture);
@ -367,7 +367,7 @@ function editCultures() {
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
const pop = rn(points / cells.length);
cells.forEach((i) => (pack.cells.pop[i] = pop));
@ -378,7 +378,7 @@ function editCultures() {
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach((b) => (b.population = population));
}
@ -402,27 +402,36 @@ function editCultures() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
const message = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
const onConfirm = () => {
cults.select('#culture' + culture).remove();
debug.select('#cultureCenter' + culture).remove();
alertMessage.innerHTML = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
$('#alert').dialog({
resizable: false,
title: 'Remove culture',
buttons: {
Remove: function () {
cults.select('#culture' + culture).remove();
debug.select('#cultureCenter' + culture).remove();
pack.burgs.filter((b) => b.culture == culture).forEach((b) => (b.culture = 0));
pack.states.forEach((s, i) => {
if (s.culture === culture) s.culture = 0;
});
pack.cells.culture.forEach((c, i) => {
if (c === culture) pack.cells.culture[i] = 0;
});
pack.cultures[culture].removed = true;
pack.burgs.filter((b) => b.culture == culture).forEach((b) => (b.culture = 0));
pack.states.forEach((s, i) => {
if (s.culture === culture) s.culture = 0;
});
pack.cells.culture.forEach((c, i) => {
if (c === culture) pack.cells.culture[i] = 0;
});
pack.cultures[culture].removed = true;
const origin = pack.cultures[culture].origin;
pack.cultures.forEach((c) => {
if (c.origin === culture) c.origin = origin;
});
refreshCulturesEditor();
};
confirmationDialog({title: 'Remove culture', message, confirm: 'Remove', onConfirm});
const origin = pack.cultures[culture].origin;
pack.cultures.forEach((c) => {
if (c.origin === culture) c.origin = origin;
});
refreshCulturesEditor();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function drawCultureCenters() {

View file

@ -2,10 +2,13 @@
function showEPForRoute(node) {
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const routeLen = node.getTotalLength() * distanceScaleInput.value;
showElevationProfile(points, routeLen, false);
@ -13,10 +16,13 @@ function showEPForRoute(node) {
function showEPForRiver(node) {
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
showElevationProfile(points, riverLen, true);
@ -29,7 +35,9 @@ function showElevationProfile(data, routeLen, isRiver) {
document.getElementById("epSave").addEventListener("click", downloadCSV);
$("#elevationProfile").dialog({
title: "Elevation profile", resizable: false, width: window.width,
title: "Elevation profile",
resizable: false,
width: window.width,
close: closeElevationProfile,
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
});
@ -37,27 +45,30 @@ function showElevationProfile(data, routeLen, isRiver) {
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
let slope = 0;
if (isRiver) {
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length-1]]) {
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length - 1]]) {
slope = 1; // up-hill
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length-1]]) {
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length - 1]]) {
slope = -1; // down-hill
}
}
const chartWidth = window.innerWidth-180, chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
const xOffset = 80, yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
const chartWidth = window.innerWidth - 180,
chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
const xOffset = 80,
yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
const biomesHeight = 40;
let lastBurgIndex = 0;
let lastBurgCell = 0;
let burgCount = 0;
let chartData = {biome:[], burg:[], cell:[], height:[], mi:1000000, ma:0, mih: 100, mah: 0, points:[]};
let chartData = {biome: [], burg: [], cell: [], height: [], mi: 1000000, ma: 0, mih: 100, mah: 0, points: []};
for (let i = 0, prevB = 0, prevH = -1; i < data.length; i++) {
let cell = data[i];
let h = pack.cells.h[cell];
if (h < 20) {
const f = pack.features[pack.cells.f[cell]];
if (f.type === "lake") h = f.height; else h = 20;
if (f.type === "lake") h = f.height;
else h = 20;
}
// check for river up-hill
@ -73,21 +84,25 @@ function showElevationProfile(data, routeLen, isRiver) {
let b = pack.cells.burg[cell];
if (b == prevB) b = 0;
else prevB = b;
if (b) { burgCount++; lastBurgIndex = i; lastBurgCell = cell; }
if (b) {
burgCount++;
lastBurgIndex = i;
lastBurgCell = cell;
}
chartData.biome[i] = pack.cells.biome[cell];
chartData.burg[i] = b;
chartData.cell[i] = cell;
let sh = getHeight(h);
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(" ")));
chartData.mih = Math.min(chartData.mih, h);
chartData.mah = Math.max(chartData.mah, h);
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
chartData.ma = Math.max(chartData.ma, chartData.height[i]);
}
if (lastBurgIndex != 0 && lastBurgCell == chartData.cell[data.length-1] && lastBurgIndex < data.length-1) {
chartData.burg[data.length-1] = chartData.burg[lastBurgIndex];
if (lastBurgIndex != 0 && lastBurgCell == chartData.cell[data.length - 1] && lastBurgIndex < data.length - 1) {
chartData.burg[data.length - 1] = chartData.burg[lastBurgIndex];
chartData.burg[lastBurgIndex] = 0;
}
@ -96,7 +111,7 @@ function showElevationProfile(data, routeLen, isRiver) {
function downloadCSV() {
let data = "Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
for (let k=0; k < chartData.points.length; k++) {
for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k];
let burg = pack.cells.burg[cell];
let biome = pack.cells.biome[cell];
@ -107,16 +122,16 @@ function showElevationProfile(data, routeLen, isRiver) {
let pop = pack.cells.pop[cell];
let h = pack.cells.h[cell];
data += k+1 + ",";
data += k + 1 + ",";
data += chartData.points[k][0] + ",";
data += chartData.points[k][1] + ",";
data += cell + ",";
data += getHeight(h) + ",";
data += h + ",";
data += rn(pop * populationRate.value) + ",";
data += rn(pop * populationRate) + ",";
if (burg) {
data += pack.burgs[burg].name + ",";
data += (pack.burgs[burg].population * populationRate.value * urbanization.value) + ",";
data += pack.burgs[burg].population * populationRate * urbanization + ",";
} else {
data += ",0,";
}
@ -142,18 +157,27 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.points = [];
let heightScale = 100 / parseInt(epScaleRange.value);
heightScale *= .9; // curves cause the heights to go slightly higher, adjust here
heightScale *= 0.9; // curves cause the heights to go slightly higher, adjust here
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
const yscale = d3.scaleLinear().domain([0, chartData.ma * heightScale]).range([chartHeight, 0]);
const yscale = d3
.scaleLinear()
.domain([0, chartData.ma * heightScale])
.range([chartHeight, 0]);
for (let i=0; i<data.length; i++) {
for (let i = 0; i < data.length; i++) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
}
document.getElementById("elevationGraph").innerHTML = "";
const chart = d3.select("#elevationGraph").append("svg").attr("width", chartWidth+120).attr("height", chartHeight+yOffset+biomesHeight).attr("id", "elevationSVG").attr("class", "epbackground");
const chart = d3
.select("#elevationGraph")
.append("svg")
.attr("width", chartWidth + 120)
.attr("height", chartHeight + yOffset + biomesHeight)
.attr("id", "elevationSVG")
.attr("class", "epbackground");
// arrow-head definition
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
@ -161,41 +185,62 @@ function showElevationProfile(data, routeLen, isRiver) {
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
if (chartData.mah == chartData.mih) {
landdef.append("stop").attr("offset", "0%").attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
landdef.append("stop").attr("offset", "100%").attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
landdef
.append("stop")
.attr("offset", "0%")
.attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
landdef
.append("stop")
.attr("offset", "100%")
.attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
} else {
for (let k=chartData.mah; k >= chartData.mih; k--) {
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
landdef.append("stop").attr("offset", perc*100 + "%").attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
for (let k = chartData.mah; k >= chartData.mih; k--) {
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
landdef
.append("stop")
.attr("offset", perc * 100 + "%")
.attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
}
}
// land
let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves
let epCurveIndex = parseInt(epCurve.selectedIndex);
switch(epCurveIndex) {
case 0 : curve = d3.line().curve(d3.curveLinear); break;
case 1 : curve = d3.line().curve(d3.curveBasis); break;
case 2 : curve = d3.line().curve(d3.curveBundle.beta(1)); break;
case 3 : curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5)); break;
case 4 : curve = d3.line().curve(d3.curveMonotoneX); break;
case 5 : curve = d3.line().curve(d3.curveNatural); break;
switch (epCurveIndex) {
case 0:
curve = d3.line().curve(d3.curveLinear);
break;
case 1:
curve = d3.line().curve(d3.curveBasis);
break;
case 2:
curve = d3.line().curve(d3.curveBundle.beta(1));
break;
case 3:
curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5));
break;
case 4:
curve = d3.line().curve(d3.curveMonotoneX);
break;
case 5:
curve = d3.line().curve(d3.curveNatural);
break;
}
// copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart
let extra = chartData.points.slice();
let path = curve(extra);
// this completes the right-hand side and bottom of our land "polygon"
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length-1][1]);
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) +"," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length - 1][1]);
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += "Z";
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)");
// biome / heights
let g = chart.append("g").attr("id", "epbiomes");
const hu = heightUnit.value;
for(let k=0; k < chartData.points.length; k++) {
for (let k = 0; k < chartData.points.length; k++) {
const x = chartData.points[k][0];
const y = yOffset + chartHeight;
const c = biomesData.color[chartData.biome[k]];
@ -207,45 +252,53 @@ function showElevationProfile(data, routeLen, isRiver) {
const state = pack.cells.state[cell];
let pop = pack.cells.pop[cell];
if (chartData.burg[k]) {
pop += pack.burgs[chartData.burg[k]].population * urbanization.value;
pop += pack.burgs[chartData.burg[k]].population * urbanization;
}
const populationDesc = rn(pop * populationRate.value);
const populationDesc = rn(pop * populationRate);
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
const dataTip = biomesData.name[chartData.biome[k]] +
provinceDesc +
", " + pack.states[state].name +
", " + pack.religions[religion].name +
", " + pack.cultures[culture].name +
" (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
const dataTip = biomesData.name[chartData.biome[k]] + provinceDesc + ", " + pack.states[state].name + ", " + pack.religions[religion].name + ", " + pack.cultures[culture].name + " (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
}
const xAxis = d3.axisBottom(xscale).ticks(10).tickFormat(function(d){ return (rn(d / chartData.points.length * routeLen) + " " + distanceUnitInput.value);});
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(function(d) { return d + " " + hu; });
const xAxis = d3
.axisBottom(xscale)
.ticks(10)
.tickFormat(function (d) {
return rn((d / chartData.points.length) * routeLen) + " " + distanceUnitInput.value;
});
const yAxis = d3
.axisLeft(yscale)
.ticks(5)
.tickFormat(function (d) {
return d + " " + hu;
});
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
chart.append("g")
chart
.append("g")
.attr("id", "epxaxis")
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "center")
.attr("transform", function(d) {
return "rotate(0)" // used to rotate labels, - anti-clockwise, + clockwise
.attr("transform", function (d) {
return "rotate(0)"; // used to rotate labels, - anti-clockwise, + clockwise
});
chart.append("g")
chart
.append("g")
.attr("id", "epyaxis")
.attr("transform", "translate(" + parseInt(+xOffset-10) + "," + parseInt(+yOffset) + ")")
.attr("transform", "translate(" + parseInt(+xOffset - 10) + "," + parseInt(+yOffset) + ")")
.call(yAxis);
// add the X gridlines
chart.append("g")
chart
.append("g")
.attr("id", "epxgrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
@ -253,7 +306,8 @@ function showElevationProfile(data, routeLen, isRiver) {
.call(xGrid);
// add the Y gridlines
chart.append("g")
chart
.append("g")
.attr("id", "epygrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
@ -266,22 +320,33 @@ function showElevationProfile(data, routeLen, isRiver) {
const add = 15;
let xwidth = chartData.points[1][0] - chartData.points[0][0];
for (let k=0; k<chartData.points.length; k++) {
for (let k = 0; k < chartData.points.length; k++) {
if (chartData.burg[k] > 0) {
let b = chartData.burg[k];
let x1 = chartData.points[k][0]; // left side of graph by default
if (k > 0) x1 += xwidth/2; // center it if not first
if (k == chartData.points.length-1) x1 = chartWidth + xOffset; // right part of graph
y1+=add;
if (k > 0) x1 += xwidth / 2; // center it if not first
if (k == chartData.points.length - 1) x1 = chartWidth + xOffset; // right part of graph
y1 += add;
if (y1 >= yOffset) y1 = add;
// burg name
g.append("text").attr("id", "ep" + b).attr("class", "epburglabel").attr("x", x1).attr("y", y1).attr("text-anchor", "middle");
g.append("text")
.attr("id", "ep" + b)
.attr("class", "epburglabel")
.attr("x", x1)
.attr("y", y1)
.attr("text-anchor", "middle");
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
// arrow from burg name to graph line
g.append("path").attr("id", "eparrow" + b).attr("d", "M" + x1.toString() + "," + (y1+3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1]-3).toString()).attr("stroke", "darkgray").attr("fill", "lightgray").attr("stroke-width", "1").attr("marker-end", "url(#arrowhead)");
g.append("path")
.attr("id", "eparrow" + b)
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString())
.attr("stroke", "darkgray")
.attr("fill", "lightgray")
.attr("stroke-width", "1")
.attr("marker-end", "url(#arrowhead)");
}
}
}

View file

@ -1,23 +1,23 @@
// Module to store general UI functions
"use strict";
'use strict';
// fit full-screen map if window is resized
$(window).resize(function(e) {
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
$(window).resize(function (e) {
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
changeMapSize();
});
window.onbeforeunload = () => "Are you sure you want to navigate away?";
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
// Tooltips
const tooltip = document.getElementById("tooltip");
const tooltip = document.getElementById('tooltip');
// show tip for non-svg elemets with data-tip
document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
/**
* @param {string} tip Tooltip text
@ -25,15 +25,15 @@ document.getElementById("exitCustomization").addEventListener("mousemove", showD
* @param {string} type Message type (color): error, warn, success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = "Tip is undefined", main, type, time) {
function tip(tip = 'Tip is undefined', main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
if (type === "error") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)"; else
if (type === "warn") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)"; else
if (type === "success") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)";
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
if (type === 'error') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)';
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
if (main) tooltip.dataset.main = tip; // set main tip
if (time) setTimeout(() => tooltip.dataset.main = "", time); // clear main in some time
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
}
function showMainTip() {
@ -41,8 +41,8 @@ function showMainTip() {
}
function clearMainTip() {
tooltip.dataset.main = "";
tooltip.innerHTML = "";
tooltip.dataset.main = '';
tooltip.innerHTML = '';
}
// show tip at the bottom of the screen, consider possible translation
@ -62,7 +62,8 @@ function mouseMove() {
if (i === undefined) return;
showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(point, d3.event, i, g);
if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g);
if (cellInfo.offsetParent) updateCellInfo(point, i, g);
}
@ -70,24 +71,24 @@ function mouseMove() {
function showNotes(e, i) {
if (notesEditor.offsetParent) return;
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
if (e.target.parentNode.parentNode.id === 'burgLabels') id = 'burg' + e.target.dataset.id;
else if (e.target.parentNode.parentNode.id === 'burgIcons') id = 'burg' + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
const note = notes.find((note) => note.id === id);
if (note !== undefined && note.legend !== '') {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
} else if (!options.pinNotes) {
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
document.getElementById('notes').style.display = 'none';
document.getElementById('notesHeader').innerHTML = '';
document.getElementById('notesBody').innerHTML = '';
}
}
// show viewbox tooltip if main tooltip is blank
function showMapTooltip(point, e, i, g) {
tip(""); // clear tip
tip(''); // clear tip
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
if (!path[path.length - 8]) return;
const group = path[path.length - 7].id;
@ -95,16 +96,14 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === "armies") {
tip(e.target.parentNode.dataset.name + ". Click to edit");
if (group === 'armies') {
tip(e.target.parentNode.dataset.name + '. Click to edit');
return;
}
if (group === "emblems" && e.target.tagName === "use") {
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
const [g, type] = parent.id === "burgEmblems" ? [pack.burgs, "burg"] :
parent.id === "provinceEmblems" ? [pack.provinces, "province"] :
[pack.states, "state"];
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
@ -116,126 +115,168 @@ function showMapTooltip(point, e, i, g) {
return;
}
if (group === "goods") {
if (group === 'goods') {
const id = +e.target.dataset.i;
const resource = pack.resources.find(resource => resource.i === id);
tip("Resource: " + resource.name);
const resource = pack.resources.find((resource) => resource.i === id);
tip('Resource: ' + resource.name);
return;
}
if (group === "rivers") {
if (group === 'rivers') {
const river = +e.target.id.slice(5);
const r = pack.rivers.find(r => r.i === river);
const name = r ? r.name + " " + r.type : "";
tip(name + ". Click to edit");
const r = pack.rivers.find((r) => r.i === river);
const name = r ? r.name + ' ' + r.type : '';
tip(name + '. Click to edit');
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
if (group === "routes") {tip("Click to edit the Route"); return;}
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
if (group === 'routes') {
tip('Click to edit the Route');
return;
}
if (group === 'terrain') {
tip('Click to edit the Relief Icon');
return;
}
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate.value * urbanization.value);
const population = si(b.population * populationRate * urbanization);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === "labels") {tip("Click to edit the Label"); return;}
if (group === "markers") {tip("Click to edit the Marker"); return;}
if (group === "ruler") {
const tag = e.target.tagName;
const className = e.target.getAttribute("class");
if (tag === "circle" && className === "edge") {tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point"); return;}
if (tag === "circle" && className === "control") {tip("Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point"); return;}
if (tag === "circle") {tip("Drag to adjust the measurer"); return;}
if (tag === "polyline") {tip("Click on drag to add a control point"); return;}
if (tag === "path") {tip("Drag to move the measurer"); return;}
if (tag === "text") {tip("Drag to move, click to remove the measurer"); return;}
if (group === 'labels') {
tip('Click to edit the Label');
return;
}
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (group === "lakes" && !land) {
if (group === 'markers') {
tip('Click to edit the Marker');
return;
}
if (group === 'ruler') {
const tag = e.target.tagName;
const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') {
tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
return;
}
if (tag === 'circle' && className === 'control') {
tip('Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point');
return;
}
if (tag === 'circle') {
tip('Drag to adjust the measurer');
return;
}
if (tag === 'polyline') {
tip('Click on drag to add a control point');
return;
}
if (tag === 'path') {
tip('Drag to move the measurer');
return;
}
if (tag === 'text') {
tip('Drag to move, click to remove the measurer');
return;
}
}
if (subgroup === 'burgIcons') {
tip('Click to edit the Burg');
return;
}
if (subgroup === 'burgLabels') {
tip('Click to edit the Burg');
return;
}
if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
const fullName = subgroup === "freshwater" ? name : name + " " + subgroup;
tip(`${fullName} lake. Click to edit`); return;
const fullName = subgroup === 'freshwater' ? name : name + ' ' + subgroup;
tip(`${fullName} lake. Click to edit`);
return;
}
if (group === "coastline") {tip("Click to edit the coastline"); return;}
if (group === "zones") {
const zone = path[path.length-8];
if (group === 'coastline') {
tip('Click to edit the coastline');
return;
}
if (group === 'zones') {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === "ice") {tip("Click to edit the Ice"); return;}
if (group === 'ice') {
tip('Click to edit the Ice');
return;
}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip(getPopulationTip(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i]
tip("Biome: " + biomesData.name[biome]);
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));
else if (layerIsOn('togglePopulation')) tip(getPopulationTip(i));
else if (layerIsOn('toggleTemp')) tip('Temperature: ' + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn('toggleBiomes') && pack.cells.biome[i]) {
const biome = pack.cells.biome[i];
tip('Biome: ' + biomesData.name[biome]);
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
} else
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
} else if (layerIsOn('toggleReligions') && pack.cells.religion[i]) {
const religion = pack.cells.religion[i];
const r = pack.religions[religion];
const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion";
tip(type + ": " + r.name);
const type = r.type === 'Cult' || r.type == 'Heresy' ? r.type : r.type + ' religion';
tip(type + ': ' + r.name);
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
} else
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
} else if (pack.cells.state[i] && (layerIsOn('toggleProvinces') || layerIsOn('toggleStates'))) {
const state = pack.cells.state[i];
const stateName = pack.states[state].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ", " : "";
const prov = province ? pack.provinces[province].fullName + ', ' : '';
tip(prov + stateName);
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
} else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) {
} else if (layerIsOn('toggleCultures') && pack.cells.culture[i]) {
const culture = pack.cells.culture[i];
tip("Culture: " + pack.cultures[culture].name);
tip('Culture: ' + pack.cultures[culture].name);
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
} else
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(point));
} else if (layerIsOn('toggleHeight')) tip('Height: ' + getFriendlyHeight(point));
}
function highlightEditorLine(editor, id, timeout = 15000) {
Array.from(editor.getElementsByClassName("states hovered")).forEach(el => el.classList.remove("hovered")); // clear all hovered
const hovered = Array.from(editor.querySelectorAll("div")).find(el => el.dataset.id == id);
if (hovered) hovered.classList.add("hovered"); // add hovered class
if (timeout) setTimeout(() => {hovered && hovered.classList.remove("hovered")}, timeout);
Array.from(editor.getElementsByClassName('states hovered')).forEach((el) => el.classList.remove('hovered')); // clear all hovered
const hovered = Array.from(editor.querySelectorAll('div')).find((el) => el.dataset.id == id);
if (hovered) hovered.classList.add('hovered'); // add hovered class
if (timeout)
setTimeout(() => {
hovered && hovered.classList.remove('hovered');
}, timeout);
}
// get cell info on mouse move
function updateCellInfo(point, i, g) {
const cells = pack.cells;
const x = infoX.innerHTML = rn(point[0]);
const y = infoY.innerHTML = rn(point[1]);
const x = (infoX.innerHTML = rn(point[0]));
const y = (infoY.innerHTML = rn(point[1]));
const f = cells.f[i];
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat");
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon");
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, 'lat');
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, 'lon');
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : 'n/a';
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : 'n/a';
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : 'no';
infoState.innerHTML = cells.h[i] >= 20 ? (cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : 'neutral lands (0)') : 'no';
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : 'no';
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : 'no';
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : 'no';
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + ' (' + cells.burg[i] + ')' : 'no';
infoFeature.innerHTML = f ? pack.features[f].group + ' (' + f + ')' : 'n/a';
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
@ -245,23 +286,29 @@ function toDMS(coord, c) {
const minutesNotTruncated = (Math.abs(coord) - degrees) * 60;
const minutes = Math.floor(minutesNotTruncated);
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
const cardinal = c === "lat" ? coord >= 0 ? "N" : "S" : coord >= 0 ? "E" : "W";
return degrees + "° " + minutes + " " + seconds + "″ " + cardinal;
const cardinal = c === 'lat' ? (coord >= 0 ? 'N' : 'S') : coord >= 0 ? 'E' : 'W';
return degrees + '° ' + minutes + ' ' + seconds + '″ ' + cardinal;
}
// get surface elevation
function getElevation(f, h) {
if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height
if (f.border) return "0 " + heightUnit.value; // ocean: 0
if (f.type === "lake") return getHeight(f.height) + " (" + f.height + ")"; // lake: defined on river generation
if (f.land) return getHeight(h) + ' (' + h + ')'; // land: usual height
if (f.border) return '0 ' + heightUnit.value; // ocean: 0
if (f.type === 'lake') return getHeight(f.height) + ' (' + f.height + ')'; // lake: defined on river generation
}
// get water depth
function getDepth(f, h, p) {
if (f.land) return "0 " + heightUnit.value; // land: 0
if (!f.border) return getHeight(h, "abs"); // lake: pack abs height
if (f.land) return '0 ' + heightUnit.value; // land: 0
// lake: difference between surface and bottom
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
return getHeight(gridH, "abs"); // ocean: grig height
if (f.type === 'lake') {
const depth = gridH === 19 ? f.height / 2 : gridH;
return getHeight(depth, 'abs');
}
return getHeight(gridH, 'abs'); // ocean: grid height
}
// get user-friendly (real-world) height value from map data
@ -275,107 +322,139 @@ function getFriendlyHeight(p) {
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
else if (unit === "f") unitRatio = 0.5468; // if fathom
if (unit === 'm') unitRatio = 1;
// if meter
else if (unit === 'f') unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
else if (h < 20 && h > 0) height = ((h - 20) / h) * 50;
if (abs) height = Math.abs(height);
return rn(height * unitRatio) + " " + unit;
return rn(height * unitRatio) + ' ' + unit;
}
// get user-friendly (real-world) precipitation value from map data
function getFriendlyPrecipitation(i) {
const prec = grid.cells.prec[pack.cells.g[i]];
return prec * 100 + " mm";
return prec * 100 + ' mm';
}
function getRiverInfo(id) {
const r = pack.rivers.find(r => r.i == id);
return r ? `${r.name} ${r.type} (${id})` : "n/a";
const r = pack.rivers.find((r) => r.i == id);
return r ? `${r.name} ${r.type} (${id})` : 'n/a';
}
function getCellPopulation(i) {
const rural = pack.cells.pop[i] * populationRate.value;
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
const rural = pack.cells.pop[i] * populationRate;
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate * urbanization : 0;
return [rural, urban];
}
// get user-friendly (real-world) population value from map data
function getFriendlyPopulation(i) {
const [rural, urban] = getCellPopulation(i);
return `${si(rural+urban)} (${si(rural)} rural, urban ${si(urban)})`;
return `${si(rural + urban)} (${si(rural)} rural, urban ${si(urban)})`;
}
function getPopulationTip(i) {
const [rural, urban] = getCellPopulation(i);
return `Cell population: ${si(rural+urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`;
return `Cell population: ${si(rural + urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`;
}
function highlightEmblemElement(type, el) {
const i = el.i, cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
const i = el.i,
cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
if (type === "burg") {
const {x, y} = el;
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
.attr("fill", "none").attr("stroke", "#d0240f").attr("stroke-width", 1).attr("opacity", 1)
.transition(animation).attr("r", 20).attr("opacity", .1).attr("stroke-width", 0).remove();
return;
}
if (type === 'burg') {
const {x, y} = el;
debug
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', 'none')
.attr('stroke', '#d0240f')
.attr('stroke-width', 1)
.attr('opacity', 1)
.transition(animation)
.attr('r', 20)
.attr('opacity', 0.1)
.attr('stroke-width', 0)
.remove();
return;
}
const [x, y] = el.pole || pack.cells.p[el.center];
const obj = type === "state" ? cells.state : cells.province;
const borderCells = cells.i.filter(id => obj[id] === i && cells.c[id].some(n => obj[n] !== i));
const data = Array.from(borderCells).filter((c, i) => !(i%2)).map(i => cells.p[i]).map(i => [i[0], i[1], Math.hypot(i[0]-x, i[1]-y)]);
const [x, y] = el.pole || pack.cells.p[el.center];
const obj = type === 'state' ? cells.state : cells.province;
const borderCells = cells.i.filter((id) => obj[id] === i && cells.c[id].some((n) => obj[n] !== i));
const data = Array.from(borderCells)
.filter((c, i) => !(i % 2))
.map((i) => cells.p[i])
.map((i) => [i[0], i[1], Math.hypot(i[0] - x, i[1] - y)]);
debug.selectAll("line").data(data).enter().append("line")
.attr("x1", x).attr("y1", y).attr("x2", d => d[0]).attr("y2", d => d[1])
.attr("stroke", "#d0240f").attr("stroke-width", .5).attr("opacity", .2)
.attr("stroke-dashoffset", d => d[2]).attr("stroke-dasharray", d => d[2])
.transition(animation).attr("stroke-dashoffset", 0).attr("opacity", 1)
.transition(animation).delay(1000).attr("stroke-dashoffset", d => d[2]).attr("opacity", 0).remove();
debug
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', x)
.attr('y1', y)
.attr('x2', (d) => d[0])
.attr('y2', (d) => d[1])
.attr('stroke', '#d0240f')
.attr('stroke-width', 0.5)
.attr('opacity', 0.2)
.attr('stroke-dashoffset', (d) => d[2])
.attr('stroke-dasharray', (d) => d[2])
.transition(animation)
.attr('stroke-dashoffset', 0)
.attr('opacity', 1)
.transition(animation)
.delay(1000)
.attr('stroke-dashoffset', (d) => d[2])
.attr('opacity', 0)
.remove();
}
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function(e) {
e.addEventListener("mouseover", function(event) {
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
else tip("Click to lock the option and always use the current value on new map generation");
document.querySelectorAll('[data-locked]').forEach(function (e) {
e.addEventListener('mouseover', function (event) {
if (this.className === 'icon-lock') tip('Click to unlock the option and allow it to be randomized on new map generation');
else tip('Click to lock the option and always use the current value on new map generation');
event.stopPropagation();
});
e.addEventListener("click", function() {
const id = (this.id).slice(5);
if (this.className === "icon-lock") unlock(id);
e.addEventListener('click', function () {
const id = this.id.slice(5);
if (this.className === 'icon-lock') unlock(id);
else lock(id);
});
});
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='"+id+"']");
const input = document.querySelector("[data-stored='" + id + "']");
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById("lock_" + id);
if(!el) return;
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 1;
el.className = "icon-lock";
el.className = 'icon-lock';
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
const el = document.getElementById("lock_" + id);
if(!el) return;
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 0;
el.className = "icon-lock-open";
el.className = 'icon-lock-open';
}
// check if option is locked
function locked(id) {
const lockEl = document.getElementById("lock_" + id);
const lockEl = document.getElementById('lock_' + id);
return lockEl.dataset.locked == 1;
}
@ -385,16 +464,16 @@ function stored(option) {
}
// assign skeaker behaviour
Array.from(document.getElementsByClassName("speaker")).forEach(el => {
Array.from(document.getElementsByClassName('speaker')).forEach((el) => {
const input = el.previousElementSibling;
el.addEventListener("click", () => speak(input.value));
el.addEventListener('click', () => speak(input.value));
});
function speak(text) {
const speaker = new SpeechSynthesisUtterance(text);
const voices = speechSynthesis.getVoices();
if (voices.length) {
const voiceId = +document.getElementById("speakerVoice").value;
const voiceId = +document.getElementById('speakerVoice').value;
speaker.voice = voices[voiceId];
}
speechSynthesis.speak(speaker);
@ -402,21 +481,21 @@ function speak(text) {
// apply drop-down menu option. If the value is not in options, add it
function applyOption(select, id, name = id) {
const custom = !Array.from(select.options).some(o => o.value == id);
const custom = !Array.from(select.options).some((o) => o.value == id);
if (custom) select.options.add(new Option(name, id));
select.value = id;
}
// show info about the generator in a popup
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 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 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');
alertMessage.innerHTML = `
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
@ -434,32 +513,39 @@ function showInfo() {
<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://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>
</ul>`;
$("#alert").dialog({resizable: false, title: document.title, width: "28em",
buttons: {OK: function() {$(this).dialog("close");}},
position: {my: "center", at: "center", of: "svg"}
$('#alert').dialog({
resizable: false,
title: document.title,
width: '28em',
buttons: {
OK: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
}
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener("keydown", event => {
document.addEventListener('keydown', (event) => {
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
if (event.ctrlKey && event.code === "KeyS") event.preventDefault(); // disallow CTRL + C
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener("keyup", event => {
document.addEventListener('keyup', (event) => {
if (!window.closeDialogs) return; // not all modules are loaded
const canvas3d = document.getElementById("canvas3d"); // check if 3d mode is active
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
event.stopPropagation();
const key = event.keyCode;
@ -467,95 +553,174 @@ document.addEventListener("keyup", event => {
const shift = event.shiftKey || key === 16;
const alt = event.altKey || key === 18;
if (key === 112) showInfo(); // "F1" to show info
else if (key === 113) regeneratePrompt(); // "F2" for new map
else if (key === 113) regeneratePrompt(); // "F2" for a new map
else if (key === 117) quickSave(); // "F6" for quick save
else if (key === 120) quickLoad(); // "F9" for quick load
else if (key === 9) toggleOptions(event); // Tab to toggle options
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions(); // "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder(); // Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap(); // Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap(); // Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes(); // Shift + "B" to edit Biomes
else if (shift && key === 83) editStates(); // Shift + "S" to edit States
else if (shift && key === 80) editProvinces(); // Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy(); // Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures(); // Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase(); // Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones(); // Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions(); // Shift + "R" to edit Religions
else if (shift && key === 81) editResources(); // Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor(); // Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits(); // Shift + "W" to edit Units
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers(); // Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary(); // Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails(); // Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg(); // Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel(); // Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver(); // Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute(); // Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker(); // Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs); // Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states); // Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures); // Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions); // Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features); // Alt + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes(); // "B" to toggle Biomes layer
else if (key === 69) toggleCells(); // "E" to toggle Cells layer
else if (key === 71) toggleGrid(); // "G" to toggle Grid layer
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
else if (key === 70) toggleRelief(); // "F" to toggle Relief icons layer
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
else if (key === 83) toggleStates(); // "S" to toggle States layer
else if (key === 80) toggleProvinces(); // "P" to toggle Provinces layer
else if (key === 90) toggleZones(); // "Z" to toggle Zones
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
else if (key === 82) toggleReligions(); // "R" to toggle Religions layer
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 78) togglePopulation(); // "N" to toggle Population layer
else if (key === 74) toggleIce(); // "J" to toggle Ice layer
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
else if (key === 81) toggleResources(); // "Q" to toggle Resources layer
else if (key === 89) toggleEmblems(); // "Y" to toggle Emblems layer
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
else if (key === 77) toggleMilitary(); // "M" to toggle Military layer
else if (key === 75) toggleMarkers(); // "K" to toggle Markers layer
else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9
if (key === 112) showInfo();
// "F1" to show info
else if (key === 113) regeneratePrompt();
// "F2" for new map
else if (key === 113) regeneratePrompt();
// "F2" for a new map
else if (key === 117) quickSave();
// "F6" for quick save
else if (key === 120) quickLoad();
// "F9" for quick load
else if (key === 9) toggleOptions(event);
// Tab to toggle options
else if (key === 27) {
closeDialogs();
hideOptions();
} // Escape to close all dialogs
else if (key === 46) removeElementOnKey();
// "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions();
// "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder();
// Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap();
// Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click();
// Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click();
// Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap();
// Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes();
// Shift + "B" to edit Biomes
else if (shift && key === 83) editStates();
// Shift + "S" to edit States
else if (shift && key === 80) editProvinces();
// Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy();
// Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures();
// Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase();
// Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones();
// Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions();
// Shift + "R" to edit Religions
else if (shift && key === 81) editResources();
// Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor();
// Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits();
// Shift + "W" to edit Units
else if (shift && key === 79) editNotes();
// Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs();
// Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers();
// Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary();
// Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails();
// Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg();
// Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel();
// Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver();
// Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute();
// Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker();
// Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs);
// Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states);
// Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures);
// Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions);
// Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features);
// Alt + "F" to log features data
else if (key === 88) toggleTexture();
// "X" to toggle Texture layer
else if (key === 72) toggleHeight();
// "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes();
// "B" to toggle Biomes layer
else if (key === 69) toggleCells();
// "E" to toggle Cells layer
else if (key === 71) toggleGrid();
// "G" to toggle Grid layer
else if (key === 79) toggleCoordinates();
// "O" to toggle Coordinates layer
else if (key === 87) toggleCompass();
// "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers();
// "V" to toggle Rivers layer
else if (key === 70) toggleRelief();
// "F" to toggle Relief icons layer
else if (key === 67) toggleCultures();
// "C" to toggle Cultures layer
else if (key === 83) toggleStates();
// "S" to toggle States layer
else if (key === 80) toggleProvinces();
// "P" to toggle Provinces layer
else if (key === 90) toggleZones();
// "Z" to toggle Zones
else if (key === 68) toggleBorders();
// "D" to toggle Borders layer
else if (key === 82) toggleReligions();
// "R" to toggle Religions layer
else if (key === 85) toggleRoutes();
// "U" to toggle Routes layer
else if (key === 84) toggleTemp();
// "T" to toggle Temperature layer
else if (key === 78) togglePopulation();
// "N" to toggle Population layer
else if (key === 74) toggleIce();
// "J" to toggle Ice layer
else if (key === 65) togglePrec();
// "A" to toggle Precipitation layer
else if (key === 81) toggleResources();
// "Q" to toggle Resources layer
else if (key === 89) toggleEmblems();
// "Y" to toggle Emblems layer
else if (key === 76) toggleLabels();
// "L" to toggle Labels layer
else if (key === 73) toggleIcons();
// "I" to toggle Icons layer
else if (key === 77) toggleMilitary();
// "M" to toggle Military layer
else if (key === 75) toggleMarkers();
// "K" to toggle Markers layer
else if (key === 187) toggleRulers();
// Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar();
// Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0);
// Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0);
// Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10);
// Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10);
// Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key);
// Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000);
// 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
// 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
// 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
// 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
// 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
// 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
// 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
// 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
// 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
// 9 to zoom to 9
else if (ctrl) pressControl(); // Control to toggle mode
});
@ -564,32 +729,32 @@ function pressNumpadSign(key) {
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById("brushRadius"); else
if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else
if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush"); else
if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush"); else
if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush"); else
if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush"); else
if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id+"Number").value = value;
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : .8;
const scaleBy = key === 107 ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$(".dialog:visible .fastDelete").click();
$('.dialog:visible .fastDelete').click();
$("button:visible:contains('Remove')").click();
}
}

View file

@ -16,15 +16,9 @@ function editHeightmap() {
title: 'Edit Heightmap',
width: '28em',
buttons: {
Erase: function () {
enterHeightmapEditMode('erase');
},
Keep: function () {
enterHeightmapEditMode('keep');
},
Risk: function () {
enterHeightmapEditMode('risk');
},
Erase: () => enterHeightmapEditMode('erase'),
Keep: () => enterHeightmapEditMode('keep'),
Risk: () => enterHeightmapEditMode('risk'),
Cancel: function () {
$(this).dialog('close');
}
@ -77,7 +71,7 @@ function editHeightmap() {
convertImage.style.display = type === 'erase' ? 'inline-block' : 'none';
// hide erosion checkbox if mode is Keep
changeHeightsBox.style.display = type === 'keep' ? 'none' : 'inline-block';
allowErosionBox.style.display = type === 'keep' ? 'none' : 'inline-block';
// show finalize button
if (!sessionStorage.getItem('noExitButtonAnimation')) {
@ -191,19 +185,22 @@ function editHeightmap() {
INFO && console.group('Edit Heightmap');
TIME && console.time('regenerateErasedData');
const change = changeHeights.checked;
const erosionAllowed = allowErosion.checked;
markFeatures();
getSignedDistanceField();
if (change) openNearSeaLakes();
markupGridOcean();
if (erosionAllowed) {
addLakesInDeepDepressions();
openNearSeaLakes();
}
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
Rivers.generate(change);
Rivers.generate(erosionAllowed);
if (!change) {
if (!erosionAllowed) {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g];
@ -248,6 +245,7 @@ function editHeightmap() {
function restoreRiskedData() {
INFO && console.group('Edit Heightmap');
TIME && console.time('restoreRiskedData');
const erosionAllowed = allowErosion.checked;
// assign pack data to grid cells
const l = grid.cells.i.length;
@ -262,7 +260,7 @@ function editHeightmap() {
const culture = new Uint16Array(l);
const religion = new Uint16Array(l);
// rivers data, stored only if changeHeights is unchecked
// rivers data, stored only if allowErosion is unchecked
const fl = new Uint16Array(l);
const r = new Uint16Array(l);
const conf = new Uint8Array(l);
@ -280,7 +278,7 @@ function editHeightmap() {
burg[g] = pack.cells.burg[i];
religion[g] = pack.cells.religion[i];
if (!changeHeights.checked) {
if (!erosionAllowed) {
fl[g] = pack.cells.fl[i];
r[g] = pack.cells.r[i];
conf[g] = pack.cells.conf[i];
@ -312,14 +310,15 @@ function editHeightmap() {
});
markFeatures();
getSignedDistanceField();
markupGridOcean();
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
if (changeHeights.checked) Rivers.generate(changeHeights.checked);
if (erosionAllowed) Rivers.generate(true);
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
@ -334,7 +333,7 @@ function editHeightmap() {
pack.cells.religion = new Uint16Array(n);
pack.cells.biome = new Uint8Array(n);
if (!changeHeights.checked) {
if (!erosionAllowed) {
pack.cells.r = new Uint16Array(n);
pack.cells.conf = new Uint8Array(n);
pack.cells.fl = new Uint16Array(n);
@ -348,7 +347,7 @@ function editHeightmap() {
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
// rivers data
if (!changeHeights.checked) {
if (!erosionAllowed) {
pack.cells.r[i] = r[g];
pack.cells.conf[i] = conf[g];
pack.cells.fl[i] = fl[g];
@ -412,7 +411,7 @@ function editHeightmap() {
drawStates();
drawBorders();
if (changeHeights.checked) {
if (erosionAllowed) {
Rivers.specify();
Lakes.generateName();
}
@ -817,10 +816,25 @@ function editHeightmap() {
const steps = body.querySelectorAll('div').length;
const changed = +body.getAttribute('data-changed');
const template = e.target.value;
if (!steps || !changed) return changeTemplate(template);
if (!steps || !changed) {
changeTemplate(template);
return;
}
const message = 'Are you sure you want to select a different template? <br>All changes will be lost';
confirmationDialog({title: 'Change template', message, confirm: 'Change', onConfirm: () => changeTemplate(template)});
alertMessage.innerHTML = 'Are you sure you want to select a different template? All changes will be lost.';
$('#alert').dialog({
resizable: false,
title: 'Change Template',
buttons: {
Change: function () {
changeTemplate(template);
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function changeTemplate(template) {
@ -952,7 +966,8 @@ function editHeightmap() {
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.getAttribute('data-type');
const type = s.dataset.type;
const elCount = s.querySelector('.templateCount') || '';
const elHeight = s.querySelector('.templateHeight') || '';

View file

@ -14,7 +14,6 @@ function getDefaultPresets() {
heightmap: ['toggleHeight', 'toggleRivers'],
physical: ['toggleCoordinates', 'toggleHeight', 'toggleIce', 'toggleRivers', 'toggleScaleBar'],
poi: ['toggleBorders', 'toggleHeight', 'toggleIce', 'toggleIcons', 'toggleMarkers', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar'],
economical: ['toggleResources', 'toggleBiomes', 'toggleBorders', 'toggleIcons', 'toggleIce', 'toggleLabels', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar'],
military: ['toggleBorders', 'toggleIcons', 'toggleLabels', 'toggleMilitary', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
emblems: ['toggleBorders', 'toggleIcons', 'toggleIce', 'toggleEmblems', 'toggleRivers', 'toggleRoutes', 'toggleScaleBar', 'toggleStates'],
landmass: ['toggleScaleBar']
@ -547,7 +546,7 @@ function drawPopulation(event) {
.transition(show)
.attr('y2', (d) => d[2]);
const urban = burgs.filter((b) => b.i && !b.removed).map((b) => [b.x, b.y, b.y - (b.population / 8) * urbanization.value]);
const urban = burgs.filter((b) => b.i && !b.removed).map((b) => [b.x, b.y, b.y - (b.population / 8) * urbanization]);
population
.select('#urban')
.selectAll('line')
@ -919,7 +918,9 @@ function drawStates() {
const bodyString = bodyData.map((d) => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join('');
const gapString = gapData.map((d) => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join('');
const clipString = bodyData.map((d) => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`).join('');
const haloString = bodyData.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`).join('');
const haloString = bodyData
.map((d) => `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${d3.color(d[2]) ? d3.color(d[2]).darker().hex() : '#666666'}"/>`)
.join('');
statesBody.html(bodyString + gapString);
defs.select('#statePaths').html(clipString);

View file

@ -20,10 +20,7 @@ class Rulers {
for (const rulerString of rulers) {
const [type, pointsString] = rulerString.split(": ");
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
const Type = type === "Ruler" ? Ruler :
type === "Opisometer" ? Opisometer :
type === "RouteOpisometer" ? RouteOpisometer :
type === "Planimeter" ? Planimeter : null;
const Type = type === "Ruler" ? Ruler : type === "Opisometer" ? Opisometer : type === "RouteOpisometer" ? RouteOpisometer : type === "Planimeter" ? Planimeter : null;
this.create(Type, points);
}
}
@ -57,7 +54,7 @@ class Measurer {
}
getSize() {
return rn(1 / scale ** .3 * 2, 2);
return rn((1 / scale ** 0.3) * 2, 2);
}
getDash() {
@ -66,10 +63,11 @@ class Measurer {
drag() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
d3.event.on("drag", function () {
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
this.setAttribute("transform", transform);
});
}
@ -89,9 +87,9 @@ class Measurer {
const MIN_DIST2 = 900;
const optimized = [];
for (let i=0, p1 = this.points[0]; i < this.points.length; i++) {
for (let i = 0, p1 = this.points[0]; i < this.points.length; i++) {
const p2 = this.points[i];
const dist2 = !i || i === this.points.length-1 ? Infinity : (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2;
const dist2 = !i || i === this.points.length - 1 ? Infinity : (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2;
if (dist2 < MIN_DIST2) continue;
optimized.push(p2);
p1 = p2;
@ -105,7 +103,6 @@ class Measurer {
undraw() {
this.el?.remove();
}
}
class Ruler extends Measurer {
@ -136,12 +133,29 @@ class Ruler extends Measurer {
const size = this.getSize();
const dash = this.getDash();
const el = this.el = ruler.append("g").attr("class", "ruler").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
el.append("polyline").attr("points", points).attr("class", "white").attr("stroke-width", size)
const el = (this.el = ruler
.append("g")
.attr("class", "ruler")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("polyline")
.attr("points", points)
.attr("class", "white")
.attr("stroke-width", size)
.call(d3.drag().on("start", () => this.addControl(this)));
el.append("polyline").attr("points", points).attr("class", "gray").attr("stroke-width", rn(size * 1.2, 2)).attr("stroke-dasharray", dash);
el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
el.append("polyline")
.attr("points", points)
.attr("class", "gray")
.attr("stroke-width", rn(size * 1.2, 2))
.attr("stroke-dasharray", dash);
el.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
this.drawPoints(el);
this.updateLabel();
return this;
@ -151,7 +165,7 @@ class Ruler extends Measurer {
const g = el.select(".rulerPoints");
g.selectAll("circle").remove();
for (let i=0; i < this.points.length; i++) {
for (let i = 0; i < this.points.length; i++) {
const [x, y] = this.points[i];
this.drawPoint(g, x, y, i);
}
@ -160,14 +174,25 @@ class Ruler extends Measurer {
drawPoint(el, x, y, i) {
const context = this;
el.append("circle")
.attr("r", "1em").attr("cx", x).attr("cy", y)
.attr("r", "1em")
.attr("cx", x)
.attr("cy", y)
.attr("class", this.isEdge(i) ? "edge" : "control")
.on("click", function() {context.removePoint(context, i)})
.call(d3.drag().clickDistance(3).on("start", function() {context.dragControl(context, i)}));
.on("click", function () {
context.removePoint(context, i);
})
.call(
d3
.drag()
.clickDistance(3)
.on("start", function () {
context.dragControl(context, i);
})
);
}
isEdge(i) {
return i === 0 || i === this.points.length-1;
return i === 0 || i === this.points.length - 1;
}
updateLabel() {
@ -179,9 +204,9 @@ class Ruler extends Measurer {
getLength() {
let length = 0;
for (let i=0; i < this.points.length - 1; i++) {
for (let i = 0; i < this.points.length - 1; i++) {
const [x1, y1] = this.points[i];
const [x2, y2] = this.points[i+1];
const [x2, y2] = this.points[i + 1];
length += Math.hypot(x1 - x2, y1 - y2);
}
return length;
@ -189,20 +214,20 @@ class Ruler extends Measurer {
dragControl(context, pointId) {
let addPoint = context.isEdge(pointId) && d3.event.sourceEvent.ctrlKey;
let circle = context.el.select(`circle:nth-child(${pointId+1})`);
let circle = context.el.select(`circle:nth-child(${pointId + 1})`);
const line = context.el.selectAll("polyline");
let x0 = rn(d3.event.x, 1);
let y0 = rn(d3.event.y, 1);
let axis;
d3.event.on("drag", function() {
d3.event.on("drag", function () {
if (addPoint) {
if (d3.event.dx < .1 && d3.event.dy < .1) return;
if (d3.event.dx < 0.1 && d3.event.dy < 0.1) return;
context.pushPoint(pointId);
context.drawPoints(context.el);
if (pointId) pointId++;
circle = context.el.select(`circle:nth-child(${pointId+1})`);
circle = context.el.select(`circle:nth-child(${pointId + 1})`);
addPoint = false;
}
@ -253,13 +278,38 @@ class Opisometer extends Measurer {
const dash = this.getDash();
const context = this;
const el = this.el = ruler.append("g").attr("class", "opisometer").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
const rulerPoints = el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 0)}));
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 1)}));
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.call(
d3.drag().on("start", function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.call(
d3.drag().on("start", function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -267,7 +317,7 @@ class Opisometer extends Measurer {
}
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(.5));
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
@ -288,7 +338,7 @@ class Opisometer extends Measurer {
const MIN_DIST = d3.event.sourceEvent.shiftKey ? 9 : 100;
let prev = rigth ? last(context.points) : context.points[0];
d3.event.on("drag", function() {
d3.event.on("drag", function () {
const point = [d3.event.x | 0, d3.event.y | 0];
const dist2 = (prev[0] - point[0]) ** 2 + (prev[1] - point[1]) ** 2;
@ -301,7 +351,7 @@ class Opisometer extends Measurer {
context.updateLabel();
});
d3.event.on("end", function() {
d3.event.on("end", function () {
if (!d3.event.sourceEvent.shiftKey) context.optimize();
});
}
@ -367,13 +417,37 @@ class RouteOpisometer extends Measurer {
const dash = this.getDash();
const context = this;
const el = this.el = ruler.append("g").attr("class", "opisometer").attr("font-size", 10 * size);
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
const rulerPoints = el.append("g").attr("class", "rulerPoints").attr("stroke-width", .5 * size).attr("font-size", 2 * size);
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 0)}));
rulerPoints.append("circle").attr("r", "1em").call(d3.drag().on("start", function() {context.dragControl(context, 1)}));
el.append("text").attr("dx", ".35em").attr("dy", "-.45em").on("click", () => rulers.remove(this.id));
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.call(
d3.drag().on("start", function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.call(
d3.drag().on("start", function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -381,7 +455,7 @@ class RouteOpisometer extends Measurer {
}
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(.5));
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
@ -399,7 +473,7 @@ class RouteOpisometer extends Measurer {
}
dragControl(context, rigth) {
d3.event.on("drag", function() {
d3.event.on("drag", function () {
const mousePoint = [d3.event.x | 0, d3.event.y | 0];
const cells = pack.cells;
@ -422,7 +496,11 @@ class Planimeter extends Measurer {
if (this.el) this.el.selectAll("*").remove();
const size = this.getSize();
const el = this.el = ruler.append("g").attr("class", "planimeter").call(d3.drag().on("start", this.drag)).attr("font-size", 10 * size);
const el = (this.el = ruler
.append("g")
.attr("class", "planimeter")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "planimeter").attr("stroke-width", size);
el.append("text").on("click", () => rulers.remove(this.id));
@ -432,7 +510,7 @@ class Planimeter extends Measurer {
}
updateCurve() {
lineGen.curve(d3.curveCatmullRomClosed.alpha(.5));
lineGen.curve(d3.curveCatmullRomClosed.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
}
@ -458,36 +536,79 @@ function drawScaleBar() {
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
const size = +barSize.value;
let val = init * size * dScale / scale; // bar length in distance unit
if (val > 900) val = rn(val, -3); // round to 1000
else if (val > 90) val = rn(val, -2); // round to 100
else if (val > 9) val = rn(val, -1); // round to 10
else val = rn(val) // round to 1
const l = val * scale / dScale; // actual length in pixels on this scale
const size = +barSizeInput.value;
let val = (init * size * dScale) / scale; // bar length in distance unit
if (val > 900) val = rn(val, -3);
// round to 1000
else if (val > 90) val = rn(val, -2);
// round to 100
else if (val > 9) val = rn(val, -1);
// round to 10
else val = rn(val); // round to 1
const l = (val * scale) / dScale; // actual length in pixels on this scale
scaleBar.append("line").attr("x1", 0.5).attr("y1", 0).attr("x2", l+size-0.5).attr("y2", 0).attr("stroke-width", size).attr("stroke", "white");
scaleBar.append("line").attr("x1", 0).attr("y1", size).attr("x2", l+size).attr("y2", size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
scaleBar
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", l + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", l + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
const dash = size + " " + rn(l / 5 - size, 2);
scaleBar.append("line").attr("x1", 0).attr("y1", 0).attr("x2", l+size).attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", l + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", dash)
.attr("stroke", "#3d3d3d");
const fontSize = rn(5 * size, 1);
scaleBar.selectAll("text").data(d3.range(0,6)).enter().append("text")
.attr("x", d => rn(d * l/5, 2)).attr("y", 0).attr("dy", "-.5em")
.attr("font-size", fontSize).text(d => rn(d * l/5 * dScale / scale) + (d<5 ? "" : " " + unit));
scaleBar
.selectAll("text")
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * l) / 5, 2))
.attr("y", 0)
.attr("dy", "-.5em")
.attr("font-size", fontSize)
.text(d => rn((((d * l) / 5) * dScale) / scale) + (d < 5 ? "" : " " + unit));
if (barLabel.value !== "") {
scaleBar.append("text").attr("x", (l+1) / 2).attr("y", 2 * size)
scaleBar
.append("text")
.attr("x", (l + 1) / 2)
.attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize).text(barLabel.value);
.attr("font-size", fontSize)
.text(barLabel.value);
}
const bbox = scaleBar.node().getBBox();
// append backbround rectangle
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
scaleBar
.insert("rect", ":first-child")
.attr("x", -10)
.attr("y", -20)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 15)
.attr("stroke-width", size)
.attr("stroke", "none")
.attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value)
.attr("opacity", +barBackOpacity.value);
fitScaleBar();
}
@ -495,9 +616,10 @@ function drawScaleBar() {
// fit ScaleBar to canvas size
function fitScaleBar() {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
const px = isNaN(+barPosX.value) ? .99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? .99 : barPosY.value / 100;
const px = isNaN(+barPosX.value) ? 0.99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 0.99 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
const x = rn(svgWidth * px - bbox.width + 10),
y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}

View file

@ -67,7 +67,7 @@ function overviewMilitary() {
const states = pack.states.filter((s) => s.i && !s.removed);
for (const s of states) {
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = (total / population) * 100;
@ -114,7 +114,7 @@ function overviewMilitary() {
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach((u) => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
const rate = (line.dataset.rate = (total / population) * 100);
line.querySelector("div[data-type='total']").innerHTML = si(total);
@ -282,12 +282,21 @@ function overviewMilitary() {
}
function militaryRecalculate() {
const message = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
const onConfirm = () => {
Military.generate();
addLines();
};
confirmationDialog({title: 'Remove regiment', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
$('#alert').dialog({
resizable: false,
title: 'Remove regiment',
buttons: {
Recalculate: function () {
$(this).dialog('close');
Military.generate();
addLines();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function downloadMilitaryData() {

View file

@ -61,6 +61,7 @@ function editNotes(id, name) {
document.getElementById('legendsToLoad').addEventListener('change', function () {
uploadFile(this, uploadLegends);
});
document.getElementById('notesClearStyle').addEventListener('click', clearStyle);
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
function showNote(note) {
@ -89,8 +90,20 @@ function editNotes(id, name) {
const element = document.getElementById(select.value);
if (element === null) {
const message = 'Related element is not found. Would you like to remove the note?';
confirmationDialog({title: 'Element not found', message, confirm: 'Remove', onConfirm: removeLegend});
alertMessage.innerHTML = 'Related element is not found. Would you like to remove the note?';
$('#alert').dialog({
resizable: false,
title: 'Element not found',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
return;
}
@ -104,15 +117,34 @@ function editNotes(id, name) {
}
function uploadLegends(dataLoaded) {
if (!dataLoaded) return tip('Cannot load the file. Please check the data format', false, 'error');
if (!dataLoaded) {
tip('Cannot load the file. Please check the data format', false, 'error');
return;
}
notes = JSON.parse(dataLoaded);
document.getElementById('notesSelect').options.length = 0;
editNotes(notes[0].id, notes[0].name);
}
function clearStyle() {
editor.content.innerHTML = editor.content.textContent;
}
function triggerNotesRemove() {
const message = 'Are you sure you want to remove the selected note? <br>This action cannot be reverted';
confirmationDialog({title: 'Remove note', message, confirm: 'Remove', onConfirm: removeLegend});
alertMessage.innerHTML = 'Are you sure you want to remove the selected note?';
$('#alert').dialog({
resizable: false,
title: 'Remove note',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
}
function removeLegend() {
@ -120,7 +152,10 @@ function editNotes(id, name) {
const index = notes.findIndex((n) => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) return $('#notesEditor').dialog('close');
if (!notes.length) {
$('#notesEditor').dialog('close');
return;
}
notesText.innerHTML = '';
editNotes(notes[0].id, notes[0].name);
}

View file

@ -95,7 +95,10 @@ function showSupporters() {
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram`;
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '')
@ -106,35 +109,51 @@ function showSupporters() {
$('#alert').dialog({resizable: false, title: 'Patreon Supporters', width: '54vw', position: {my: 'center', at: 'center', of: 'svg'}});
}
// on any option or dialog change
document.getElementById('options').addEventListener('change', checkIfStored);
document.getElementById('dialogs').addEventListener('change', checkIfStored);
document.getElementById('options').addEventListener('input', updateOutputToFollowInput);
document.getElementById('dialogs').addEventListener('input', updateOutputToFollowInput);
function checkIfStored(ev) {
if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
}
function updateOutputToFollowInput(ev) {
const id = ev.target.id;
const value = ev.target.value;
// specific cases
if (id === 'manorsInput') return (manorsOutput.value = value == 1000 ? 'auto' : value);
// generic case
if (id.slice(-5) === 'Input') {
const output = document.getElementById(id.slice(0, -5) + 'Output');
if (output) output.value = value;
} else if (id.slice(-6) === 'Output') {
const input = document.getElementById(id.slice(0, -6) + 'Input');
if (input) input.value = value;
}
}
// Option listeners
const optionsContent = document.getElementById('optionsContent');
optionsContent.addEventListener('input', function (event) {
const id = event.target.id,
value = event.target.value;
const id = event.target.id;
const value = event.target.value;
if (id === 'mapWidthInput' || id === 'mapHeightInput') mapSizeInputChange();
else if (id === 'pointsInput') changeCellsDensity(+value);
else if (id === 'culturesInput') culturesOutput.value = value;
else if (id === 'culturesOutput') culturesInput.value = value;
else if (id === 'culturesSet') changeCultureSet();
else if (id === 'regionsInput' || id === 'regionsOutput') changeStatesNumber(value);
else if (id === 'provincesInput') provincesOutput.value = value;
else if (id === 'provincesOutput') provincesOutput.value = value;
else if (id === 'provincesOutput') powerOutput.value = value;
else if (id === 'powerInput') powerOutput.value = value;
else if (id === 'powerOutput') powerInput.value = value;
else if (id === 'neutralInput') neutralOutput.value = value;
else if (id === 'neutralOutput') neutralInput.value = value;
else if (id === 'manorsInput') changeBurgsNumberSlider(value);
else if (id === 'religionsInput') religionsOutput.value = value;
else if (id === 'emblemShape') changeEmblemShape(value);
else if (id === 'tooltipSizeInput' || id === 'tooltipSizeOutput') changeTooltipSize(value);
else if (id === 'transparencyInput') changeDialogsTransparency(value);
});
optionsContent.addEventListener('change', function (event) {
if (event.target.dataset.stored) lock(event.target.dataset.stored);
const id = event.target.id,
value = event.target.value;
const id = event.target.id;
const value = event.target.value;
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
@ -330,8 +349,8 @@ function changeCellsDensity(value) {
const cells = convert(value);
pointsInput.setAttribute('data-cells', cells);
pointsOutput.value = cells / 1000 + 'K';
pointsOutput.style.color = cells > 50000 ? '#b12117' : cells !== 10000 ? '#dfdf12' : '#053305';
pointsOutput_formatted.value = cells / 1000 + 'K';
pointsOutput_formatted.style.color = cells > 50000 ? '#b12117' : cells !== 10000 ? '#dfdf12' : '#053305';
}
function changeCultureSet() {
@ -382,16 +401,11 @@ function changeEmblemShape(emblemShape) {
}
function changeStatesNumber(value) {
regionsInput.value = regionsOutput.value = value;
regionsOutput.style.color = +value ? null : '#b12117';
burgLabels.select('#capitals').attr('data-size', Math.max(rn(6 - value / 20), 3));
labels.select('#countries').attr('data-size', Math.max(rn(18 - value / 6), 4));
}
function changeBurgsNumberSlider(value) {
manorsOutput.value = value == 1000 ? 'auto' : value;
}
function changeUIsize(value) {
if (isNaN(+value) || +value < 0.5) return;
@ -408,7 +422,6 @@ function getUImaxSize() {
}
function changeTooltipSize(value) {
tooltipSizeInput.value = tooltipSizeOutput.value = value;
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
}
@ -446,8 +459,9 @@ function applyStoredOptions() {
if (localStorage.getItem('heightUnit')) applyOption(heightUnit, localStorage.getItem('heightUnit'));
for (let i = 0; i < localStorage.length; i++) {
const stored = localStorage.key(i),
value = localStorage.getItem(stored);
const stored = localStorage.key(i);
const value = localStorage.getItem(stored);
if (stored === 'speakerVoice') continue;
const input = document.getElementById(stored + 'Input') || document.getElementById(stored);
const output = document.getElementById(stored + 'Output');
@ -606,23 +620,40 @@ document.getElementById('sticked').addEventListener('click', function (event) {
});
function regeneratePrompt() {
if (customization) return tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
const workingMinutes = (Date.now() - last(mapHistory).created) / 60000;
if (workingMinutes < 5) return regenerateMap();
const message = 'Are you sure you want to generate a new map? <br>All unsaved changes made to the current map will be lost';
const onConfirm = () => {
closeDialogs();
if (customization) {
tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
return;
}
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
regenerateMap();
};
confirmationDialog({title: 'Generate new map', message, confirm: 'Generate', onConfirm});
return;
}
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`;
$('#alert').dialog({
resizable: false,
title: 'Generate new map',
buttons: {
Cancel: function () {
$(this).dialog('close');
},
Generate: function () {
closeDialogs();
regenerateMap();
}
}
});
}
function showSavePane() {
document.getElementById('showLabels').checked = !hideLabels.checked;
$('#saveMapData').dialog({
title: 'Save map',
resizable: false,
width: '27em',
width: '30em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Close: function () {
@ -703,6 +734,74 @@ document.getElementById('mapToLoad').addEventListener('change', function () {
uploadMap(fileToLoad);
});
function openSaveTiles() {
closeDialogs();
updateTilesOptions();
const status = document.getElementById('tileStatus');
status.innerHTML = '';
let loading = null;
$('#saveTilesScreen').dialog({
resizable: false,
title: 'Download tiles',
width: '23em',
buttons: {
Download: function () {
status.innerHTML = 'Preparing for download...';
setTimeout(() => (status.innerHTML = 'Downloading. It may take some time.'), 1000);
loading = setInterval(() => (status.innerHTML += '.'), 1000);
saveTiles().then(() => {
clearInterval(loading);
status.innerHTML = `Done. Check file in "Downloads" (crtl + J)`;
setTimeout(() => (status.innerHTML = ''), 8000);
});
},
Cancel: function () {
$(this).dialog('close');
}
},
close: () => {
debug.selectAll('*').remove();
clearInterval(loading);
}
});
}
document
.getElementById('saveTilesScreen')
.querySelectorAll('input')
.forEach((el) => el.addEventListener('input', updateTilesOptions));
function updateTilesOptions() {
const tileSize = document.getElementById('tileSize');
const tilesX = +document.getElementById('tileColsOutput').value;
const tilesY = +document.getElementById('tileRowsOutput').value;
const scale = +document.getElementById('tileScaleOutput').value;
// calculate size
const sizeX = graphWidth * scale * tilesX;
const sizeY = graphHeight * scale * tilesY;
const totalSize = sizeX * sizeY;
tileSize.innerHTML = `${sizeX} x ${sizeY} px`;
tileSize.style.color = totalSize > 1e9 ? '#d00b0b' : totalSize > 1e8 ? '#9e6409' : '#1a941a';
// draw tiles
const rects = [];
const labels = [];
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${i}</text>`);
}
}
const rectsG = "<g fill='none' stroke='#000'>" + rects.join('') + '</g>';
const labelsG = "<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='24px'>" + labels.join('') + '</g>';
debug.html(rectsG + labelsG);
}
// View mode
viewMode.addEventListener('click', changeViewMode);
function changeViewMode(event) {

View file

@ -47,7 +47,7 @@ function editProvinces() {
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);
else if (cl.contains('icon-flag-empty')) triggerIndependence(p);
else if (cl.contains('icon-flag-empty')) triggerIndependencePromps(p);
else if (cl.contains('culturePopulation')) changePopulation(p);
else if (cl.contains('icon-pin')) toggleFog(p, cl);
else if (cl.contains('icon-trash-empty')) removeProvince(p);
@ -118,8 +118,8 @@ function editProvinces() {
for (const p of filtered) {
const area = p.area * distanceScaleInput.value ** 2;
totalArea += area;
const rural = p.rural * populationRate.value;
const urban = p.urban * populationRate.value * urbanization.value;
const rural = p.rural * populationRate;
const urban = p.urban * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalPopulation += population;
@ -233,9 +233,21 @@ function editProvinces() {
zoomTo(x, y, 8, 2000);
}
function triggerIndependence(p) {
const message = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
confirmationDialog({title: 'Declare independence', message, confirm: 'Declare', onConfirm: () => declareProvinceIndependence(p)});
function triggerIndependencePromps(p) {
alertMessage.innerHTML = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
$('#alert').dialog({
resizable: false,
title: 'Declare independence',
buttons: {
Declare: function () {
declareProvinceIndependence(p);
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function declareProvinceIndependence(p) {
@ -328,8 +340,8 @@ function editProvinces() {
tip('Province does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(p.rural * populationRate.value);
const urban = rn(p.urban * populationRate.value * urbanization.value);
const rural = rn(p.rural * populationRate);
const urban = rn(p.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
@ -370,7 +382,7 @@ function editProvinces() {
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const points = ruralPop.value / populationRate;
const pop = rn(points / cells.length);
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
@ -380,7 +392,7 @@ function editProvinces() {
p.burgs.forEach((b) => (pack.burgs[b].population = rn(pack.burgs[b].population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
p.burgs.forEach((b) => (pack.burgs[b].population = population));
}
@ -397,31 +409,40 @@ function editProvinces() {
}
function removeProvince(p) {
const message = 'Are you sure you want to remove the province? <br>This action cannot be reverted';
const onConfirm = () => {
pack.cells.province.forEach((province, i) => {
if (province === p) pack.cells.province[i] = 0;
});
const s = pack.provinces[p].state;
const state = pack.states[s];
if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1);
alertMessage.innerHTML = `Are you sure you want to remove the province? <br>This action cannot be reverted`;
$('#alert').dialog({
resizable: false,
title: 'Remove province',
buttons: {
Remove: function () {
pack.cells.province.forEach((province, i) => {
if (province === p) pack.cells.province[i] = 0;
});
const s = pack.provinces[p].state,
state = pack.states[s];
if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1);
unfog('focusProvince' + p);
unfog('focusProvince' + p);
const coaId = 'provinceCOA' + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
const coaId = 'provinceCOA' + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
pack.provinces[p] = {i: p, removed: true};
pack.provinces[p] = {i: p, removed: true};
const g = provs.select('#provincesBody');
g.select('#province' + p).remove();
g.select('#province-gap' + p).remove();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
refreshProvincesEditor();
};
confirmationDialog({title: 'Remove province', message, confirm: 'Remove', onConfirm});
const g = provs.select('#provincesBody');
g.select('#province' + p).remove();
g.select('#province-gap' + p).remove();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
refreshProvincesEditor();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function editProvinceName(province) {
@ -571,8 +592,8 @@ function editProvinces() {
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
const rural = rn(d.data.rural * populationRate.value);
const urban = rn(d.data.urban * populationRate.value * urbanization.value);
const rural = rn(d.data.rural * populationRate);
const urban = rn(d.data.urban * populationRate * urbanization);
const value =
provincesTreeType.value === 'area'
@ -938,8 +959,8 @@ function editProvinces() {
data += el.dataset.capital + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.provinces[key].rural * populationRate.value)},`;
data += `${Math.round(pack.provinces[key].urban * populationRate.value * urbanization.value)}\n`;
data += `${Math.round(pack.provinces[key].rural * populationRate)},`;
data += `${Math.round(pack.provinces[key].urban * populationRate * urbanization)}\n`;
});
const name = getFileName('Provinces') + '.csv';
@ -947,26 +968,36 @@ function editProvinces() {
}
function removeAllProvinces() {
const message = `Are you sure you want to remove all provinces? <br>This action cannot be reverted`;
const onConfirm = () => {
// remove emblems
document.querySelectorAll("[id^='provinceCOA']").forEach((el) => el.remove());
emblems.select('#provinceEmblems').selectAll('*').remove();
alertMessage.innerHTML = `Are you sure you want to remove all provinces? <br>This action cannot be reverted`;
$('#alert').dialog({
resizable: false,
title: 'Remove all provinces',
buttons: {
Remove: function () {
$(this).dialog('close');
// remove data
pack.provinces = [0];
pack.cells.province = new Uint16Array(pack.cells.i.length);
pack.states.forEach((s) => (s.provinces = []));
// remove emblems
document.querySelectorAll("[id^='provinceCOA']").forEach((el) => el.remove());
emblems.select('#provinceEmblems').selectAll('*').remove();
unfog();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
provs.select('#provincesBody').remove();
turnButtonOff('toggleProvinces');
// remove data
pack.provinces = [0];
pack.cells.province = new Uint16Array(pack.cells.i.length);
pack.states.forEach((s) => (s.provinces = []));
provincesEditorAddLines();
};
confirmationDialog({title: 'Remove all provinces', message, confirm: 'Remove', onConfirm});
unfog();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
provs.select('#provincesBody').remove();
turnButtonOff('toggleProvinces');
provincesEditorAddLines();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function dragLabel() {

View file

@ -68,8 +68,8 @@ function editReligions() {
if (r.removed) continue;
const area = r.area * distanceScaleInput.value ** 2;
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const rural = r.rural * populationRate;
const urban = r.urban * populationRate * urbanization;
const population = rn(rural + urban);
if (r.i && !r.cells && body.dataset.extinct !== 'show') continue; // hide extinct religions
const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(urban)}. Click to change`;
@ -160,8 +160,8 @@ function editReligions() {
const r = pack.religions[religion];
const type = r.name.includes(r.type) ? '' : r.type === 'Folk' || r.type === 'Organized' ? '. ' + r.type + ' religion' : '. ' + r.type;
const form = r.form === r.type || r.name.includes(r.form) ? '' : '. ' + r.form;
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const rural = r.rural * populationRate;
const urban = r.urban * populationRate * urbanization;
const population = rural + urban > 0 ? '. ' + si(rn(rural + urban)) + ' believers' : '. Extinct';
info.innerHTML = `${r.name}${type}${form}${population}`;
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
@ -273,8 +273,8 @@ function editReligions() {
tip('Religion does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(r.rural * populationRate.value);
const urban = rn(r.urban * populationRate.value * urbanization.value);
const rural = rn(r.rural * populationRate);
const urban = rn(r.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
const burgs = pack.burgs.filter((b) => !b.removed && pack.cells.religion[b.cell] === religion);
@ -318,7 +318,7 @@ function editReligions() {
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter((i) => pack.cells.religion[i] === religion);
const pop = rn(points / cells.length);
cells.forEach((i) => (pack.cells.pop[i] = pop));
@ -329,7 +329,7 @@ function editReligions() {
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach((b) => (b.population = population));
}
@ -342,24 +342,33 @@ function editReligions() {
if (customization) return;
const religion = +this.parentNode.dataset.id;
const message = 'Are you sure you want to remove the religion? <br>This action cannot be reverted';
const onConfirm = () => {
relig.select('#religion' + religion).remove();
relig.select('#religion-gap' + religion).remove();
debug.select('#religionsCenter' + religion).remove();
alertMessage.innerHTML = 'Are you sure you want to remove the religion? <br>This action cannot be reverted';
$('#alert').dialog({
resizable: false,
title: 'Remove religion',
buttons: {
Remove: function () {
relig.select('#religion' + religion).remove();
relig.select('#religion-gap' + religion).remove();
debug.select('#religionsCenter' + religion).remove();
pack.cells.religion.forEach((r, i) => {
if (r === religion) pack.cells.religion[i] = 0;
});
pack.religions[religion].removed = true;
const origin = pack.religions[religion].origin;
pack.religions.forEach((r) => {
if (r.origin === religion) r.origin = origin;
});
pack.cells.religion.forEach((r, i) => {
if (r === religion) pack.cells.religion[i] = 0;
});
pack.religions[religion].removed = true;
const origin = pack.religions[religion].origin;
pack.religions.forEach((r) => {
if (r.origin === religion) r.origin = origin;
});
refreshReligionsEditor();
};
confirmationDialog({title: 'Remove religion', message, confirm: 'Remove', onConfirm});
refreshReligionsEditor();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function drawReligionCenters() {

View file

@ -89,8 +89,8 @@ function editStates() {
for (const s of pack.states) {
if (s.removed) continue;
const area = s.area * distanceScaleInput.value ** 2;
const rural = s.rural * populationRate.value;
const urban = s.urban * populationRate.value * urbanization.value;
const rural = s.rural * populationRate;
const urban = s.urban * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
totalArea += area;
@ -362,8 +362,8 @@ function editStates() {
tip('State does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(s.rural * populationRate.value);
const urban = rn(s.urban * populationRate.value * urbanization.value);
const rural = rn(s.rural * populationRate);
const urban = rn(s.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
@ -405,7 +405,7 @@ function editStates() {
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter((i) => pack.cells.state[i] === state);
const pop = points / cells.length;
cells.forEach((i) => (pack.cells.pop[i] = pop));
@ -417,7 +417,7 @@ function editStates() {
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const points = urbanPop.value / populationRate / urbanization;
const burgs = pack.burgs.filter((b) => !b.removed && b.state === state);
const population = rn(points / burgs.length, 4);
burgs.forEach((b) => (b.population = population));
@ -459,8 +459,21 @@ function editStates() {
function stateRemovePrompt(state) {
if (customization) return;
const message = 'Are you sure you want to remove the state? <br>This action cannot be reverted';
confirmationDialog({title: 'Remove state', message, confirm: 'Remove', onConfirm: () => stateRemove(state)});
alertMessage.innerHTML = 'Are you sure you want to remove the state? <br>This action cannot be reverted';
$('#alert').dialog({
resizable: false,
title: 'Remove state',
buttons: {
Remove: function () {
$(this).dialog('close');
stateRemove(state);
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function stateRemove(state) {
@ -627,8 +640,8 @@ function editStates() {
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = d.data.area * distanceScaleInput.value ** 2 + unit;
const rural = rn(d.data.rural * populationRate.value);
const urban = rn(d.data.urban * populationRate.value * urbanization.value);
const rural = rn(d.data.rural * populationRate);
const urban = rn(d.data.urban * populationRate * urbanization);
const option = statesTreeType.value;
const value =
@ -1056,8 +1069,8 @@ function editStates() {
data += el.dataset.burgs + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.states[key].rural * populationRate.value)},`;
data += `${Math.round(pack.states[key].urban * populationRate.value * urbanization.value)}\n`;
data += `${Math.round(pack.states[key].rural * populationRate)},`;
data += `${Math.round(pack.states[key].urban * populationRate * urbanization)}\n`;
});
const name = getFileName('States') + '.csv';

File diff suppressed because one or more lines are too long

View file

@ -1,93 +1,110 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
"use strict";
'use strict';
toolsContent.addEventListener("click", function(event) {
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
if (event.target.tagName !== "BUTTON") return;
toolsContent.addEventListener('click', function (event) {
if (customization) {
tip('Please exit the customization mode first', false, 'warning');
return;
}
if (event.target.tagName !== 'BUTTON') return;
const button = event.target.id;
// Click to open Editor buttons
if (button === "editHeightmapButton") editHeightmap(); else
if (button === "editBiomesButton") editBiomes(); else
if (button === "editStatesButton") editStates(); else
if (button === "editProvincesButton") editProvinces(); else
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editResources") editResources(); else
if (button === "editEmblemButton") openEmblemEditor(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
if (button === "editZonesButton") editZones(); else
if (button === "overviewBurgsButton") overviewBurgs(); else
if (button === "overviewRiversButton") overviewRivers(); else
if (button === "overviewMilitaryButton") overviewMilitary(); else
if (button === "overviewCellsButton") viewCellDetails();
if (button === 'editHeightmapButton') editHeightmap();
else if (button === 'editBiomesButton') editBiomes();
else if (button === 'editStatesButton') editStates();
else if (button === 'editProvincesButton') editProvinces();
else if (button === 'editDiplomacyButton') editDiplomacy();
else if (button === 'editCulturesButton') editCultures();
else if (button === 'editReligions') editReligions();
else if (button === 'editEmblemButton') openEmblemEditor();
else if (button === 'editNamesBaseButton') editNamesbase();
else if (button === 'editUnitsButton') editUnits();
else if (button === 'editNotesButton') editNotes();
else if (button === 'editZonesButton') editZones();
else if (button === 'overviewBurgsButton') overviewBurgs();
else if (button === 'overviewRiversButton') overviewRivers();
else if (button === 'overviewMilitaryButton') overviewMilitary();
else if (button === 'overviewCellsButton') viewCellDetails();
// Click to Regenerate buttons
if (event.target.parentNode.id === "regenerateFeature") {
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(event, button); return;}
if (event.target.parentNode.id === 'regenerateFeature') {
if (sessionStorage.getItem('regenerateFeatureDontAsk')) {
processFeatureRegeneration(event, button);
return;
}
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
$("#alert").dialog({resizable: false, title: "Regenerate element",
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`;
$('#alert').dialog({
resizable: false,
title: 'Regenerate element',
buttons: {
Proceed: function() {processFeatureRegeneration(event, button); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
Proceed: function () {
processFeatureRegeneration(event, button);
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
},
open: function() {
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
open: function () {
const pane = $(this).dialog('widget').find('.ui-dialog-buttonpane');
$('<span><input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label><span>').prependTo(pane);
},
close: function() {
const box = $(this).dialog("widget").find(".checkbox")[0];
close: function () {
const box = $(this).dialog('widget').find('.checkbox')[0];
if (!box) return;
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
$(this).dialog("destroy");
if (box.checked) sessionStorage.setItem('regenerateFeatureDontAsk', true);
$(this).dialog('destroy');
}
});
}
// Click to Add buttons
if (button === "addLabel") toggleAddLabel(); else
if (button === "addBurgTool") toggleAddBurg(); else
if (button === "addRiver") toggleAddRiver(); else
if (button === "addRoute") toggleAddRoute(); else
if (button === "addMarker") toggleAddMarker();
if (button === 'addLabel') toggleAddLabel();
else if (button === 'addBurgTool') toggleAddBurg();
else if (button === 'addRiver') toggleAddRiver();
else if (button === 'addRoute') toggleAddRoute();
else if (button === 'addMarker') toggleAddMarker();
});
function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateStates") regenerateStates(); else
if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateResources") regenerateResources(); else
if (button === "regenerateEmblems") regenerateEmblems(); else
if (button === "regenerateReligions") regenerateReligions(); else
if (button === "regenerateCultures") regenerateCultures(); else
if (button === "regenerateMilitary") regenerateMilitary(); else
if (button === "regenerateIce") regenerateIce(); else
if (button === "regenerateMarkers") regenerateMarkers(event); else
if (button === "regenerateZones") regenerateZones(event);
if (button === 'regenerateStateLabels') {
BurgsAndStates.drawStateLabels();
if (!layerIsOn('toggleLabels')) toggleLabels();
} else if (button === 'regenerateReliefIcons') {
ReliefIcons();
if (!layerIsOn('toggleRelief')) toggleRelief();
} else if (button === 'regenerateRoutes') {
Routes.regenerate();
if (!layerIsOn('toggleRoutes')) toggleRoutes();
} else if (button === 'regenerateRivers') regenerateRivers();
else if (button === 'regeneratePopulation') recalculatePopulation();
else if (button === 'regenerateStates') regenerateStates();
else if (button === 'regenerateProvinces') regenerateProvinces();
else if (button === 'regenerateBurgs') regenerateBurgs();
else if (button === 'regenerateEmblems') regenerateEmblems();
else if (button === 'regenerateReligions') regenerateReligions();
else if (button === 'regenerateCultures') regenerateCultures();
else if (button === 'regenerateMilitary') regenerateMilitary();
else if (button === 'regenerateIce') regenerateIce();
else if (button === 'regenerateMarkers') regenerateMarkers(event);
else if (button === 'regenerateZones') regenerateZones(event);
}
async function openEmblemEditor() {
let type, id, el;
if (pack.states[1]?.coa) {
type = "state";
id = "stateCOA1";
type = 'state';
id = 'stateCOA1';
el = pack.states[1];
} else if (pack.burgs[1]?.coa) {
type = "burg";
id = "burgCOA1";
type = 'burg';
id = 'burgCOA1';
el = pack.burgs[1];
} else {
tip("No emblems to edit, please generate states and burgs first", false, "error");
tip('No emblems to edit, please generate states and burgs first', false, 'error');
return;
}
@ -99,98 +116,105 @@ function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
if (!layerIsOn('toggleRivers')) toggleRivers();
}
function recalculatePopulation() {
rankCells();
pack.burgs.forEach(b => {
pack.burgs.forEach((b) => {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
});
}
function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (!burgs.length) {
tip("No burgs to generate states. Please create burgs first", false, "error");
tip('No burgs to generate states. Please create burgs first', false, 'error');
return;
}
if (burgs.length < +regionsInput.value) {
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn");
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, 'warn');
}
// burg local ids sorted by a bit randomized population:
const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
const sorted = burgs
.map((b, i) => [i, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map((b) => b[0]);
const capitalsTree = d3.quadtree();
// turn all old capitals into towns
burgs.filter(b => b.capital).forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = 0;
});
burgs
.filter((b) => b.capital)
.forEach((b) => {
moveBurgToGroup(b.i, 'towns');
b.capital = 0;
});
// remove emblems
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
emblems.selectAll('use').remove();
unfog();
// if desired states number is 0
if (regionsInput.value == 0) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
pack.states = pack.states.slice(0,1); // remove all except of neutrals
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, 'warn');
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
pack.provinces = [0]; // remove all provinces
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
borders.selectAll("path").remove(); // remove borders
regions.selectAll("path").remove(); // remove states fill
labels.select("#states").selectAll("text"); // remove state labels
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
borders.selectAll('path').remove(); // remove borders
regions.selectAll('path').remove(); // remove states fill
labels.select('#states').selectAll('text'); // remove state labels
defs.select('#textPaths').selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
return;
}
const neutral = pack.states[0].name;
const count = Math.min(+regionsInput.value, burgs.length);
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
pack.states = d3.range(count).map(i => {
pack.states = d3.range(count).map((i) => {
if (!i) return {i, name: neutral};
let capital = null, x = 0, y = 0;
let capital = null,
x = 0,
y = 0;
for (const i of sorted) {
capital = burgs[i];
x = capital.x, y = capital.y;
(x = capital.x), (y = capital.y);
if (capitalsTree.find(x, y, spacing) === undefined) break;
spacing = Math.max(spacing - 1, 1);
}
capitalsTree.add([x, y]);
capital.capital = 1;
moveBurgToGroup(capital.i, "cities");
moveBurgToGroup(capital.i, 'cities');
const culture = capital.culture;
const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, '', 0);
const name = Names.getState(basename, culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type;
const type = nomadic ? 'Nomadic' : pack.cultures[culture].type === 'Nomadic' ? 'Generic' : pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, .3, null, cultureType);
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
coa.shield = capital.coa.shield;
return {i, name, type, capital:capital.i, center:capital.cell, culture, expansionism, coa};
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
});
BurgsAndStates.expandStates();
@ -201,15 +225,17 @@ function regenerateStates() {
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn('toggleStates')) toggleStates();
else drawStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
BurgsAndStates.drawStateLabels();
Military.generate();
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
if (layerIsOn('toggleEmblems')) drawEmblems(); // redrawEmblems
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
if (document.getElementById('militaryOverviewRefresh').offsetParent) militaryOverviewRefresh.click();
}
function regenerateProvinces() {
@ -217,49 +243,61 @@ function regenerateProvinces() {
BurgsAndStates.generateProvinces(true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn('toggleProvinces')) drawProvinces();
// remove emblems
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems();
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
emblems.selectAll('use').remove();
if (layerIsOn('toggleEmblems')) drawEmblems();
}
function regenerateBurgs() {
const cells = pack.cells, states = pack.states, Lockedburgs = pack.burgs.filter(b =>b.lock);
const cells = pack.cells,
states = pack.states,
Lockedburgs = pack.burgs.filter((b) => b.lock);
rankCells();
cells.burg = new Uint16Array(cells.i.length);
const burgs = pack.burgs = [0]; // clear burgs array
states.filter(s => s.i).forEach(s => s.capital = 0); // clear state capitals
pack.provinces.filter(p => p.i).forEach(p => p.burg = 0); // clear province capitals
const burgs = (pack.burgs = [0]); // clear burgs array
states.filter((s) => s.i).forEach((s) => (s.capital = 0)); // clear state capitals
pack.provinces.filter((p) => p.i).forEach((p) => (p.burg = 0)); // clear province capitals
const burgsTree = d3.quadtree();
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** .8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** .7 / 66); // base min distance between towns
const score = new Int16Array(cells.s.map((s) => s * Math.random())); // cell score for capitals placement
const sorted = cells.i.filter((i) => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns
for (let j=0; j < Lockedburgs.length; j++) {
//clear locked list since ids will change
//burglock.selectAll("text").remove();
for (let j = 0; j < Lockedburgs.length; j++) {
const id = burgs.length;
const oldBurg = Lockedburgs[j];
oldBurg.i = id;
burgs.push(oldBurg);
burgsTree.add([oldBurg.x, oldBurg.y]);
cells.burg[oldBurg.cell] = id;
if (oldBurg.capital) {states[oldBurg.state].capital = id; states[oldBurg.state].center = oldBurg.cell;}
if (oldBurg.capital) {
states[oldBurg.state].capital = id;
states[oldBurg.state].center = oldBurg.cell;
}
//burglock.append("text").attr("data-id", id);
}
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
const cell = sorted[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const x = cells.p[cell][0],
y = cells.p[cell][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const state = cells.state[cell];
const capital = state && !states[state].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands
if (capital) {states[state].capital = id; states[state].center = cell;}
if (capital) {
states[state].capital = id;
states[state].center = cell;
}
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
@ -269,92 +307,97 @@ function regenerateBurgs() {
}
// add a capital at former place for states without added capitals
states.filter(s => s.i && !s.removed && !s.capital).forEach(s => {
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
s.capital = burg;
s.center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1;
pack.burgs[burg].state = s.i;
moveBurgToGroup(burg, "cities");
});
states
.filter((s) => s.i && !s.removed && !s.capital)
.forEach((s) => {
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
s.capital = burg;
s.center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1;
pack.burgs[burg].state = s.i;
moveBurgToGroup(burg, 'cities');
});
pack.features.forEach(f => {if (f.port) f.port = 0}); // reset features ports counter
pack.features.forEach((f) => {
if (f.port) f.port = 0;
}); // reset features ports counter
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
Routes.regenerate();
// remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems();
document.querySelectorAll('[id^=burgCOA]').forEach((el) => el.remove());
emblems.selectAll('use').remove();
if (layerIsOn('toggleEmblems')) drawEmblems();
if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
if (document.getElementById('burgsOverviewRefresh').offsetParent) burgsOverviewRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
}
function regenerateResources() {
Resources.generate();
goods.selectAll("*").remove();
if (layerIsOn("toggleResources")) drawResources();
goods.selectAll('*').remove();
if (layerIsOn('toggleResources')) drawResources();
refreshAllEditors();
}
function regenerateEmblems() {
// remove old emblems
document.querySelectorAll("[id^=stateCOA]").forEach(el => el.remove());
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
document.querySelectorAll('[id^=provinceCOA]').forEach((el) => el.remove());
document.querySelectorAll('[id^=burgCOA]').forEach((el) => el.remove());
emblems.selectAll('use').remove();
// generate new emblems
pack.states.forEach(state => {
pack.states.forEach((state) => {
if (!state.i || state.removed) return;
const cultureType = pack.cultures[state.culture].type;
state.coa = COA.generate(null, null, null, cultureType);
state.coa.shield = COA.getShield(state.culture, null);
});
pack.burgs.forEach(burg => {
pack.burgs.forEach((burg) => {
if (!burg.i || burg.removed) return;
const state = pack.states[burg.state];
let kinship = state ? .25 : 0;
if (burg.capital) kinship += .1;
else if (burg.port) kinship -= .1;
if (state && burg.culture !== state.culture) kinship -= .25;
let kinship = state ? 0.25 : 0;
if (burg.capital) kinship += 0.1;
else if (burg.port) kinship -= 0.1;
if (state && burg.culture !== state.culture) kinship -= 0.25;
burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type);
burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0);
});
pack.provinces.forEach(province => {
pack.provinces.forEach((province) => {
if (!province.i || province.removed) return;
const parent = province.burg ? pack.burgs[province.burg] : pack.states[province.state];
let dominion = false;
if (!province.burg) {
dominion = P(.2);
if (province.formName === "Colony") dominion = P(.95); else
if (province.formName === "Island") dominion = P(.6); else
if (province.formName === "Islands") dominion = P(.5); else
if (province.formName === "Territory") dominion = P(.4); else
if (province.formName === "Land") dominion = P(.3);
dominion = P(0.2);
if (province.formName === 'Colony') dominion = P(0.95);
else if (province.formName === 'Island') dominion = P(0.6);
else if (province.formName === 'Islands') dominion = P(0.5);
else if (province.formName === 'Territory') dominion = P(0.4);
else if (province.formName === 'Land') dominion = P(0.3);
}
const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3);
const kinship = dominion ? 0 : nameByBurg ? .8 : .4;
const kinship = dominion ? 0 : nameByBurg ? 0.8 : 0.4;
const culture = pack.cells.culture[province.center];
const type = BurgsAndStates.getType(province.center, parent.port);
province.coa = COA.generate(parent.coa, kinship, dominion, type);
province.coa.shield = COA.getShield(culture, province.state);
});
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
if (layerIsOn('toggleEmblems')) drawEmblems(); // redrawEmblems
}
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
if (!layerIsOn('toggleReligions')) toggleReligions();
else drawReligions();
}
function regenerateCultures() {
@ -362,66 +405,73 @@ function regenerateCultures() {
Cultures.expand();
BurgsAndStates.updateCultures();
Religions.updateCultures();
if (!layerIsOn("toggleCultures")) toggleCultures(); else drawCultures();
if (!layerIsOn('toggleCultures')) toggleCultures();
else drawCultures();
refreshAllEditors();
}
function regenerateMilitary() {
Military.generate();
if (!layerIsOn("toggleMilitary")) toggleMilitary();
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
if (!layerIsOn('toggleMilitary')) toggleMilitary();
if (document.getElementById('militaryOverviewRefresh').offsetParent) militaryOverviewRefresh.click();
}
function regenerateIce() {
if (!layerIsOn("toggleIce")) toggleIce();
ice.selectAll("*").remove();
if (!layerIsOn('toggleIce')) toggleIce();
ice.selectAll('*').remove();
drawIce();
}
function regenerateMarkers(event) {
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, .5, .3, 5, 2));
if (isCtrlClick(event)) prompt('Please provide markers number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2));
function addNumberOfMarkers(number) {
// remove existing markers and assigned notes
markers.selectAll("use").each(function() {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
}).remove();
markers
.selectAll('use')
.each(function () {
const index = notes.findIndex((n) => n.id === this.id);
if (index != -1) notes.splice(index, 1);
})
.remove();
addMarkers(number);
if (!layerIsOn("toggleMarkers")) toggleMarkers();
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
}
function regenerateZones(event) {
if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfZones(v));
else addNumberOfZones(gauss(1, .5, .6, 5, 2));
if (isCtrlClick(event)) prompt('Please provide zones number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfZones(v));
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
function addNumberOfZones(number) {
zones.selectAll("g").remove(); // remove existing zones
zones.selectAll('g').remove(); // remove existing zones
addZones(number);
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (!layerIsOn("toggleZones")) toggleZones();
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click();
if (!layerIsOn('toggleZones')) toggleZones();
}
}
function unpressClickToAddButton() {
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
restoreDefaultEvents();
clearMainTip();
}
function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
const pressed = document.getElementById('addLabel').classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addLabel.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
tip("Click on map to place label. Hold Shift to add multiple", true);
if (!layerIsOn("toggleLabels")) toggleLabels();
closeDialogs('.stable');
viewbox.style('cursor', 'crosshair').on('click', addLabelOnClick);
tip('Click on map to place label. Hold Shift to add multiple', true);
if (!layerIsOn('toggleLabels')) toggleLabels();
}
function addLabelOnClick() {
@ -431,53 +481,71 @@ function addLabelOnClick() {
const cell = findCell(point[0], point[1]);
const culture = pack.cells.culture[cell];
const name = Names.getCulture(culture);
const id = getNextId("label");
const id = getNextId('label');
let group = labels.select("#addedLabels");
if (!group.size()) group = labels.append("g").attr("id", "addedLabels")
.attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a")
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
let group = labels.select('#addedLabels');
if (!group.size())
group = labels
.append('g')
.attr('id', 'addedLabels')
.attr('fill', '#3e3e4b')
.attr('opacity', 1)
.attr('stroke', '#3a3a3a')
.attr('stroke-width', 0)
.attr('font-family', 'Almendra SC')
.attr('data-font', 'Almendra+SC')
.attr('font-size', 18)
.attr('data-size', 18)
.attr('filter', null);
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
const example = group.append('text').attr('x', 0).attr('x', 0).text(name);
const width = example.node().getBBox().width;
const x = width / -2; // x offset;
example.remove();
group.classed("hidden", false);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
.append("tspan").attr("x", x).text(name);
group.classed('hidden', false);
group
.append('text')
.attr('id', id)
.append('textPath')
.attr('xlink:href', '#textPath_' + id)
.attr('startOffset', '50%')
.attr('font-size', '100%')
.append('tspan')
.attr('x', x)
.text(name);
defs.select("#textPaths")
.append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
defs
.select('#textPaths')
.append('path')
.attr('id', 'textPath_' + id)
.attr('d', `M${point[0] - width},${point[1]} h${width * 2}`);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}
function toggleAddBurg() {
unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed");
document.getElementById('addBurgTool').classList.add('pressed');
overviewBurgs();
document.getElementById("addNewBurg").click();
document.getElementById('addNewBurg').click();
}
function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed");
const pressed = document.getElementById('addRiver').classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
document.getElementById('addNewRiver').classList.remove('pressed');
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addRiver.classList.add('pressed');
document.getElementById("addNewRiver").classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById('addNewRiver').classList.add('pressed');
closeDialogs('.stable');
viewbox.style('cursor', 'crosshair').on('click', addRiverOnClick);
tip('Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers', true, 'warn');
if (!layerIsOn('toggleRivers')) toggleRivers();
}
function addRiverOnClick() {
@ -487,27 +555,26 @@ function addRiverOnClick() {
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const dataRiver = []; // to store river points
let river = +getNextId("river").slice(5); // river id
let river = +getNextId('river').slice(5); // river id
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
// height with added t value to make map less depressed
const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
const h = Rivers.alterHeights();
Lakes.prepareLakeData(h);
Rivers.resolveDepressions(h);
while (i) {
cells.r[i] = river;
const x = cells.p[i][0], y = cells.p[i][1];
dataRiver.push({x, y, cell:i});
const [x, y] = cells.p[i];
dataRiver.push({x, y, cell: i});
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
if (h[i] <= h[min]) {tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); return;}
const tx = cells.p[min][0], ty = cells.p[min][1];
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, 'error');
const [tx, ty] = cells.p[min];
if (h[min] < 20) {
// pour to water body
dataRiver.push({x: tx, y: ty, cell:i});
dataRiver.push({x: tx, y: ty, cell: i});
break;
}
@ -518,10 +585,10 @@ function addRiverOnClick() {
continue;
}
// hadnle case when lowest cell already has a river
// handle case when lowest cell already has a river
const r = cells.r[min];
const riverCells = cells.i.filter(i => cells.r[i] === r);
const riverCellsUpper = riverCells.filter(i => h[i] > h[min]);
const riverCells = cells.i.filter((i) => cells.r[i] === r);
const riverCellsUpper = riverCells.filter((i) => h[i] > h[min]);
// finish new river if old river is longer
if (dataRiver.length <= riverCellsUpper.length) {
@ -532,22 +599,25 @@ function addRiverOnClick() {
}
// extend old river
rivers.select("#river"+r).remove();
cells.i.filter(i => cells.r[i] === river).forEach(i => cells.r[i] = r);
riverCells.forEach(i => cells.r[i] = 0);
rivers.select('#river' + r).remove();
cells.i.filter((i) => cells.r[i] === river).forEach((i) => (cells.r[i] = r));
riverCells.forEach((i) => (cells.r[i] = 0));
river = r;
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
}
const points = Rivers.addMeandering(dataRiver, 1, .5);
const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2]
const sourceWidth = .1;
const points = Rivers.addMeandering(dataRiver, 1, 0.5);
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = 0.1;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
rivers.append("path").attr("d", path).attr("id", "river"+river);
rivers
.append('path')
.attr('d', path)
.attr('id', 'river' + river);
// add new river to data or change extended river attributes
const r = pack.rivers.find(r => r.i === river);
const r = pack.rivers.find((r) => r.i === river);
const mouth = last(dataRiver).cell;
const discharge = cells.fl[mouth]; // in m3/s
@ -561,74 +631,98 @@ function addRiverOnClick() {
const source = dataRiver[0].cell;
const width = rn(offset ** 2, 2); // mounth width in km
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
pack.rivers.push({i:river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
}
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
document.getElementById('addNewRiver').classList.remove('pressed');
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
}
}
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
const pressed = document.getElementById('addRoute').classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addRoute.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
tip("Click on map to add a first control point", true);
if (!layerIsOn("toggleRoutes")) toggleRoutes();
closeDialogs('.stable');
viewbox.style('cursor', 'crosshair').on('click', addRouteOnClick);
tip('Click on map to add a first control point', true);
if (!layerIsOn('toggleRoutes')) toggleRoutes();
}
function addRouteOnClick() {
unpressClickToAddButton();
const point = d3.mouse(this);
const id = getNextId("route");
elSelected = routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${point[0]},${point[1]}`);
const id = getNextId('route');
elSelected = routes.select('g').append('path').attr('id', id).attr('data-new', 1).attr('d', `M${point[0]},${point[1]}`);
editRoute(true);
}
function toggleAddMarker() {
const pressed = document.getElementById("addMarker").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
const pressed = document.getElementById('addMarker').classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addMarker.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
tip("Click on map to add a marker. Hold Shift to add multiple", true);
if (!layerIsOn("toggleMarkers")) toggleMarkers();
closeDialogs('.stable');
viewbox.style('cursor', 'crosshair').on('click', addMarkerOnClick);
tip('Click on map to add a marker. Hold Shift to add multiple', true);
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
function addMarkerOnClick() {
const point = d3.mouse(this);
const x = rn(point[0], 2), y = rn(point[1], 2);
const id = getNextId("markerElement");
const x = rn(point[0], 2),
y = rn(point[1], 2);
const id = getNextId('markerElement');
const selected = markerSelectGroup.value;
const valid = selected && d3.select("#defs-markers").select("#"+selected).size();
const symbol = valid ? "#"+selected : "#marker0";
const valid =
selected &&
d3
.select('#defs-markers')
.select('#' + selected)
.size();
const symbol = valid ? '#' + selected : '#marker0';
const added = markers.select("[data-id='" + symbol + "']").size();
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr('data-size') : 1;
if (isNaN(desired)) desired = 1;
const size = desired * 5 + 25 / scale;
markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
.attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
.attr("data-size", desired).attr("width", size).attr("height", size);
markers
.append('use')
.attr('id', id)
.attr('xlink:href', symbol)
.attr('data-id', symbol)
.attr('data-x', x)
.attr('data-y', y)
.attr('x', x - size / 2)
.attr('y', y - size)
.attr('data-size', desired)
.attr('width', size)
.attr('height', size);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}
function viewCellDetails() {
$("#cellInfo").dialog({
resizable: false, width: "22em", title: "Cell Details",
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#cellInfo').dialog({
resizable: false,
width: '22em',
title: 'Cell Details',
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
}

View file

@ -15,23 +15,22 @@ function editUnits() {
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
document.getElementById('areaUnit').addEventListener('change', () => lock('areaUnit'));
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
document.getElementById('barSizeOutput').addEventListener('input', changeScaleBarSize);
document.getElementById('barSize').addEventListener('input', changeScaleBarSize);
document.getElementById('barLabel').addEventListener('input', changeScaleBarLabel);
document.getElementById('barPosX').addEventListener('input', changeScaleBarPosition);
document.getElementById('barPosY').addEventListener('input', changeScaleBarPosition);
document.getElementById('barSizeOutput').addEventListener('input', drawScaleBar);
document.getElementById('barSizeInput').addEventListener('input', drawScaleBar);
document.getElementById('barLabel').addEventListener('input', drawScaleBar);
document.getElementById('barPosX').addEventListener('input', fitScaleBar);
document.getElementById('barPosY').addEventListener('input', fitScaleBar);
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
document.getElementById('populationRate').addEventListener('change', changePopulationRate);
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanization').addEventListener('change', changeUrbanizationRate);
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
@ -51,114 +50,53 @@ function editUnits() {
return;
}
lock('distanceUnit');
drawScaleBar();
calculateFriendlyGridSize();
}
function changeDistanceScale() {
const scale = +this.value;
if (!scale || isNaN(scale) || scale < 0) {
tip('Distance scale should be a positive number', false, 'error');
this.value = document.getElementById('distanceScaleInput').dataset.value;
return;
}
document.getElementById('distanceScaleOutput').value = scale;
document.getElementById('distanceScaleInput').value = scale;
document.getElementById('distanceScaleInput').dataset.value = scale;
lock('distanceScale');
drawScaleBar();
calculateFriendlyGridSize();
}
function changeHeightUnit() {
if (this.value === 'custom_name') {
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('heightUnit');
});
return;
}
if (this.value !== 'custom_name') return;
lock('heightUnit');
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('heightUnit');
});
}
function changeHeightExponent() {
document.getElementById('heightExponentInput').value = this.value;
document.getElementById('heightExponentOutput').value = this.value;
calculateTemperatures();
if (layerIsOn('toggleTemp')) drawTemp();
lock('heightExponent');
}
function changeTemperatureScale() {
lock('temperatureScale');
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeScaleBarSize() {
document.getElementById('barSize').value = this.value;
document.getElementById('barSizeOutput').value = this.value;
drawScaleBar();
lock('barSize');
}
function changeScaleBarPosition() {
lock('barPosX');
lock('barPosY');
fitScaleBar();
}
function changeScaleBarLabel() {
lock('barLabel');
drawScaleBar();
}
function changeScaleBarOpacity() {
scaleBar.select('rect').attr('opacity', this.value);
lock('barBackOpacity');
}
function changeScaleBarColor() {
scaleBar.select('rect').attr('fill', this.value);
lock('barBackColor');
}
function changePopulationRate() {
const rate = +this.value;
if (!rate || isNaN(rate) || rate <= 0) {
tip('Population rate should be a positive number', false, 'error');
this.value = document.getElementById('populationRate').dataset.value;
return;
}
document.getElementById('populationRateOutput').value = rate;
document.getElementById('populationRate').value = rate;
document.getElementById('populationRate').dataset.value = rate;
lock('populationRate');
populationRate = +this.value;
}
function changeUrbanizationRate() {
const rate = +this.value;
if (!rate || isNaN(rate) || rate < 0) {
tip('Urbanization rate should be a number', false, 'error');
this.value = document.getElementById('urbanization').dataset.value;
return;
}
document.getElementById('urbanizationOutput').value = rate;
document.getElementById('urbanization').value = rate;
document.getElementById('urbanization').dataset.value = rate;
lock('urbanization');
urbanization = +this.value;
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById('distanceScaleOutput').value = 3;
document.getElementById('distanceScaleInput').value = 3;
document.getElementById('distanceScaleInput').dataset.value = 3;
unlock('distanceScale');
// units
@ -180,7 +118,7 @@ function editUnits() {
calculateTemperatures();
// scale bar
barSizeOutput.value = barSize.value = 2;
barSizeOutput.value = barSizeInput.value = 2;
barLabel.value = '';
barBackOpacity.value = 0.2;
barBackColor.value = '#ffffff';
@ -195,8 +133,8 @@ function editUnits() {
drawScaleBar();
// population
populationRateOutput.value = populationRate.value = 1000;
urbanizationOutput.value = urbanization.value = 1;
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
localStorage.removeItem('populationRate');
localStorage.removeItem('urbanization');
}
@ -328,12 +266,22 @@ function editUnits() {
function removeAllRulers() {
if (!rulers.data.length) return;
const message = 'Are you sure you want to remove all placed rulers?<br>If you just want to hide rulers, toggle the Rulers layer off in Menu';
const onConfirm = () => {
rulers.undraw();
rulers = new Rulers();
};
confirmationDialog({title: 'Remove all rulers', message, confirm: 'Remove', onConfirm});
alertMessage.innerHTML = `
Are you sure you want to remove all placed rulers?
<br>If you just want to hide rulers, toggle the Rulers layer off in Menu`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rulers',
buttons: {
Remove: function () {
$(this).dialog('close');
rulers.undraw();
rulers = new Rulers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
}

View file

@ -9,7 +9,10 @@ function editZones() {
modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor", resizable: false, width: fitContent(), close: () => exitZonesManualAssignment("close"),
title: "Zones Editor",
resizable: false,
width: fitContent(),
close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@ -25,19 +28,37 @@ function editZones() {
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
body.addEventListener("click", function(ev) {
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;}
body.addEventListener("click", function (ev) {
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 (customization) selectZone(el);
});
body.addEventListener("input", function(ev) {
const el = ev.target, zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#"+zone).attr("data-description", el.value);
body.addEventListener("input", function (ev) {
const el = ev.target,
zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
});
// add line for each zone
@ -45,17 +66,17 @@ function editZones() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * (distanceScaleInput.value ** 2);
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate.value;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value;
const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus"+this.id).size();
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>
@ -67,8 +88,8 @@ function editZones() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused?'':' inactive'} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive?' inactive':''} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
});
@ -76,8 +97,8 @@ function editZones() {
body.innerHTML = lines;
// update footer
const totalArea = zonesFooterArea.dataset.area = graphWidth * graphHeight * (distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization.value) * populationRate.value;
const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterCells.innerHTML = pack.cells.i.length;
@ -88,48 +109,53 @@ function editZones() {
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
togglePercentageMode();
}
$("#zonesEditor").dialog({width: fitContent()});
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", "1px solid red");
zones.select("#" + zone).style("outline", "1px solid red");
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", null);
zones.select("#" + zone).style("outline", null);
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
function movezone(ev, ui) {
const zone = $("#"+ui.item.attr("data-id"));
const prev = $("#"+ui.item.prev().attr("data-id"));
if (prev) {zone.insertAfter(prev); return;}
const next = $("#"+ui.item.next().attr("data-id"));
const zone = $("#" + ui.item.attr("data-id"));
const prev = $("#" + ui.item.prev().attr("data-id"));
if (prev) {
zone.insertAfter(prev);
return;
}
const next = $("#" + ui.item.next().attr("data-id"));
if (next) zone.insertBefore(next);
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "none");
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair")
.on("click", selectZoneOnMapClick)
.call(d3.drag().on("start", dragZoneBrush))
.on("touchmove mousemove", moveZoneBrush);
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function() {this.setAttribute("data-init", this.getAttribute("data-cells"));});
zones.selectAll("g").each(function () {
this.setAttribute("data-init", this.getAttribute("data-cells"));
});
}
function selectZone(el) {
@ -154,9 +180,9 @@ function editZones() {
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
const selected = body.querySelector("div.selected");
const zone = zones.select("#"+selected.dataset.id);
const zone = zones.select("#" + selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
@ -175,7 +201,10 @@ function editZones() {
selection.forEach(i => {
if (cells.includes(i)) return;
cells.push(i);
zone.append("polygon").attr("points", getPackPolygon(i)).attr("id", base + i);
zone
.append("polygon")
.attr("points", getPackPolygon(i))
.attr("id", base + i);
});
}
@ -191,10 +220,10 @@ function editZones() {
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
if (this.dataset.cells) return;
// all zone cells are removed
unfog("focusZone"+this.id);
unfog("focusZone" + this.id);
this.style.display = "block";
});
@ -204,15 +233,20 @@ function editZones() {
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone.selectAll("*").data(cells).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
exitZonesManualAssignment();
@ -221,56 +255,68 @@ function editZones() {
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function() {this.removeAttribute("data-init");});
zones.selectAll("g").each(function () {
this.removeAttribute("data-init");
});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function changeFill(el) {
const fill = el.getAttribute("fill");
const callback = function(fill) {
const callback = function (fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
}
};
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#"+el.parentNode.dataset.id);
const zone = zones.select("#" + el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
}
function toggleFog(z, cl) {
const dataCells = zones.select("#"+z).attr("data-cells");
const dataCells = zones.select("#" + z).attr("data-cells");
if (!dataCells) return;
const path = "M" + dataCells.split(",").map(c => getPackPolygon(+c)).join("M") + "Z", id = "focusZone"+z;
const path =
"M" +
dataCells
.split(",")
.map(c => getPackPolygon(+c))
.join("M") +
"Z",
id = "focusZone" + z;
cl.contains("inactive") ? fog(id, path) : unfog(id);
cl.toggle("inactive");
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
if (legend.selectAll("*").size()) {
clearLegend();
return;
} // hide legend
const data = [];
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
data.push([id, fill, description])
data.push([id, fill, description]);
});
drawLegend("Zones", data);
@ -283,12 +329,11 @@ function editZones() {
const totalArea = +zonesFooterArea.dataset.area;
const totalPopulation = +zonesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100, 2) + "%";
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
zonesEditorAddLines();
@ -298,7 +343,7 @@ function editZones() {
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + id.slice(4)%14 + ")";
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
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;
@ -323,9 +368,9 @@ function editZones() {
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area "+unit+",Population\n"; // headers
let data = "Id,Fill,Description,Cells,Area " + unit + ",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
@ -343,68 +388,83 @@ function editZones() {
}
function changePopulation(zone) {
const dataCells = zones.select("#"+zone).attr("data-cells");
const cells = dataCells ? dataCells.split(",").map(i => +i).filter(i => pack.cells.h[i] >= 20) : [];
if (!cells.length) {tip("Zone does not have any land cells, cannot change population", false, "error"); return;}
const dataCells = zones.select("#" + zone).attr("data-cells");
const cells = dataCells
? dataCells
.split(",")
.map(i => +i)
.filter(i => pack.cells.h[i] >= 20)
: [];
if (!cells.length) {
tip("Zone does not have any land cells, cannot change population", false, "error");
return;
}
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate.value);
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value);
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
alertMessage.innerHTML = `
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length?'':"disabled"}>
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"}>
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function() {
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
if (isNaN(totalNew)) return;
totalPop.innerHTML = l(totalNew);
totalPopPerc.innerHTML = rn(totalNew / total * 100);
}
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
};
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$("#alert").dialog({
resizable: false, title: "Change zone population", width: "24em", buttons: {
Apply: function() {applyPopulationChange(); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
}, position: {my: "center", at: "center", of: "svg"}
resizable: false,
title: "Change zone population",
width: "24em",
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
cells.forEach(i => pack.cells.pop[i] *= ruralChange);
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const points = ruralPop.value / populationRate;
const pop = rn(points / cells.length);
cells.forEach(i => pack.cells.pop[i] = pop);
cells.forEach(i => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => b.population = rn(b.population * urbanChange, 4));
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => b.population = population);
burgs.forEach(b => (b.population = population));
}
zonesEditorAddLines();
}
}
function zoneRemove(zone) {
zones.select("#"+zone).remove();
unfog("focusZone"+zone);
zones.select("#" + zone).remove();
unfog("focusZone" + zone);
zonesEditorAddLines();
}
}