mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 09:21:24 +01:00
Zones generator update (#1113)
* feat: style - store emblem size mod in style (v1.99.10) * fix the isOutdated function for versions past 1.99 * fix: showUploadMessage function not called correctly for isUpdated case * feat: load - improve version detection * feat: improve version detection and update process * feat: Update version and use constant for VERSION in multiple files * Update versioning.js to fix incorrect message display for stored version * feat: zones editor - update to work with pack data * feat: zones editor - update editor * feat: zones editor - update editor * chore: update version * feat: zones - regenerate * feat: zones - render zones as continuius line * feat: zones - editot changes * feat: zones - auto-update * feat: zones - generation fixes * feat: zones - generation fixes * feat: zones - restore layer * feat: zones - proselytism - check population --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
e77202a08a
commit
eb29c5ec5d
25 changed files with 1057 additions and 885 deletions
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
|
@ -14,8 +14,8 @@
|
|||
|
||||
# Versioning
|
||||
|
||||
<!-- Update the version if you want the PR to be merged fast. Currently it's a manual 3-steps process:
|
||||
* update version in `versioning.js` using semver principle. Just set the next patch (for fixes) or minor version (for new features)
|
||||
<!-- Update the VERSION if you want the PR to be merged. Currently it's a manual 3-steps process:
|
||||
* update VERSION in `versioning.js` using semver principle
|
||||
* for all changed files update hash (the part after `?`) in place where file is requested (usually it's `index.html`)
|
||||
* if the change can be really interesting for end-users, describe it inside the `showUpdateWindow()` function in `versioning.js` -->
|
||||
|
||||
|
|
|
|||
38
index.html
38
index.html
|
|
@ -4846,7 +4846,11 @@
|
|||
<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="zonesLegend" data-tip="Toggle Legend box" class="icon-list-bullet"></button>
|
||||
<button
|
||||
id="zonesLegend"
|
||||
data-tip="Toggle Legend box (shows all non-hidden zones)"
|
||||
class="icon-list-bullet"
|
||||
></button>
|
||||
<button
|
||||
id="zonesPercentage"
|
||||
data-tip="Toggle percentage / absolute values views"
|
||||
|
|
@ -7985,13 +7989,14 @@
|
|||
<script src="utils/stringUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/languageUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/unitUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.100.00"></script>
|
||||
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
|
||||
|
||||
<script src="modules/voronoi.js"></script>
|
||||
<script src="config/heightmap-templates.js"></script>
|
||||
<script src="config/precreated-heightmaps.js"></script>
|
||||
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/ocean-layers.js?v=1.99.10"></script>
|
||||
<script src="modules/ocean-layers.js?v=1.100.00"></script>
|
||||
<script src="modules/river-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/lakes.js?v=1.99.00"></script>
|
||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
||||
|
|
@ -8003,26 +8008,27 @@
|
|||
<script src="modules/religions-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/military-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/markers-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/zones-generator.js?v=1.100.00"></script>
|
||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/submap.js?v=1.99.00"></script>
|
||||
<script src="modules/submap.js?v=1.100.00"></script>
|
||||
<script src="libs/polylabel.min.js"></script>
|
||||
<script src="libs/lineclip.min.js"></script>
|
||||
<script src="libs/alea.min.js"></script>
|
||||
<script src="modules/fonts.js?v=1.99.03"></script>
|
||||
<script src="modules/ui/layers.js?v=1.99.05"></script>
|
||||
<script src="modules/ui/measurers.js?v=1.99.00"></script>
|
||||
<script src="modules/ui/stylePresets.js?v=1.99.10"></script>
|
||||
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||
|
||||
<script src="modules/ui/general.js?v=1.99.05"></script>
|
||||
<script src="modules/ui/options.js?v=1.99.05"></script>
|
||||
<script src="main.js?v=1.99.00"></script>
|
||||
<script src="modules/ui/general.js?v=1.100.00"></script>
|
||||
<script src="modules/ui/options.js?v=1.100.00"></script>
|
||||
<script src="main.js?v=1.100.00"></script>
|
||||
|
||||
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/style.js?v=1.99.12"></script>
|
||||
<script defer src="modules/ui/editors.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.99.06"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/world-configurator.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script>
|
||||
|
|
@ -8038,14 +8044,14 @@
|
|||
<script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/burg-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/burg-editor.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script>
|
||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/routes-overview.js?v=1.99.02"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/burgs-overview.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/routes-overview.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/regiments-overview.js?v=1.99.00"></script>
|
||||
|
|
@ -8060,9 +8066,9 @@
|
|||
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
|
||||
<script defer src="libs/rgbquant.min.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="modules/io/save.js?v=1.99.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.99.07"></script>
|
||||
<script defer src="modules/io/save.js?v=1.100.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.100.00"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
|
||||
<script defer src="modules/io/export.js?v=1.99.14"></script>
|
||||
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
445
main.js
445
main.js
|
|
@ -62,7 +62,7 @@ let regions = viewbox.append("g").attr("id", "regions");
|
|||
let statesBody = regions.append("g").attr("id", "statesBody");
|
||||
let statesHalo = regions.append("g").attr("id", "statesHalo");
|
||||
let provs = viewbox.append("g").attr("id", "provs");
|
||||
let zones = viewbox.append("g").attr("id", "zones").style("display", "none");
|
||||
let zones = viewbox.append("g").attr("id", "zones");
|
||||
let borders = viewbox.append("g").attr("id", "borders");
|
||||
let stateBorders = borders.append("g").attr("id", "stateBorders");
|
||||
let provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
||||
|
|
@ -662,7 +662,7 @@ async function generate(options) {
|
|||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
addZones();
|
||||
Zones.generate();
|
||||
|
||||
drawScaleBar(scaleBar, scale);
|
||||
Names.getMapName();
|
||||
|
|
@ -684,7 +684,7 @@ async function generate(options) {
|
|||
buttons: {
|
||||
"Clear data": function () {
|
||||
localStorage.clear();
|
||||
localStorage.setItem("version", version);
|
||||
localStorage.setItem("version", VERSION);
|
||||
},
|
||||
Regenerate: function () {
|
||||
regenerateMap("generation error");
|
||||
|
|
@ -1484,442 +1484,6 @@ function rankCells() {
|
|||
TIME && console.timeEnd("rankCells");
|
||||
}
|
||||
|
||||
// generate zones
|
||||
function addZones(number = 1) {
|
||||
TIME && console.time("addZones");
|
||||
const {cells, states, burgs} = pack;
|
||||
const used = new Uint8Array(cells.i.length); // to store used cells
|
||||
const zonesData = [];
|
||||
|
||||
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
|
||||
|
||||
drawZones();
|
||||
|
||||
function addInvasion() {
|
||||
const atWar = states.filter(s => s.diplomacy && 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 = P(0.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,
|
||||
Skirmishes: 1,
|
||||
Incursion: 2,
|
||||
Pillaging: 1,
|
||||
Intervention: 1
|
||||
});
|
||||
const name = getAdjective(invader.name) + " " + invasion;
|
||||
zonesData.push({name, type: "Invasion", cells: cellsArray, fill: "url(#hatch1)"});
|
||||
}
|
||||
|
||||
function addRebels() {
|
||||
const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(n => n)));
|
||||
if (!state) return;
|
||||
|
||||
const neib = ra(state.neighbors.filter(n => n && !states[n].removed));
|
||||
if (!neib) return;
|
||||
const cell = cells.i.find(
|
||||
i => cells.state[i] === state.i && !state.removed && cells.c[i].some(c => cells.state[c] === neib)
|
||||
);
|
||||
const cellsArray = [];
|
||||
const queue = [];
|
||||
if (cell) queue.push(cell);
|
||||
|
||||
const 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,
|
||||
Mutineers: 1,
|
||||
Rioters: 1,
|
||||
Separatists: 1,
|
||||
Secessionists: 1,
|
||||
Insurrection: 2,
|
||||
Rebellion: 1,
|
||||
Conspiracy: 2
|
||||
});
|
||||
const name = getAdjective(states[neib].name) + " " + rebels;
|
||||
zonesData.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";
|
||||
zonesData.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";
|
||||
zonesData.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 = [];
|
||||
const cost = [];
|
||||
const 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(nextCellId => {
|
||||
const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100;
|
||||
const p = next.p + c;
|
||||
if (p > power) return;
|
||||
|
||||
if (!cost[nextCellId] || p < cost[nextCellId]) {
|
||||
cost[nextCellId] = p;
|
||||
queue.queue({e: nextCellId, 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,
|
||||
Dropsy: 1,
|
||||
Leprosy: 2
|
||||
});
|
||||
const name = rw({[color()]: 4, [animal()]: 2, [adjective()]: 1}) + " " + type;
|
||||
zonesData.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;
|
||||
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch5)"});
|
||||
}
|
||||
|
||||
function addEruption() {
|
||||
const volcano = byId("markers").querySelector("use[data-id='#marker_volcano']");
|
||||
if (!volcano) return;
|
||||
|
||||
const x = +volcano.dataset.x,
|
||||
y = +volcano.dataset.y,
|
||||
cell = findCell(x, y);
|
||||
const id = volcano.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 = P(0.5) ? queue.shift() : queue.pop();
|
||||
cellsArray.push(q);
|
||||
if (cellsArray.length > power) break;
|
||||
cells.c[q].forEach(e => {
|
||||
if (used[e] || cells.h[e] < 20) return;
|
||||
used[e] = 1;
|
||||
queue.push(e);
|
||||
});
|
||||
}
|
||||
|
||||
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch7)"});
|
||||
}
|
||||
|
||||
function addAvalanche() {
|
||||
const routes = cells.i.filter(i => !used[i] && Routes.isConnected(i) && cells.h[i] >= 70);
|
||||
if (!routes.length) return;
|
||||
|
||||
const cell = +ra(routes);
|
||||
const cellsArray = [],
|
||||
queue = [cell],
|
||||
power = rand(3, 15);
|
||||
|
||||
while (queue.length) {
|
||||
const q = P(0.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";
|
||||
zonesData.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";
|
||||
zonesData.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";
|
||||
zonesData.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.t[e] > 2) 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";
|
||||
zonesData.push({name, type: "Disaster", cells: cellsArray, fill: "url(#hatch13)"});
|
||||
}
|
||||
|
||||
function drawZones() {
|
||||
zones
|
||||
.selectAll("g")
|
||||
.data(zonesData)
|
||||
.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;
|
||||
});
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("addZones");
|
||||
}
|
||||
|
||||
// show map stats on generation complete
|
||||
function showStatistics() {
|
||||
const heightmap = byId("templateInput").value;
|
||||
|
|
@ -1971,8 +1535,7 @@ function undraw() {
|
|||
viewbox
|
||||
.selectAll("path, circle, polygon, line, text, use, #texture > image, #zones > g, #armies > g, #ruler > g")
|
||||
.remove();
|
||||
document
|
||||
.getElementById("deftemp")
|
||||
byId("deftemp")
|
||||
.querySelectorAll("path, clipPath, svg")
|
||||
.forEach(el => el.remove());
|
||||
byId("coas").innerHTML = ""; // remove auto-generated emblems
|
||||
|
|
|
|||
|
|
@ -611,8 +611,7 @@ window.BurgsAndStates = (() => {
|
|||
// generate Diplomatic Relationships
|
||||
const generateDiplomacy = () => {
|
||||
TIME && console.time("generateDiplomacy");
|
||||
const cells = pack.cells,
|
||||
states = pack.states;
|
||||
const {cells, states} = pack;
|
||||
const chronicle = (states[0].diplomacy = []);
|
||||
const valid = states.filter(s => s.i && !states.removed);
|
||||
|
||||
|
|
@ -696,21 +695,23 @@ window.BurgsAndStates = (() => {
|
|||
const defender = ra(
|
||||
ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d)
|
||||
);
|
||||
let ap = states[attacker].area * states[attacker].expansionism,
|
||||
dp = states[defender].area * states[defender].expansionism;
|
||||
let ap = states[attacker].area * states[attacker].expansionism;
|
||||
let dp = states[defender].area * states[defender].expansionism;
|
||||
if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
|
||||
const an = states[attacker].name,
|
||||
dn = states[defender].name; // names
|
||||
const attackers = [attacker],
|
||||
defenders = [defender]; // attackers and defenders array
|
||||
|
||||
const an = states[attacker].name;
|
||||
const dn = states[defender].name; // names
|
||||
const attackers = [attacker];
|
||||
const defenders = [defender]; // attackers and defenders array
|
||||
const dd = states[defender].diplomacy; // defender relations;
|
||||
|
||||
// start a war
|
||||
const war = [`${an}-${trimVowels(dn)}ian War`, `${an} declared a war on its rival ${dn}`];
|
||||
const end = options.year;
|
||||
const start = end - gauss(2, 2, 0, 5);
|
||||
states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end});
|
||||
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end});
|
||||
// start an ongoing war
|
||||
const name = `${an}-${trimVowels(dn)}ian War`;
|
||||
const start = options.year - gauss(2, 3, 0, 10);
|
||||
const war = [name, `${an} declared a war on its rival ${dn}`];
|
||||
const campaign = {name, start, attacker, defender};
|
||||
states[attacker].campaigns.push(campaign);
|
||||
states[defender].campaigns.push(campaign);
|
||||
|
||||
// attacker vassals join the war
|
||||
ad.forEach((r, d) => {
|
||||
|
|
@ -790,7 +791,6 @@ window.BurgsAndStates = (() => {
|
|||
}
|
||||
|
||||
TIME && console.timeEnd("generateDiplomacy");
|
||||
//console.table(states.map(s => s.diplomacy));
|
||||
};
|
||||
|
||||
// select a forms for listed or all valid states
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
// update old map file to the current version
|
||||
export function resolveVersionConflicts(version) {
|
||||
if (version < 1) {
|
||||
export function resolveVersionConflicts(mapVersion) {
|
||||
const isOlderThan = tagVersion => compareVersions(mapVersion, tagVersion).isOlder;
|
||||
|
||||
if (isOlderThan("1.0.0")) {
|
||||
// v1.0 added a new religions layer
|
||||
relig = viewbox.insert("g", "#terrain").attr("id", "relig");
|
||||
Religions.generate();
|
||||
|
|
@ -63,7 +65,7 @@ export function resolveVersionConflicts(version) {
|
|||
.attr("stroke-width", 0)
|
||||
.attr("stroke-dasharray", null)
|
||||
.attr("stroke-linecap", "butt");
|
||||
addZones();
|
||||
Zones.generate();
|
||||
if (!markers.selectAll("*").size()) {
|
||||
Markers.generate();
|
||||
turnButtonOn("toggleMarkers");
|
||||
|
|
@ -107,11 +109,11 @@ export function resolveVersionConflicts(version) {
|
|||
biomesData.habitability.push(12);
|
||||
}
|
||||
|
||||
if (version < 1.1) {
|
||||
// v1.0 initial code had a bug with religion layer id
|
||||
if (isOlderThan("1.1.0")) {
|
||||
// v1.0 code had a bug with religion layer id
|
||||
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig");
|
||||
|
||||
// v1.0 initially has Sympathy status then relaced with Friendly
|
||||
// v1.0 had Sympathy status then relaced with Friendly
|
||||
for (const s of pack.states) {
|
||||
if (!s.diplomacy) continue;
|
||||
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
|
||||
|
|
@ -203,7 +205,7 @@ export function resolveVersionConflicts(version) {
|
|||
drawCoastline();
|
||||
}
|
||||
|
||||
if (version < 1.11) {
|
||||
if (isOlderThan("1.11.0")) {
|
||||
// v1.11 added new attributes
|
||||
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
|
||||
svg.select("#oceanic > *").attr("id", "oceanicPattern");
|
||||
|
|
@ -229,7 +231,7 @@ export function resolveVersionConflicts(version) {
|
|||
if (!terrain.attr("density")) terrain.attr("density", 0.4);
|
||||
}
|
||||
|
||||
if (version < 1.21) {
|
||||
if (isOlderThan("1.21.0")) {
|
||||
// v1.11 replaced "display" attribute by "display" style
|
||||
viewbox.selectAll("g").each(function () {
|
||||
if (this.hasAttribute("display")) {
|
||||
|
|
@ -255,12 +257,12 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.22) {
|
||||
if (isOlderThan("1.22.0")) {
|
||||
// v1.22 changed state neighbors from Set object to array
|
||||
BurgsAndStates.collectStatistics();
|
||||
}
|
||||
|
||||
if (version < 1.3) {
|
||||
if (isOlderThan("1.3.0")) {
|
||||
// v1.3 added global options object
|
||||
const winds = options.slice(); // previostly wind was saved in settings[19]
|
||||
const year = rand(100, 2000);
|
||||
|
|
@ -285,7 +287,7 @@ export function resolveVersionConflicts(version) {
|
|||
Military.generate();
|
||||
}
|
||||
|
||||
if (version < 1.4) {
|
||||
if (isOlderThan("1.4.0")) {
|
||||
// v1.35 added dry lakes
|
||||
if (!lakes.select("#dry").size()) {
|
||||
lakes.append("g").attr("id", "dry");
|
||||
|
|
@ -329,7 +331,7 @@ export function resolveVersionConflicts(version) {
|
|||
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
|
||||
}
|
||||
|
||||
if (version < 1.5) {
|
||||
if (isOlderThan("1.5.0")) {
|
||||
// not need to store default styles from v 1.5
|
||||
localStorage.removeItem("styleClean");
|
||||
localStorage.removeItem("styleGloom");
|
||||
|
|
@ -367,7 +369,7 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.6) {
|
||||
if (isOlderThan("1.6.0")) {
|
||||
// v1.6 changed rivers data
|
||||
for (const river of pack.rivers) {
|
||||
const el = document.getElementById("river" + river.i);
|
||||
|
|
@ -399,7 +401,7 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.61) {
|
||||
if (isOlderThan("1.61.0")) {
|
||||
// v1.61 changed rulers data
|
||||
ruler.style("display", null);
|
||||
rulers = new Rulers();
|
||||
|
|
@ -453,12 +455,12 @@ export function resolveVersionConflicts(version) {
|
|||
pattern.innerHTML = /* html */ `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
|
||||
}
|
||||
|
||||
if (version < 1.62) {
|
||||
if (isOlderThan("1.62.0")) {
|
||||
// v1.62 changed grid data
|
||||
gridOverlay.attr("size", null);
|
||||
}
|
||||
|
||||
if (version < 1.63) {
|
||||
if (isOlderThan("1.63.0")) {
|
||||
// v1.63 changed ocean pattern opacity element
|
||||
const oceanPattern = document.getElementById("oceanPattern");
|
||||
if (oceanPattern) oceanPattern.removeAttribute("opacity");
|
||||
|
|
@ -472,7 +474,7 @@ export function resolveVersionConflicts(version) {
|
|||
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
|
||||
}
|
||||
|
||||
if (version < 1.64) {
|
||||
if (isOlderThan("1.64.0")) {
|
||||
// v1.64 change states style
|
||||
const opacity = regions.attr("opacity");
|
||||
const filter = regions.attr("filter");
|
||||
|
|
@ -481,7 +483,7 @@ export function resolveVersionConflicts(version) {
|
|||
regions.attr("opacity", null).attr("filter", null);
|
||||
}
|
||||
|
||||
if (version < 1.65) {
|
||||
if (isOlderThan("1.65.0")) {
|
||||
// v1.65 changed rivers data
|
||||
d3.select("#rivers").attr("style", null); // remove style to unhide layer
|
||||
const {cells, rivers} = pack;
|
||||
|
|
@ -523,13 +525,13 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.652) {
|
||||
if (isOlderThan("1.652.0")) {
|
||||
// remove style to unhide layers
|
||||
rivers.attr("style", null);
|
||||
borders.attr("style", null);
|
||||
}
|
||||
|
||||
if (version < 1.7) {
|
||||
if (isOlderThan("1.7.0")) {
|
||||
// v1.7 changed markers data
|
||||
const defs = document.getElementById("defs-markers");
|
||||
const markersGroup = document.getElementById("markers");
|
||||
|
|
@ -587,7 +589,7 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.72) {
|
||||
if (isOlderThan("1.72.0")) {
|
||||
// v1.72 renamed custom style presets
|
||||
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style"));
|
||||
storedStyles.forEach(styleName => {
|
||||
|
|
@ -598,7 +600,7 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.73) {
|
||||
if (isOlderThan("1.73.0")) {
|
||||
// v1.73 moved the hatching patterns out of the user's SVG
|
||||
document.getElementById("hatching")?.remove();
|
||||
|
||||
|
|
@ -609,17 +611,17 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.84) {
|
||||
if (isOlderThan("1.84.0")) {
|
||||
// v1.84.0 added grid.cellsDesired to stored data
|
||||
if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
|
||||
}
|
||||
|
||||
if (version < 1.85) {
|
||||
if (isOlderThan("1.85.0")) {
|
||||
// v1.84.0 moved intial screen out of maon svg
|
||||
svg.select("#initial").remove();
|
||||
}
|
||||
|
||||
if (version < 1.86) {
|
||||
if (isOlderThan("1.86.0")) {
|
||||
// v1.86.0 added multi-origin culture and religion hierarchy trees
|
||||
for (const culture of pack.cultures) {
|
||||
culture.origins = [culture.origin];
|
||||
|
|
@ -632,14 +634,14 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.88) {
|
||||
if (isOlderThan("1.88.0")) {
|
||||
// v1.87 may have incorrect shield for some reason
|
||||
pack.states.forEach(({coa}) => {
|
||||
if (coa?.shield === "state") delete coa.shield;
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 1.91) {
|
||||
if (isOlderThan("1.91.0")) {
|
||||
// from 1.91.00 custom coa is moved to coa object
|
||||
pack.states.forEach(state => {
|
||||
if (state.coa === "custom") state.coa = {custom: true};
|
||||
|
|
@ -688,14 +690,14 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.92) {
|
||||
if (isOlderThan("1.92.0")) {
|
||||
// v1.92 change labels text-anchor from 'start' to 'middle'
|
||||
labels.selectAll("tspan").each(function () {
|
||||
this.setAttribute("x", 0);
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 1.94) {
|
||||
if (isOlderThan("1.94.0")) {
|
||||
// from v1.94.00 texture image is removed when layer is off
|
||||
texture.style("display", null);
|
||||
|
||||
|
|
@ -713,7 +715,7 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.95) {
|
||||
if (isOlderThan("1.95.0")) {
|
||||
// v1.95.00 added vignette visual layer
|
||||
const mask = defs.append("mask").attr("id", "vignette-mask");
|
||||
mask.append("rect").attr("fill", "white").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
|
|
@ -739,7 +741,7 @@ export function resolveVersionConflicts(version) {
|
|||
vignette.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
}
|
||||
|
||||
if (version < 1.96) {
|
||||
if (isOlderThan("1.96.0")) {
|
||||
// v1.96 added ocean rendering for heightmap
|
||||
terrs.selectAll("*").remove();
|
||||
|
||||
|
|
@ -833,7 +835,7 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.97) {
|
||||
if (isOlderThan("1.97.0")) {
|
||||
// v1.97.00 changed MFCG link to an arbitrary preview URL
|
||||
options.villageMaxPopulation = 2000;
|
||||
options.showBurgPreview = options.showMFCGMap;
|
||||
|
|
@ -849,7 +851,7 @@ export function resolveVersionConflicts(version) {
|
|||
});
|
||||
}
|
||||
|
||||
if (version < 1.98) {
|
||||
if (isOlderThan("1.98.0")) {
|
||||
// v1.98.00 changed compass layer and rose element id
|
||||
const rose = compass.select("use");
|
||||
rose.attr("xlink:href", "#defs-compass-rose");
|
||||
|
|
@ -861,7 +863,7 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
|
||||
if (version < 1.99) {
|
||||
if (isOlderThan("1.99.0")) {
|
||||
// v1.99.00 changed routes generation algorithm and data format
|
||||
routes.attr("display", null).attr("style", null);
|
||||
|
||||
|
|
@ -923,4 +925,19 @@ export function resolveVersionConflicts(version) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isOlderThan("1.100.0")) {
|
||||
// v1.100.00 added zones to pack data
|
||||
pack.zones = [];
|
||||
zones.selectAll("g").each(function () {
|
||||
const i = pack.zones.length;
|
||||
const name = this.dataset.description;
|
||||
const type = this.dataset.type;
|
||||
const color = this.getAttribute("fill");
|
||||
const cells = this.dataset.cells.split(",").map(Number);
|
||||
pack.zones.push({i, name, type, cells, color});
|
||||
});
|
||||
zones.style("display", null).selectAll("*").remove();
|
||||
if (layerIsOn("toggleZones")) drawZones();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ function getMinimalDataJson() {
|
|||
religions: pack.religions,
|
||||
rivers: pack.rivers,
|
||||
markers: pack.markers,
|
||||
routes: pack.routes
|
||||
routes: pack.routes,
|
||||
zones: pack.zones
|
||||
};
|
||||
return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases});
|
||||
}
|
||||
|
|
@ -72,7 +73,7 @@ function getGridDataJson() {
|
|||
|
||||
function getMapInfo() {
|
||||
return {
|
||||
version,
|
||||
version: VERSION,
|
||||
description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator",
|
||||
exportedAt: new Date().toISOString(),
|
||||
mapName: mapName.value,
|
||||
|
|
@ -172,7 +173,8 @@ function getPackCellsData() {
|
|||
religions: pack.religions,
|
||||
rivers: pack.rivers,
|
||||
markers: pack.markers,
|
||||
routes: pack.routes
|
||||
routes: pack.routes,
|
||||
zones: pack.zones
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,8 +104,6 @@ function showUploadErrorMessage(error, URL, random) {
|
|||
|
||||
function uploadMap(file, callback) {
|
||||
uploadMap.timeStart = performance.now();
|
||||
const OLDEST_SUPPORTED_VERSION = 0.7;
|
||||
const currentVersion = parseFloat(version);
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = async function (fileLoadedEvent) {
|
||||
|
|
@ -114,14 +112,14 @@ function uploadMap(file, callback) {
|
|||
const result = fileLoadedEvent.target.result;
|
||||
const [mapData, mapVersion] = await parseLoadedResult(result);
|
||||
|
||||
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
|
||||
const isUpdated = mapVersion === currentVersion;
|
||||
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
|
||||
const isNewer = mapVersion > currentVersion;
|
||||
const isOutdated = mapVersion < currentVersion;
|
||||
const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 26 || !mapData[5];
|
||||
const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
|
||||
const isAncient = compareVersions(mapVersion, "0.7.0").isOlder;
|
||||
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
|
||||
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
|
||||
|
||||
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
|
||||
if (isUpdated) return parseLoadedData(mapData);
|
||||
if (isUpdated) return showUploadMessage("updated", mapData, mapVersion);
|
||||
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
|
||||
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
|
||||
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
|
||||
|
|
@ -154,8 +152,8 @@ async function parseLoadedResult(result) {
|
|||
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
|
||||
const mapData = decoded.split("\r\n");
|
||||
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
|
||||
return [mapData, mapVersion];
|
||||
const mapVersionString = mapData[0].split("|")[0] || mapData[0] || "";
|
||||
return [mapData, mapVersionString];
|
||||
} catch (error) {
|
||||
// map file can be compressed with gzip
|
||||
const uncompressedData = await uncompress(result);
|
||||
|
|
@ -167,35 +165,36 @@ async function parseLoadedResult(result) {
|
|||
}
|
||||
|
||||
function showUploadMessage(type, mapData, mapVersion) {
|
||||
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
||||
let message, title, canBeLoaded;
|
||||
let message, title;
|
||||
|
||||
if (type === "invalid") {
|
||||
message = `The file does not look like a valid save file.<br>Please check the data format`;
|
||||
message = "The file does not look like a valid save file.<br>Please check the data format";
|
||||
title = "Invalid file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "updated") {
|
||||
parseLoadedData(mapData, mapVersion);
|
||||
return;
|
||||
} else if (type === "ancient") {
|
||||
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
||||
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
|
||||
title = "Ancient file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "newer") {
|
||||
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
|
||||
title = "Newer file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "outdated") {
|
||||
INFO && console.info(`Loading map. Auto-update from ${mapVersion} to ${version}`);
|
||||
INFO && console.info(`Loading map. Auto-updating from ${mapVersion} to ${VERSION}`);
|
||||
parseLoadedData(mapData, mapVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
alertMessage.innerHTML = message;
|
||||
const buttons = {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
if (canBeLoaded) parseLoadedData(mapData, mapVersion);
|
||||
$("#alert").dialog({
|
||||
title,
|
||||
buttons: {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
};
|
||||
$("#alert").dialog({title, buttons});
|
||||
});
|
||||
}
|
||||
|
||||
async function parseLoadedData(data, mapVersion) {
|
||||
|
|
@ -380,6 +379,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
|
||||
pack.markers = data[35] ? JSON.parse(data[35]) : [];
|
||||
pack.routes = data[37] ? JSON.parse(data[37]) : [];
|
||||
pack.zones = data[38] ? JSON.parse(data[38]) : [];
|
||||
|
||||
const cells = pack.cells;
|
||||
cells.biome = Uint8Array.from(data[16].split(","));
|
||||
|
|
@ -462,9 +462,8 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
{
|
||||
// dynamically import and run auto-update script
|
||||
const versionNumber = parseFloat(params[0]);
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.01");
|
||||
resolveVersionConflicts(versionNumber);
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.100.00");
|
||||
resolveVersionConflicts(mapVersion);
|
||||
}
|
||||
|
||||
// add custom heightmap color scheme if any
|
||||
|
|
@ -709,7 +708,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
ERROR && console.error(error);
|
||||
clearMainTip();
|
||||
|
||||
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${version}.
|
||||
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
|
||||
<p id="errorBox">${parseError(error)}</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function prepareMapData() {
|
|||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
|
||||
const params = [VERSION, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
|
||||
const settings = [
|
||||
distanceUnitInput.value,
|
||||
distanceScale,
|
||||
|
|
@ -100,6 +100,7 @@ function prepareMapData() {
|
|||
const markers = JSON.stringify(pack.markers);
|
||||
const cellRoutes = JSON.stringify(pack.cells.routes);
|
||||
const routes = JSON.stringify(pack.routes);
|
||||
const zones = JSON.stringify(pack.zones);
|
||||
|
||||
// store name array only if not the same as default
|
||||
const defaultNB = Names.getNameBases();
|
||||
|
|
@ -152,7 +153,8 @@ function prepareMapData() {
|
|||
fonts,
|
||||
markers,
|
||||
cellRoutes,
|
||||
routes
|
||||
routes,
|
||||
zones
|
||||
].join("\r\n");
|
||||
return mapData;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,9 @@ window.Military = (function () {
|
|||
: "";
|
||||
|
||||
const campaign = s.campaigns ? ra(s.campaigns) : null;
|
||||
const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year - 100, 150, 1, options.year - 6);
|
||||
const year = campaign
|
||||
? rand(campaign.start, campaign.end || options.year)
|
||||
: gauss(options.year - 100, 150, 1, options.year - 6);
|
||||
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
||||
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
||||
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ window.Submap = (function () {
|
|||
stage("Redraw emblems.");
|
||||
drawEmblems();
|
||||
stage("Regenerating Zones.");
|
||||
addZones();
|
||||
Zones.generate();
|
||||
Names.getMapName();
|
||||
stage("Restoring Notes.");
|
||||
notes = parentMap.notes;
|
||||
|
|
|
|||
|
|
@ -228,34 +228,26 @@ function editBurg(id) {
|
|||
const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock));
|
||||
const capital = burgsToRemove.length < burgsInGroup.length;
|
||||
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
|
||||
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
|
||||
}?
|
||||
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
|
||||
burgsToRemove.length
|
||||
}`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
confirmationDialog({
|
||||
title: "Remove burg group",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
$("#burgEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
burgsToRemove.forEach(b => removeBurg(b));
|
||||
message: `Are you sure you want to remove ${
|
||||
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
|
||||
}?<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
|
||||
burgsToRemove.length
|
||||
}. This action cannot be reverted`,
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
$("#burgEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
burgsToRemove.forEach(b => removeBurg(b));
|
||||
|
||||
if (!basic && !capital) {
|
||||
// entirely remove group
|
||||
const labelG = document.querySelector("#burgLabels > #" + group.id);
|
||||
const iconG = document.querySelector("#burgIcons > #" + group.id);
|
||||
const anchorG = document.querySelector("#anchors > #" + group.id);
|
||||
if (labelG) labelG.remove();
|
||||
if (iconG) iconG.remove();
|
||||
if (anchorG) anchorG.remove();
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
if (!basic && !capital) {
|
||||
const labelG = document.querySelector("#burgLabels > #" + group.id);
|
||||
const iconG = document.querySelector("#burgIcons > #" + group.id);
|
||||
const anchorG = document.querySelector("#anchors > #" + group.id);
|
||||
if (labelG) labelG.remove();
|
||||
if (iconG) iconG.remove();
|
||||
if (anchorG) anchorG.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -509,19 +501,13 @@ function editBurg(id) {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
confirmationDialog({
|
||||
title: "Remove burg",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
removeBurg(id); // see Editors module
|
||||
$("#burgEditor").dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
removeBurg(id); // see Editors module
|
||||
$("#burgEditor").dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
|
|||
|
||||
confirmationDialog({
|
||||
title: "Remove burg",
|
||||
message: "Are you sure you want to remove the burg? This actiove cannot be reverted",
|
||||
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
removeBurg(burg);
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ function showMapTooltip(point, e, i, g) {
|
|||
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (group === "labels") return tip("Click to edit the Label");
|
||||
|
||||
if (group === "markers") return tip("Click to edit the Marker. Hold Shift to not close the assosiated note");
|
||||
|
|
@ -199,9 +200,11 @@ function showMapTooltip(point, e, i, g) {
|
|||
if (group === "coastline") return tip("Click to edit the coastline");
|
||||
|
||||
if (group === "zones") {
|
||||
const zone = path[path.length - 8];
|
||||
tip(zone.dataset.description);
|
||||
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
|
||||
const element = path[path.length - 8];
|
||||
const zoneId = +element.dataset.id;
|
||||
const zone = pack.zones.find(zone => zone.i === zoneId);
|
||||
tip(zone.name);
|
||||
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zoneId, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ function editHeightmap(options) {
|
|||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
addZones();
|
||||
Zones.generate();
|
||||
TIME && console.timeEnd("regenerateErasedData");
|
||||
INFO && console.groupEnd("Edit Heightmap");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ function restoreLayers() {
|
|||
if (layerIsOn("toggleIce")) drawIce();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
if (layerIsOn("toggleMarkers")) drawMarkers();
|
||||
if (layerIsOn("toggleZones")) drawZones();
|
||||
|
||||
// some layers are rendered each time, remove them if they are not on
|
||||
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
|
||||
|
|
@ -1872,18 +1873,29 @@ function fitScaleBar(scaleBar, fullWidth, fullHeight) {
|
|||
function toggleZones(event) {
|
||||
if (!layerIsOn("toggleZones")) {
|
||||
turnButtonOn("toggleZones");
|
||||
$("#zones").fadeIn();
|
||||
drawZones();
|
||||
if (event && isCtrlClick(event)) editStyle("zones");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) {
|
||||
editStyle("zones");
|
||||
return;
|
||||
}
|
||||
if (event && isCtrlClick(event)) return editStyle("zones");
|
||||
turnButtonOff("toggleZones");
|
||||
$("#zones").fadeOut();
|
||||
zones.selectAll("*").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawZones() {
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(
|
||||
({hidden, cells, type}) => !hidden && cells.length && (!isFiltered || type === filterBy)
|
||||
);
|
||||
zones.html(visibleZones.map(drawZone).join(""));
|
||||
}
|
||||
|
||||
function drawZone({i, cells, type, color}) {
|
||||
const path = getVertexPath(cells);
|
||||
return `<path id="zone${i}" data-id="${i}" data-type="${type}" d="${path}" fill="${color}" />`;
|
||||
}
|
||||
|
||||
function toggleEmblems(event) {
|
||||
if (!layerIsOn("toggleEmblems")) {
|
||||
turnButtonOn("toggleEmblems");
|
||||
|
|
|
|||
|
|
@ -782,7 +782,7 @@ function showExportPane() {
|
|||
}
|
||||
|
||||
async function exportToJson(type) {
|
||||
const {exportToJson} = await import("../dynamic/export-json.js?v=1.97.08");
|
||||
const {exportToJson} = await import("../dynamic/export-json.js?v=1.100.00");
|
||||
exportToJson(type);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -389,20 +389,13 @@ function editRoute(id) {
|
|||
}
|
||||
|
||||
function removeRoute() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the route";
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
width: "22em",
|
||||
confirmationDialog({
|
||||
title: "Remove route",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
Routes.remove(getRoute());
|
||||
$(this).dialog("close");
|
||||
$("#routeEditor").dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
message: "Are you sure you want to remove the route? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
Routes.remove(getRoute());
|
||||
$("#routeEditor").dialog("close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,22 +144,14 @@ function overviewRoutes() {
|
|||
|
||||
function triggerRouteRemove() {
|
||||
const routeId = +this.parentNode.dataset.id;
|
||||
|
||||
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
width: "22em",
|
||||
confirmationDialog({
|
||||
title: "Remove route",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
const route = pack.routes.find(r => r.i === routeId);
|
||||
Routes.remove(route);
|
||||
routesOverviewAddLines();
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
message: "Are you sure you want to remove the route? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
const route = pack.routes.find(r => r.i === routeId);
|
||||
Routes.remove(route);
|
||||
routesOverviewAddLines();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ async function getStylePreset(desiredPreset) {
|
|||
|
||||
async function fetchSystemPreset(preset) {
|
||||
try {
|
||||
const res = await fetch(`./styles/${preset}.json?v=${version}`);
|
||||
const res = await fetch(`./styles/${preset}.json?v=${VERSION}`);
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
throw new Error("Cannot fetch style preset", preset);
|
||||
|
|
@ -159,18 +159,6 @@ window.UISubmap = (function () {
|
|||
return canvas;
|
||||
}
|
||||
|
||||
// currently unused alternative to PNG version
|
||||
async function loadPreviewSVG($container, w, h) {
|
||||
$container.innerHTML = /*html*/ `
|
||||
<svg id="submapPreviewSVG" viewBox="0 0 ${graphWidth} ${graphHeight}">
|
||||
<rect width="100%" height="100%" fill="${byId("styleOceanFill").value}" />
|
||||
<rect fill="url(#oceanic)" width="100%" height="100%" />
|
||||
<use href="#map"></use>
|
||||
</svg>
|
||||
`;
|
||||
return byId("submapPreviewSVG");
|
||||
}
|
||||
|
||||
// Resample the whole map to different cell resolution or shape
|
||||
const resampleCurrentMap = debounce(function () {
|
||||
WARN && console.warn("Resampling current map");
|
||||
|
|
|
|||
|
|
@ -167,9 +167,9 @@ function regenerateStates() {
|
|||
Military.generate();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
|
||||
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
|
||||
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
|
||||
}
|
||||
|
||||
function recreateStates() {
|
||||
|
|
@ -445,8 +445,8 @@ function regenerateBurgs() {
|
|||
emblems.selectAll("use").remove();
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||
|
||||
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
|
||||
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateEmblems() {
|
||||
|
|
@ -521,7 +521,7 @@ function regenerateCultures() {
|
|||
function regenerateMilitary() {
|
||||
Military.generate();
|
||||
if (!layerIsOn("toggleMilitary")) toggleMilitary();
|
||||
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
||||
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateIce() {
|
||||
|
|
@ -534,7 +534,7 @@ function regenerateMarkers() {
|
|||
Markers.regenerate();
|
||||
turnButtonOn("toggleMarkers");
|
||||
drawMarkers();
|
||||
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
|
||||
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateZones(event) {
|
||||
|
|
@ -545,10 +545,9 @@ function regenerateZones(event) {
|
|||
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
|
||||
|
||||
function addNumberOfZones(number) {
|
||||
zones.selectAll("g").remove(); // remove existing zones
|
||||
addZones(number);
|
||||
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
|
||||
if (!layerIsOn("toggleZones")) toggleZones();
|
||||
Zones.generate(number);
|
||||
if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
|
||||
if (layerIsOn("toggleZones")) drawZones();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -559,7 +558,7 @@ function unpressClickToAddButton() {
|
|||
}
|
||||
|
||||
function toggleAddLabel() {
|
||||
const pressed = document.getElementById("addLabel").classList.contains("pressed");
|
||||
const pressed = byId("addLabel").classList.contains("pressed");
|
||||
if (pressed) {
|
||||
unpressClickToAddButton();
|
||||
return;
|
||||
|
|
@ -627,22 +626,22 @@ function addLabelOnClick() {
|
|||
|
||||
function toggleAddBurg() {
|
||||
unpressClickToAddButton();
|
||||
document.getElementById("addBurgTool").classList.add("pressed");
|
||||
byId("addBurgTool").classList.add("pressed");
|
||||
overviewBurgs();
|
||||
document.getElementById("addNewBurg").click();
|
||||
byId("addNewBurg").click();
|
||||
}
|
||||
|
||||
function toggleAddRiver() {
|
||||
const pressed = document.getElementById("addRiver").classList.contains("pressed");
|
||||
const pressed = byId("addRiver").classList.contains("pressed");
|
||||
if (pressed) {
|
||||
unpressClickToAddButton();
|
||||
document.getElementById("addNewRiver").classList.remove("pressed");
|
||||
byId("addNewRiver").classList.remove("pressed");
|
||||
return;
|
||||
}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addRiver.classList.add("pressed");
|
||||
document.getElementById("addNewRiver").classList.add("pressed");
|
||||
byId("addNewRiver").classList.add("pressed");
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
|
||||
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
|
||||
|
|
@ -728,7 +727,7 @@ function addRiverOnClick() {
|
|||
}
|
||||
|
||||
// continue old river
|
||||
document.getElementById("river" + oldRiverId)?.remove();
|
||||
byId("river" + oldRiverId)?.remove();
|
||||
riverCells.forEach(i => (cells.r[i] = oldRiverId));
|
||||
oldRiverCells.forEach(cell => {
|
||||
if (h[cell] > h[min]) {
|
||||
|
|
@ -796,13 +795,13 @@ function addRiverOnClick() {
|
|||
if (d3.event.shiftKey === false) {
|
||||
Lakes.cleanupLakeData();
|
||||
unpressClickToAddButton();
|
||||
document.getElementById("addNewRiver").classList.remove("pressed");
|
||||
byId("addNewRiver").classList.remove("pressed");
|
||||
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
|
||||
const pressed = byId("addMarker")?.classList.contains("pressed");
|
||||
if (pressed) {
|
||||
unpressClickToAddButton();
|
||||
return;
|
||||
|
|
@ -830,7 +829,7 @@ function addMarkerOnClick() {
|
|||
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
|
||||
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
|
||||
|
||||
const selectedType = document.getElementById("addedMarkerType").value;
|
||||
const selectedType = byId("addedMarkerType").value;
|
||||
const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
|
||||
|
||||
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
|
||||
|
|
@ -840,13 +839,13 @@ function addMarkerOnClick() {
|
|||
selectedConfig.add("marker" + marker.i, cell);
|
||||
}
|
||||
|
||||
const markersElement = document.getElementById("markers");
|
||||
const markersElement = byId("markers");
|
||||
const rescale = +markersElement.getAttribute("rescale");
|
||||
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
|
||||
|
||||
if (d3.event.shiftKey === false) {
|
||||
document.getElementById("markerAdd").classList.remove("pressed");
|
||||
document.getElementById("markersAddFromOverview").classList.remove("pressed");
|
||||
byId("markerAdd").classList.remove("pressed");
|
||||
byId("markersAddFromOverview").classList.remove("pressed");
|
||||
unpressClickToAddButton();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ function editZones() {
|
|||
$("#zonesEditor").dialog({
|
||||
title: "Zones Editor",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
close: () => exitZonesManualAssignment("close"),
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
|
@ -31,34 +30,40 @@ function editZones() {
|
|||
byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent);
|
||||
byId("zonesAdd").on("click", addZonesLayer);
|
||||
byId("zonesExport").on("click", downloadZonesData);
|
||||
byId("zonesRemove").on("click", toggleEraseMode);
|
||||
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
|
||||
|
||||
body.on("click", function (ev) {
|
||||
const el = ev.target,
|
||||
cl = el.classList,
|
||||
zone = el.parentNode.dataset.id;
|
||||
if (el.tagName === "FILL-BOX") changeFill(el);
|
||||
else if (cl.contains("culturePopulation")) changePopulation(zone);
|
||||
else if (cl.contains("icon-trash-empty")) zoneRemove(zone);
|
||||
else if (cl.contains("icon-eye")) toggleVisibility(el);
|
||||
else if (cl.contains("icon-pin")) toggleFog(zone, cl);
|
||||
if (customization) selectZone(el);
|
||||
const line = ev.target.closest("div.states");
|
||||
const zone = pack.zones.find(z => z.i === +line.dataset.id);
|
||||
if (!zone) return;
|
||||
|
||||
if (customization) {
|
||||
if (zone.hidden) return;
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
line.classList.add("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
|
||||
else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
|
||||
else if (ev.target.classList.contains("zoneRemove")) zoneRemove(zone);
|
||||
else if (ev.target.classList.contains("zoneHide")) toggleVisibility(zone);
|
||||
else if (ev.target.classList.contains("zoneFog")) toggleFog(zone, ev.target.classList);
|
||||
});
|
||||
|
||||
body.on("input", function (ev) {
|
||||
const el = ev.target;
|
||||
const zone = zones.select("#" + el.parentNode.dataset.id);
|
||||
const line = ev.target.closest("div.states");
|
||||
const zone = pack.zones.find(z => z.i === +line.dataset.id);
|
||||
if (!zone) return;
|
||||
|
||||
if (el.classList.contains("zoneName")) zone.attr("data-description", el.value);
|
||||
else if (el.classList.contains("zoneType")) zone.attr("data-type", el.value);
|
||||
if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value);
|
||||
else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value);
|
||||
});
|
||||
|
||||
// update type filter with a list of used types
|
||||
function updateFilters() {
|
||||
const zones = Array.from(document.querySelectorAll("#zones > g"));
|
||||
const types = unique(zones.map(zone => zone.dataset.type));
|
||||
|
||||
const filterSelect = byId("zonesFilterType");
|
||||
const types = unique(pack.zones.map(zone => zone.type));
|
||||
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
|
||||
|
||||
filterSelect.innerHTML =
|
||||
|
|
@ -68,47 +73,42 @@ function editZones() {
|
|||
|
||||
// add line for each zone
|
||||
function zonesEditorAddLines() {
|
||||
const unit = " " + getAreaUnit();
|
||||
|
||||
const typeToFilterBy = byId("zonesFilterType").value;
|
||||
const zones = Array.from(document.querySelectorAll("#zones > g"));
|
||||
const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy);
|
||||
const filteredZones =
|
||||
typeToFilterBy === "all" ? pack.zones : pack.zones.filter(zone => zone.type === typeToFilterBy);
|
||||
|
||||
const lines = filteredZones.map(zoneEl => {
|
||||
const c = zoneEl.dataset.cells ? zoneEl.dataset.cells.split(",").map(c => +c) : [];
|
||||
const description = zoneEl.dataset.description;
|
||||
const type = zoneEl.dataset.type;
|
||||
const fill = zoneEl.getAttribute("fill");
|
||||
const area = getArea(d3.sum(c.map(i => pack.cells.area[i])));
|
||||
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
|
||||
const lines = filteredZones.map(({i, name, type, cells, color, hidden}) => {
|
||||
const area = getArea(d3.sum(cells.map(i => pack.cells.area[i])));
|
||||
const rural = d3.sum(cells.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;
|
||||
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
|
||||
rural
|
||||
)}; Urban population: ${si(urban)}. Click to change`;
|
||||
const inactive = zoneEl.style.display === "none";
|
||||
const focused = defs.select("#fog #focus" + zoneEl.id).size();
|
||||
const focused = defs.select("#fog #focusZone" + i).size();
|
||||
|
||||
return `<div class="states" data-id="${zoneEl.id}" data-fill="${fill}" data-description="${description}"
|
||||
data-type="${type}" data-cells=${c.length} data-area=${area} data-population=${population}>
|
||||
<fill-box fill="${fill}"></fill-box>
|
||||
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${description}" autocorrect="off" spellcheck="false">
|
||||
return /* html */ `<div class="states" data-id="${i}" data-color="${color}" data-description="${name}"
|
||||
data-type="${type}" data-cells=${cells.length} data-area=${area} data-population=${population} style="${
|
||||
hidden && "opacity: 0.5"
|
||||
}">
|
||||
<fill-box fill="${color}"></fill-box>
|
||||
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${name}" autocorrect="off" spellcheck="false">
|
||||
<input data-tip="Zone type. Click and type to change" class="zoneType" value="${type}">
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
|
||||
<div data-tip="Cells count" class="stateCells hide">${cells.length}</div>
|
||||
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<div data-tip="Zone area" class="biomeArea hide">${si(area) + " " + getAreaUnit()}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
<div data-tip="${populationTip}" class="zonePopulation hide pointer">${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 data-tip="Toggle zone focus" class="zoneFog icon-pin ${focused ? "" : "inactive"} hide ${
|
||||
cells.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>
|
||||
<span data-tip="Toggle zone visibility" class="zoneHide icon-eye hide ${
|
||||
cells.length ? "" : " placeholder"
|
||||
}"></span>
|
||||
<span data-tip="Remove zone" class="zoneRemove icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
|
|
@ -121,14 +121,13 @@ function editZones() {
|
|||
(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 = /* html */ `${filteredZones.length} of ${zones.length}`;
|
||||
zonesFooterNumber.innerHTML = `${filteredZones.length} of ${pack.zones.length}`;
|
||||
zonesFooterCells.innerHTML = pack.cells.i.length;
|
||||
zonesFooterArea.innerHTML = si(totalArea) + unit;
|
||||
zonesFooterArea.innerHTML = si(totalArea) + " " + getAreaUnit();
|
||||
zonesFooterPopulation.innerHTML = si(totalPop);
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => zoneHighlightOn(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => zoneHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", zoneHighlightOn));
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", zoneHighlightOff));
|
||||
|
||||
if (body.dataset.type === "percentage") {
|
||||
body.dataset.type = "absolute";
|
||||
|
|
@ -138,25 +137,17 @@ function editZones() {
|
|||
}
|
||||
|
||||
function zoneHighlightOn(event) {
|
||||
const zone = event.target.dataset.id;
|
||||
zones.select("#" + zone).style("outline", "1px solid red");
|
||||
const zoneId = event.target.dataset.id;
|
||||
zones.select("#zone" + zoneId).style("outline", "1px solid red");
|
||||
}
|
||||
|
||||
function zoneHighlightOff(event) {
|
||||
const zone = event.target.dataset.id;
|
||||
zones.select("#" + zone).style("outline", null);
|
||||
const zoneId = event.target.dataset.id;
|
||||
zones.select("#zone" + zoneId).style("outline", null);
|
||||
}
|
||||
|
||||
function filterZonesByType() {
|
||||
const typeToFilterBy = this.value;
|
||||
const zones = Array.from(document.querySelectorAll("#zones > g"));
|
||||
|
||||
for (const zone of zones) {
|
||||
const type = zone.dataset.type;
|
||||
const visible = typeToFilterBy === "all" || type === typeToFilterBy;
|
||||
zone.style.display = visible ? "block" : "none";
|
||||
}
|
||||
|
||||
drawZones();
|
||||
zonesEditorAddLines();
|
||||
}
|
||||
|
||||
|
|
@ -167,23 +158,24 @@ function editZones() {
|
|||
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"));
|
||||
if (next) zone.insertBefore(next);
|
||||
|
||||
function movezone(_ev, ui) {
|
||||
const zone = pack.zones.find(z => z.i === +ui.item[0].dataset.id);
|
||||
const oldIndex = pack.zones.indexOf(zone);
|
||||
const newIndex = ui.item.index();
|
||||
if (oldIndex === newIndex) return;
|
||||
|
||||
pack.zones.splice(oldIndex, 1);
|
||||
pack.zones.splice(newIndex, 0, zone);
|
||||
drawZones();
|
||||
}
|
||||
|
||||
function enterZonesManualAssignent() {
|
||||
if (!layerIsOn("toggleZones")) toggleZones();
|
||||
customization = 10;
|
||||
|
||||
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
|
||||
byId("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"));
|
||||
|
|
@ -197,21 +189,32 @@ function editZones() {
|
|||
.on("touchmove mousemove", moveZoneBrush);
|
||||
|
||||
body.querySelector("div").classList.add("selected");
|
||||
zones.selectAll("g").each(function () {
|
||||
this.setAttribute("data-init", this.getAttribute("data-cells"));
|
||||
});
|
||||
}
|
||||
|
||||
function selectZone(el) {
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
el.classList.add("selected");
|
||||
// draw zones as individual cells
|
||||
zones.selectAll("*").remove();
|
||||
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
|
||||
const data = visibleZones.map(({i, cells, color}) => cells.map(cell => ({cell, zoneId: i, fill: color}))).flat();
|
||||
zones
|
||||
.selectAll("polygon")
|
||||
.data(data, d => `${d.zoneId}-${d.cell}`)
|
||||
.enter()
|
||||
.append("polygon")
|
||||
.attr("points", d => getPackPolygon(d.cell))
|
||||
.attr("fill", d => d.fill)
|
||||
.attr("data-zone", d => d.zoneId)
|
||||
.attr("data-cell", d => d.cell);
|
||||
}
|
||||
|
||||
function selectZoneOnMapClick() {
|
||||
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
|
||||
const zone = d3.event.target.parentElement.id;
|
||||
const el = body.querySelector("div[data-id='" + zone + "']");
|
||||
selectZone(el);
|
||||
if (d3.event.target.parentElement.id !== "zones") return;
|
||||
const zoneId = d3.event.target.dataset.zone;
|
||||
const el = body.querySelector("div[data-id='" + zoneId + "']");
|
||||
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
el.classList.add("selected");
|
||||
}
|
||||
|
||||
function dragZoneBrush() {
|
||||
|
|
@ -219,43 +222,41 @@ function editZones() {
|
|||
const eraseMode = byId("zonesRemove").classList.contains("pressed");
|
||||
const landOnly = byId("zonesBrushLandOnly").checked;
|
||||
|
||||
const selected = body.querySelector("div.selected");
|
||||
const zone = zones.select("#" + selected.dataset.id);
|
||||
const base = zone.attr("id") + "_"; // id generic part
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const [x, y] = d3.mouse(this);
|
||||
moveCircle(x, y, radius);
|
||||
|
||||
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y, radius)];
|
||||
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y)];
|
||||
if (landOnly) selection = selection.filter(i => pack.cells.h[i] >= 20);
|
||||
if (!selection) return;
|
||||
if (!selection.length) return;
|
||||
|
||||
const dataCells = zone.attr("data-cells");
|
||||
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
|
||||
const zoneId = +body.querySelector("div.selected")?.dataset.id;
|
||||
const zone = pack.zones.find(z => z.i === zoneId);
|
||||
if (!zone) return;
|
||||
|
||||
if (eraseMode) {
|
||||
// remove
|
||||
selection.forEach(i => {
|
||||
const index = cells.indexOf(i);
|
||||
if (index === -1) return;
|
||||
zone.select("polygon#" + base + i).remove();
|
||||
cells.splice(index, 1);
|
||||
});
|
||||
const data = zones
|
||||
.selectAll("polygon")
|
||||
.data()
|
||||
.filter(d => !(d.zoneId === zoneId && selection.includes(d.cell)));
|
||||
zones
|
||||
.selectAll("polygon")
|
||||
.data(data, d => `${d.zoneId}-${d.cell}`)
|
||||
.exit()
|
||||
.remove();
|
||||
} else {
|
||||
// add
|
||||
selection.forEach(i => {
|
||||
if (cells.includes(i)) return;
|
||||
cells.push(i);
|
||||
zone
|
||||
.append("polygon")
|
||||
.attr("points", getPackPolygon(i))
|
||||
.attr("id", base + i);
|
||||
});
|
||||
const data = selection.map(cell => ({cell, zoneId, fill: zone.color}));
|
||||
zones
|
||||
.selectAll("polygon")
|
||||
.data(data, d => `${d.zoneId}-${d.cell}`)
|
||||
.enter()
|
||||
.append("polygon")
|
||||
.attr("points", d => getPackPolygon(d.cell))
|
||||
.attr("fill", d => d.fill)
|
||||
.attr("data-zone", d => d.zoneId)
|
||||
.attr("data-cell", d => d.cell);
|
||||
}
|
||||
|
||||
zone.attr("data-cells", cells);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -263,39 +264,29 @@ function editZones() {
|
|||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +zonesBrush.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
moveCircle(...point, radius);
|
||||
}
|
||||
|
||||
function applyZonesManualAssignent() {
|
||||
zones.selectAll("g").each(function () {
|
||||
if (this.dataset.cells) return;
|
||||
// all zone cells are removed
|
||||
unfog("focusZone" + this.id);
|
||||
this.style.display = "block";
|
||||
});
|
||||
const data = zones.selectAll("polygon").data();
|
||||
const zoneCells = data.reduce((acc, d) => {
|
||||
if (!acc[d.zoneId]) acc[d.zoneId] = [];
|
||||
acc[d.zoneId].push(d.cell);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
|
||||
visibleZones.forEach(zone => (zone.cells = zoneCells[zone.i] || []));
|
||||
|
||||
drawZones();
|
||||
zonesEditorAddLines();
|
||||
exitZonesManualAssignment();
|
||||
}
|
||||
|
||||
// restore initial zone cells
|
||||
function cancelZonesManualAssignent() {
|
||||
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);
|
||||
});
|
||||
|
||||
drawZones();
|
||||
exitZonesManualAssignment();
|
||||
}
|
||||
|
||||
|
|
@ -313,60 +304,47 @@ function editZones() {
|
|||
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
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");
|
||||
function changeFill(fill, zone) {
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
byId(el.parentNode.dataset.id).setAttribute("fill", newFill);
|
||||
zone.color = newFill;
|
||||
drawZones();
|
||||
zonesEditorAddLines();
|
||||
};
|
||||
|
||||
openPicker(fill, callback);
|
||||
}
|
||||
|
||||
function toggleVisibility(el) {
|
||||
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 toggleVisibility(zone) {
|
||||
const isHidden = Boolean(zone.hidden);
|
||||
if (isHidden) delete zone.hidden;
|
||||
else zone.hidden = true;
|
||||
|
||||
drawZones();
|
||||
zonesEditorAddLines();
|
||||
}
|
||||
|
||||
function toggleFog(z, cl) {
|
||||
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;
|
||||
cl.contains("inactive") ? fog(id, path) : unfog(id);
|
||||
function toggleFog(zone, cl) {
|
||||
const inactive = cl.contains("inactive");
|
||||
cl.toggle("inactive");
|
||||
|
||||
if (inactive) {
|
||||
const path = zones.select("#zone" + zone.i).attr("d");
|
||||
fog("focusZone" + zone.i, path);
|
||||
} else {
|
||||
unfog("focusZone" + zone.i);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) {
|
||||
clearLegend();
|
||||
return;
|
||||
} // hide legend
|
||||
const data = [];
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
|
||||
const data = visibleZones.map(({i, name, color}) => ["zone" + i, color, name]);
|
||||
drawLegend("Zones", data);
|
||||
}
|
||||
|
||||
|
|
@ -380,8 +358,7 @@ function editZones() {
|
|||
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) + "%";
|
||||
el.querySelector(".zonePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
|
||||
});
|
||||
} else {
|
||||
body.dataset.type = "absolute";
|
||||
|
|
@ -390,28 +367,23 @@ function editZones() {
|
|||
}
|
||||
|
||||
function addZonesLayer() {
|
||||
const id = getNextId("zone");
|
||||
const description = "Unknown zone";
|
||||
const zoneId = pack.zones.length ? Math.max(...pack.zones.map(z => z.i)) + 1 : 0;
|
||||
const name = "Unknown zone";
|
||||
const type = "Unknown";
|
||||
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
|
||||
zones
|
||||
.append("g")
|
||||
.attr("id", id)
|
||||
.attr("data-description", description)
|
||||
.attr("data-type", type)
|
||||
.attr("data-cells", "")
|
||||
.attr("fill", fill);
|
||||
const color = "url(#hatch" + (zoneId % 42) + ")";
|
||||
pack.zones.push({i: zoneId, name, type, color, cells: []});
|
||||
|
||||
zonesEditorAddLines();
|
||||
drawZones();
|
||||
}
|
||||
|
||||
function downloadZonesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Fill,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
|
||||
let data = "Id,Color,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.fill + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.description + ",";
|
||||
data += el.dataset.type + ",";
|
||||
data += el.dataset.cells + ",";
|
||||
|
|
@ -423,27 +395,24 @@ function editZones() {
|
|||
downloadFile(data, name);
|
||||
}
|
||||
|
||||
function toggleEraseMode() {
|
||||
this.classList.toggle("pressed");
|
||||
function changeDescription(zone, value) {
|
||||
zone.name = value;
|
||||
zones.select("#zone" + zone.i).attr("data-description", value);
|
||||
}
|
||||
|
||||
function changeType(zone, value) {
|
||||
zone.type = value;
|
||||
zones.select("#zone" + zone.i).attr("data-type", value);
|
||||
}
|
||||
|
||||
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 burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
|
||||
const landCells = zone.cells.filter(i => pack.cells.h[i] >= 20);
|
||||
if (!landCells.length) return tip("Zone does not have any land cells, cannot change population", false, "error");
|
||||
|
||||
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
|
||||
const burgs = pack.burgs.filter(b => !b.removed && landCells.includes(b.cell));
|
||||
const rural = rn(d3.sum(landCells.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
|
||||
d3.sum(landCells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
|
||||
);
|
||||
const total = rural + urban;
|
||||
const l = n => Number(n).toLocaleString();
|
||||
|
|
@ -485,12 +454,12 @@ function editZones() {
|
|||
function applyPopulationChange() {
|
||||
const ruralChange = ruralPop.value / rural;
|
||||
if (isFinite(ruralChange) && ruralChange !== 1) {
|
||||
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
|
||||
landCells.forEach(i => (pack.cells.pop[i] *= ruralChange));
|
||||
}
|
||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||
const points = ruralPop.value / populationRate;
|
||||
const pop = rn(points / cells.length);
|
||||
cells.forEach(i => (pack.cells.pop[i] = pop));
|
||||
const pop = rn(points / landCells.length);
|
||||
landCells.forEach(i => (pack.cells.pop[i] = pop));
|
||||
}
|
||||
|
||||
const urbanChange = urbanPop.value / urban;
|
||||
|
|
@ -508,8 +477,16 @@ function editZones() {
|
|||
}
|
||||
|
||||
function zoneRemove(zone) {
|
||||
zones.select("#" + zone).remove();
|
||||
unfog("focusZone" + zone);
|
||||
zonesEditorAddLines();
|
||||
confirmationDialog({
|
||||
title: "Remove zone",
|
||||
message: "Are you sure you want to remove the zone? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => {
|
||||
pack.zones = pack.zones.filter(z => z.i !== zone.i);
|
||||
zones.select("#zone" + zone.i).remove();
|
||||
unfog("focusZone" + zone.i);
|
||||
zonesEditorAddLines();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
447
modules/zones-generator.js
Normal file
447
modules/zones-generator.js
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
"use strict";
|
||||
|
||||
window.Zones = (function () {
|
||||
const config = {
|
||||
invasion: {quantity: 2, generate: addInvasion}, // invasion of enemy lands
|
||||
rebels: {quantity: 1.5, generate: addRebels}, // rebels along a state border
|
||||
proselytism: {quantity: 1.6, generate: addProselytism}, // proselitism of organized religion
|
||||
crusade: {quantity: 1.6, generate: addCrusade}, // crusade on heresy lands
|
||||
disease: {quantity: 1.4, generate: addDisease}, // disease starting in a random city
|
||||
disaster: {quantity: 1, generate: addDisaster}, // disaster starting in a random city
|
||||
eruption: {quantity: 1, generate: addEruption}, // eruption aroung volcano
|
||||
avalanche: {quantity: 0.8, generate: addAvalanche}, // avalanche impacting highland road
|
||||
fault: {quantity: 1, generate: addFault}, // fault line in elevated areas
|
||||
flood: {quantity: 1, generate: addFlood}, // flood on river banks
|
||||
tsunami: {quantity: 1, generate: addTsunami} // tsunami starting near coast
|
||||
};
|
||||
|
||||
const generate = function (globalModifier = 1) {
|
||||
TIME && console.time("generateZones");
|
||||
|
||||
const usedCells = new Uint8Array(pack.cells.i.length);
|
||||
pack.zones = [];
|
||||
|
||||
Object.values(config).forEach(type => {
|
||||
const expectedNumber = type.quantity * globalModifier;
|
||||
let number = gauss(expectedNumber, expectedNumber / 2, 0, 100);
|
||||
while (number--) type.generate(usedCells);
|
||||
});
|
||||
|
||||
TIME && console.timeEnd("generateZones");
|
||||
};
|
||||
|
||||
function addInvasion(usedCells) {
|
||||
const {cells, states} = pack;
|
||||
|
||||
const ongoingConflicts = states
|
||||
.filter(s => s.i && !s.removed && s.campaigns)
|
||||
.map(s => s.campaigns)
|
||||
.flat()
|
||||
.filter(c => !c.end);
|
||||
if (!ongoingConflicts.length) return;
|
||||
const {defender, attacker} = ra(ongoingConflicts);
|
||||
|
||||
const borderCells = cells.i.filter(cellId => {
|
||||
if (usedCells[cellId]) return false;
|
||||
if (cells.state[cellId] !== defender) return false;
|
||||
return cells.c[cellId].some(c => cells.state[c] === attacker);
|
||||
});
|
||||
|
||||
const startCell = ra(borderCells);
|
||||
if (startCell === undefined) return;
|
||||
|
||||
const invationCells = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(5, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = P(0.4) ? queue.shift() : queue.pop();
|
||||
invationCells.push(cellId);
|
||||
if (invationCells.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId]) return;
|
||||
if (cells.state[neibCellId] !== defender) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const subtype = rw({
|
||||
Invasion: 5,
|
||||
Occupation: 4,
|
||||
Conquest: 3,
|
||||
Incursion: 2,
|
||||
Intervention: 2,
|
||||
Subjugation: 1,
|
||||
Foray: 1,
|
||||
Skirmishes: 1,
|
||||
Pillaging: 1,
|
||||
Raid: 1
|
||||
});
|
||||
const name = getAdjective(states[attacker].name) + " " + subtype;
|
||||
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invationCells, color: "url(#hatch1)"});
|
||||
}
|
||||
|
||||
function addRebels(usedCells) {
|
||||
const {cells, states} = pack;
|
||||
|
||||
const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(Boolean)));
|
||||
if (!state) return;
|
||||
|
||||
const neibStateId = ra(state.neighbors.filter(n => n && !states[n].removed));
|
||||
if (!neibStateId) return;
|
||||
|
||||
const cellsArray = [];
|
||||
const queue = [];
|
||||
const borderCellId = cells.i.find(
|
||||
i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neibStateId)
|
||||
);
|
||||
if (borderCellId) queue.push(borderCellId);
|
||||
const maxCells = rand(10, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.shift();
|
||||
cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId]) return;
|
||||
if (cells.state[neibCellId] !== state.i) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
if (neibCellId % 4 !== 0 && !cells.c[neibCellId].some(c => cells.state[c] === neibStateId)) return;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const rebels = rw({
|
||||
Rebels: 5,
|
||||
Insurrection: 2,
|
||||
Mutineers: 1,
|
||||
Insurgents: 1,
|
||||
Rioters: 1,
|
||||
Separatists: 1,
|
||||
Secessionists: 1,
|
||||
Rebellion: 1,
|
||||
Conspiracy: 1
|
||||
});
|
||||
|
||||
const name = getAdjective(states[neibStateId].name) + " " + rebels;
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Rebels", cells: cellsArray, color: "url(#hatch3)"});
|
||||
}
|
||||
|
||||
function addProselytism(usedCells) {
|
||||
const {cells, religions} = pack;
|
||||
|
||||
const organizedReligions = religions.filter(r => r.i && !r.removed && r.type === "Organized");
|
||||
const religion = ra(organizedReligions);
|
||||
if (!religion) return;
|
||||
|
||||
const targetBorderCells = cells.i.filter(
|
||||
i =>
|
||||
cells.h[i] < 20 &&
|
||||
cells.pop[i] &&
|
||||
cells.religion[i] !== religion.i &&
|
||||
cells.c[i].some(c => cells.religion[c] === religion.i)
|
||||
);
|
||||
const startCell = ra(targetBorderCells);
|
||||
if (!startCell) return;
|
||||
|
||||
const targetReligionId = cells.religion[startCell];
|
||||
const proselytismCells = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(10, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.shift();
|
||||
proselytismCells.push(cellId);
|
||||
if (proselytismCells.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId]) return;
|
||||
if (cells.religion[neibCellId] !== targetReligionId) return;
|
||||
if (cells.h[neibCellId] < 20 || !cells.pop[i]) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const name = `${getAdjective(religion.name.split(" ")[0])} Proselytism`;
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Proselytism", cells: proselytismCells, color: "url(#hatch6)"});
|
||||
}
|
||||
|
||||
function addCrusade(usedCells) {
|
||||
const {cells, religions} = pack;
|
||||
|
||||
const heresies = religions.filter(r => !r.removed && r.type === "Heresy");
|
||||
if (!heresies.length) return;
|
||||
|
||||
const heresy = ra(heresies);
|
||||
const crusadeCells = cells.i.filter(i => !usedCells[i] && cells.religion[i] === heresy.i);
|
||||
if (!crusadeCells.length) return;
|
||||
crusadeCells.forEach(i => (usedCells[i] = 1));
|
||||
|
||||
const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade";
|
||||
pack.zones.push({
|
||||
i: pack.zones.length,
|
||||
name,
|
||||
type: "Crusade",
|
||||
cells: Array.from(crusadeCells),
|
||||
color: "url(#hatch6)"
|
||||
});
|
||||
}
|
||||
|
||||
function addDisease(usedCells) {
|
||||
const {cells, burgs} = pack;
|
||||
|
||||
const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); // random burg
|
||||
if (!burg) return;
|
||||
|
||||
const cellsArray = [];
|
||||
const cost = [];
|
||||
const maxCells = 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);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
cells.c[next.e].forEach(nextCellId => {
|
||||
const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100;
|
||||
const p = next.p + c;
|
||||
if (p > maxCells) return;
|
||||
|
||||
if (!cost[nextCellId] || p < cost[nextCellId]) {
|
||||
cost[nextCellId] = p;
|
||||
queue.queue({e: nextCellId, p});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const name = `${(() => {
|
||||
const model = rw({color: 2, animal: 1, adjective: 1});
|
||||
if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]);
|
||||
if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Dog", "Fox", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]);
|
||||
if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]);
|
||||
})()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`;
|
||||
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Disease", cells: cellsArray, color: "url(#hatch12)"});
|
||||
}
|
||||
|
||||
function addDisaster(usedCells) {
|
||||
const {cells, burgs} = pack;
|
||||
|
||||
const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed));
|
||||
if (!burg) return;
|
||||
usedCells[burg.cell] = 1;
|
||||
|
||||
const cellsArray = [];
|
||||
const cost = [];
|
||||
const maxCells = 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);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
cells.c[next.e].forEach(function (e) {
|
||||
const c = rand(1, 10);
|
||||
const p = next.p + c;
|
||||
if (p > maxCells) return;
|
||||
|
||||
if (!cost[e] || p < cost[e]) {
|
||||
cost[e] = p;
|
||||
queue.queue({e, p});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const type = rw({
|
||||
Famine: 5,
|
||||
Drought: 3,
|
||||
Earthquake: 3,
|
||||
Dearth: 1,
|
||||
Tornadoes: 1,
|
||||
Wildfires: 1,
|
||||
Storms: 1,
|
||||
Blight: 1
|
||||
});
|
||||
const name = getAdjective(burg.name) + " " + type;
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Disaster", cells: cellsArray, color: "url(#hatch5)"});
|
||||
}
|
||||
|
||||
function addEruption(usedCells) {
|
||||
const {cells, markers} = pack;
|
||||
|
||||
const volcanoe = markers.find(m => m.type === "volcanoes" && !usedCells[m.cell]);
|
||||
if (!volcanoe) return;
|
||||
usedCells[volcanoe.cell] = 1;
|
||||
|
||||
const note = notes.find(n => n.id === "marker" + volcanoe.i);
|
||||
if (note) note.legend = note.legend.replace("Active volcano", "Erupting volcano");
|
||||
const name = note ? note.name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption";
|
||||
|
||||
const cellsArray = [];
|
||||
const queue = [volcanoe.cell];
|
||||
const maxCells = rand(10, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = P(0.5) ? queue.shift() : queue.pop();
|
||||
cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId] || cells.h[neibCellId] < 20) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Eruption", cells: cellsArray, color: "url(#hatch7)"});
|
||||
}
|
||||
|
||||
function addAvalanche(usedCells) {
|
||||
const {cells} = pack;
|
||||
|
||||
const routeCells = cells.i.filter(i => !usedCells[i] && Routes.isConnected(i) && cells.h[i] >= 70);
|
||||
if (!routeCells.length) return;
|
||||
|
||||
const startCell = ra(routeCells);
|
||||
usedCells[startCell] = 1;
|
||||
|
||||
const cellsArray = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(3, 15);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = P(0.3) ? queue.shift() : queue.pop();
|
||||
cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId] || cells.h[neibCellId] < 65) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Avalanche";
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Avalanche", cells: cellsArray, color: "url(#hatch5)"});
|
||||
}
|
||||
|
||||
function addFault(usedCells) {
|
||||
const cells = pack.cells;
|
||||
|
||||
const elevatedCells = cells.i.filter(i => !usedCells[i] && cells.h[i] > 50 && cells.h[i] < 70);
|
||||
if (!elevatedCells.length) return;
|
||||
|
||||
const startCell = ra(elevatedCells);
|
||||
usedCells[startCell] = 1;
|
||||
|
||||
const cellsArray = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(3, 15);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.pop();
|
||||
if (cells.h[cellId] >= 20) cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId] || cells.r[neibCellId]) return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Fault";
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Fault", cells: cellsArray, color: "url(#hatch2)"});
|
||||
}
|
||||
|
||||
function addFlood(usedCells) {
|
||||
const cells = pack.cells;
|
||||
|
||||
const fl = cells.fl.filter(Boolean);
|
||||
const meanFlux = d3.mean(fl);
|
||||
const maxFlux = d3.max(fl);
|
||||
const fluxThreshold = (maxFlux - meanFlux) / 2 + meanFlux;
|
||||
|
||||
const bigRiverCells = cells.i.filter(
|
||||
i => !usedCells[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > fluxThreshold && cells.burg[i]
|
||||
);
|
||||
if (!bigRiverCells.length) return;
|
||||
|
||||
const startCell = ra(bigRiverCells);
|
||||
usedCells[startCell] = 1;
|
||||
|
||||
const riverId = cells.r[startCell];
|
||||
const cellsArray = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(5, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.pop();
|
||||
cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (
|
||||
usedCells[neibCellId] ||
|
||||
cells.h[neibCellId] < 20 ||
|
||||
cells.r[neibCellId] !== riverId ||
|
||||
cells.h[neibCellId] > 50 ||
|
||||
cells.fl[neibCellId] < meanFlux
|
||||
)
|
||||
return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const name = getAdjective(pack.burgs[cells.burg[startCell]].name) + " Flood";
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Flood", cells: cellsArray, color: "url(#hatch13)"});
|
||||
}
|
||||
|
||||
function addTsunami(usedCells) {
|
||||
const {cells, features} = pack;
|
||||
|
||||
const coastalCells = cells.i.filter(
|
||||
i => !usedCells[i] && cells.t[i] === -1 && features[cells.f[i]].type !== "lake"
|
||||
);
|
||||
if (!coastalCells.length) return;
|
||||
|
||||
const startCell = ra(coastalCells);
|
||||
usedCells[startCell] = 1;
|
||||
|
||||
const cellsArray = [];
|
||||
const queue = [startCell];
|
||||
const maxCells = rand(10, 30);
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.shift();
|
||||
if (cells.t[cellId] === 1) cellsArray.push(cellId);
|
||||
if (cellsArray.length >= maxCells) break;
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
if (usedCells[neibCellId]) return;
|
||||
if (cells.t[neibCellId] > 2) return;
|
||||
if (pack.features[cells.f[neibCellId]].type === "lake") return;
|
||||
usedCells[neibCellId] = 1;
|
||||
queue.push(neibCellId);
|
||||
});
|
||||
}
|
||||
|
||||
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Tsunami";
|
||||
pack.zones.push({i: pack.zones.length, name, type: "Tsunami", cells: cellsArray, color: "url(#hatch13)"});
|
||||
}
|
||||
|
||||
return {generate};
|
||||
})();
|
||||
155
utils/pathUtils.js
Normal file
155
utils/pathUtils.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"use strict";
|
||||
|
||||
// get continuous paths for all cells at once based on getType(cellId) comparison
|
||||
function getVertexPaths({getType, options}) {
|
||||
const {cells, vertices} = pack;
|
||||
const paths = {};
|
||||
|
||||
const checkedCells = new Uint8Array(cells.c.length);
|
||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||
const isChecked = cellId => checkedCells[cellId] === 1;
|
||||
|
||||
for (let cellId = 0; cellId < cells.c.length; cellId++) {
|
||||
if (isChecked(cellId) || getType(cellId) === 0) continue;
|
||||
addToChecked(cellId);
|
||||
|
||||
const type = getType(cellId);
|
||||
const ofSameType = cellId => getType(cellId) === type;
|
||||
const ofDifferentType = cellId => getType(cellId) !== type;
|
||||
|
||||
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
||||
if (onborderCell === undefined) continue;
|
||||
|
||||
const feature = pack.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||
|
||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
addPath(type, vertexChain);
|
||||
}
|
||||
|
||||
return Object.entries(paths);
|
||||
|
||||
function getBorderPath(vertexChain, discontinue) {
|
||||
let discontinued = true;
|
||||
let lastOperation = "";
|
||||
const path = vertexChain.map(vertex => {
|
||||
if (discontinue(vertex)) {
|
||||
discontinued = true;
|
||||
return "";
|
||||
}
|
||||
|
||||
const operation = discontinued ? "M" : "L";
|
||||
const command = operation === lastOperation ? "" : operation;
|
||||
|
||||
discontinued = false;
|
||||
lastOperation = operation;
|
||||
|
||||
return ` ${command}${getVertexPoint(vertex)}`;
|
||||
});
|
||||
|
||||
return path.join("").trim();
|
||||
}
|
||||
|
||||
function isBorderVertex(vertex) {
|
||||
const adjacentCells = vertices.c[vertex];
|
||||
return adjacentCells.some(i => cells.b[i]);
|
||||
}
|
||||
|
||||
function isLandVertex(vertex) {
|
||||
const adjacentCells = vertices.c[vertex];
|
||||
return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT);
|
||||
}
|
||||
|
||||
function addPath(index, vertexChain) {
|
||||
if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""};
|
||||
if (options.fill) paths[index].fill += getFillPath(vertexChain);
|
||||
if (options.halo) paths[index].halo += getBorderPath(vertexChain, isBorderVertex);
|
||||
if (options.waterGap) paths[index].waterGap += getBorderPath(vertexChain, isLandVertex);
|
||||
}
|
||||
}
|
||||
|
||||
function getVertexPoint(vertexId) {
|
||||
return pack.vertices.p[vertexId];
|
||||
}
|
||||
|
||||
function getFillPath(vertexChain) {
|
||||
const points = vertexChain.map(getVertexPoint);
|
||||
const firstPoint = points.shift();
|
||||
return `M${firstPoint} L${points.join(" ")}`;
|
||||
}
|
||||
|
||||
// get single path for an non-continuous array of cells
|
||||
function getVertexPath(cellsArray) {
|
||||
const {cells, vertices} = pack;
|
||||
|
||||
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
|
||||
const ofSameType = cellId => cellsObj[cellId];
|
||||
const ofDifferentType = cellId => !cellsObj[cellId];
|
||||
|
||||
const checkedCells = new Uint8Array(cells.c.length);
|
||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||
const isChecked = cellId => checkedCells[cellId] === 1;
|
||||
|
||||
let path = "";
|
||||
|
||||
for (const cellId of cellsArray) {
|
||||
if (isChecked(cellId)) continue;
|
||||
|
||||
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
||||
if (onborderCell === undefined) continue;
|
||||
|
||||
const feature = pack.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake" && feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||
|
||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
path += getFillPath(vertexChain);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) {
|
||||
const vertices = pack.vertices;
|
||||
const MAX_ITERATIONS = pack.cells.i.length;
|
||||
const chain = []; // vertices chain to form a path
|
||||
|
||||
let next = startingVertex;
|
||||
for (let i = 0; i === 0 || next !== startingVertex; i++) {
|
||||
const previous = chain.at(-1);
|
||||
const current = next;
|
||||
chain.push(current);
|
||||
|
||||
const neibCells = vertices.c[current];
|
||||
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
|
||||
|
||||
const [c1, c2, c3] = neibCells.map(ofSameType);
|
||||
const [v1, v2, v3] = vertices.v[current];
|
||||
|
||||
if (v1 !== previous && c1 !== c2) next = v1;
|
||||
else if (v2 !== previous && c2 !== c3) next = v2;
|
||||
else if (v3 !== previous && c1 !== c3) next = v3;
|
||||
|
||||
if (next === current) {
|
||||
ERROR && console.error("ConnectVertices: next vertex is not found");
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === MAX_ITERATIONS) {
|
||||
ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (closeRing) chain.push(startingVertex);
|
||||
return chain;
|
||||
}
|
||||
|
|
@ -1,21 +1,26 @@
|
|||
"use strict";
|
||||
|
||||
// version and caching control
|
||||
const version = "1.99.15"; // generator version, update each time
|
||||
/**
|
||||
* Version Control Guidelines
|
||||
* --------------------------
|
||||
* We use Semantic Versioning: major.minor.patch. Refer to https://semver.org
|
||||
* Our .map file format is considered the public API.
|
||||
*
|
||||
* Update the version MANUALLY on each merge to main:
|
||||
* 1. MAJOR version: Incompatible changes that break existing maps
|
||||
* 2. MINOR version: Backwards-compatible changes requiring old .map files to be updated
|
||||
* 3. PATCH version: Backwards-compatible bug fixes not affecting .map file format
|
||||
*
|
||||
* Example: 1.102.0 -> Major version 1, Minor version 102, Patch version 0
|
||||
*/
|
||||
const VERSION = "1.100.00";
|
||||
|
||||
{
|
||||
document.title += " v" + version;
|
||||
document.title += " v" + VERSION;
|
||||
const loadingScreenVersion = document.getElementById("versionText");
|
||||
if (loadingScreenVersion) loadingScreenVersion.innerText = `v${version}`;
|
||||
if (loadingScreenVersion) loadingScreenVersion.innerText = `v${VERSION}`;
|
||||
|
||||
const versionNumber = parseFloat(version);
|
||||
const storedVersion = localStorage.getItem("version") ? parseFloat(localStorage.getItem("version")) : 0;
|
||||
|
||||
const isOutdated = storedVersion !== versionNumber;
|
||||
if (isOutdated) clearCache();
|
||||
|
||||
const showUpdate = storedVersion < versionNumber;
|
||||
if (showUpdate) setTimeout(showUpdateWindow, 6000);
|
||||
const storedVersion = localStorage.getItem("version");
|
||||
if (compareVersions(storedVersion, VERSION).isOlder) setTimeout(showUpdateWindow, 6000);
|
||||
|
||||
function showUpdateWindow() {
|
||||
const changelog = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog";
|
||||
|
|
@ -23,11 +28,12 @@ const version = "1.99.15"; // generator version, update each time
|
|||
const discord = "https://discordapp.com/invite/X7E84HU";
|
||||
const patreon = "https://www.patreon.com/azgaar";
|
||||
|
||||
alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version <strong>${version}</strong>. This version is compatible with <a href="${changelog}" target="_blank">previous versions</a>, loaded save files will be auto-updated.
|
||||
${storedVersion ? "<span>Reload the page to fetch fresh code.</span>" : ""}
|
||||
alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version <strong>${VERSION}</strong>. This version is compatible with <a href="${changelog}" target="_blank">previous versions</a>, loaded save files will be auto-updated.
|
||||
${storedVersion ? "<span>Click on OK and then reload the page to fetch fresh code.</span>" : ""}
|
||||
|
||||
<ul>
|
||||
<strong>Latest changes:</strong>
|
||||
<li>Zones update</li>
|
||||
<li>Notes Editor: on-demand AI text generation</li>
|
||||
<li>New style preset: Dark Seas</li>
|
||||
<li>New routes generation algorithm</li>
|
||||
|
|
@ -51,15 +57,19 @@ const version = "1.99.15"; // generator version, update each time
|
|||
const buttons = {
|
||||
Ok: function () {
|
||||
$(this).dialog("close");
|
||||
if (storedVersion) localStorage.clear();
|
||||
localStorage.setItem("version", version);
|
||||
if (storedVersion) {
|
||||
clearCache();
|
||||
localStorage.clear();
|
||||
}
|
||||
localStorage.setItem("version", VERSION);
|
||||
}
|
||||
};
|
||||
|
||||
if (storedVersion) {
|
||||
buttons.Reload = () => {
|
||||
clearCache();
|
||||
localStorage.clear();
|
||||
localStorage.setItem("version", version);
|
||||
localStorage.setItem("version", VERSION);
|
||||
location.reload();
|
||||
};
|
||||
}
|
||||
|
|
@ -75,6 +85,25 @@ const version = "1.99.15"; // generator version, update each time
|
|||
|
||||
async function clearCache() {
|
||||
const cacheNames = await caches.keys();
|
||||
Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)));
|
||||
return Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)));
|
||||
}
|
||||
}
|
||||
|
||||
function isValidVersion(versionString) {
|
||||
if (!versionString) return false;
|
||||
const [major, minor, patch] = versionString.split(".");
|
||||
return !isNaN(major) && !isNaN(minor) && !isNaN(patch);
|
||||
}
|
||||
|
||||
function compareVersions(version1, version2) {
|
||||
if (!isValidVersion(version1) || !isValidVersion(version2)) return {isEqual: false, isNewer: false, isOlder: false};
|
||||
|
||||
const [major1, minor1, patch1] = version1.split(".").map(Number);
|
||||
const [major2, minor2, patch2] = version2.split(".").map(Number);
|
||||
|
||||
const isEqual = major1 === major2 && minor1 === minor2 && patch1 === patch2;
|
||||
const isNewer = major1 > major2 || (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 > patch2)));
|
||||
const isOlder = major1 < major2 || (major1 === major2 && (minor1 < minor2 || (minor1 === minor2 && patch1 < patch2)));
|
||||
|
||||
return {isEqual, isNewer, isOlder};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue