This commit is contained in:
Azgaar 2019-09-23 22:57:23 +03:00
parent 5320279289
commit 729d91c053
11 changed files with 557 additions and 193 deletions

View file

@ -1185,6 +1185,11 @@ div.states .icon-pin {
cursor: pointer; cursor: pointer;
} }
div.states .icon-flag-empty {
cursor: pointer;
font-size: .9em;
}
div.states .icon-resize-vertical { div.states .icon-resize-vertical {
cursor: row-resize; cursor: row-resize;
font-size: .9em; font-size: .9em;

View file

@ -8,6 +8,7 @@
<meta name="author" content="Azgaar (Max Ganiev)"> <meta name="author" content="Azgaar (Max Ganiev)">
<meta name="description" content="Azgaar's Fantasy Map Generator and Editor"> <meta name="description" content="Azgaar's Fantasy Map Generator and Editor">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<meta name="google-site-verification" content="6N9TRdPptDN1dCZKaMA5zJ-_UmNQE-3c4VizSlQcEeU"/>
<meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator"> <meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator">
<meta property="og:title" content="Azgaar's Fantasy Map Generator"> <meta property="og:title" content="Azgaar's Fantasy Map Generator">
<meta property="og:description" content="Web application generating interactive and customizable maps"> <meta property="og:description" content="Web application generating interactive and customizable maps">
@ -1769,6 +1770,7 @@
<button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">Provinces</button> <button id="regenerateProvinces" data-tip="Click to regenerate provinces. States will remain as they are">Provinces</button>
<button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button> <button id="regenerateReligions" data-tip="Click to regenerate religions">Religions</button>
<button id="regenerateMarkers" data-tip="Click to regenerate markers">Markers</button> <button id="regenerateMarkers" data-tip="Click to regenerate markers">Markers</button>
<button id="regenerateZones" data-tip="Click to regenerate zones">Zones</button>
</div> </div>
<div id="addFeature"> <div id="addFeature">
@ -2772,6 +2774,7 @@
</div> </div>
<div id="zonesBottom"> <div id="zonesBottom">
<button id="zonesEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="zonesEditStyle" data-tip="Edit zones style in Style Editor" class="icon-adjust"></button> <button id="zonesEditStyle" data-tip="Edit zones style in Style Editor" class="icon-adjust"></button>
<button id="zonesLegend" data-tip="Toggle Legend box" class="icon-list-bullet"></button> <button id="zonesLegend" data-tip="Toggle Legend box" class="icon-list-bullet"></button>
<button id="zonesPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button> <button id="zonesPercentage" data-tip="Toggle percentage / absolute values views" class="icon-percent"></button>
@ -3043,6 +3046,7 @@
<script defer src="modules/ui/cultures-editor.js"></script> <script defer src="modules/ui/cultures-editor.js"></script>
<script defer src="modules/ui/namesbase-editor.js"></script> <script defer src="modules/ui/namesbase-editor.js"></script>
<script defer src="modules/ui/routes-editor.js"></script> <script defer src="modules/ui/routes-editor.js"></script>
<script defer src="modules/ui/lakes-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script> <script defer src="modules/ui/labels-editor.js"></script>
<script defer src="modules/ui/rivers-editor.js"></script> <script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/relief-editor.js"></script>

480
main.js
View file

@ -646,7 +646,7 @@ function generateSeed() {
// Place points to calculate Voronoi diagram // Place points to calculate Voronoi diagram
function placePoints() { function placePoints() {
console.time("placePoints"); console.time("placePoints");
const cellsDesired = 10000 * densityInput.value; // generate 10k points for graphSize = 1 const cellsDesired = 10000 * densityInput.value; // generate 10k points for each densityInput point
const spacing = grid.spacing = rn(Math.sqrt(graphWidth * graphHeight / cellsDesired), 2); // spacing between points before jirrering const spacing = grid.spacing = rn(Math.sqrt(graphWidth * graphHeight / cellsDesired), 2); // spacing between points before jirrering
grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing); grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid
@ -1175,163 +1175,7 @@ function rankCells() {
console.timeEnd('rankCells'); console.timeEnd('rankCells');
} }
// add a some zones // generate some markers
function addZones(number = 1) {
console.time("addZones");
const data = [], cells = pack.cells, states = pack.states, burgs = pack.burgs;
const used = new Uint8Array(cells.i.length); // to store used cells
if (Math.random() < .8 * number) addRebels(); // rebels along a state border
for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addDisease(); // disease starting in a random city
for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addDisaster(); // disaster starting in a random city
for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addEruption(); // volcanic eruption afecing cells aroung volcanoes
for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addFault(); // fault line
function addRebels() {
const state = states.find(s => s.i && s.neighbors.size > 0 && s.neighbors.values().next().value);
if (!state) return;
const neib = state.neighbors.values().next().value;
const cellsArray = cells.i.filter(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib));
const rebels = rw({"Rebels":5, "Insurgents":2, "Recusants":1,
"Mutineers":1, "Rioters":1, "Dissenters":1, "Secessionists":1,
"Insurrection":2, "Rebellion":1, "Conspiracy":2});
const name = getAdjective(states[neib].name) + " " + rebels;
data.push({name, type:"Rebels", cells:cellsArray, fill:"url(#hatch3)"});
}
function addDisease() {
const burg = ra(burgs.filter(b => b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [], cost = [], power = rand(20, 40);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
queue.queue({e:burg.cell, p:0});
while (queue.length) {
const next = queue.dequeue();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
used[next.e] = 1;
cells.c[next.e].forEach(function(e) {
const r = cells.road[next.e];
const c = r ? Math.max(10 - r, 1) : 100;
const p = next.p + c;
if (p > power) return;
if (!cost[e] || p < cost[e]) {
cost[e] = p;
queue.queue({e, p});
}
});
}
const adjective = () => ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]);
const animal = () => ra(["Ape", "Bear", "Boar", "Cat", "Cow", "Dog", "Pig", "Fox", "Bird", "Horse", "Rat", "Raven", "Sheep", "Spider", "Wolf"]);
const color = () => ra(["Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]);
const type = rw({"Fever":5, "Pestilence":2, "Flu":2, "Pox":2, "Smallpox":2, "Plague":4, "Cholera":2, "Ague":1, "Dropsy":1, "Leprosy":2});
const name = rw({[color()]:4, [animal()]:2, [adjective()]:1}) + " " + type;
data.push({name, type:"Disease", cells:cellsArray, fill:"url(#hatch12)"});
}
function addDisaster() {
const burg = ra(burgs.filter(b => b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [], cost = [], power = rand(5, 28);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
queue.queue({e:burg.cell, p:0});
while (queue.length) {
const next = queue.dequeue();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
used[next.e] = 1;
cells.c[next.e].forEach(function(e) {
const c = rand(1, 10);
const p = next.p + c;
if (p > power) return;
if (!cost[e] || p < cost[e]) {
cost[e] = p;
queue.queue({e, p});
}
});
}
// Avalanche, Tsunami
const type = rw({"Famine":5, "Drought":3, "Dearth":1, "Earthquake":3, "Tornadoes":1, "Wildfires":1, "Flood":3});
const name = getAdjective(burg.name) + " " + type;
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch5)"});
}
function addEruption() {
const volcanoes = [];
markers.selectAll("use[data-id='#marker_volcano']").each(function() {
volcanoes.push(this.dataset.cell);
});
if (!volcanoes.length) return;
const cell = +ra(volcanoes);
const id = markers.select("use[data-cell='"+cell+"']").attr("id");
const note = notes.filter(n => n.id === id);
if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano");
const name = note[0] ? note[0].name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption";
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = Math.random() < .5 ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
used[e] = 1;
queue.push(e);
});
}
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch7)"});
}
function addFault() {
const elevated = cells.i.filter(i => cells.h[i] > 50 && cells.h[i] < 70 && !used[i]);
if (!elevated.length) return;
const cell = ra(elevated);
const cellsArray = [], queue = [cell], power = rand(3, 15);
while (queue.length) {
const q = queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Fault";
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch2)"});
}
void function drawZones() {
zones.selectAll("g").data(data).enter().append("g")
.attr("id", (d, i) => "zone"+i).attr("data-description", d => d.name).attr("data-type", d => d.type)
.attr("data-cells", d => d.cells.join(",")).attr("fill", d => d.fill)
.selectAll("polygon").data(d => d.cells).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("id", function(d) {return this.parentNode.id+"_"+d});
}()
console.timeEnd("addZones");
}
// add some markers as an example
function addMarkers(number = 1) { function addMarkers(number = 1) {
console.time("addMarkers"); console.time("addMarkers");
const cells = pack.cells; const cells = pack.cells;
@ -1536,6 +1380,326 @@ function addMarkers(number = 1) {
console.timeEnd("addMarkers"); console.timeEnd("addMarkers");
} }
// regenerate some zones
function addZones(number = 1) {
console.time("addZones");
const data = [], cells = pack.cells, states = pack.states, burgs = pack.burgs;
const used = new Uint8Array(cells.i.length); // to store used cells
for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addInvasion(); // invasion of enemy lands
for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addRebels(); // rebels along a state border
for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addProselytism(); // proselitism of organized religion
for (let i=0; i < rn(Math.random() * 1.6 * number); i++) addCrusade(); // crusade on heresy lands
for (let i=0; i < rn(Math.random() * 1.8 * number); i++) addDisease(); // disease starting in a random city
for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addDisaster(); // disaster starting in a random city
for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addEruption(); // volcanic eruption aroung volcano
for (let i=0; i < rn(Math.random() * 1.0 * number); i++) addAvalanche(); // avalanche impacting highland road
for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addFault(); // fault line in elevated areas
for (let i=0; i < rn(Math.random() * 1.4 * number); i++) addFlood() // flood on river banks
for (let i=0; i < rn(Math.random() * 1.2 * number); i++) addTsunami() // tsunami starting near coast
function addInvasion() {
const atWar = states.filter(s => s.diplomacy.some(d => d === "Enemy"));
if (!atWar.length) return;
const invader = ra(atWar);
const target = invader.diplomacy.findIndex(d => d === "Enemy");
const cell = ra(cells.i.filter(i => cells.state[i] === target && cells.c[i].some(c => cells.state[c] === invader.i)));
if (!cell) return;
const cellsArray = [], queue = [cell], power = rand(5, 30);
while (queue.length) {
const q = Math.random() < .4 ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.state[e] !== target) return;
used[e] = 1;
queue.push(e);
});
}
const invasion = rw({"Invasion":4, "Occupation":3, "Raid":2, "Conquest":2,
"Subjugation":1, "Foray":1, "Irruption":1, "Incursion":2, "Pillage":1, "Intervention":1});
const name = getAdjective(invader.name) + " " + invasion;
data.push({name, type:"Invasion", cells:cellsArray, fill:"url(#hatch1)"});
}
function addRebels() {
const state = ra(states.filter(s => s.i && Array.from(s.neighbors).some(n => n)));
if (!state) return;
const neib = ra(Array.from(state.neighbors).filter(n => n));
const cell = cells.i.find(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib));
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.state[e] !== state.i) return;
used[e] = 1;
if (e%4 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return;
queue.push(e);
});
}
const rebels = rw({"Rebels":5, "Insurgents":2, "Recusants":1,
"Mutineers":1, "Rioters":1, "Dissenters":1, "Secessionists":1,
"Insurrection":2, "Rebellion":1, "Conspiracy":2});
const name = getAdjective(states[neib].name) + " " + rebels;
data.push({name, type:"Rebels", cells:cellsArray, fill:"url(#hatch3)"});
}
function addProselytism() {
const organized = ra(pack.religions.filter(r => r.type === "Organized"));
if (!organized) return;
const cell = ra(cells.i.filter(i => cells.religion[i] && cells.religion[i] !== organized.i && cells.c[i].some(c => cells.religion[c] === organized.i)));
if (!cell) return;
const target = cells.religion[cell];
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.religion[e] !== target) return;
if (cells.h[e] < 20) return;
used[e] = 1;
//if (e%2 !== 0 && !cells.c[e].some(c => cells.state[c] === neib)) return;
queue.push(e);
});
}
const name = getAdjective(organized.name.split(" ")[0]) + " Proselytism";
data.push({name, type:"Proselytism", cells:cellsArray, fill:"url(#hatch6)"});
}
function addCrusade() {
const heresy = ra(pack.religions.filter(r => r.type === "Heresy"));
if (!heresy) return;
const cellsArray = cells.i.filter(i => !used[i] && cells.religion[i] === heresy.i);
if (!cellsArray.length) return;
cellsArray.forEach(i => used[i] = 1);
const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade";
data.push({name, type:"Crusade", cells:cellsArray, fill:"url(#hatch6)"});
}
function addDisease() {
const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [], cost = [], power = rand(20, 37);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
queue.queue({e:burg.cell, p:0});
while (queue.length) {
const next = queue.dequeue();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
used[next.e] = 1;
cells.c[next.e].forEach(function(e) {
const r = cells.road[next.e];
const c = r ? Math.max(10 - r, 1) : 100;
const p = next.p + c;
if (p > power) return;
if (!cost[e] || p < cost[e]) {
cost[e] = p;
queue.queue({e, p});
}
});
}
const adjective = () => ra(["Great", "Silent", "Severe", "Blind", "Unknown", "Loud", "Deadly", "Burning", "Bloody", "Brutal", "Fatal"]);
const animal = () => ra(["Ape", "Bear", "Boar", "Cat", "Cow", "Dog", "Pig", "Fox", "Bird", "Horse", "Rat", "Raven", "Sheep", "Spider", "Wolf"]);
const color = () => ra(["Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]);
const type = rw({"Fever":5, "Pestilence":2, "Flu":2, "Pox":2, "Smallpox":2, "Plague":4, "Cholera":2, "Ague":1, "Dropsy":1, "Leprosy":2});
const name = rw({[color()]:4, [animal()]:2, [adjective()]:1}) + " " + type;
data.push({name, type:"Disease", cells:cellsArray, fill:"url(#hatch12)"});
}
function addDisaster() {
const burg = ra(burgs.filter(b => !used[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [], cost = [], power = rand(5, 25);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
queue.queue({e:burg.cell, p:0});
while (queue.length) {
const next = queue.dequeue();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
used[next.e] = 1;
cells.c[next.e].forEach(function(e) {
const c = rand(1, 10);
const p = next.p + c;
if (p > power) return;
if (!cost[e] || p < cost[e]) {
cost[e] = p;
queue.queue({e, p});
}
});
}
const type = rw({"Famine":5, "Dearth":1, "Drought":3, "Earthquake":3, "Tornadoes":1, "Wildfires":1});
const name = getAdjective(burg.name) + " " + type;
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch5)"});
}
function addEruption() {
const volcanoes = [];
markers.selectAll("use[data-id='#marker_volcano']").each(function() {
volcanoes.push(this.dataset.cell);
});
if (!volcanoes.length) return;
const cell = +ra(volcanoes);
const id = markers.select("use[data-cell='"+cell+"']").attr("id");
const note = notes.filter(n => n.id === id);
if (note[0]) note[0].legend = note[0].legend.replace("Active volcano", "Erupting volcano");
const name = note[0] ? note[0].name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption";
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = Math.random() < .5 ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
used[e] = 1;
queue.push(e);
});
}
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch7)"});
}
function addAvalanche() {
const roads = cells.i.filter(i => !used[i] && cells.road[i] && cells.h[i] >= 70);
if (!roads.length) return;
const cell = +ra(roads);
const cellsArray = [], queue = [cell], power = rand(3, 15);
while (queue.length) {
const q = Math.random() < .3 ? queue.shift() : queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.h[e] < 65) return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Avalanche";
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch5)"});
}
function addFault() {
const elevated = cells.i.filter(i => !used[i] && cells.h[i] > 50 && cells.h[i] < 70);
if (!elevated.length) return;
const cell = ra(elevated);
const cellsArray = [], queue = [cell], power = rand(3, 15);
while (queue.length) {
const q = queue.pop();
if (cells.h[q] >= 20) cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.r[e]) return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Fault";
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch2)"});
}
function addFlood() {
const fl = cells.fl.filter(fl => fl), meanFlux = d3.mean(fl), maxFlux = d3.max(fl), flux = (maxFlux - meanFlux) / 2 + meanFlux;
const rivers = cells.i.filter(i => !used[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > flux && cells.burg[i]);
if (!rivers.length) return;
const cell = +ra(rivers), river = cells.r[cell];
const cellsArray = [], queue = [cell], power = rand(5, 30);
while (queue.length) {
const q = queue.pop();
cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e] || cells.h[e] < 20 || cells.r[e] !== river || cells.h[e] > 50 || cells.fl[e] < meanFlux) return;
used[e] = 1;
queue.push(e);
});
}
const name = getAdjective(burgs[cells.burg[cell]].name) + " Flood";
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch13)"});
}
function addTsunami() {
const coastal = cells.i.filter(i => !used[i] && cells.t[i] === -1 && pack.features[cells.f[i]].type !== "lake");
if (!coastal.length) return;
const cell = +ra(coastal);
const cellsArray = [], queue = [cell], power = rand(10, 30);
while (queue.length) {
const q = queue.shift();
if (cells.t[q] === 1) cellsArray.push(q);
if (cellsArray.length > power) break;
cells.c[q].forEach(e => {
if (used[e]) return;
if (cells.h[e] >= 20 && !cells.t[e]) return;
if (pack.features[cells.f[e]].type === "lake") return;
used[e] = 1;
queue.push(e);
});
}
const proper = getAdjective(Names.getCultureShort(cells.culture[cell]));
const name = proper + " Tsunami";
data.push({name, type:"Disaster", cells:cellsArray, fill:"url(#hatch13)"});
}
void function drawZones() {
zones.selectAll("g").data(data).enter().append("g")
.attr("id", (d, i) => "zone"+i).attr("data-description", d => d.name).attr("data-type", d => d.type)
.attr("data-cells", d => d.cells.join(",")).attr("fill", d => d.fill)
.selectAll("polygon").data(d => d.cells).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("id", function(d) {return this.parentNode.id+"_"+d});
}()
console.timeEnd("addZones");
}
// show map stats on generation complete // show map stats on generation complete
function showStatistics() { function showStatistics() {
const template = templateInput.value; const template = templateInput.value;

View file

@ -106,7 +106,7 @@
const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // a bit randomized cell score for towns placement const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // a bit randomized cell score for towns placement
const sorted = cells.i.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes const sorted = cells.i.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber; const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / (grid.points.length / 10000) ** .8) : manorsInput.valueAsNumber;
const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 0; let burgsAdded = 0;

View file

@ -18,13 +18,14 @@ function clicked() {
const el = d3.event.target; const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return; if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, grand = parent.parentElement; const parent = el.parentElement, grand = parent.parentElement;
if (parent.id === "rivers") editRiver(); else if (parent.id === "rivers") editRiver();
if (grand.id === "routes") editRoute(); else else if (grand.id === "routes") editRoute();
if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
if (grand.id === "burgLabels") editBurg(); else else if (grand.id === "burgLabels") editBurg();
if (grand.id === "burgIcons") editBurg(); else else if (grand.id === "burgIcons") editBurg();
if (parent.id === "terrain") editReliefIcon(); else else if (parent.id === "terrain") editReliefIcon();
if (parent.id === "markers") editMarker(); else if (parent.id === "markers") editMarker();
// else if (grand.id === "lakes") editLake();
} }
// clear elSelected variable // clear elSelected variable

