diff --git a/index.html b/index.html
index adb0c7e8..80fb7da6 100644
--- a/index.html
+++ b/index.html
@@ -24,12 +24,8 @@
font-size: 10px;
overflow: hidden;
}
- #loading > * {
- pointer-events: none;
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
+ #map {
+ position: absolute;
}
#loading > * {
pointer-events: none;
@@ -121,7 +117,6 @@
id="map"
width="100%"
height="100%"
- style="position: absolute"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -340,11 +335,20 @@
@@ -8092,6 +8117,8 @@
+
+
diff --git a/main.js b/main.js
index 6881edeb..fded92e3 100644
--- a/main.js
+++ b/main.js
@@ -517,13 +517,13 @@ function handleZoom(isScaleChanged, isPositionChanged) {
// zoom image converter overlay
if (customization === 1) {
- const canvas = document.getElementById('canvas');
- if (!canvas || canvas.style.opacity === '0') return;
+ const canvas = document.getElementById("canvas");
+ if (!canvas || canvas.style.opacity === "0") return;
- const img = document.getElementById('imageToConvert');
+ const img = document.getElementById("imageToConvert");
if (!img) return;
- const ctx = canvas.getContext('2d');
+ const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(scale, 0, 0, scale, viewX, viewY);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
@@ -565,11 +565,11 @@ function invokeActiveZooming() {
if (this.id === "burgLabels") return;
const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
- if (rescaleLabels.checked) this.setAttribute('font-size', relative);
+ if (rescaleLabels.checked) this.setAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 60);
- if (hidden) this.classList.add('hidden');
- else this.classList.remove('hidden');
+ if (hidden) this.classList.add("hidden");
+ else this.classList.remove("hidden");
});
}
@@ -595,7 +595,7 @@ function invokeActiveZooming() {
if (!customization && !isOptimized) {
const desired = +statesHalo.attr("data-width");
const haloSize = rn(desired / scale ** 0.8, 2);
- statesHalo.attr('stroke-width', haloSize).style('display', haloSize > 0.1 ? 'block' : 'none');
+ statesHalo.attr("stroke-width", haloSize).style("display", haloSize > 0.1 ? "block" : "none");
}
// rescale map markers
@@ -721,13 +721,17 @@ async function generate(options) {
rankCells();
Cultures.generate();
Cultures.expand();
-
BurgsAndStates.generate();
-
Religions.generate();
BurgsAndStates.defineStateForms();
+ BurgsAndStates.generateProvinces();
+ BurgsAndStates.defineBurgFeatures();
BurgsAndStates.defineTaxes();
+ drawStates();
+ drawBorders();
+ BurgsAndStates.drawStateLabels();
+
Production.collectResources();
Production.defineExport();
@@ -740,13 +744,6 @@ async function generate(options) {
// pack.cells.road = new Uint16Array(pack.cells.i.length);
// pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
- BurgsAndStates.generateProvinces();
- BurgsAndStates.defineBurgFeatures();
-
- drawStates();
- drawBorders();
- BurgsAndStates.drawStateLabels();
-
Rivers.specify();
Lakes.generateName();
@@ -1435,9 +1432,9 @@ function reMarkFeatures() {
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
- const defineHaven = (i) => {
- const water = cells.c[i].filter((c) => cells.h[c] < 20);
- const dist2 = water.map((c) => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
+ const defineHaven = i => {
+ const water = cells.c[i].filter(c => cells.h[c] < 20);
+ const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
cells.haven[i] = closest;
@@ -1506,12 +1503,6 @@ function isWetLand(moisture, temperature, height) {
return false;
}
-function isWetLand(moisture, temperature, height) {
- if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
- if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
- return false;
-}
-
// assign biome id for each cell
function defineBiomes() {
TIME && console.time("defineBiomes");
@@ -1551,12 +1542,12 @@ function getBiomeId(moisture, temperature, height) {
return biomesData.biomesMartix[moistureBand][temperatureBand];
}
-// assess cells suitability to calculate population and rang cells for culture center and burgs placement
+// assess cells suitability to calculate population and rand cells for culture center and burgs placement
function rankCells() {
TIME && console.time("rankCells");
const {cells, features} = pack;
- cells.s = new Int16Array(cells.i.length); // cell suitability score
- cells.pop = new Float32Array(cells.i.length); // cell population
+ cells.s = new Int16Array(cells.i.length); // cell suitability array
+ cells.pop = new Float32Array(cells.i.length); // cell population array
const flMean = d3.median(cells.fl.filter(f => f)) || 0,
flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
@@ -1567,11 +1558,10 @@ function rankCells() {
for (const i of cells.i) {
if (cells.b[i]) continue; // avoid adding burgs on map border
if (cells.h[i] < 20) continue; // no population in water
- let s = biomesData.habitability[cells.biome[i]] / 10; // base suitability derived from biome habitability
+ let s = +biomesData.habitability[cells.biome[i]]; // base suitability derived from biome habitability
if (!s) continue; // uninhabitable biomes has 0 suitability
-
- if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 50; // big rivers and confluences are valued
- s -= (cells.h[i] - 50) / 25; // low elevation is valued, high is not;
+ if (flMean) s += normalize(cells.fl[i] + cells.conf[i], flMean, flMax) * 250; // big rivers and confluences are valued
+ s -= (cells.h[i] - 50) / 5; // low elevation is valued, high is not;
if (cells.t[i] === 1) {
if (cells.r[i]) s += 15; // estuary is valued
@@ -1584,8 +1574,8 @@ function rankCells() {
else if (feature.group == "sinkhole") s -= 5;
else if (feature.group == "lava") s -= 30;
} else {
- s += 1; // ocean coast is valued
- if (cells.harbor[i] === 1) s += 4; // safe sea harbor is valued
+ s += 5; // ocean coast is valued
+ if (cells.harbor[i] === 1) s += 20; // safe sea harbor is valued
}
}
@@ -1595,8 +1585,7 @@ function rankCells() {
const resBonus = (cellRes ? cellRes + 10 : 0) + neibRes;
// cell rural population is suitability adjusted by cell area
- cells.pop[i] = s > 0 ? (s * POP_BALANCER * cells.area[i]) / areaMean : 0;
- cells.s[i] = s + resBonus;
+ cells.pop[i] = cells.s[i] > 0 ? (cells.s[i] * cells.area[i]) / areaMean : 0;
}
TIME && console.timeEnd("rankCells");
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index e7cc57d5..bfa84e44 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -5,22 +5,28 @@ window.BurgsAndStates = (function () {
const {cells, cultures} = pack;
const n = cells.i.length;
- cells.burg = new Uint16Array(n);
- pack.burgs = placeCapitals();
- const {burgs} = pack;
+ cells.burg = new Uint16Array(n); // cell burg
+ cells.road = new Uint16Array(n); // cell road power
+ cells.crossroad = new Uint16Array(n); // cell crossroad power
+ const burgs = (pack.burgs = placeCapitals());
pack.states = createStates();
+ const capitalRoutes = Routes.getRoads();
placeTowns();
expandStates();
normalizeStates();
+ const townRoutes = Routes.getTrails();
specifyBurgs();
+ const oceanRoutes = Routes.getSearoutes();
+
collectStatistics();
assignColors();
generateCampaigns();
generateDiplomacy();
+ Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs();
function placeCapitals() {
@@ -55,7 +61,7 @@ window.BurgsAndStates = (function () {
}
if (i === sorted.length - 1) {
- WARN && console.warn('Cannot place capitals with current spacing. Trying again with reduced spacing');
+ WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
burgsTree = d3.quadtree();
i = -1;
burgs = [0];
@@ -68,13 +74,12 @@ window.BurgsAndStates = (function () {
return burgs;
}
- // for each capital create a state
+ // For each capital create a state
function createStates() {
TIME && console.time("createStates");
const states = [{i: 0, name: "Neutrals"}];
const colors = getColors(burgs.length - 1);
const each5th = each(5);
- const maxExpansionism = +powerInput.value;
burgs.forEach(function (b, i) {
if (!i) return; // skip first element
@@ -87,7 +92,7 @@ window.BurgsAndStates = (function () {
b.capital = 1;
// states data
- const expansionism = rn(Math.random() * maxExpansionism + 1, 1);
+ const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
@@ -133,10 +138,9 @@ window.BurgsAndStates = (function () {
while (burgsAdded < burgsNumber && spacing > 1) {
for (let i = 0; burgsAdded < burgsNumber && i < sorted.length; i++) {
if (cells.burg[sorted[i]]) continue;
-
- const cell = sorted[i];
- const [x, y] = cells.p[cell];
-
+ const cell = sorted[i],
+ x = cells.p[cell][0],
+ y = cells.p[cell][1];
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length;
@@ -180,10 +184,8 @@ window.BurgsAndStates = (function () {
b.port = port ? f : 0; // port is defined by water body id it lays on
} else b.port = 0;
- // define burg population
- const primaryPopulation = cells.s[i];
- const secondaryPopulation = b.i / 1000 + (i % 100) / 1000;
- b.population = rn(Math.max(primaryPopulation + secondaryPopulation, 0.1), 3);
+ // define burg population (keep urbanization at about 10% rate)
+ b.population = rn(Math.max((cells.s[i] + cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) {
@@ -219,10 +221,10 @@ window.BurgsAndStates = (function () {
}
// de-assign port status if it's the only one on feature
- const ports = pack.burgs.filter((b) => !b.removed && b.port > 0);
+ const ports = pack.burgs.filter(b => !b.removed && b.port > 0);
for (const f of features) {
if (!f.i || f.land || f.border) continue;
- const featurePorts = ports.filter((b) => b.port === f.i);
+ const featurePorts = ports.filter(b => b.port === f.i);
if (featurePorts.length === 1) featurePorts[0].port = 0;
}
@@ -231,14 +233,14 @@ window.BurgsAndStates = (function () {
const getType = function (i, port) {
const cells = pack.cells;
- if (port) return 'Naval';
- if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === 'lake') return 'Lake';
- if (cells.h[i] > 60) return 'Highland';
- if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return 'River';
+ if (port) return "Naval";
+ if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === "lake") return "Lake";
+ if (cells.h[i] > 60) return "Highland";
+ if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return "River";
if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) {
- if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return 'Nomadic';
- if (cells.biome[i] > 4 && cells.biome[i] < 10) return 'Hunting';
+ if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
+ if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
}
return "Generic";
@@ -264,9 +266,9 @@ window.BurgsAndStates = (function () {
TIME && console.time("drawBurgs");
// remove old data
- burgIcons.selectAll('circle').remove();
- burgLabels.selectAll('text').remove();
- icons.selectAll('use').remove();
+ burgIcons.selectAll("circle").remove();
+ burgLabels.selectAll("text").remove();
+ icons.selectAll("use").remove();
// capitals
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
@@ -383,7 +385,7 @@ window.BurgsAndStates = (function () {
const {e, p, s, b} = next;
const {type, culture} = states[s];
- cells.c[e].forEach((e) => {
+ cells.c[e].forEach(e => {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
const cultureCost = culture === cells.culture[e] ? -9 : 100;
@@ -409,33 +411,33 @@ window.BurgsAndStates = (function () {
function getBiomeCost(b, biome, type) {
if (b === biome) return 10; // tiny penalty for native biome
- if (type === 'Hunting') return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
- if (type === 'Nomadic' && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
+ if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
+ if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
return biomesData.cost[biome]; // general non-native biome penalty
}
function getHeightCost(f, h, type) {
- if (type === 'Lake' && f.type === 'lake') return 10; // low lake crossing penalty for Lake cultures
- if (type === 'Naval' && h < 20) return 300; // low sea crossing penalty for Navals
- if (type === 'Nomadic' && h < 20) return 10000; // giant sea crossing penalty for Nomads
+ if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
+ if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
+ if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
if (h < 20) return 1000; // general sea crossing penalty
- if (type === 'Highland' && h < 62) return 1100; // penalty for highlanders on lowlands
- if (type === 'Highland') return 0; // no penalty for highlanders on highlands
+ if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
+ if (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 67) return 2200; // general mountains crossing penalty
if (h >= 44) return 300; // general hills crossing penalty
return 0;
}
function getRiverCost(r, i, type) {
- if (type === 'River') return r ? 0 : 100; // penalty for river cultures
+ if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
- if (t === 1) return type === 'Naval' || type === 'Lake' ? 0 : type === 'Nomadic' ? 60 : 20; // penalty for coastline
- if (t === 2) return type === 'Naval' || type === 'Nomadic' ? 30 : 0; // low penalty for land level 2 for Navals and nomads
- if (t !== -1) return type === 'Naval' || type === 'Lake' ? 100 : 0; // penalty for mainland for navals
+ if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
+ if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
+ if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
return 0;
}
@@ -449,11 +451,11 @@ window.BurgsAndStates = (function () {
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
- if (cells.c[i].some((c) => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
- const neibs = cells.c[i].filter((c) => cells.h[c] >= 20);
- const adversaries = neibs.filter((c) => cells.state[c] !== cells.state[i]);
+ if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
+ const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
+ const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
if (adversaries.length < 2) continue;
- const buddies = neibs.filter((c) => cells.state[c] === cells.state[i]);
+ const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
if (buddies.length > 2) continue;
if (adversaries.length <= buddies.length) continue;
cells.state[i] = cells.state[adversaries[0]];
@@ -501,7 +503,7 @@ window.BurgsAndStates = (function () {
const visualCenter = findCell(s.pole[0], s.pole[1]);
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
const hull = getHull(start, s.i, s.cells / 10);
- const points = [...hull].map((v) => pack.vertices.p[v]);
+ const points = [...hull].map(v => pack.vertices.p[v]);
const delaunay = Delaunator.from(points);
const voronoi = new Voronoi(delaunay, points, points.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
@@ -539,7 +541,7 @@ window.BurgsAndStates = (function () {
return used[findCell(p[0], p[1])];
});
- const pointsInside = d3.range(c.p.length).filter((i) => inside[i]);
+ const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
if (!pointsInside.length) return [0];
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? 0.5 : 1; // power of horyzontality shift
const end =
@@ -598,14 +600,14 @@ window.BurgsAndStates = (function () {
if (!list) {
// remove all labels and textpaths
- g.selectAll('text').remove();
+ g.selectAll("text").remove();
t.selectAll("path[id*='stateLabel']").remove();
}
- const example = g.append('text').attr('x', 0).attr('x', 0).text('Average');
+ const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
- paths.forEach((p) => {
+ paths.forEach(p => {
const id = p[0];
const state = states[p[0]];
const {name, fullName} = state;
@@ -736,7 +738,7 @@ window.BurgsAndStates = (function () {
}
// convert neighbors Set object into array
- states.forEach((s) => {
+ states.forEach(s => {
if (!s.neighbors) return;
s.neighbors = Array.from(s.neighbors);
});
@@ -752,14 +754,14 @@ window.BurgsAndStates = (function () {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const neibs = s.neighbors;
- s.color = colors.find((c) => neibs.every((n) => pack.states[n].color !== c));
+ s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
if (!s.color) s.color = getRandomColor();
colors.push(colors.shift());
});
// randomize each already used color a bit
- colors.forEach((c) => {
- const sameColored = pack.states.filter((s) => s.color === c);
+ colors.forEach(c => {
+ const sameColored = pack.states.filter(s => s.color === c);
sameColored.forEach((s, d) => {
if (!d) return;
s.color = getMixedColor(s.color);
@@ -821,9 +823,9 @@ window.BurgsAndStates = (function () {
for (let f = 1; f < states.length; f++) {
if (states[f].removed) continue;
- if (states[f].diplomacy.includes('Vassal')) {
+ if (states[f].diplomacy.includes("Vassal")) {
// Vassals copy relations from their Suzerains
- const suzerain = states[f].diplomacy.indexOf('Vassal');
+ const suzerain = states[f].diplomacy.indexOf("Vassal");
for (let i = 1; i < states.length; i++) {
if (i === f || i === suzerain) continue;
@@ -831,7 +833,7 @@ window.BurgsAndStates = (function () {
if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally";
for (let e = 1; e < states.length; e++) {
if (e === f || e === suzerain) continue;
- if (states[e].diplomacy[suzerain] === 'Suzerain' || states[e].diplomacy[suzerain] === 'Vassal') continue;
+ if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue;
states[e].diplomacy[f] = states[e].diplomacy[suzerain];
}
}
@@ -841,8 +843,8 @@ window.BurgsAndStates = (function () {
for (let t = f + 1; t < states.length; t++) {
if (states[t].removed) continue;
- if (states[t].diplomacy.includes('Vassal')) {
- const suzerain = states[t].diplomacy.indexOf('Vassal');
+ if (states[t].diplomacy.includes("Vassal")) {
+ const suzerain = states[t].diplomacy.indexOf("Vassal");
states[f].diplomacy[t] = states[f].diplomacy[suzerain];
continue;
}
@@ -880,9 +882,9 @@ window.BurgsAndStates = (function () {
for (let attacker = 1; attacker < states.length; attacker++) {
const ad = states[attacker].diplomacy; // attacker relations;
if (states[attacker].removed) continue;
- if (!ad.includes('Rival')) continue; // no rivals to attack
- if (ad.includes('Vassal')) continue; // not independent
- if (ad.includes('Enemy')) continue; // already at war
+ if (!ad.includes("Rival")) continue; // no rivals to attack
+ if (ad.includes("Vassal")) continue; // not independent
+ if (ad.includes("Enemy")) continue; // already at war
// random independent rival
const defender = ra(
@@ -920,8 +922,8 @@ window.BurgsAndStates = (function () {
}
});
- ap = d3.sum(attackers.map((a) => states[a].area * states[a].expansionism)); // attackers joined power
- dp = d3.sum(defenders.map((d) => states[d].area * states[d].expansionism)); // defender joined power
+ ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power
+ dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
// defender allies join
dd.forEach((r, d) => {
@@ -929,7 +931,7 @@ window.BurgsAndStates = (function () {
if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > 2 * gauss(1.6, 0.8, 0, 10, 2)) {
const reason = states[d].diplomacy.includes("Enemy") ? "Being already at war," : `Frightened by ${an},`;
war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
- dd[d] = states[d].diplomacy[defender] = 'Suspicion';
+ dd[d] = states[d].diplomacy[defender] = "Suspicion";
return;
}
defenders.push(d);
@@ -949,7 +951,7 @@ window.BurgsAndStates = (function () {
// attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
ad.forEach((r, d) => {
- if (r !== 'Ally' || states[d].diplomacy.includes('Vassal') || defenders.includes(d)) return;
+ if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
const name = states[d].name;
if (states[d].diplomacy[defender] !== "Rival" && (P(0.2) || ap <= dp * 1.2)) {
war.push(`${an}'s ally ${name} avoided entering the war`);
@@ -981,7 +983,7 @@ window.BurgsAndStates = (function () {
chronicle.push(war); // add a record to diplomatical history
}
- TIME && console.timeEnd('generateDiplomacy');
+ TIME && console.timeEnd("generateDiplomacy");
//console.table(states.map(s => s.diplomacy));
};
@@ -1062,33 +1064,33 @@ window.BurgsAndStates = (function () {
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
}
- if (base === 16 && (form === 'Empire' || form === 'Kingdom')) return 'Sultanate'; // Turkic
- if (base === 5 && (form === 'Empire' || form === 'Kingdom')) return 'Tsardom'; // Ruthenian
- if ([16, 31].includes(base) && (form === 'Empire' || form === 'Kingdom')) return 'Khaganate'; // Turkic, Mongolian
- if (base === 12 && (form === 'Kingdom' || form === 'Grand Duchy')) return 'Shogunate'; // Japanese
- if ([18, 17].includes(base) && form === 'Empire') return 'Caliphate'; // Arabic, Berber
- if (base === 18 && (form === 'Grand Duchy' || form === 'Duchy')) return 'Emirate'; // Arabic
- if (base === 7 && (form === 'Grand Duchy' || form === 'Duchy')) return 'Despotate'; // Greek
- if (base === 31 && (form === 'Grand Duchy' || form === 'Duchy')) return 'Ulus'; // Mongolian
- if (base === 16 && (form === 'Grand Duchy' || form === 'Duchy')) return 'Horde'; // Turkic
- if (base === 24 && (form === 'Grand Duchy' || form === 'Duchy')) return 'Satrapy'; // Iranian
+ if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
+ if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
+ if ([16, 31].includes(base) && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic, Mongolian
+ if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese
+ if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
+ if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic
+ if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek
+ if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian
+ if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Horde"; // Turkic
+ if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian
return form;
}
- if (s.form === 'Republic') {
+ if (s.form === "Republic") {
// Default name is from weighted array, special case for small states with only 1 burg
if (tier < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name;
- return 'Free City';
+ return "Free City";
}
if (P(0.3)) return "City-state";
}
return rw(republic);
}
- if (s.form === 'Union') return rw(union);
- if (s.form === 'Anarchy') return rw(anarchy);
+ if (s.form === "Union") return rw(union);
+ if (s.form === "Anarchy") return rw(anarchy);
if (s.form === "Theocracy") {
// European
@@ -1211,7 +1213,7 @@ window.BurgsAndStates = (function () {
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form);
form[formName] += 10;
- const fullName = name + ' ' + formName;
+ const fullName = name + " " + formName;
const color = getMixedColor(s.color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port);
@@ -1256,10 +1258,10 @@ window.BurgsAndStates = (function () {
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
- const neibs = cells.c[i].filter((c) => cells.state[c] === cells.state[i]).map((c) => cells.province[c]);
- const adversaries = neibs.filter((c) => c !== cells.province[i]);
+ const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
+ const adversaries = neibs.filter(c => c !== cells.province[i]);
if (adversaries.length < 2) continue;
- const buddies = neibs.filter((c) => c === cells.province[i]).length;
+ const buddies = neibs.filter(c => c === cells.province[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors);
@@ -1268,8 +1270,8 @@ window.BurgsAndStates = (function () {
}
// add "wild" provinces if some cells don't have a province assigned
- const noProvince = Array.from(cells.i).filter((i) => cells.state[i] && !cells.province[i]); // cells without province assigned
- states.forEach((s) => {
+ const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
+ states.forEach(s => {
if (!s.provinces.length) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
@@ -1286,7 +1288,7 @@ window.BurgsAndStates = (function () {
while (stateNoProvince.length) {
// add new province
const province = provinces.length;
- const burgCell = stateNoProvince.find((i) => cells.burg[i]);
+ const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
cells.province[center] = province;
@@ -1360,7 +1362,7 @@ window.BurgsAndStates = (function () {
while (queue.length) {
const current = queue.pop();
if (current === to) return true; // way is found
- cells.c[current].forEach((c) => {
+ cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
@@ -1370,7 +1372,7 @@ window.BurgsAndStates = (function () {
}
// re-check
- stateNoProvince = noProvince.filter((i) => cells.state[i] === s.i && !cells.province[i]);
+ stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
}
});
diff --git a/modules/dynamic/export-json.js b/modules/dynamic/export-json.js
index 593187f3..d9e6d42e 100644
--- a/modules/dynamic/export-json.js
+++ b/modules/dynamic/export-json.js
@@ -149,6 +149,7 @@ function getPackCellsData() {
religion: Array.from(pack.cells.religion),
province: Array.from(pack.cells.province)
};
+
const cellObjArr = [];
{
cellConverted.i.forEach(value => {
@@ -222,14 +223,3 @@ function getPackVerticesData() {
}
return verticesArray;
}
-
-function getGridCellsDataJson() {
- TIME && console.time("getGridCellsDataJson");
-
- const info = getMapInfo();
- const gridCells = getGridCellsData()
- const exportData = {info,gridCells};
-
- TIME && console.log("getGridCellsDataJson");
- return JSON.stringify(exportData);
-}
\ No newline at end of file
diff --git a/modules/markers-generator.js b/modules/markers-generator.js
index 5999f965..ac66cc63 100644
--- a/modules/markers-generator.js
+++ b/modules/markers-generator.js
@@ -66,55 +66,72 @@ window.Markers = (function () {
generateTypes();
};
- const regenerate = requestedMultiplier => {
- if (requestedMultiplier === 0) return;
- if (requestedMultiplier) multiplier = requestedMultiplier;
+ const regenerate = () => {
+ pack.markers = pack.markers.filter(({i, lock, cell}) => {
+ if (lock) {
+ occupied[cell] = true;
+ return true;
+ }
+ const id = `marker${i}`;
+ document.getElementById(id)?.remove();
+ const index = notes.findIndex(note => note.id === id);
+ if (index != -1) notes.splice(index, 1);
+ return false;
+ });
+
generateTypes();
};
- const generateTypes = () => {
- TIME && console.time("addMarkers");
+ const add = marker => {
+ const base = config.find(c => c.type === marker.type);
+ if (base) {
+ const {icon, type, dx, dy, px} = base;
+ marker = addMarker({icon, type, dx, dy, px}, marker);
+ base.add("marker" + marker.i, marker.cell);
+ return marker;
+ }
- const culturesSet = document.getElementById("culturesSet").value;
- // TODO: don't put multiple markers to the same cell
-
- addVolcanoes();
- addHotSprings();
- addMines();
- addBridges();
- addInns();
- addLighthouses();
- addWaterfalls();
- addBattlefields();
- addDungeons();
- addLakeMonsters();
- addSeaMonsters();
- addHillMonsters();
- addSacredMountains();
- addSacredForests();
- addSacredPineries();
- addSacredPalmGroves();
- addBrigands();
- addPirates();
- addStatues();
- addRuines();
- if (culturesSet.includes("Fantasy")) addPortals();
-
- TIME && console.timeEnd("addMarkers");
+ const i = last(pack.markers)?.i + 1 || 0;
+ pack.markers.push({...marker, i});
+ occupied[marker.cell] = true;
+ return {...marker, i};
};
- const getQuantity = (array, min, each) => {
+ function generateTypes() {
+ TIME && console.time("addMarkers");
+
+ config.forEach(({type, icon, dx, dy, px, min, each, multiplier, list, add}) => {
+ if (multiplier === 0) return;
+
+ let candidates = Array.from(list(pack));
+ let quantity = getQuantity(candidates, min, each, multiplier);
+ // uncomment for debugging:
+ // console.log(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`);
+
+ while (quantity && candidates.length) {
+ const [cell] = extractAnyElement(candidates);
+ const marker = addMarker({icon, type, dx, dy, px}, {cell});
+ add("marker" + marker.i, cell);
+ quantity--;
+ }
+ });
+
+ occupied = [];
+ TIME && console.timeEnd("addMarkers");
+ }
+
+ function getQuantity(array, min, each, multiplier) {
if (!array.length || array.length < min / multiplier) return 0;
const requestQty = Math.ceil((array.length / each) * multiplier);
return array.length < requestQty ? array.length : requestQty;
- };
+ }
- const extractAnyElement = array => {
+ function extractAnyElement(array) {
const index = Math.floor(Math.random() * array.length);
return array.splice(index, 1);
- };
+ }
- const getMarkerCoordinates = cell => {
+ function getMarkerCoordinates(cell) {
const {cells, burgs} = pack;
const burgId = cells.burg[cell];
@@ -124,69 +141,70 @@ window.Markers = (function () {
}
return cells.p[cell];
- };
-
- function addVolcanoes() {
- const {cells} = pack;
-
- let mountains = Array.from(cells.i.filter(i => cells.h[i] >= 70).sort((a, b) => cells.h[b] - cells.h[a]));
- let quantity = getQuantity(mountains, 10, 300);
- if (!quantity) return;
- const highestMountains = mountains.slice(0, 20);
-
- while (quantity) {
- const [cell] = extractAnyElement(highestMountains);
- const id = addMarker({cell, icon: "🌋", type: "volcano", dx: 52, px: 13});
- const proper = Names.getCulture(cells.culture[cell]);
- const name = P(0.3) ? "Mount " + proper : Math.random() > 0.3 ? proper + " Volcano" : proper;
- notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight(cells.p[cell])}`});
- quantity--;
- }
}
- function addHotSprings() {
- const {cells} = pack;
-
- let springs = Array.from(cells.i.filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b] - cells.h[a]));
- let quantity = getQuantity(springs, 30, 800);
- if (!quantity) return;
- const highestSprings = springs.slice(0, 40);
-
- while (quantity) {
- const [cell] = extractAnyElement(highestSprings);
- const id = addMarker({cell, icon: "♨️", type: "hot_springs", dy: 52});
- const proper = Names.getCulture(cells.culture[cell]);
- const temp = convertTemperature(gauss(35, 15, 20, 100));
- notes.push({id, name: proper + " Hot Springs", legend: `A hot springs area. Average temperature: ${temp}`});
- quantity--;
- }
+ function addMarker(base, marker) {
+ const i = last(pack.markers)?.i + 1 || 0;
+ const [x, y] = getMarkerCoordinates(marker.cell);
+ marker = {...base, x, y, ...marker, i};
+ pack.markers.push(marker);
+ occupied[marker.cell] = true;
+ return marker;
}
- function addMines() {
+ function deleteMarker(markerId) {
+ const noteId = "marker" + markerId;
+ notes = notes.filter(note => note.id !== noteId);
+ pack.markers = pack.markers.filter(m => m.i !== markerId);
+ }
+
+ function listVolcanoes({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70);
+ }
+
+ function addVolcano(id, cell) {
const {cells} = pack;
- let hillyBurgs = Array.from(cells.i.filter(i => cells.h[i] > 47 && cells.burg[i]));
- let quantity = getQuantity(hillyBurgs, 1, 15);
- if (!quantity) return;
+ const proper = Names.getCulture(cells.culture[cell]);
+ const name = P(0.3) ? "Mount " + proper : P(0.7) ? proper + " Volcano" : proper;
+ const status = P(0.6) ? "Dormant" : P(0.4) ? "Active" : "Erupting";
+ notes.push({id, name, legend: `${status} volcano. Height: ${getFriendlyHeight(cells.p[cell])}.`});
+ }
+
+ function listHotSprings({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 50);
+ }
+
+ function addHotSpring(id, cell) {
+ const {cells} = pack;
+
+ const proper = Names.getCulture(cells.culture[cell]);
+ const temp = convertTemperature(gauss(35, 15, 20, 100));
+ const status = P(0.6) ? "geothermal" : P(0.4) ? "springwater" : "natural";
+ notes.push({
+ id,
+ name: proper + " Hot Springs",
+ legend: `A ${status} hot springs area. Average temperature: ${temp}.`
+ });
+ }
+
+ function listMines({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] > 47 && cells.burg[i]);
+ }
+
+ function addMine(id, cell) {
+ const {cells} = pack;
const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
-
- while (quantity && hillyBurgs.length) {
- const [cell] = extractAnyElement(hillyBurgs);
- const id = addMarker({cell, icon: "⛏️", type: "mine", dx: 48, px: 13});
- const resource = rw(resources);
- const burg = pack.burgs[cells.burg[cell]];
- const name = `${burg.name} — ${resource} mining town`;
- const population = rn(burg.population * populationRate * urbanization);
- const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const resource = rw(resources);
+ const burg = pack.burgs[cells.burg[cell]];
+ const name = `${burg.name} — ${resource} mining town`;
+ const population = rn(burg.population * populationRate * urbanization);
+ const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine.`;
+ notes.push({id, name, legend});
}
- function addBridges() {
- const {cells, burgs} = pack;
-
+ function listBridges({cells, burgs}) {
const meanFlux = d3.mean(cells.fl.filter(fl => fl));
return cells.i.filter(
i =>
@@ -197,28 +215,43 @@ window.Markers = (function () {
cells.r[i] &&
cells.fl[i] > meanFlux
);
- let quantity = getQuantity(bridges, 1, 5);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(bridges);
- const id = addMarker({cell, icon: "🌉", type: "bridge", px: 14});
- const burg = pack.burgs[cells.burg[cell]];
- const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
- const riverName = river ? `${river.name} ${river.type}` : "river";
- const name = river && P(0.2) ? river.name : burg.name;
- notes.push({id, name: `${name} Bridge`, legend: `A stone bridge over the ${riverName} near ${burg.name}`});
- quantity--;
- }
}
- function addInns() {
+ function addBridge(id, cell) {
const {cells} = pack;
- let taverns = Array.from(cells.i.filter(i => cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10));
- let quantity = getQuantity(taverns, 1, 100);
- if (!quantity) return;
+ const burg = pack.burgs[cells.burg[cell]];
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const name = river && P(0.2) ? `${river.name} Bridge` : `${burg.name} Bridge`;
+ const weightedAdjectives = {
+ stone: 10,
+ wooden: 1,
+ lengthy: 2,
+ formidable: 2,
+ rickety: 1,
+ beaten: 1,
+ weathered: 1
+ };
+ const barriers = [
+ "its collapse during the flood",
+ "being rumoured to attract trolls",
+ "the drying up of local trade",
+ "banditry infested the area",
+ "the old waypoints crumbled"
+ ];
+ const legend = P(0.7)
+ ? `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}.`
+ : `An old crossing of the ${riverName}, rarely used since ${ra(barriers)}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listInns({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10);
+ }
+
+ function addInn(id, cell) {
const colors = [
"Dark",
"Light",
@@ -461,137 +494,187 @@ window.Markers = (function () {
"sap"
];
- while (quantity) {
- const [cell] = extractAnyElement(taverns);
- const id = addMarker({cell, icon: "🍻", type: "inn", px: 14});
- const type = P(0.3) ? "inn" : "tavern";
- const isAnimalThemed = P(0.7);
- const animal = ra(animals);
- const name = isAnimalThemed ? (P(0.6) ? ra(colors) + " " + animal : ra(adjectives) + " " + animal) : ra(adjectives) + " " + capitalize(type);
- const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
- const course = `${ra(methods)} ${meal}`.toLowerCase();
- const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
- const legend = `A big and famous roadside ${type}. Delicious ${course} with ${drink} is served here`;
- notes.push({id, name: "The " + name, legend});
- quantity--;
- }
+ const typeName = P(0.3) ? "inn" : "tavern";
+ const isAnimalThemed = P(0.7);
+ const animal = ra(animals);
+ const name = isAnimalThemed
+ ? P(0.6)
+ ? ra(colors) + " " + animal
+ : ra(adjectives) + " " + animal
+ : ra(adjectives) + " " + capitalize(typeName);
+ const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
+ const course = `${ra(methods)} ${meal}`.toLowerCase();
+ const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
+ const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here.`;
+ notes.push({id, name: "The " + name, legend});
}
- function addLighthouses() {
+ function listLighthouses({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])
+ );
+ }
+
+ function addLighthouse(id, cell) {
const {cells} = pack;
- const lighthouses = Array.from(cells.i.filter(i => cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])));
- let quantity = getQuantity(lighthouses, 1, 2);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(lighthouses);
- const id = addMarker({cell, icon: "🚨", type: "lighthouse", px: 14});
- const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
- notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to keep the navigation safe`});
- quantity--;
- }
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({
+ id,
+ name: getAdjective(proper) + " Lighthouse" + name,
+ legend: `A lighthouse to serve as a beacon for ships in the open sea.`
+ });
}
- function addWaterfalls() {
+ function listWaterfalls({cells}) {
+ return cells.i.filter(
+ i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])
+ );
+ }
+
+ function addWaterfall(id, cell) {
const {cells} = pack;
- const waterfalls = Array.from(cells.i.filter(i => cells.r[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c])));
- const quantity = getQuantity(waterfalls, 1, 5);
- if (!quantity) return;
+ const descriptions = [
+ "A gorgeous waterfall flows here.",
+ "The rapids of an exceptionally beautiful waterfall.",
+ "An impressive waterfall has cut through the land.",
+ "The cascades of a stunning waterfall.",
+ "A river drops down from a great height forming a wonderous waterfall.",
+ "A breathtaking waterfall cuts through the landscape."
+ ];
- for (let i = 0; i < waterfalls.length && i < quantity; i++) {
- const cell = waterfalls[i];
- const id = addMarker({cell, icon: "⟱", type: "waterfall", dy: 54, px: 16});
- const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
- notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `An extremely beautiful waterfall`});
- }
+ const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
+ notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `${ra(descriptions)}`});
}
- function addBattlefields() {
+ function listBattlefields({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25
+ );
+ }
+
+ function addBattlefield(id, cell) {
const {cells, states} = pack;
- let battlefields = Array.from(cells.i.filter(i => cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25));
- let quantity = getQuantity(battlefields, 50, 700);
- if (!quantity) return;
-
- while (quantity && battlefields.length) {
- const [cell] = extractAnyElement(battlefields);
- const id = addMarker({cell, icon: "⚔️", type: "battlefield", dy: 52});
- const campaign = ra(states[cells.state[cell]].campaigns);
- const date = generateDate(campaign.start, campaign.end);
- const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
- const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const state = states[cells.state[cell]];
+ if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
+ const campaign = ra(state.campaigns);
+ const date = generateDate(campaign.start, campaign.end);
+ const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
+ const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}.`;
+ notes.push({id, name, legend});
}
- function addDungeons() {
+ function listDungeons({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.pop[i] && cells.pop[i] < 3);
+ }
+
+ function addDungeon(id, cell) {
+ const dungeonSeed = `${seed}${cell}`;
+ const name = "Dungeon";
+ const legend = `
`;
+ notes.push({id, name, legend});
+ }
+
+ function listLakeMonsters({features}) {
+ return features
+ .filter(feature => feature.type === "lake" && feature.group === "freshwater" && !occupied[feature.firstCell])
+ .map(feature => feature.firstCell);
+ }
+
+ function addLakeMonster(id, cell) {
+ const lake = pack.features[pack.cells.f[cell]];
+
+ // Check that the feature is a lake in case the user clicked on a wrong
+ // square
+ if (lake.type !== "lake") return;
+
+ const name = `${lake.name} Monster`;
+ const length = gauss(10, 5, 5, 100);
+ const subjects = [
+ "Locals",
+ "Elders",
+ "Inscriptions",
+ "Tipplers",
+ "Legends",
+ "Whispers",
+ "Rumors",
+ "Journeying folk",
+ "Tales"
+ ];
+ const legend = `${ra(subjects)} say a relic monster of ${length} ${heightUnit.value} long inhabits ${
+ lake.name
+ } Lake. Truth or lie, folks are afraid to fish in the lake.`;
+ notes.push({id, name, legend});
+ }
+
+ function listSeaMonsters({cells, features}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"
+ );
+ }
+
+ function addSeaMonster(id, cell) {
+ const name = `${Names.getCultureShort(0)} Monster`;
+ const length = gauss(25, 10, 10, 100);
+ const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long.`;
+ notes.push({id, name, legend});
+ }
+
+ function listHillMonsters({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+ }
+
+ function addHillMonster(id, cell) {
const {cells} = pack;
- let dungeons = Array.from(cells.i.filter(i => cells.pop[i] && cells.pop[i] < 3));
- let quantity = getQuantity(dungeons, 30, 200);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(dungeons);
- const id = addMarker({cell, icon: "🗝️", type: "dungeon", dy: 51, px: 13});
-
- const dungeonSeed = `${seed}${cell}`;
- const name = "Dungeon";
- const legend = `
`;
- notes.push({id, name, legend});
- quantity--;
- }
- }
-
- function addLakeMonsters() {
- const {features} = pack;
-
- const lakes = features.filter(feature => feature.type === "lake" && feature.group === "freshwater");
- let quantity = getQuantity(lakes, 2, 10);
- if (!quantity) return;
-
- while (quantity) {
- const [lake] = extractAnyElement(lakes);
- const cell = lake.firstCell;
- const id = addMarker({cell, icon: "🐉", type: "lake_monster", dy: 48});
- const name = `${lake.name} Monster`;
- const length = gauss(10, 5, 5, 100);
- const legend = `Rumors said a relic monster of ${length} ${heightUnit.value} long inhabits ${lake.name} Lake. Truth or lie, but folks are affraid to fish in the lake`;
- notes.push({id, name, legend});
- quantity--;
- }
- }
-
- function addSeaMonsters() {
- const {cells, features} = pack;
-
- const sea = Array.from(cells.i.filter(i => cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === "ocean"));
- let quantity = getQuantity(sea, 50, 700);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(sea);
- const id = addMarker({cell, icon: "🦑", type: "sea_monster"});
- const name = `${Names.getCultureShort(0)} Monster`;
- const length = gauss(25, 10, 10, 100);
- const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long`;
- notes.push({id, name, legend});
- quantity--;
- }
- }
-
- function addHillMonsters() {
- const {cells} = pack;
-
- const hills = Array.from(cells.i.filter(i => cells.h[i] >= 50 && cells.pop[i]));
- let quantity = getQuantity(hills, 30, 600);
- if (!quantity) return;
-
- const subjects = ["Locals", "Old folks", "Old books", "Tipplers"];
- const species = ["Ogre", "Troll", "Cyclops", "Giant", "Monster", "Beast", "Dragon", "Undead", "Ghoul", "Vampire"];
+ const adjectives = [
+ "great",
+ "big",
+ "huge",
+ "prime",
+ "golden",
+ "proud",
+ "lucky",
+ "fat",
+ "giant",
+ "hungry",
+ "magical",
+ "superior",
+ "terrifying",
+ "horrifying",
+ "feared"
+ ];
+ const subjects = [
+ "Locals",
+ "Elders",
+ "Inscriptions",
+ "Tipplers",
+ "Legends",
+ "Whispers",
+ "Rumors",
+ "Journeying folk",
+ "Tales"
+ ];
+ const species = [
+ "Ogre",
+ "Troll",
+ "Cyclops",
+ "Giant",
+ "Monster",
+ "Beast",
+ "Dragon",
+ "Undead",
+ "Ghoul",
+ "Vampire",
+ "Hag",
+ "Banshee",
+ "Bearded Devil",
+ "Roc",
+ "Hydra",
+ "Warg"
+ ];
const modusOperandi = [
"steals cattle at night",
"prefers eating children",
@@ -606,98 +689,99 @@ window.Markers = (function () {
"attacks unsuspecting victims"
];
- while (quantity) {
- const [cell] = extractAnyElement(hills);
- const id = addMarker({cell, icon: "👹", type: "hill_monster", dy: 54, px: 13});
- const monster = ra(species);
- const toponym = Names.getCulture(cells.culture[cell]);
- const name = `${toponym} ${monster}`;
- const legend = `${ra(subjects)} tell tales of an old ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const monster = ra(species);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ const name = `${toponym} ${monster}`;
+ const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(
+ modusOperandi
+ )}.`;
+ notes.push({id, name, legend});
}
- function addSacredMountains() {
- const {cells, cultures} = pack;
-
- let lonelyMountains = Array.from(cells.i.filter(i => cells.h[i] >= 70 && cells.c[i].some(c => cells.culture[c]) && cells.c[i].every(c => cells.h[c] < 60)));
- let quantity = getQuantity(lonelyMountains, 1, 5);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(lonelyMountains);
- const id = addMarker({cell, icon: "🗻", type: "sacred_mountain", dy: 48});
- const culture = cells.c[cell].map(c => cells.culture[c]).find(c => c);
- const name = `${Names.getCulture(culture)} Mountain`;
- const height = getFriendlyHeight(cells.p[cell]);
- const legend = `A sacred mountain of ${cultures[culture].name} culture. Height: ${height}`;
- notes.push({id, name, legend});
- quantity--;
- }
+ // Sacred mountains spawn on lonely mountains
+ function listSacredMountains({cells}) {
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.h[i] >= 70 &&
+ cells.c[i].some(c => cells.culture[c]) &&
+ cells.c[i].every(c => cells.h[c] < 60)
+ );
}
- function addSacredForests() {
- const {cells, cultures} = pack;
+ function addSacredMountain(id, cell) {
+ const {cells, religions} = pack;
- let temperateForests = Array.from(cells.i.filter(i => cells.culture[i] && [6, 8].includes(cells.biome[i])));
- let quantity = getQuantity(temperateForests, 30, 1000);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(temperateForests);
- const id = addMarker({cell, icon: "🌳", type: "sacred_forest"});
- const culture = cells.culture[cell];
- const name = `${Names.getCulture(culture)} Forest`;
- const legend = `A sacred forest of ${cultures[culture].name} culture`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const culture = cells.c[cell].map(c => cells.culture[c]).find(c => c);
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Mountain`;
+ const height = getFriendlyHeight(cells.p[cell]);
+ const legend = `A sacred mountain of ${religions[religion].name}. Height: ${height}.`;
+ notes.push({id, name, legend});
}
- function addSacredPineries() {
- const {cells, cultures} = pack;
-
- let borealForests = Array.from(cells.i.filter(i => cells.culture[i] && cells.biome[i] === 9));
- let quantity = getQuantity(borealForests, 30, 800);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(borealForests);
- const id = addMarker({cell, icon: "🌲", type: "pinery", px: 13});
- const culture = cells.culture[cell];
- const name = `${Names.getCulture(culture)} Pinery`;
- const legend = `A sacred pinery of ${cultures[culture].name} culture`;
- notes.push({id, name, legend});
- quantity--;
- }
+ // Sacred forests spawn on temperate forests
+ function listSacredForests({cells}) {
+ return cells.i.filter(
+ i => !occupied[i] && cells.culture[i] && cells.religion[i] && [6, 8].includes(cells.biome[i])
+ );
}
- function addSacredPalmGroves() {
- const {cells, cultures} = pack;
+ function addSacredForest(id, cell) {
+ const {cells, cultures, religions} = pack;
- let oasises = Array.from(cells.i.filter(i => cells.culture[i] && cells.biome[i] === 1 && cells.pop[i] > 1 && cells.road[i]));
- let quantity = getQuantity(oasises, 1, 100);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(oasises);
- const id = addMarker({cell, icon: "🌴", type: "palm_grove", px: 13});
- const culture = cells.culture[cell];
- const name = `${Names.getCulture(culture)} Palm Grove`;
- const legend = `A sacred palm grove of ${cultures[culture].name} culture`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Forest`;
+ const legend = `A forest sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
}
- function addBrigands() {
+ // Sacred pineries spawn on boreal forests
+ function listSacredPineries({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.religion[i] && cells.biome[i] === 9);
+ }
+
+ function addSacredPinery(id, cell) {
+ const {cells, cultures, religions} = pack;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Pinery`;
+ const legend = `A pinery sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+ }
+
+ // Sacred palm groves spawn on oasises
+ function listSacredPalmGroves({cells}) {
+ return cells.i.filter(
+ i =>
+ !occupied[i] &&
+ cells.culture[i] &&
+ cells.religion[i] &&
+ cells.biome[i] === 1 &&
+ cells.pop[i] > 1 &&
+ cells.road[i]
+ );
+ }
+
+ function addSacredPalmGrove(id, cell) {
+ const {cells, cultures, religions} = pack;
+
+ const culture = cells.culture[cell];
+ const religion = cells.religion[cell];
+ const name = `${Names.getCulture(culture)} Palm Grove`;
+ const legend = `A palm grove sacred to local ${religions[religion].name}.`;
+ notes.push({id, name, legend});
+ }
+
+ function listBrigands({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.road[i] > 4);
+ }
+
+ function addBrigands(id, cell) {
const {cells} = pack;
- let roads = Array.from(cells.i.filter(i => cells.culture[i] && cells.road[i] > 4));
- let quantity = getQuantity(roads, 50, 100);
- if (!quantity) return;
-
const animals = [
"Apes",
"Badgers",
@@ -732,55 +816,58 @@ window.Markers = (function () {
];
const types = {brigands: 4, bandits: 3, robbers: 1, highwaymen: 1};
- while (quantity) {
- const [cell] = extractAnyElement(roads);
- const id = addMarker({cell, icon: "💰", type: "brigands", px: 13});
- const culture = cells.culture[cell];
- const biome = cells.biome[cell];
- const height = cells.p[cell];
- const locality =
- height >= 70
- ? "highlander"
- : [1, 2].includes(biome)
- ? "desert"
- : [3, 4].includes(biome)
- ? "mounted"
- : [5, 6, 7, 8, 9].includes(biome)
- ? "forest"
- : biome === 12
- ? "swamp"
- : "angry";
- const name = `${Names.getCulture(culture)} ${ra(animals)}`;
- const legend = `A gang of ${locality} ${rw(types)}`;
- notes.push({id, name, legend});
- quantity--;
- }
+ const culture = cells.culture[cell];
+ const biome = cells.biome[cell];
+ const height = cells.p[cell];
+ const locality =
+ height >= 70
+ ? "highlander"
+ : [1, 2].includes(biome)
+ ? "desert"
+ : [3, 4].includes(biome)
+ ? "mounted"
+ : [5, 6, 7, 8, 9].includes(biome)
+ ? "forest"
+ : biome === 12
+ ? "swamp"
+ : "angry";
+ const name = `${Names.getCulture(culture)} ${ra(animals)}`;
+ const legend = `A gang of ${locality} ${rw(types)}.`;
+ notes.push({id, name, legend});
}
- function addPirates() {
- const {cells} = pack;
-
- let searoutes = Array.from(cells.i.filter(i => cells.h[i] < 20 && cells.road[i]));
- let quantity = getQuantity(searoutes, 40, 300);
- if (!quantity) return;
-
- while (quantity) {
- const [cell] = extractAnyElement(searoutes);
- const id = addMarker({cell, type: "pirates", icon: "🏴☠️", dx: 51});
- const name = `Pirates`;
- const legend = `Pirate ships have been spotted in these waters`;
- notes.push({id, name, legend});
- quantity--;
- }
+ // Pirates spawn on sea routes
+ function listPirates({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] < 20 && cells.road[i]);
}
- function addStatues() {
- const {cells} = pack;
- let statues = Array.from(cells.i.filter(i => cells.h[i] >= 20 && cells.h[i] < 40));
- let quantity = getQuantity(statues, 80, 1200);
- if (!quantity) return;
+ function addPirates(id, cell) {
+ const name = `Pirates`;
+ const legend = `Pirate ships have been spotted in these waters.`;
+ notes.push({id, name, legend});
+ }
- const types = ["Statue", "Obelisk", "Monument", "Column", "Monolith", "Pillar", "Megalith", "Stele", "Runestone"];
+ function listStatues({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.h[i] < 40);
+ }
+
+ function addStatue(id, cell) {
+ const {cells} = pack;
+
+ const variants = [
+ "Statue",
+ "Obelisk",
+ "Monument",
+ "Column",
+ "Monolith",
+ "Pillar",
+ "Megalith",
+ "Stele",
+ "Runestone",
+ "Sculpture",
+ "Effigy",
+ "Idol"
+ ];
const scripts = {
cypriot: "𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ",
geez: "ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ",
@@ -789,19 +876,16 @@ window.Markers = (function () {
mongolian: "᠀᠐᠑᠒ᠠᠡᠦᠧᠨᠩᠪᠭᠮᠯᠰᠱᠲᠳᠵᠻᠼᠽᠾᠿᡀᡁᡆᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡙᡜᡝᡞᡟᡠᡡᡭᡮᡯᡰᡱᡲᡳᡴᢀᢁᢂᢋᢏᢐᢑᢒᢓᢛᢜᢞᢟᢠᢡᢢᢤᢥᢦ"
};
- while (quantity) {
- const [cell] = extractAnyElement(statues);
- const id = addMarker({cell, icon: "🗿", type: "statues"});
- const culture = cells.culture[cell];
+ const culture = cells.culture[cell];
- const type = ra(types);
- const name = `${Names.getCulture(culture)} ${type}`;
- const script = scripts[ra(Object.keys(scripts))];
- const inscription = Array(rand(40, 100))
- .fill(null)
- .map(() => ra(script))
- .join("");
- const legend = `An ancient ${type.toLowerCase()}. It has an inscription, but no one can translate it:
+ const variant = ra(variants);
+ const name = `${Names.getCulture(culture)} ${variant}`;
+ const script = scripts[ra(Object.keys(scripts))];
+ const inscription = Array(rand(40, 100))
+ .fill(null)
+ .map(() => ra(script))
+ .join("");
+ const legend = `An ancient ${variant.toLowerCase()}. It has an inscription, but no one can translate it:
${inscription}
`;
notes.push({id, name, legend});
}
@@ -810,11 +894,22 @@ window.Markers = (function () {
return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60);
}
- function addRuines() {
- const {cells} = pack;
- let ruins = Array.from(cells.i.filter(i => cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60));
- let quantity = getQuantity(ruins, 80, 1200);
- if (!quantity) return;
+ function addRuins(id, cell) {
+ const types = [
+ "City",
+ "Town",
+ "Settlement",
+ "Pyramid",
+ "Fort",
+ "Stronghold",
+ "Temple",
+ "Sacred site",
+ "Mausoleum",
+ "Outpost",
+ "Fortification",
+ "Fortress",
+ "Castle"
+ ];
const ruinType = ra(types);
const name = `Ruined ${ruinType}`;
@@ -822,46 +917,249 @@ window.Markers = (function () {
notes.push({id, name, legend});
}
- while (quantity) {
- const [cell] = extractAnyElement(ruins);
- const id = addMarker({cell, icon: "🏺", type: "ruins"});
+ function listCircuses({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && pack.cells.road[i]);
+ }
- const type = ra(types);
- const name = `Ruined ${type}`;
- const legend = `Ruins of an ancient ${type.toLowerCase()}. A good place for a treasures hunt`;
- notes.push({id, name, legend});
- quantity--;
+ function addCircuses(id, cell) {
+ const adjectives = [
+ "Fantastical",
+ "Wonderous",
+ "Incomprehensible",
+ "Magical",
+ "Extraordinary",
+ "Unmissable",
+ "World-famous",
+ "Breathtaking"
+ ];
+
+ const adjective = ra(adjectives);
+ const name = `Travelling ${adjective} Circus`;
+ const legend = `Roll up, roll up, this ${adjective.toLowerCase()} circus is here for a limited time only.`;
+ notes.push({id, name, legend});
+ }
+
+ function listJousts({cells, burgs}) {
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 20);
+ }
+
+ function addJousts(id, cell) {
+ const {cells, burgs} = pack;
+ const types = ["Joust", "Competition", "Melee", "Tournament", "Contest"];
+ const virtues = ["cunning", "might", "speed", "the greats", "acumen", "brutality"];
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+ const type = ra(types);
+ const virtue = ra(virtues);
+
+ const name = `${burgName} ${type}`;
+ const legend = `Warriors from around the land gather for a ${type.toLowerCase()} of ${virtue} in ${burgName}, with fame, fortune and favour on offer to the victor.`;
+ notes.push({id, name, legend});
+ }
+
+ function listCanoes({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.r[i]);
+ }
+
+ function addCanoes(id, cell) {
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+
+ const name = `Minor Jetty`;
+ const riverName = river ? `${river.name} ${river.type}` : "river";
+ const legend = `A small location along the ${riverName} to launch boats from sits here, along with a weary looking owner, willing to sell passage along the river.`;
+ notes.push({id, name, legend});
+ }
+
+ function listMigrations({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.pop[i] <= 2);
+ }
+
+ function addMigrations(id, cell) {
+ const animals = [
+ "Antelopes",
+ "Apes",
+ "Badgers",
+ "Bears",
+ "Beavers",
+ "Bisons",
+ "Boars",
+ "Buffalo",
+ "Cats",
+ "Cranes",
+ "Crocodiles",
+ "Crows",
+ "Deer",
+ "Dogs",
+ "Eagles",
+ "Elk",
+ "Foxes",
+ "Goats",
+ "Geese",
+ "Hares",
+ "Hawks",
+ "Herons",
+ "Horses",
+ "Hyenas",
+ "Ibises",
+ "Jackals",
+ "Jaguars",
+ "Larks",
+ "Leopards",
+ "Lions",
+ "Mantises",
+ "Martens",
+ "Mooses",
+ "Mules",
+ "Owls",
+ "Panthers",
+ "Rats",
+ "Ravens",
+ "Rooks",
+ "Scorpions",
+ "Sharks",
+ "Sheep",
+ "Snakes",
+ "Spiders",
+ "Tigers",
+ "Wolves",
+ "Wolverines",
+ "Camels",
+ "Falcons",
+ "Hounds",
+ "Oxen"
+ ];
+ const animalChoice = ra(animals);
+
+ const name = `${animalChoice} migration`;
+ const legend = `A huge group of ${animalChoice.toLowerCase()} are migrating, whether part of their annual routine, or something more extraordinary.`;
+ notes.push({id, name, legend});
+ }
+
+ function listDances({cells, burgs}) {
+ return cells.i.filter(i => !occupied[i] && cells.burg[i] && burgs[cells.burg[i]].population > 15);
+ }
+
+ function addDances(id, cell) {
+ const {cells, burgs} = pack;
+ const burgName = burgs[cells.burg[cell]].name;
+ const socialTypes = [
+ "gala",
+ "dance",
+ "performance",
+ "ball",
+ "soiree",
+ "jamboree",
+ "exhibition",
+ "carnival",
+ "festival",
+ "jubilee"
+ ];
+ const people = [
+ "great and the good",
+ "nobility",
+ "local elders",
+ "foreign dignitaries",
+ "spiritual leaders",
+ "suspected revolutionaries"
+ ];
+ const socialType = ra(socialTypes);
+
+ const name = `${burgName} ${socialType}`;
+ const legend = `A ${socialType} has been organised at ${burgName} as a chance to gather the ${ra(
+ people
+ )} of the area together to be merry, make alliances and scheme around the crisis.`;
+ notes.push({id, name, legend});
+ }
+
+ function listMirage({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.biome[i] === 1);
+ }
+
+ function addMirage(id, cell) {
+ const adjectives = ["Entrancing", "Diaphanous", "Illusory", "Distant", "Perculiar"];
+
+ const mirageAdjective = ra(adjectives);
+ const name = `${mirageAdjective} mirage`;
+ const legend = `This ${mirageAdjective.toLowerCase()} mirage has been luring travellers out of their way for eons.`;
+ notes.push({id, name, legend});
+ }
+
+ function listCaves({cells}) {
+ return cells.i.filter(i => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]);
+ }
+
+ function addCaves(id, cell) {
+ const {cells} = pack;
+
+ const formations = {
+ Cave: 10,
+ Cavern: 8,
+ Chasm: 6,
+ Ravine: 6,
+ Fracture: 5,
+ Grotto: 4,
+ Pit: 4,
+ Sinkhole: 2,
+ Hole: 2
+ };
+ const status = {
+ "a good spot to hid treasure": 5,
+ "the home of strange monsters": 5,
+ "totally empty": 4,
+ "endlessly deep and unexplored": 4,
+ "completely flooded": 2,
+ "slowly filling with lava": 1
+ };
+
+ let formation = rw(formations);
+ const toponym = Names.getCulture(cells.culture[cell]);
+ if (cells.biome[cell] === 11) {
+ formation = "Glacial " + formation;
}
+ const name = `${toponym} ${formation}`;
+ const legend = `The ${name}. Locals claim that it is ${rw(status)}.`;
+ notes.push({id, name, legend});
}
- function addPortals() {
- const {burgs} = pack;
-
- let quantity = rand(5, 15);
- if (burgs.length < quantity + 1) return;
- let portals = burgs.slice(1, quantity + 1).map(burg => [burg.name, burg.cell]);
-
- while (quantity) {
- const [portal] = extractAnyElement(portals);
- const [burgName, cell] = portal;
- const id = addMarker({cell, icon: "🌀", type: "portals", px: 14});
- const name = `${burgName} Portal`;
- const legend = `An element of the magic portal system connecting major cities. Portals installed centuries ago, but still work fine`;
- notes.push({id, name, legend});
- quantity--;
- }
+ function listPortals({burgs}) {
+ return burgs
+ .slice(1, Math.ceil(burgs.length / 10) + 1)
+ .filter(({cell}) => !occupied[cell])
+ .map(burg => burg.cell);
}
- function addMarker({cell, type, icon, dx, dy, px}) {
- const i = pack.markers.length;
- const [x, y] = getMarkerCoordinates(cell);
- const marker = {i, icon, type, x, y};
- if (dx) marker.dx = dx;
- if (dy) marker.dy = dy;
- if (px) marker.px = px;
- pack.markers.push(marker);
- return "marker" + i;
+ function addPortal(id, cell) {
+ const {cells, burgs} = pack;
+
+ if (!cells.burg[cell]) return;
+ const burgName = burgs[cells.burg[cell]].name;
+
+ const name = `${burgName} Portal`;
+ const legend = `An element of the magic portal system connecting major cities. The portals were installed centuries ago, but still work fine.`;
+ notes.push({id, name, legend});
}
- return {generate, regenerate};
+ function listRifts({cells}) {
+ return cells.i.filter(i => !occupied[i] && pack.cells.pop[i] <= 3 && biomesData.habitability[pack.cells.biome[i]]);
+ }
+
+ function addRifts(id, cell) {
+ const types = ["Demonic", "Interdimensional", "Abyssal", "Cosmic", "Cataclysmic", "Subterranean", "Ancient"];
+
+ const descriptions = [
+ "all known nearby beings to flee in terror",
+ "cracks in reality itself to form",
+ "swarms of foes to spill forth",
+ "nearby plants to wither and decay",
+ "an emmissary to step through with an all-powerful relic"
+ ];
+
+ const riftType = ra(types);
+ const name = `${riftType} Rift`;
+ const legend = `A rumoured ${riftType.toLowerCase()} rift in this area is causing ${ra(descriptions)}.`;
+ notes.push({id, name, legend});
+ }
+
+ return {add, generate, regenerate, getConfig, setConfig, deleteMarker};
})();
diff --git a/modules/submap.js b/modules/submap.js
index 263d3294..85408487 100644
--- a/modules/submap.js
+++ b/modules/submap.js
@@ -41,6 +41,7 @@ window.Submap = (function () {
// create new grid
applyMapSize();
grid = generateGrid();
+
drawScaleBar(scale);
const resampler = (points, qtree, f) => {