View file

@ -0,0 +1,86 @@
"use strict";
function editLake() {
if (customization) return;
closeDialogs(".stable");
$("#routeEditor").dialog({
title: "Edit Route", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
close: closeLakesEditor
});
const node = d3.event.target;
debug.append("g").attr("id", "controlPoints");
elSelected = d3.select(node).on("click", addInterimControlPoint);
drawControlPoints(node);
//selectRouteGroup(elSelected.node());
if (modules.editLake) return;
modules.editLake = true;
// add listeners
function drawControlPoints(node) {
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 10);
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
//.on("click", clickControlPoint);
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a-b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest+1; else index = next+1;
}
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
redrawLake();
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawLake();
}
function redrawLake() {
lineGen.curve(d3.curveCatmullRom.alpha(.1));
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
});
elSelected.attr("d", round(lineGen(points)));
}
function closeLakesEditor() {
elSelected.on("click", null);
clearMainTip();
debug.select("#controlPoints").remove();
unselect();
}
}

View file

@ -38,6 +38,7 @@ function editProvinces() {
if (cl.contains("zoneFill")) changeFill(el); else if (cl.contains("zoneFill")) changeFill(el); else
if (cl.contains("icon-fleur")) provinceOpenCOA(p); else if (cl.contains("icon-fleur")) provinceOpenCOA(p); else
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
if (cl.contains("icon-flag-empty")) declareProvinceIndependence(p); else
if (cl.contains("icon-pin")) focusOn(p, cl); else if (cl.contains("icon-pin")) focusOn(p, cl); else
if (cl.contains("icon-trash-empty")) removeProvince(p); if (cl.contains("icon-trash-empty")) removeProvince(p);
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(p, line); else if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(p, line); else
@ -106,6 +107,7 @@ function editProvinces() {
const stateName = pack.states[p.state].name; const stateName = pack.states[p.state].name;
const capital = p.burg ? pack.burgs[p.burg].name : ''; const capital = p.burg ? pack.burgs[p.burg].name : '';
const separable = p.burg && p.burg !== pack.states[p.state].capital;
const focused = defs.select("#fog #focusProvince"+p.i).size(); const focused = defs.select("#fog #focusProvince"+p.i).size();
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}> lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="zoneFill"></svg> <svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="zoneFill"></svg>
@ -121,6 +123,7 @@ function editProvinces() {
<div data-tip="Province area" class="biomeArea hide">${si(area) + unit}</div> <div data-tip="Province area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div> <div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Declare province independence" class="icon-flag-empty ${separable ? '' : 'placeholder'} hide"></span>
<span data-tip="Toggle province focus" class="icon-pin ${focused?'':' inactive'} hide"></span> <span data-tip="Toggle province focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span> <span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`; </div>`;
@ -202,6 +205,67 @@ function editProvinces() {
zoomTo(x, y, 8, 2000); zoomTo(x, y, 8, 2000);
} }
function declareProvinceIndependence(p) {
const states = pack.states, provinces = pack.provinces, cells = pack.cells;
const oldState = pack.provinces[p].state;
const newState = pack.states.length;
// turn burg into a capital
const burg = provinces[p].burg;
if (!burg) return;
pack.burgs[burg].capital = true;
pack.burgs[burg].state = newState;
moveBurgToGroup(burg, "cities");
// difine new state attributes
const center = provinces[p].center;
const culture = cells.culture[center];
const name = provinces[p].name;
const color = getRandomColor();
// update cells
cells.i.filter(i => cells.province[i] === p).forEach(i => {
cells.province[i] = 0;
cells.state[i] = newState;
});
// update diplomacy and reverse relations
const diplomacy = states.map(s => {
if (!s.i) return "x";
let relations = states[oldState].diplomacy[s.i]; // relations between Nth state and old overlord
if (s.i === oldState) relations = "Enemy"; // new state is Enemy to its old overlord
else if (relations === "Ally") relations = "Suspicion";
else if (relations === "Friendly") relations = "Suspicion";
else if (relations === "Suspicion") relations = "Neutral";
else if (relations === "Enemy") relations = "Friendly";
else if (relations === "Rival") relations = "Friendly";
else if (relations === "Vassal") relations = "Suspicion";
else if (relations === "Suzerain") relations = "Enemy";
s.diplomacy.push(relations);
return relations;
});
diplomacy.push("x");
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]);
// create new state
states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels([newState, oldState]);
// remove old province
unfocus(p);
if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1);
provinces[p].removed = true;
closeDialogs();
editStates();
}
function focusOn(p, cl) { function focusOn(p, cl) {
const inactive = cl.contains("inactive"); const inactive = cl.contains("inactive");
cl.toggle("inactive"); cl.toggle("inactive");

View file

@ -643,7 +643,7 @@ function editStates() {
const provCells = cells.i.filter(i => cells.province[i] === p); const provCells = cells.i.filter(i => cells.province[i] === p);
const provStates = [...new Set(provCells.map(i => cells.state[i]))]; const provStates = [...new Set(provCells.map(i => cells.state[i]))];
// assign province its center owner; if center is neutral, remove province // assign province to its center owner; if center is neutral, remove province
const owner = cells.state[provinces[p].center]; const owner = cells.state[provinces[p].center];
if (owner) { if (owner) {
const name = provinces[p].name; const name = provinces[p].name;
@ -715,46 +715,73 @@ function editStates() {
} }
function addState() { function addState() {
const states = pack.states, burgs = pack.burgs, cells = pack.cells;
const point = d3.mouse(this); const point = d3.mouse(this);
const center = findCell(point[0], point[1]); const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;} if (cells.h[center] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
let burg = pack.cells.burg[center]; let burg = cells.burg[center];
if (burg && pack.burgs[burg].capital) {tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); return;} if (burg && burgs[burg].capital) {tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); return;}
if (!burg) burg = addBurg(point); // add new burg if (!burg) burg = addBurg(point); // add new burg
const oldState = cells.state[center];
const newState = states.length;
// turn burg into a capital // turn burg into a capital
pack.burgs[burg].capital = true; burgs[burg].capital = true;
pack.burgs[burg].state = pack.states.length; burgs[burg].state = newState;
moveBurgToGroup(burg, "cities"); moveBurgToGroup(burg, "cities");
if (d3.event.shiftKey === false) exitAddStateMode(); if (d3.event.shiftKey === false) exitAddStateMode();
const i = pack.states.length; const culture = cells.culture[center];
const culture = pack.cells.culture[center]; const basename = center%5 === 0 ? burgs[burg].name : Names.getCulture(culture);
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture); const name = Names.getState(basename, culture);
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex(); const color = getRandomColor();
const diplomacy = pack.states.map(s => s.i ? "Neutral" : "x")
diplomacy.push("x");
pack.states.forEach(s => {if (s.i) {s.diplomacy.push("Neutral");}});
const provinces = [];
const affected = [pack.states.length, pack.cells.state[center]]; // update diplomacy and reverse relations
const diplomacy = states.map(s => {
if (!s.i) return "x";
if (!oldState) {
s.diplomacy.push("Neutral");
return "Neutral";
}
pack.cells.state[center] = pack.states.length; let relations = states[oldState].diplomacy[s.i]; // relations between Nth state and old overlord
pack.cells.c[center].forEach(c => { if (s.i === oldState) relations = "Enemy"; // new state is Enemy to its old overlord
if (pack.cells.h[c] < 20) return; else if (relations === "Ally") relations = "Suspicion";
if (pack.cells.burg[c]) return; else if (relations === "Friendly") relations = "Suspicion";
affected.push(pack.cells.state[c]); else if (relations === "Suspicion") relations = "Neutral";
pack.cells.state[c] = pack.states.length; else if (relations === "Enemy") relations = "Friendly";
else if (relations === "Rival") relations = "Friendly";
else if (relations === "Vassal") relations = "Suspicion";
else if (relations === "Suzerain") relations = "Enemy";
s.diplomacy.push(relations);
return relations;
}); });
pack.states.push({i, name, diplomacy, provinces, color, expansionism:.5, capital:burg, type:"Generic", center, culture}); diplomacy.push("x");
BurgsAndStates.collectStatistics(); states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]);
BurgsAndStates.defineStateForms([i]);
const affectedStates = [newState, oldState];
const affectedProvinces = [cells.province[center]];
cells.state[center] = newState;
cells.province[center] = 0;
cells.c[center].forEach(c => {
if (cells.h[c] < 20) return;
if (cells.burg[c]) return;
affectedStates.push(cells.state[c]);
affectedProvinces.push(cells.province[c]);
cells.state[c] = newState;
cells.province[c] = 0;
});
states.push({i:newState, name, diplomacy, provinces:[], color, expansionism:.5, capital:burg, type:"Generic", center, culture});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
adjustProvinces([...new Set(affectedProvinces)]);
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates(); if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders(); if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels(affected); BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
statesEditorAddLines(); statesEditorAddLines();
} }

View file

@ -61,7 +61,8 @@ function processFeatureRegeneration(button) {
if (button === "regenerateStates") regenerateStates(); else if (button === "regenerateStates") regenerateStates(); else
if (button === "regenerateProvinces") regenerateProvinces(); else if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateReligions") regenerateReligions(); else if (button === "regenerateReligions") regenerateReligions(); else
if (button === "regenerateMarkers") regenerateMarkers(); if (button === "regenerateMarkers") regenerateMarkers(); else
if (button === "regenerateZones") regenerateZones();
} }
function regenerateRivers() { function regenerateRivers() {
@ -94,7 +95,7 @@ function regenerateBurgs() {
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement 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 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 / 10 / densityInput.value ** .8) + states.length : +manorsInput.value + states.length; const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / (grid.points.length / 10000) ** .8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) / 200 / (burgsCount / 500); // base min distance between towns const spacing = (graphWidth + graphHeight) / 200 / (burgsCount / 500); // base min distance between towns
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) { for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
@ -219,6 +220,12 @@ function regenerateMarkers() {
addMarkers(gauss(1, .5, .3, 5, 2)); addMarkers(gauss(1, .5, .3, 5, 2));
} }
function regenerateZones() {
zones.selectAll("g").remove(); // remove existing zones
addZones(gauss(1, .5, .6, 5, 2));
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
}
function unpressClickToAddButton() { function unpressClickToAddButton() {
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
restoreDefaultEvents(); restoreDefaultEvents();

View file

@ -14,6 +14,7 @@ function editZones() {
}); });
// add listeners // add listeners
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones")); document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend); document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode); document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);

View file

@ -388,6 +388,11 @@ function getAdjective(string) {
if (Math.random() < .5 && string.slice(-4) === "land") return string + "ic"; if (Math.random() < .5 && string.slice(-4) === "land") return string + "ic";
if (string.slice(-4) === " Guo") string = string.slice(0, -4); if (string.slice(-4) === " Guo") string = string.slice(0, -4);
// don't change is name ends on suffix
if (string.slice(-2) === "an") return string;
if (string.slice(-3) === "ese") return string;
if (string.slice(-1) === "i") return string;
const end = string.slice(-1); // last letter of string const end = string.slice(-1); // last letter of string
if (end === "a") return string += "n"; if (end === "a") return string += "n";
if (end === "i") return string += "an"; if (end === "i") return string += "an";