diff --git a/index.html b/index.html
index c4280dd9..4806603e 100644
--- a/index.html
+++ b/index.html
@@ -1639,7 +1639,7 @@
Religions number |
-
+
|
diff --git a/package.json b/package.json
index 4ee3b0c0..5754d5fc 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@types/delaunator": "^5.0.0",
"@types/jquery": "^3.5.14",
"@types/jqueryui": "^1.12.16",
+ "@types/polylabel": "^1.0.5",
"c8": "^7.12.0",
"happy-dom": "^6.0.4",
"rollup": "^2.75.7",
diff --git a/src/assets/styles/ancient.json b/src/assets/styles/ancient.json
index ffc3e775..3f5b3281 100644
--- a/src/assets/styles/ancient.json
+++ b/src/assets/styles/ancient.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#404040",
- "stroke-width": 0.7,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/atlas.json b/src/assets/styles/atlas.json
index c26848a0..6a6917c3 100644
--- a/src/assets/styles/atlas.json
+++ b/src/assets/styles/atlas.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#777777",
- "stroke-width": 0,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/clean.json b/src/assets/styles/clean.json
index fd00c5ad..4fed8dea 100644
--- a/src/assets/styles/clean.json
+++ b/src/assets/styles/clean.json
@@ -79,13 +79,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#404040",
- "stroke-width": 0.7,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/cyberpunk.json b/src/assets/styles/cyberpunk.json
index 579d093b..31c493f2 100644
--- a/src/assets/styles/cyberpunk.json
+++ b/src/assets/styles/cyberpunk.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.5,
"stroke": "#404040",
- "stroke-width": 2,
+ "stroke-width": 3,
"filter": "url(#splotch)"
},
"#cults": {
"opacity": 0.35,
"stroke": "#777777",
- "stroke-width": 2,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": "url(#splotch)"
diff --git a/src/assets/styles/default.json b/src/assets/styles/default.json
index 37a52b33..775bdbd2 100644
--- a/src/assets/styles/default.json
+++ b/src/assets/styles/default.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#777777",
- "stroke-width": 0,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/gloom.json b/src/assets/styles/gloom.json
index 95dee382..b5096a95 100644
--- a/src/assets/styles/gloom.json
+++ b/src/assets/styles/gloom.json
@@ -79,13 +79,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#404040",
- "stroke-width": 1,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.7,
"stroke": "#777777",
- "stroke-width": 1.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/light.json b/src/assets/styles/light.json
index 6a225e86..b6dd72ed 100644
--- a/src/assets/styles/light.json
+++ b/src/assets/styles/light.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.5,
"stroke": null,
- "stroke-width": 0,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.5,
"stroke": "#777777",
- "stroke-width": 0,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/monochrome.json b/src/assets/styles/monochrome.json
index e94f5a7b..0c843f0c 100644
--- a/src/assets/styles/monochrome.json
+++ b/src/assets/styles/monochrome.json
@@ -79,13 +79,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#404040",
- "stroke-width": 0.7,
+ "stroke-width": 3,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
diff --git a/src/assets/styles/watercolor.json b/src/assets/styles/watercolor.json
index 99908126..24d7787b 100644
--- a/src/assets/styles/watercolor.json
+++ b/src/assets/styles/watercolor.json
@@ -78,13 +78,13 @@
"#relig": {
"opacity": 0.7,
"stroke": "#777777",
- "stroke-width": 0,
+ "stroke-width": 3,
"filter": "url(#bluredSplotch)"
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
- "stroke-width": 0.5,
+ "stroke-width": 3,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": "url(#splotch)"
diff --git a/src/config/religionsData.ts b/src/config/religionsData.ts
new file mode 100644
index 00000000..b7d70665
--- /dev/null
+++ b/src/config/religionsData.ts
@@ -0,0 +1,358 @@
+// name generation approach and relative chance to be selected
+const approach = {
+ Number: 1,
+ Being: 3,
+ Adjective: 5,
+ "Color + Animal": 5,
+ "Adjective + Animal": 5,
+ "Adjective + Being": 5,
+ "Adjective + Genitive": 1,
+ "Color + Being": 3,
+ "Color + Genitive": 3,
+ "Being + of + Genitive": 2,
+ "Being + of the + Genitive": 1,
+ "Animal + of + Genitive": 1,
+ "Adjective + Being + of + Genitive": 2,
+ "Adjective + Animal + of + Genitive": 2
+};
+
+// turn weighted data into a flat array
+const approaches: string[] = Object.entries(approach)
+ .map(([approach, weight]) => new Array(weight).fill(approach))
+ .flat();
+
+const base = {
+ number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
+ being: [
+ "Ancestor",
+ "Ancient",
+ "Brother",
+ "Chief",
+ "Council",
+ "Creator",
+ "Deity",
+ "Elder",
+ "Father",
+ "Forebear",
+ "Forefather",
+ "Giver",
+ "God",
+ "Goddess",
+ "Guardian",
+ "Lady",
+ "Lord",
+ "Maker",
+ "Master",
+ "Mother",
+ "Numen",
+ "Overlord",
+ "Reaper",
+ "Ruler",
+ "Sister",
+ "Spirit",
+ "Virgin"
+ ],
+ animal: [
+ "Antelope",
+ "Ape",
+ "Badger",
+ "Basilisk",
+ "Bear",
+ "Beaver",
+ "Bison",
+ "Boar",
+ "Buffalo",
+ "Camel",
+ "Cat",
+ "Centaur",
+ "Chimera",
+ "Cobra",
+ "Crane",
+ "Crocodile",
+ "Crow",
+ "Cyclope",
+ "Deer",
+ "Dog",
+ "Dragon",
+ "Eagle",
+ "Elk",
+ "Falcon",
+ "Fox",
+ "Goat",
+ "Goose",
+ "Hare",
+ "Hawk",
+ "Heron",
+ "Horse",
+ "Hound",
+ "Hyena",
+ "Ibis",
+ "Jackal",
+ "Jaguar",
+ "Kraken",
+ "Lark",
+ "Leopard",
+ "Lion",
+ "Mantis",
+ "Marten",
+ "Moose",
+ "Mule",
+ "Narwhal",
+ "Owl",
+ "Ox",
+ "Panther",
+ "Pegasus",
+ "Phoenix",
+ "Rat",
+ "Raven",
+ "Rook",
+ "Scorpion",
+ "Serpent",
+ "Shark",
+ "Sheep",
+ "Snake",
+ "Sphinx",
+ "Spider",
+ "Swan",
+ "Tiger",
+ "Turtle",
+ "Unicorn",
+ "Viper",
+ "Vulture",
+ "Walrus",
+ "Wolf",
+ "Wolverine",
+ "Worm",
+ "Wyvern"
+ ],
+ adjective: [
+ "Aggressive",
+ "Almighty",
+ "Ancient",
+ "Beautiful",
+ "Benevolent",
+ "Big",
+ "Blind",
+ "Blond",
+ "Bloody",
+ "Brave",
+ "Broken",
+ "Brutal",
+ "Burning",
+ "Calm",
+ "Cheerful",
+ "Crazy",
+ "Cruel",
+ "Dead",
+ "Deadly",
+ "Devastating",
+ "Distant",
+ "Disturbing",
+ "Divine",
+ "Dying",
+ "Eternal",
+ "Evil",
+ "Explicit",
+ "Fair",
+ "Far",
+ "Fat",
+ "Fatal",
+ "Favorable",
+ "Flying",
+ "Friendly",
+ "Frozen",
+ "Giant",
+ "Good",
+ "Grateful",
+ "Great",
+ "Happy",
+ "High",
+ "Holy",
+ "Honest",
+ "Huge",
+ "Hungry",
+ "Immutable",
+ "Infallible",
+ "Inherent",
+ "Last",
+ "Latter",
+ "Lost",
+ "Loud",
+ "Lucky",
+ "Mad",
+ "Magical",
+ "Main",
+ "Major",
+ "Marine",
+ "Naval",
+ "New",
+ "Old",
+ "Patient",
+ "Peaceful",
+ "Pregnant",
+ "Prime",
+ "Proud",
+ "Pure",
+ "Sacred",
+ "Sad",
+ "Scary",
+ "Secret",
+ "Selected",
+ "Severe",
+ "Silent",
+ "Sleeping",
+ "Slumbering",
+ "Strong",
+ "Sunny",
+ "Superior",
+ "Sustainable",
+ "Troubled",
+ "Unhappy",
+ "Unknown",
+ "Waking",
+ "Wild",
+ "Wise",
+ "Worried",
+ "Young"
+ ],
+ genitive: [
+ "Cold",
+ "Day",
+ "Death",
+ "Doom",
+ "Fate",
+ "Fire",
+ "Fog",
+ "Frost",
+ "Gates",
+ "Heaven",
+ "Home",
+ "Ice",
+ "Justice",
+ "Life",
+ "Light",
+ "Lightning",
+ "Love",
+ "Nature",
+ "Night",
+ "Pain",
+ "Snow",
+ "Springs",
+ "Summer",
+ "Thunder",
+ "Time",
+ "Victory",
+ "War",
+ "Winter"
+ ],
+ theGenitive: [
+ "Abyss",
+ "Blood",
+ "Dawn",
+ "Earth",
+ "East",
+ "Eclipse",
+ "Fall",
+ "Harvest",
+ "Moon",
+ "North",
+ "Peak",
+ "Rainbow",
+ "Sea",
+ "Sky",
+ "South",
+ "Stars",
+ "Storm",
+ "Sun",
+ "Tree",
+ "Underworld",
+ "West",
+ "Wild",
+ "Word",
+ "World"
+ ],
+ color: [
+ "Amber",
+ "Black",
+ "Blue",
+ "Bright",
+ "Brown",
+ "Dark",
+ "Golden",
+ "Green",
+ "Grey",
+ "Light",
+ "Orange",
+ "Pink",
+ "Purple",
+ "Red",
+ "White",
+ "Yellow"
+ ]
+};
+
+const forms = {
+ Folk: {Shamanism: 2, Animism: 2, "Ancestor worship": 1, Polytheism: 2},
+ Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, "Non-theism": 1},
+ Cult: {Cult: 1, "Dark Cult": 1},
+ Heresy: {Heresy: 1}
+};
+
+const namingMethods = {
+ Folk: {
+ "Culture + type": 1
+ },
+
+ Organized: {
+ "Random + type": 3,
+ "Random + ism": 1,
+ "Supreme + ism": 5,
+ "Faith of + Supreme": 5,
+ "Place + ism": 1,
+ "Culture + ism": 2,
+ "Place + ian + type": 6,
+ "Culture + type": 4
+ },
+
+ Cult: {
+ "Burg + ian + type": 2,
+ "Random + ian + type": 1,
+ "Type + of the + meaning": 2
+ },
+
+ Heresy: {
+ "Burg + ian + type": 3,
+ "Random + ism": 3,
+ "Random + ian + type": 2,
+ "Type + of the + meaning": 1
+ }
+};
+
+const types = {
+ Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
+ Animism: {Spirits: 1, Beliefs: 1},
+ "Ancestor worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
+ Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
+
+ Dualism: {Religion: 3, Faith: 1, Cult: 1},
+ Monotheism: {Religion: 1, Church: 1},
+ "Non-theism": {Beliefs: 3, Spirits: 1},
+
+ Cult: {Cult: 4, Sect: 4, Arcanum: 1, Coterie: 1, Order: 1, Worship: 1},
+ "Dark Cult": {Cult: 2, Sect: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
+
+ Heresy: {
+ Heresy: 3,
+ Sect: 2,
+ Apostates: 1,
+ Brotherhood: 1,
+ Circle: 1,
+ Dissent: 1,
+ Dissenters: 1,
+ Iconoclasm: 1,
+ Schism: 1,
+ Society: 1
+ }
+};
+
+export const religionsData = {approaches, base, forms, namingMethods, types};
diff --git a/src/dialogs/dialogs/states-editor.js b/src/dialogs/dialogs/states-editor.js
index a4f96fd5..c5d797a7 100644
--- a/src/dialogs/dialogs/states-editor.js
+++ b/src/dialogs/dialogs/states-editor.js
@@ -1119,7 +1119,7 @@ function adjustProvinces(affectedProvinces) {
// reassign province ownership to province center owner
prevOwner.provinces = prevOwner.provinces.filter(province => province !== provinceId);
province.state = stateId;
- province.color = getMixedColor(states[stateId].color);
+ province.color = brighter(getMixedColor(states[stateId].color, 0.2), 0.3);
states[stateId].provinces.push(provinceId);
return;
}
@@ -1163,7 +1163,7 @@ function adjustProvinces(affectedProvinces) {
const formOptions = ["Zone", "Area", "Territory", "Province"];
const formName = burgCell && oldProvince.formName ? oldProvince.formName : ra(formOptions);
- const color = getMixedColor(states[stateId].color);
+ const color = brighter(getMixedColor(states[stateId].color, 0.2), 0.3);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = BurgsAndStates.getType(center, burg?.port);
diff --git a/src/index.css b/src/index.css
index 9653262e..a626de1c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -102,7 +102,7 @@ a {
}
#biomes {
- stroke-width: 0.7;
+ stroke-width: 3;
}
#landmass {
@@ -137,7 +137,8 @@ a {
stroke-linejoin: round;
}
-t,
+/* TODO: turn on after debugging */
+/* t,
#regions,
#cults,
#relig,
@@ -150,7 +151,7 @@ t,
#landmass,
#fogging {
pointer-events: none;
-}
+} */
#armies text {
pointer-events: none;
diff --git a/src/layers/renderers/drawBiomes.js b/src/layers/renderers/drawBiomes.js
deleted file mode 100644
index 5e406cb5..00000000
--- a/src/layers/renderers/drawBiomes.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import {clipPoly} from "utils/lineUtils";
-import {TIME} from "config/logging";
-
-export function drawBiomes() {
- TIME && console.time("drawBiomes");
- biomes.selectAll("path").remove();
-
- const {cells, vertices} = pack;
- const n = cells.i.length;
-
- const used = new Uint8Array(cells.i.length);
- const paths = new Array(biomesData.i.length).fill("");
-
- for (const i of cells.i) {
- if (!cells.biome[i]) continue; // no need to mark marine biome (liquid water)
- if (used[i]) continue; // already marked
- const b = cells.biome[i];
- const onborder = cells.c[i].some(n => cells.biome[n] !== b);
- if (!onborder) continue;
- const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
- const chain = connectVertices(edgeVerticle, b);
- if (chain.length < 3) continue;
- const points = clipPoly(chain.map(v => vertices.p[v]));
- paths[b] += "M" + points.join("L") + "Z";
- }
-
- paths.forEach(function (d, i) {
- if (d.length < 10) return;
- biomes
- .append("path")
- .attr("d", d)
- .attr("fill", biomesData.color[i])
- .attr("stroke", biomesData.color[i])
- .attr("id", "biome" + i);
- });
-
- // connect vertices to chain
- function connectVertices(start, b) {
- const chain = []; // vertices chain to form a path
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain[chain.length - 1]; // previous vertex in chain
- chain.push(current); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.biome[c] === b).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.biome[c[0]] !== b;
- const c1 = c[1] >= n || cells.biome[c[1]] !== b;
- const c2 = c[2] >= n || cells.biome[c[2]] !== b;
- const v = vertices.v[current]; // neighboring vertices
- if (v[0] !== prev && c0 !== c1) current = v[0];
- else if (v[1] !== prev && c1 !== c2) current = v[1];
- else if (v[2] !== prev && c0 !== c2) current = v[2];
- if (current === chain[chain.length - 1]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- return chain;
- }
-
- TIME && console.timeEnd("drawBiomes");
-}
diff --git a/src/layers/renderers/drawBiomes.ts b/src/layers/renderers/drawBiomes.ts
new file mode 100644
index 00000000..382f858f
--- /dev/null
+++ b/src/layers/renderers/drawBiomes.ts
@@ -0,0 +1,29 @@
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {getPaths} from "./utils/getVertexPaths";
+
+export function drawBiomes() {
+ /* global */ const {cells, vertices, features} = pack;
+ /* global */ const colors = biomesData.color;
+
+ const paths = getPaths({
+ getType: (cellId: number) => cells.biome[cellId],
+ cells: pick(cells, "c", "v", "b", "h", "f"),
+ vertices,
+ features,
+ options: {fill: true, waterGap: true, halo: false}
+ });
+
+ console.log(paths);
+
+ const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
+ const color = colors[Number(index)];
+
+ return /* html */ `
+
+
+ `;
+ });
+
+ byId("biomes")!.innerHTML = htmlPaths.join("");
+}
diff --git a/src/layers/renderers/drawCultures.ts b/src/layers/renderers/drawCultures.ts
index 7090f417..9d3cdeae 100644
--- a/src/layers/renderers/drawCultures.ts
+++ b/src/layers/renderers/drawCultures.ts
@@ -1,22 +1,28 @@
-import * as d3 from "d3";
-
-import {getPaths} from "./utilts";
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {getPaths} from "./utils/getVertexPaths";
export function drawCultures() {
- /* uses */ const {cells, vertices, cultures} = pack;
+ /* global */ const {cells, vertices, features, cultures} = pack;
- const getType = (cellId: number) => cells.culture[cellId];
- const paths = getPaths(cells.c, cells.v, vertices, getType);
+ const paths = getPaths({
+ getType: (cellId: number) => cells.culture[cellId],
+ cells: pick(cells, "c", "v", "b", "h", "f"),
+ vertices,
+ features,
+ options: {fill: true, waterGap: true, halo: false}
+ });
- const getColor = (i: number) => i && (cultures[i] as ICulture).color;
+ const getColor = (i: string) => (cultures[Number(i)] as ICulture).color;
- d3.select("#cults")
- .selectAll("path")
- .remove()
- .data(Object.entries(paths))
- .enter()
- .append("path")
- .attr("d", ([, path]) => path)
- .attr("fill", ([i]) => getColor(Number(i)))
- .attr("id", ([i]) => "culture" + i);
+ const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
+ const color = getColor(index);
+
+ return /* html */ `
+
+
+ `;
+ });
+
+ byId("cults")!.innerHTML = htmlPaths.join("");
}
diff --git a/src/layers/renderers/drawFeatures.ts b/src/layers/renderers/drawFeatures.ts
index 7832e84a..41ccba46 100644
--- a/src/layers/renderers/drawFeatures.ts
+++ b/src/layers/renderers/drawFeatures.ts
@@ -5,7 +5,7 @@ import {filterOutOfCanvasPoints} from "utils/lineUtils";
import {round} from "utils/stringUtils";
export function drawFeatures() {
- /* uses */ const {vertices, features} = pack;
+ /* global */ const {vertices, features} = pack;
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
diff --git a/src/layers/renderers/drawReligions.js b/src/layers/renderers/drawReligions.js
deleted file mode 100644
index a8df70ae..00000000
--- a/src/layers/renderers/drawReligions.js
+++ /dev/null
@@ -1,93 +0,0 @@
-export function drawReligions() {
- relig.selectAll("path").remove();
- const {cells, vertices, religions} = pack;
- const n = cells.i.length;
-
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(religions.length); // store vertices array
- const body = new Array(religions.length).fill(""); // store path around each religion
- const gap = new Array(religions.length).fill(""); // store path along water for each religion to fill the gaps
-
- for (const i of cells.i) {
- if (!cells.religion[i]) continue;
- if (used[i]) continue;
- used[i] = 1;
- const r = cells.religion[i];
- const onborder = cells.c[i].filter(n => cells.religion[n] !== r);
- if (!onborder.length) continue;
- const borderWith = cells.c[i].map(c => cells.religion[c]).find(n => n !== r);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] === borderWith));
- const chain = connectVertices(vertex, r, borderWith);
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v[0]]);
- if (!vArray[r]) vArray[r] = [];
- vArray[r].push(points);
- body[r] += "M" + points.join("L") + "Z";
- gap[r] +=
- "M" +
- vertices.p[chain[0][0]] +
- chain.reduce(
- (r2, v, i, d) =>
- !i ? r2 : !v[2] ? r2 + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r2 + "M" + vertices.p[v[0]] : r2,
- ""
- );
- }
-
- const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
- relig
- .selectAll("path")
- .data(bodyData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", d => d[2])
- .attr("id", d => "religion" + d[1]);
-
- const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
- relig
- .selectAll(".path")
- .data(gapData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", "none")
- .attr("stroke", d => d[2])
- .attr("id", d => "religion-gap" + d[1])
- .attr("stroke-width", "10px");
-
- // connect vertices to chain
- function connectVertices(start, t, religion) {
- const chain = []; // vertices chain to form a path
- let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.religion[c] !== t);
- function check(i) {
- religion = cells.religion[i];
- land = cells.h[i] >= 20;
- }
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
- chain.push([current, religion, land]); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.religion[c] === t).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.religion[c[0]] !== t;
- const c1 = c[1] >= n || cells.religion[c[1]] !== t;
- const c2 = c[2] >= n || cells.religion[c[2]] !== t;
- const v = vertices.v[current]; // neighboring vertices
- if (v[0] !== prev && c0 !== c1) {
- current = v[0];
- check(c0 ? c[0] : c[1]);
- } else if (v[1] !== prev && c1 !== c2) {
- current = v[1];
- check(c1 ? c[1] : c[2]);
- } else if (v[2] !== prev && c0 !== c2) {
- current = v[2];
- check(c2 ? c[2] : c[0]);
- }
- if (current === chain[chain.length - 1][0]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- return chain;
- }
-}
diff --git a/src/layers/renderers/drawReligions.ts b/src/layers/renderers/drawReligions.ts
new file mode 100644
index 00000000..19c63354
--- /dev/null
+++ b/src/layers/renderers/drawReligions.ts
@@ -0,0 +1,28 @@
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {getPaths} from "./utils/getVertexPaths";
+
+export function drawReligions() {
+ /* global */ const {cells, vertices, features, religions} = pack;
+
+ const paths = getPaths({
+ getType: (cellId: number) => cells.religion[cellId],
+ cells: pick(cells, "c", "v", "b", "h", "f"),
+ vertices,
+ features,
+ options: {fill: true, waterGap: true, halo: false}
+ });
+
+ const getColor = (i: string) => (religions[Number(i)] as IReligion).color;
+
+ const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
+ const color = getColor(index);
+
+ return /* html */ `
+
+
+ `;
+ });
+
+ byId("relig")!.innerHTML = htmlPaths.join("");
+}
diff --git a/src/layers/renderers/drawRoutes.ts b/src/layers/renderers/drawRoutes.ts
index 1518dd8d..dc269981 100644
--- a/src/layers/renderers/drawRoutes.ts
+++ b/src/layers/renderers/drawRoutes.ts
@@ -10,9 +10,7 @@ const lineGenTypeMap: {[key in IRoute["type"]]: d3.CurveFactory | d3.CurveFactor
};
export function drawRoutes() {
- routes.selectAll("path").remove();
-
- const {cells, burgs} = pack;
+ /* global */ const {cells, burgs} = pack;
const lineGen = d3.line();
const SHARP_ANGLE = 135;
@@ -32,6 +30,7 @@ export function drawRoutes() {
routePaths[type].push(``);
}
+ routes.selectAll("path").remove();
for (const type in routePaths) {
routes.select(`[data-type=${type}]`).html(routePaths[type].join(""));
}
diff --git a/src/layers/renderers/drawStates.js b/src/layers/renderers/drawStates.js
deleted file mode 100644
index 2c7e1cd7..00000000
--- a/src/layers/renderers/drawStates.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import * as d3 from "d3";
-
-import polylabel from "polylabel";
-
-export function drawStates() {
- regions.selectAll("path").remove();
-
- const {cells, vertices, features} = pack;
- const states = pack.states;
- const n = cells.i.length;
-
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(states.length); // store vertices array
- const body = new Array(states.length).fill(""); // path around each state
- const gap = new Array(states.length).fill(""); // path along water for each state to fill the gaps
- const halo = new Array(states.length).fill(""); // path around states, but not lakes
-
- const getStringPoint = v => vertices.p[v[0]].join(",");
-
- // define inner-state lakes to omit on border render
- const innerLakes = features.map(feature => {
- if (feature.type !== "lake") return false;
-
- const shoreline = feature.shoreline || [];
- const states = shoreline.map(i => cells.state[i]);
- return new Set(states).size > 1 ? false : true;
- });
-
- for (const i of cells.i) {
- if (!cells.state[i] || used[i]) continue;
- const state = cells.state[i];
-
- const onborder = cells.c[i].some(n => cells.state[n] !== state);
- if (!onborder) continue;
-
- const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== state);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
- const chain = connectVertices(vertex, state);
-
- const noInnerLakes = chain.filter(v => v[1] !== "innerLake");
- if (noInnerLakes.length < 3) continue;
-
- // get path around the state
- if (!vArray[state]) vArray[state] = [];
- const points = noInnerLakes.map(v => vertices.p[v[0]]);
- vArray[state].push(points);
- body[state] += "M" + points.join("L");
-
- // connect path for halo
- let discontinued = true;
- halo[state] += noInnerLakes
- .map(v => {
- if (v[1] === "border") {
- discontinued = true;
- return "";
- }
-
- const operation = discontinued ? "M" : "L";
- discontinued = false;
- return `${operation}${getStringPoint(v)}`;
- })
- .join("");
-
- // connect gaps between state and water into a single path
- discontinued = true;
- gap[state] += chain
- .map(v => {
- if (v[1] === "land") {
- discontinued = true;
- return "";
- }
-
- const operation = discontinued ? "M" : "L";
- discontinued = false;
- return `${operation}${getStringPoint(v)}`;
- })
- .join("");
- }
-
- // find state visual center
- vArray.forEach((ar, i) => {
- const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
- states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
- });
-
- const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
- const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
- const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
-
- const bodyString = bodyData.map(d => ``).join("");
- const gapString = gapData.map(d => ``).join("");
- const clipString = bodyData
- .map(d => ``)
- .join("");
- const haloString = haloData
- .map(
- d =>
- ``
- )
- .join("");
-
- statesBody.html(bodyString + gapString);
- defs.select("#statePaths").html(clipString);
- statesHalo.html(haloString);
-
- // connect vertices to chain
- function connectVertices(start, state) {
- const chain = []; // vertices chain to form a path
- const getType = c => {
- const borderCell = c.find(i => cells.b[i]);
- if (borderCell) return "border";
-
- const waterCell = c.find(i => cells.h[i] < 20);
- if (!waterCell) return "land";
- if (innerLakes[cells.f[waterCell]]) return "innerLake";
- return features[cells.f[waterCell]].type;
- };
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
-
- const c = vertices.c[current]; // cells adjacent to vertex
- chain.push([current, getType(c)]); // add current vertex to sequence
-
- c.filter(c => cells.state[c] === state).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.state[c[0]] !== state;
- const c1 = c[1] >= n || cells.state[c[1]] !== state;
- const c2 = c[2] >= n || cells.state[c[2]] !== state;
-
- const v = vertices.v[current]; // neighboring vertices
-
- if (v[0] !== prev && c0 !== c1) current = v[0];
- else if (v[1] !== prev && c1 !== c2) current = v[1];
- else if (v[2] !== prev && c0 !== c2) current = v[2];
-
- if (current === prev) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
-
- if (chain.length) chain.push(chain[0]);
- return chain;
- }
-
- Zoom.invoke();
-}
diff --git a/src/layers/renderers/drawStates.ts b/src/layers/renderers/drawStates.ts
new file mode 100644
index 00000000..1c5f65b2
--- /dev/null
+++ b/src/layers/renderers/drawStates.ts
@@ -0,0 +1,48 @@
+import * as d3 from "d3";
+
+import {pick} from "utils/functionUtils";
+import {byId} from "utils/shorthands";
+import {getPaths} from "./utils/getVertexPaths";
+
+export function drawStates() {
+ /* global */ const {cells, vertices, features, states} = pack;
+
+ const paths = getPaths({
+ getType: (cellId: number) => cells.state[cellId],
+ cells: pick(cells, "c", "v", "b", "h", "f"),
+ vertices,
+ features,
+ options: {fill: true, waterGap: true, halo: true}
+ });
+
+ const getColor = (i: number) => (states[i] as IState).color;
+
+ const maxLength = states.length - 1;
+ const bodyPaths = new Array(maxLength);
+ const clipPaths = new Array(maxLength);
+ const haloPaths = new Array(maxLength);
+
+ for (const [index, {fill, waterGap, halo}] of paths) {
+ const color = getColor(Number(index));
+ const haloColor = d3.color(color)?.darker().formatHex() || "#666666";
+
+ bodyPaths.push(/* html */ `
+
+
+ `);
+
+ clipPaths.push(/* html */ `
+
+ `);
+
+ haloPaths.push(/* html */ `
+
+ `);
+ }
+
+ byId("statesBody")!.innerHTML = bodyPaths.join("");
+ byId("statePaths")!.innerHTML = clipPaths.join("");
+ byId("statesHalo")!.innerHTML = haloPaths.join("");
+
+ /* global */ window.Zoom.invoke();
+}
diff --git a/src/layers/renderers/utils/getVertexPaths.ts b/src/layers/renderers/utils/getVertexPaths.ts
new file mode 100644
index 00000000..6231c359
--- /dev/null
+++ b/src/layers/renderers/utils/getVertexPaths.ts
@@ -0,0 +1,106 @@
+import {MIN_LAND_HEIGHT} from "config/generation";
+import {connectVertices} from "scripts/connectVertices";
+import {isLake} from "utils/typeUtils";
+
+type TPath = {fill: string; waterGap: string; halo: string};
+
+export function getPaths({
+ vertices,
+ getType,
+ features,
+ cells,
+ options
+}: {
+ vertices: IGraphVertices;
+ getType: (cellId: number) => number;
+ features: TPackFeatures;
+ cells: Pick;
+ options: {[key in keyof TPath]: boolean};
+}) {
+ const paths: Dict = {};
+
+ const checkedCells = new Uint8Array(cells.c.length);
+ const addToChecked = (cellId: number) => {
+ checkedCells[cellId] = 1;
+ };
+ const isChecked = (cellId: number) => 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: number) => getType(cellId) === type;
+ const ofDifferentType = (cellId: number) => getType(cellId) !== type;
+
+ const onborderCell = cells.c[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ const feature = features[cells.f[onborderCell]];
+ if (isInnerLake(feature, ofSameType)) continue;
+
+ 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({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
+ if (vertexChain.length < 3) continue;
+
+ addPath(type, vertexChain);
+ }
+
+ return Object.entries(paths);
+
+ function getVertexPoint(vertex: number) {
+ return vertices.p[vertex];
+ }
+
+ function getFillPath(vertexChain: number[]) {
+ const points: TPoints = vertexChain.map(getVertexPoint);
+ const firstPoint = points.shift();
+ return `M${firstPoint} L${points.join(" ")}`;
+ }
+
+ function getBorderPath(vertexChain: number[], discontinue: (vertex: number) => boolean) {
+ 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: number) {
+ const adjacentCells = vertices.c[vertex];
+ return adjacentCells.some(i => cells.b[i]);
+ }
+
+ function isLandVertex(vertex: number) {
+ const adjacentCells = vertices.c[vertex];
+ return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT);
+ }
+
+ function addPath(index: number, vertexChain: number[]) {
+ 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 isInnerLake(feature: 0 | TPackFeature, ofSameType: (cellId: number) => boolean) {
+ if (!isLake(feature)) return false;
+ return feature.shoreline.every(ofSameType);
+}
diff --git a/src/layers/renderers/utilts.ts b/src/layers/renderers/utilts.ts
deleted file mode 100644
index cf454606..00000000
--- a/src/layers/renderers/utilts.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {connectVertices} from "scripts/connectVertices";
-
-export function getPaths(
- cellNeighbors: number[][],
- cellVertices: number[][],
- vertices: IGraphVertices,
- getType: (cellId: number) => number
-) {
- const paths: Dict = {};
-
- function addPath(index: number, points: TPoints) {
- if (!paths[index]) paths[index] = "";
- paths[index] += "M" + points.join("L") + "Z";
- }
-
- const checkedCells = new Uint8Array(cellNeighbors.length);
- for (let cellId = 0; cellId < cellNeighbors.length; cellId++) {
- if (checkedCells[cellId]) continue;
- if (!getType(cellId)) continue;
- checkedCells[cellId] = 1;
-
- const type = getType(cellId);
- const ofSameType = (cellId: number) => getType(cellId) === type;
-
- const isOnborder = cellNeighbors[cellId].some(cellId => !ofSameType(cellId));
- if (!isOnborder) continue;
-
- const startingVertex = cellVertices[cellId].find(v => vertices.c[v].some(cellId => !ofSameType(cellId)));
- if (startingVertex === undefined) throw new Error(`getPath: starting vertex for cell ${cellId} is not found`);
-
- const chain = connectVertices({vertices, startingVertex, ofSameType, checkedCellsMutable: checkedCells});
-
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v]);
-
- addPath(type, points);
- }
-
- return paths;
-}
diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js
index 5d6eb086..be4b359e 100644
--- a/src/modules/burgs-and-states.js
+++ b/src/modules/burgs-and-states.js
@@ -780,7 +780,7 @@ window.BurgsAndStates = (function () {
const sameColored = pack.states.filter(s => s.color === c);
sameColored.forEach((s, d) => {
if (!d) return;
- s.color = getMixedColor(s.color);
+ s.color = brighter(getMixedColor(s.color, 0.2), 0.3);
});
});
@@ -1209,7 +1209,7 @@ window.BurgsAndStates = (function () {
const formName = rw(form);
form[formName] += 10;
const fullName = name + " " + formName;
- const color = getMixedColor(s.color);
+ const color = brighter(getMixedColor(s.color, 0.2), 0.3);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
@@ -1317,7 +1317,7 @@ window.BurgsAndStates = (function () {
// generate "wild" province name
const cultureId = cells.culture[center];
const f = pack.features[cells.f[center]];
- const color = getMixedColor(s.color);
+ const color = brighter(getMixedColor(s.color, 0.2), 0.3);
const provCells = stateNoProvince.filter(i => cells.province[i] === province);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
diff --git a/src/modules/ui/options.js b/src/modules/ui/options.js
index 5c864778..fcb5dd6c 100644
--- a/src/modules/ui/options.js
+++ b/src/modules/ui/options.js
@@ -566,7 +566,7 @@ export function randomizeOptions() {
manorsInput.value = 1000;
manorsOutput.value = "auto";
}
- if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10);
+ if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10);
if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2);
if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
diff --git a/src/scripts/connectVertices.ts b/src/scripts/connectVertices.ts
index cec69aba..f493e257 100644
--- a/src/scripts/connectVertices.ts
+++ b/src/scripts/connectVertices.ts
@@ -88,38 +88,32 @@ function findStartingVertex({
throw new Error(`Markup: firstCell of feature ${featureId} has no neighbors of other features or external vertices`);
}
-const CONNECT_VERTICES_MAX_ITERATIONS = 50000;
+const MAX_ITERATIONS = 50000;
// connect vertices around feature
export function connectVertices({
vertices,
startingVertex,
ofSameType,
- checkedCellsMutable
+ addToChecked,
+ closeRing
}: {
vertices: IGraphVertices;
startingVertex: number;
ofSameType: (cellId: number) => boolean;
- checkedCellsMutable?: Uint8Array;
+ addToChecked?: (cellId: number) => void;
+ closeRing?: boolean;
}) {
const chain: number[] = []; // vertices chain to form a path
- const addToChecked = (cellIds: number[]) => {
- if (checkedCellsMutable) {
- cellIds.forEach(cellId => {
- checkedCellsMutable[cellId] = 1;
- });
- }
- };
-
let next = startingVertex;
- for (let i = 0; i === 0 || (next !== startingVertex && i < CONNECT_VERTICES_MAX_ITERATIONS); i++) {
+ for (let i = 0; i === 0 || (next !== startingVertex && i < MAX_ITERATIONS); i++) {
const previous = chain.at(-1);
const current = next;
chain.push(current);
const neibCells = vertices.c[current];
- addToChecked(neibCells);
+ if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
const [c1, c2, c3] = neibCells.map(ofSameType);
const [v1, v2, v3] = vertices.v[current];
@@ -134,5 +128,6 @@ export function connectVertices({
}
}
+ if (closeRing) chain.push(startingVertex);
return chain;
}
diff --git a/src/scripts/events/index.ts b/src/scripts/events/index.ts
index 0d3d7c8c..977cfb3f 100644
--- a/src/scripts/events/index.ts
+++ b/src/scripts/events/index.ts
@@ -4,14 +4,16 @@ import {openDialog} from "dialogs";
import {tip} from "scripts/tooltips";
import {handleMapClick} from "./onclick";
import {onMouseMove} from "./onhover";
-// @ts-expect-error js module
import {clearLegend, dragLegendBox} from "modules/legend";
export function setDefaultEventHandlers() {
window.Zoom.setZoomBehavior();
- viewbox.style("cursor", "default").on(".drag", null).on("click", handleMapClick);
- //.on("touchmove mousemove", onMouseMove);
+ viewbox
+ .style("cursor", "default")
+ .on(".drag", null)
+ .on("click", handleMapClick)
+ .on("touchmove mousemove", onMouseMove);
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor"));
diff --git a/src/scripts/events/onhover.ts b/src/scripts/events/onhover.ts
index e0b2693c..ac1ce961 100644
--- a/src/scripts/events/onhover.ts
+++ b/src/scripts/events/onhover.ts
@@ -1,9 +1,6 @@
import * as d3 from "d3";
import {layerIsOn} from "layers";
-// @ts-expect-error js module
-import {clearLegend, dragLegendBox} from "modules/legend";
-// @ts-expect-error js module
import {updateCellInfo} from "modules/ui/cell-info";
import {debounce} from "utils/functionUtils";
import {findCell, findGridCell, isLand} from "utils/graphUtils";
@@ -78,7 +75,7 @@ const getHoveredElement = (tagName: string, group: string, subgroup: string, isL
if (layerIsOn("togglePopulation")) return "populationLayer";
if (layerIsOn("toggleTemp")) return "temperatureLayer";
if (layerIsOn("toggleBiomes") && biome[cellId]) return "biomesLayer";
- if (layerIsOn("toggleReligions") && religion[cellId]) return "religionsLayer";
+ if (religion[cellId]) return "religionsLayer"; // layerIsOn("toggleReligions") &&
if (layerIsOn("toggleProvinces") || (layerIsOn("toggleStates") && state[cellId])) return "statesLayer";
if (layerIsOn("toggleCultures") && culture[cellId]) return "culturesLayer";
if (layerIsOn("toggleHeight")) return "heightLayer";
diff --git a/src/scripts/generation/generation.ts b/src/scripts/generation/generation.ts
index 256210e9..614840b4 100644
--- a/src/scripts/generation/generation.ts
+++ b/src/scripts/generation/generation.ts
@@ -26,6 +26,7 @@ import {createGrid} from "./grid/grid";
import {createPack} from "./pack/pack";
import {getInputValue, setInputValue} from "utils/nodeUtils";
import {calculateMapCoordinates} from "modules/coordinates";
+import {drawPoint} from "utils/debugUtils";
const {Zoom, ThreeD} = window;
@@ -69,6 +70,12 @@ async function generate(options?: IGenerationOptions) {
// renderLayer("biomes");
renderLayer("burgs");
renderLayer("routes");
+ // renderLayer("states");
+ renderLayer("religions");
+
+ // pack.cells.route.forEach((route, index) => {
+ // if (route === 2) drawPoint(pack.cells.p[index], {color: "black"});
+ // });
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
// showStatistics();
diff --git a/src/scripts/generation/pack/burgsAndStates/createStates.ts b/src/scripts/generation/pack/burgsAndStates/createStates.ts
index 8f232734..73c5591b 100644
--- a/src/scripts/generation/pack/burgsAndStates/createStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/createStates.ts
@@ -10,7 +10,7 @@ const {Names, COA} = window;
type TCapitals = ReturnType;
-export function createStates(capitals: TCapitals, cultures: TCultures) {
+export function createStates(capitals: TCapitals, cultures: TCultures): TStates {
TIME && console.time("createStates");
const colors = getColors(capitals.length);
@@ -28,7 +28,7 @@ export function createStates(capitals: TCapitals, cultures: TCultures) {
const shield = COA.getShield(cultureShield, null);
const coa: ICoa = {...COA.generate(null, null, null, type), shield};
- return {i: id, name, type, center: cellId, color, expansionism, capital: id, culture: cultureId, coa};
+ return {i: id, name, type, center: cellId, color, expansionism, capital: id, culture: cultureId, coa} as IState;
});
TIME && console.timeEnd("createStates");
diff --git a/src/scripts/generation/pack/burgsAndStates/createTowns.ts b/src/scripts/generation/pack/burgsAndStates/createTowns.ts
index c5e28243..fd13a14c 100644
--- a/src/scripts/generation/pack/burgsAndStates/createTowns.ts
+++ b/src/scripts/generation/pack/burgsAndStates/createTowns.ts
@@ -8,8 +8,8 @@ import {gauss} from "utils/probabilityUtils";
const {Names} = window;
export function createTowns(
- capitalCells: Map,
cultures: TCultures,
+ scoredCellIds: UintArray,
cells: Pick
) {
TIME && console.time("createTowns");
@@ -17,9 +17,6 @@ export function createTowns(
// randomize cells score a bit for more natural towns placement
const randomizeScore = (suitability: number) => suitability * gauss(1, 3, 0, 20, 3);
const scores = new Int16Array(cells.s.map(randomizeScore));
-
- // take populated cells without capitals
- const scoredCellIds = cells.i.filter(i => scores[i] > 0 && cells.culture[i] && !capitalCells.has(i));
scoredCellIds.sort((a, b) => scores[b] - scores[a]); // sort by randomized suitability score
const townsNumber = getTownsNumber();
@@ -53,13 +50,13 @@ function placeTowns(townsNumber: number, scoredCellIds: UintArray, points: TPoin
const townCells: number[] = [];
const townsQuadtree = d3.quadtree();
- const randomizeScaping = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2);
+ const randomizeSpacing = (spacing: number) => spacing * gauss(1, 0.3, 0.2, 2, 2);
for (const cellId of scoredCellIds) {
const [x, y] = points[cellId];
// randomize min spacing a bit to make placement not that uniform
- const currentSpacing = randomizeScaping(spacing);
+ const currentSpacing = randomizeSpacing(spacing);
if (townsQuadtree.find(x, y, currentSpacing) === undefined) {
townCells.push(cellId);
diff --git a/src/scripts/generation/pack/burgsAndStates/expandStates.ts b/src/scripts/generation/pack/burgsAndStates/expandStates.ts
index 85630262..1de4dcf8 100644
--- a/src/scripts/generation/pack/burgsAndStates/expandStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/expandStates.ts
@@ -3,12 +3,8 @@ import FlatQueue from "flatqueue";
import {TIME} from "config/logging";
import {getInputNumber} from "utils/nodeUtils";
import {minmax} from "utils/numberUtils";
-import type {createCapitals} from "./createCapitals";
-import type {createStates} from "./createStates";
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
-
-type TCapitals = ReturnType;
-type TStates = ReturnType;
+import {isNeutals} from "utils/typeUtils";
// growth algorithm to assign cells to states
export function expandStates(
@@ -104,13 +100,9 @@ export function expandStates(
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
- function isNeutrals(state: Entry): state is TNeutrals {
- return state.i === 0;
- }
-
function getState(stateId: number) {
const state = states[stateId];
- if (isNeutrals(state)) throw new Error("Neutrals cannot expand");
+ if (isNeutals(state)) throw new Error("Neutrals cannot expand");
return state;
}
diff --git a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
index 061d804e..eeefba07 100644
--- a/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
+++ b/src/scripts/generation/pack/burgsAndStates/generateBurgsAndStates.ts
@@ -16,7 +16,7 @@ export function generateBurgsAndStates(
vertices: IGraphVertices,
cells: Pick<
IPack["cells"],
- "v" | "c" | "p" | "i" | "g" | "h" | "f" | "t" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "culture"
+ "v" | "c" | "p" | "b" | "i" | "g" | "h" | "f" | "t" | "haven" | "harbor" | "r" | "fl" | "biome" | "s" | "culture"
>
): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates} {
const cellsNumber = cells.i.length;
@@ -34,9 +34,13 @@ export function generateBurgsAndStates(
const capitals = createCapitals(statesNumber, scoredCellIds, cultures, pick(cells, "p", "f", "culture"));
const capitalCells = new Map(capitals.map(({cell}) => [cell, true]));
-
const states = createStates(capitals, cultures);
- const towns = createTowns(capitalCells, cultures, pick(cells, "p", "i", "f", "s", "culture"));
+
+ const towns = createTowns(
+ cultures,
+ scoredCellIds.filter(i => !capitalCells.has(i)),
+ pick(cells, "p", "i", "f", "s", "culture")
+ );
const stateIds = expandStates(
capitalCells,
@@ -63,11 +67,12 @@ export function generateBurgsAndStates(
return {burgIds, stateIds, burgs, states};
function getScoredCellIds() {
- // cell score for capitals placement
const score = new Int16Array(cells.s.map(s => s * Math.random()));
- // filtered and sorted array of indexes
- const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]);
+ // filtered and sorted array of indexes: only populated cells not on map edge
+ const sorted = cells.i
+ .filter(i => !cells.b[i] && score[i] > 0 && cells.culture[i])
+ .sort((a, b) => score[b] - score[a]);
return sorted;
}
diff --git a/src/scripts/generation/pack/cultures.ts b/src/scripts/generation/pack/cultures.ts
index d26c197f..bf598782 100644
--- a/src/scripts/generation/pack/cultures.ts
+++ b/src/scripts/generation/pack/cultures.ts
@@ -18,6 +18,7 @@ import {minmax, rn} from "utils/numberUtils";
import {biased, P, rand} from "utils/probabilityUtils";
import {byId} from "utils/shorthands";
import {defaultNameBases} from "config/namebases";
+import {isCulture} from "utils/typeUtils";
const {COA} = window;
@@ -284,9 +285,7 @@ export const expandCultures = function (
const cultureIds = new Uint16Array(cells.h.length); // cell cultures
const queue = new FlatQueue<{cellId: number; cultureId: number}>();
- const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0;
- cultures.forEach(culture => {
- if (isWilderness(culture) || culture.removed) return;
+ cultures.filter(isCulture).forEach(culture => {
queue.push({cellId: culture.center, cultureId: culture.i}, 0);
});
@@ -323,7 +322,7 @@ export const expandCultures = function (
function getCulture(cultureId: number) {
const culture = cultures[cultureId];
- if (isWilderness(culture)) throw new Error("Wilderness culture cannot expand");
+ if (!isCulture(culture)) throw new Error("Wilderness cannot expand");
return culture;
}
diff --git a/src/scripts/generation/pack/generateRoutes.ts b/src/scripts/generation/pack/generateRoutes.ts
index 94ca3adc..b7f3e76a 100644
--- a/src/scripts/generation/pack/generateRoutes.ts
+++ b/src/scripts/generation/pack/generateRoutes.ts
@@ -4,8 +4,9 @@ import FlatQueue from "flatqueue";
import {TIME} from "config/logging";
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
import {dist2} from "utils/functionUtils";
+import {isBurg} from "utils/typeUtils";
-type TCellsData = Pick;
+type TCellsData = Pick;
export function generateRoutes(burgs: TBurgs, temp: Int8Array, cells: TCellsData) {
const cellRoutes = new Uint8Array(cells.h.length);
@@ -25,7 +26,6 @@ export function generateRoutes(burgs: TBurgs, temp: Int8Array, cells: TCellsData
const capitalsByFeature: Dict = {};
const portsByFeature: Dict = {};
- const isBurg = (burg: IBurg | TNoBurg): burg is IBurg => burg.i !== 0;
const addBurg = (object: Dict, feature: number, burg: IBurg) => {
if (!object[feature]) object[feature] = [];
object[feature].push(burg);
@@ -103,7 +103,7 @@ export function generateRoutes(burgs: TBurgs, temp: Int8Array, cells: TCellsData
const segments = findPathSegments({isWater: true, cellRoutes, connections, start, exit});
for (const segment of segments) {
- addConnections(segment, ROUTES.MAIN_ROAD);
+ addConnections(segment, ROUTES.SEA_ROUTE);
mainRoads.push({feature: Number(key), cells: segment});
}
});
@@ -118,7 +118,7 @@ export function generateRoutes(burgs: TBurgs, temp: Int8Array, cells: TCellsData
const cellId = segment[i];
const nextCellId = segment[i + 1];
if (nextCellId) connections.set(`${cellId}-${nextCellId}`, true);
- cellRoutes[cellId] = roadTypeId;
+ if (!cellRoutes[cellId]) cellRoutes[cellId] = roadTypeId;
}
}
diff --git a/src/scripts/generation/pack/pack.ts b/src/scripts/generation/pack/pack.ts
index 0b42466d..d110e75d 100644
--- a/src/scripts/generation/pack/pack.ts
+++ b/src/scripts/generation/pack/pack.ts
@@ -13,6 +13,7 @@ import {generateCultures, expandCultures} from "./cultures";
import {generateRivers} from "./rivers";
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
import {generateRoutes} from "./generateRoutes";
+import {generateReligions} from "./religions/generateReligions";
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
const {Biomes} = window;
@@ -103,7 +104,7 @@ export function createPack(grid: IGrid): IPack {
rawRivers,
vertices,
{
- ...pick(cells, "v", "c", "p", "i", "g"),
+ ...pick(cells, "v", "c", "p", "b", "i", "g"),
h: heights,
f: featureIds,
t: distanceField,
@@ -124,10 +125,29 @@ export function createPack(grid: IGrid): IPack {
h: heights,
t: distanceField,
biome,
- state: stateIds,
burg: burgIds
});
+ const {religionIds, religions} = generateReligions({
+ states,
+ cultures,
+ burgs,
+ cells: {
+ i: cells.i,
+ c: cells.c,
+ p: cells.p,
+ g: cells.g,
+ h: heights,
+ t: distanceField,
+ biome,
+ pop: population,
+ culture: cultureIds,
+ burg: burgIds,
+ state: stateIds,
+ route: cellRoutes
+ }
+ });
+
// Religions.generate();
// BurgsAndStates.defineStateForms();
// BurgsAndStates.generateProvinces();
@@ -167,15 +187,17 @@ export function createPack(grid: IGrid): IPack {
culture: cultureIds,
burg: burgIds,
state: stateIds,
- route: cellRoutes
- // religion, province
+ route: cellRoutes,
+ religion: religionIds,
+ province: new Uint16Array(cells.i.length)
},
features: mergedFeatures,
rivers: rawRivers, // "name" | "basin" | "type"
cultures,
states,
burgs,
- routes
+ routes,
+ religions
};
return pack;
diff --git a/src/scripts/generation/pack/religions/expandReligions.ts b/src/scripts/generation/pack/religions/expandReligions.ts
new file mode 100644
index 00000000..9b0809bf
--- /dev/null
+++ b/src/scripts/generation/pack/religions/expandReligions.ts
@@ -0,0 +1,88 @@
+import FlatQueue from "flatqueue";
+
+import {MIN_LAND_HEIGHT, ROUTES} from "config/generation";
+import {getInputNumber} from "utils/nodeUtils";
+import {gauss} from "utils/probabilityUtils";
+import {isReligion} from "utils/typeUtils";
+
+type TReligionData = Pick;
+type TCellsData = Pick;
+
+export function expandReligions(religions: TReligionData[], cells: TCellsData) {
+ const religionIds = spreadFolkReligions(religions, cells);
+
+ const queue = new FlatQueue<{cellId: number; religionId: number}>();
+ const cost: number[] = [];
+
+ const neutralInput = getInputNumber("neutralInput");
+ const maxExpansionCost = (cells.i.length / 20) * gauss(1, 0.3, 0.2, 2, 2) * neutralInput;
+
+ const biomePassageCost = (cellId: number) => biomesData.cost[cells.biome[cellId]];
+
+ for (const religion of religions) {
+ if (!isReligion(religion as IReligion) || (religion as IReligion).type === "Folk") continue;
+
+ const {i: religionId, center: cellId} = religion;
+ religionIds[cellId] = religionId;
+ cost[cellId] = 1;
+ queue.push({cellId, religionId}, 0);
+ }
+
+ const religionsMap = new Map(religions.map(religion => [religion.i, religion]));
+
+ const isMainRoad = (cellId: number) => cells.route[cellId] === ROUTES.MAIN_ROAD;
+ const isTrail = (cellId: number) => cells.route[cellId] === ROUTES.TRAIL;
+ const isSeaRoute = (cellId: number) => cells.route[cellId] === ROUTES.SEA_ROUTE;
+ const isWater = (cellId: number) => cells.h[cellId] < MIN_LAND_HEIGHT;
+
+ while (queue.length) {
+ const priority = queue.peekValue()!;
+ const {cellId, religionId} = queue.pop()!;
+
+ const {culture, center, expansion, expansionism} = religionsMap.get(religionId)!;
+
+ cells.c[cellId].forEach(neibCellId => {
+ if (expansion === "culture" && culture !== cells.culture[neibCellId]) return;
+ if (expansion === "state" && cells.state[center] !== cells.state[neibCellId]) return;
+
+ const cultureCost = culture !== cells.culture[neibCellId] ? 10 : 0;
+ const stateCost = cells.state[center] !== cells.state[neibCellId] ? 10 : 0;
+ const passageCost = getPassageCost(neibCellId);
+
+ const cellCost = cultureCost + stateCost + passageCost;
+ const totalCost = priority + 10 + cellCost / expansionism;
+ if (totalCost > maxExpansionCost) return;
+
+ if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
+ if (cells.culture[neibCellId]) religionIds[neibCellId] = religionId; // assign religion to cell
+ cost[neibCellId] = totalCost;
+
+ queue.push({cellId: neibCellId, religionId}, totalCost);
+ }
+ });
+ }
+
+ return religionIds;
+
+ function getPassageCost(cellId: number) {
+ if (isWater(cellId)) return isSeaRoute(cellId) ? 50 : 500;
+ if (isMainRoad(cellId)) return 1;
+ const biomeCost = biomePassageCost(cellId); // [1, 5000]
+ return isTrail(cellId) ? biomeCost / 1.5 : biomeCost;
+ }
+}
+
+// folk religions initially get all cells of their culture
+function spreadFolkReligions(religions: TReligionData[], cells: TCellsData) {
+ const religionIds = new Uint16Array(cells.i.length);
+
+ const folkReligions = religions.filter(({type}) => type === "Folk");
+ const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i]));
+
+ for (const cellId of cells.i) {
+ const cultureId = cells.culture[cellId];
+ religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0;
+ }
+
+ return religionIds;
+}
diff --git a/src/scripts/generation/pack/religions/generateDeityName.ts b/src/scripts/generation/pack/religions/generateDeityName.ts
new file mode 100644
index 00000000..d1342ec9
--- /dev/null
+++ b/src/scripts/generation/pack/religions/generateDeityName.ts
@@ -0,0 +1,38 @@
+import {religionsData} from "config/religionsData";
+import {ra} from "utils/probabilityUtils";
+
+const {Names} = window;
+
+const {base, approaches} = religionsData;
+
+export function getDeityName(cultures: TCultures, cultureId: number) {
+ if (cultureId === undefined) throw "CultureId is undefined";
+
+ const meaning = generateMeaning();
+
+ const base = cultures[cultureId].base;
+ const cultureName = Names.getBase(base);
+ return cultureName + ", The " + meaning;
+}
+
+export function generateMeaning() {
+ const approach = ra(approaches);
+ if (approach === "Number") return ra(base.number);
+ if (approach === "Being") return ra(base.being);
+ if (approach === "Adjective") return ra(base.adjective);
+ if (approach === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`;
+ if (approach === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`;
+ if (approach === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`;
+ if (approach === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`;
+ if (approach === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`;
+ if (approach === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`;
+ if (approach === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`;
+ if (approach === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`;
+ if (approach === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`;
+ if (approach === "Adjective + Being + of + Genitive")
+ return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`;
+ if (approach === "Adjective + Animal + of + Genitive")
+ return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`;
+
+ throw "Unknown generation approach";
+}
diff --git a/src/scripts/generation/pack/religions/generateFolkReligions.ts b/src/scripts/generation/pack/religions/generateFolkReligions.ts
new file mode 100644
index 00000000..37f05c72
--- /dev/null
+++ b/src/scripts/generation/pack/religions/generateFolkReligions.ts
@@ -0,0 +1,14 @@
+import {religionsData} from "config/religionsData";
+import {rw} from "utils/probabilityUtils";
+import {isCulture} from "utils/typeUtils";
+
+const {forms} = religionsData;
+
+export function generateFolkReligions(cultures: TCultures): Pick[] {
+ return cultures.filter(isCulture).map(culture => {
+ const {i: cultureId, center} = culture;
+ const form = rw(forms.Folk);
+
+ return {type: "Folk", form, culture: cultureId, center};
+ });
+}
diff --git a/src/scripts/generation/pack/religions/generateOrganizedReligions.ts b/src/scripts/generation/pack/religions/generateOrganizedReligions.ts
new file mode 100644
index 00000000..9049a0be
--- /dev/null
+++ b/src/scripts/generation/pack/religions/generateOrganizedReligions.ts
@@ -0,0 +1,69 @@
+import * as d3 from "d3";
+
+import {WARN} from "config/logging";
+import {religionsData} from "config/religionsData";
+import {getInputNumber} from "utils/nodeUtils";
+import {rand, rw} from "utils/probabilityUtils";
+import {isBurg} from "utils/typeUtils";
+
+const {forms} = religionsData;
+
+export function generateOrganizedReligions(
+ burgs: TBurgs,
+ cells: Pick
+): Pick[] {
+ const religionsNumber = getInputNumber("religionsInput");
+ if (religionsNumber === 0) return [];
+
+ const canditateCells = getCandidateCells();
+ const religionCells = placeReligions();
+
+ const cultsNumber = Math.floor((rand(1, 4) / 10) * religionCells.length); // 10-40%
+ const heresiesNumber = Math.floor((rand(0, 2) / 10) * religionCells.length); // 0-20%
+ const organizedNumber = religionCells.length - cultsNumber - heresiesNumber;
+
+ const getType = (index: number) => {
+ if (index < organizedNumber) return "Organized";
+ if (index < organizedNumber + cultsNumber) return "Cult";
+ return "Heresy";
+ };
+
+ return religionCells.map((cellId, index) => {
+ const type = getType(index);
+ const form = rw(forms[type]);
+ const cultureId = cells.culture[cellId];
+
+ return {type, form, culture: cultureId, center: cellId};
+ });
+
+ function placeReligions() {
+ const religionCells = [];
+ const religionsTree = d3.quadtree();
+
+ // min distance between religions
+ const spacing = (graphWidth + graphHeight) / 2 / religionsNumber;
+
+ for (const cellId of canditateCells) {
+ const [x, y] = cells.p[cellId];
+
+ if (religionsTree.find(x, y, spacing) === undefined) {
+ religionCells.push(cellId);
+ religionsTree.add([x, y]);
+
+ if (religionCells.length === religionsNumber) return religionCells;
+ }
+ }
+
+ WARN && console.warn(`Placed only ${religionCells.length} of ${religionsNumber} religions`);
+ return religionCells;
+ }
+
+ function getCandidateCells() {
+ const validBurgs = burgs.filter(isBurg);
+
+ if (validBurgs.length >= religionsNumber)
+ return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell);
+
+ return cells.i.filter(i => cells.pop[i] > 2).sort((a, b) => cells.pop[b] - cells.pop[a]);
+ }
+}
diff --git a/src/scripts/generation/pack/religions/generateReligionName.ts b/src/scripts/generation/pack/religions/generateReligionName.ts
new file mode 100644
index 00000000..967bd820
--- /dev/null
+++ b/src/scripts/generation/pack/religions/generateReligionName.ts
@@ -0,0 +1,120 @@
+import {religionsData} from "config/religionsData";
+import {trimVowels, getAdjective} from "utils/languageUtils";
+import {rw, ra} from "utils/probabilityUtils";
+import {generateMeaning} from "./generateDeityName";
+
+const {Names} = window;
+const {namingMethods, types} = religionsData;
+
+interface IContext {
+ cultureId: number;
+ stateId: number;
+ burgId: number;
+ cultures: TCultures;
+ states: TStates;
+ burgs: TBurgs;
+ form: string;
+ supreme: string;
+}
+
+const context = {
+ data: {} as IContext,
+
+ // data setter
+ set current(data: IContext) {
+ this.data = data;
+ },
+
+ // data getters
+ get culture() {
+ return this.data.cultures[this.data.cultureId];
+ },
+
+ get state() {
+ return this.data.states[this.data.stateId];
+ },
+
+ get burg() {
+ return this.data.burgs[this.data.burgId];
+ },
+
+ get form() {
+ return this.data.form;
+ },
+
+ get supreme() {
+ return this.data.supreme;
+ },
+
+ // generation methods
+ get random() {
+ return Names.getBase(this.culture.base);
+ },
+
+ get type() {
+ return rw(types[this.form as keyof typeof types]);
+ },
+
+ get supremeName() {
+ return this.supreme.split(/[ ,]+/)[0];
+ },
+
+ get cultureName() {
+ return this.culture.name;
+ },
+
+ get place() {
+ const base = this.burg.name || this.state.name;
+ return trimVowels(base.split(/[ ,]+/)[0]);
+ },
+
+ get meaning() {
+ return generateMeaning();
+ }
+};
+
+const nameMethodsMap = {
+ "Random + type": {getName: () => `${context.random} ${context.type}`, expansion: "global"},
+ "Random + ism": {getName: () => `${trimVowels(context.random)}ism`, expansion: "global"},
+ "Supreme + ism": {getName: () => `${trimVowels(context.supremeName)}ism`, expansion: "global"},
+ "Faith of + Supreme": {
+ getName: () => `${ra(["Faith", "Way", "Path", "Word", "Witnesses"])} of ${context.supremeName}`,
+ expansion: "global"
+ },
+ "Place + ism": {getName: () => `${context.place}ism`, expansion: "state"},
+ "Culture + ism": {getName: () => `${trimVowels(context.cultureName)}ism`, expansion: "culture"},
+ "Place + ian + type": {
+ getName: () => `${getAdjective(context.place)} ${context.type}`,
+ expansion: "state"
+ },
+ "Culture + type": {getName: () => `${context.cultureName} ${context.type}`, expansion: "culture"},
+ "Burg + ian + type": {
+ getName: () => context.burg.name && `${getAdjective(context.burg.name)} ${context.type}`,
+ expansion: "global"
+ },
+ "Random + ian + type": {getName: () => `${getAdjective(context.random)} ${context.type}`, expansion: "global"},
+ "Type + of the + meaning": {getName: () => `${context.type} of the ${context.meaning}`, expansion: "global"}
+};
+
+const fallbackMethod = nameMethodsMap["Random + type"];
+
+function getMethod(type: IReligion["type"]) {
+ const methods: {[key in string]: number} = namingMethods[type];
+ const method = rw(methods);
+ return nameMethodsMap[method as keyof typeof nameMethodsMap];
+}
+
+export function generateReligionName(
+ type: IReligion["type"],
+ data: IContext
+): {name: string; expansion: IReligion["expansion"]} {
+ context.current = data;
+ const method = getMethod(type);
+ const name = method.getName() || fallbackMethod.getName();
+
+ let expansion = method.expansion as IReligion["expansion"];
+ if (expansion === "state" && !data.stateId) expansion = "global";
+ else if (expansion === "culture" && !data.cultureId) expansion = "global";
+
+ return {name, expansion};
+}
diff --git a/src/scripts/generation/pack/religions/generateReligions.ts b/src/scripts/generation/pack/religions/generateReligions.ts
new file mode 100644
index 00000000..aafa2d27
--- /dev/null
+++ b/src/scripts/generation/pack/religions/generateReligions.ts
@@ -0,0 +1,38 @@
+import {TIME} from "config/logging";
+import {drawPoint} from "utils/debugUtils";
+import {pick} from "utils/functionUtils";
+import {generateFolkReligions} from "./generateFolkReligions";
+import {generateOrganizedReligions} from "./generateOrganizedReligions";
+import {specifyReligions} from "./specifyReligions";
+
+type TCellsData = Pick<
+ IPack["cells"],
+ "i" | "c" | "p" | "g" | "h" | "t" | "biome" | "pop" | "culture" | "burg" | "state" | "route"
+>;
+
+export function generateReligions({
+ states,
+ cultures,
+ burgs,
+ cells
+}: {
+ states: TStates;
+ cultures: TCultures;
+ burgs: TBurgs;
+ cells: TCellsData;
+}) {
+ TIME && console.time("generateReligions");
+
+ const folkReligions = generateFolkReligions(cultures);
+ const basicReligions = generateOrganizedReligions(burgs, pick(cells, "i", "p", "pop", "culture"));
+ const {religions, religionIds} = specifyReligions(
+ [...folkReligions, ...basicReligions],
+ cultures,
+ states,
+ burgs,
+ pick(cells, "i", "c", "h", "biome", "culture", "burg", "state", "route")
+ );
+
+ TIME && console.timeEnd("generateReligions");
+ return {religionIds, religions};
+}
diff --git a/src/scripts/generation/pack/religions/specifyReligions.ts b/src/scripts/generation/pack/religions/specifyReligions.ts
new file mode 100644
index 00000000..d35be07e
--- /dev/null
+++ b/src/scripts/generation/pack/religions/specifyReligions.ts
@@ -0,0 +1,146 @@
+import {brighter, darker, getMixedColor} from "utils/colorUtils";
+import {each, gauss} from "utils/probabilityUtils";
+import {isCulture} from "utils/typeUtils";
+import {expandReligions} from "./expandReligions";
+import {getDeityName} from "./generateDeityName";
+import {generateReligionName} from "./generateReligionName";
+
+const expansionismMap = {
+ Folk: () => 0,
+ Organized: () => gauss(5, 3, 0, 10, 1),
+ Cult: () => gauss(0.5, 0.5, 0, 5, 1),
+ Heresy: () => gauss(1, 0.5, 0, 5, 1)
+};
+
+type TReligionData = Pick;
+type TCellsData = Pick;
+
+export function specifyReligions(
+ religionsData: TReligionData[],
+ cultures: TCultures,
+ states: TStates,
+ burgs: TBurgs,
+ cells: TCellsData
+): {religions: TReligions; religionIds: Uint16Array} {
+ const rawReligions = religionsData.map(({type, form, culture: cultureId, center}, index) => {
+ const supreme = getDeityName(cultures, cultureId);
+ const deity = form === "Non-theism" || form === "Animism" ? null : supreme;
+
+ const stateId = cells.state[center];
+ const burgId = cells.burg[center];
+
+ const {name, expansion} = generateReligionName(type, {
+ cultureId,
+ stateId,
+ burgId,
+ cultures,
+ states,
+ burgs,
+ form,
+ supreme
+ });
+
+ const expansionism = expansionismMap[type]();
+
+ const color = getReligionColor(cultureId, type);
+
+ return {i: index + 1, name, type, form, culture: cultureId, center, deity, expansion, expansionism, color};
+ });
+
+ const religionIds = expandReligions(rawReligions, cells);
+ const names = renameOldReligions(rawReligions);
+ const origins = defineOrigins(religionIds, rawReligions, cells.c);
+
+ return {religions: combineReligionsData(), religionIds};
+
+ function getReligionColor(cultureId: number, type: IReligion["type"]) {
+ const culture = cultures[cultureId];
+ if (!isCulture(culture)) throw new Error(`Culture ${cultureId} is not a valid culture`);
+
+ if (type === "Folk") return culture.color;
+ if (type === "Heresy") return darker(getMixedColor(culture.color, 0.35), 0.3);
+ if (type === "Cult") return darker(getMixedColor(culture.color, 0.5), 0.8);
+ return brighter(getMixedColor(culture.color, 0.25), 0.3);
+ }
+
+ function combineReligionsData(): TReligions {
+ const noReligion: TNoReligion = {i: 0, name: "No religion"};
+
+ const religions = rawReligions.map((religion, index) => ({
+ ...religion,
+ name: names[index],
+ origins: origins[index]
+ }));
+
+ return [noReligion, ...religions];
+ }
+}
+
+// add 'Old' to names of folk religions which have organized competitors
+function renameOldReligions(religions: Pick[]) {
+ return religions.map(({name, type, culture: cultureId}) => {
+ if (type !== "Folk") return name;
+
+ const haveOrganized = religions.some(
+ ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
+ );
+ if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`;
+ return name;
+ });
+}
+
+const religionOriginsParamsMap = {
+ Organized: {clusterSize: 100, maxReligions: 2},
+ Cult: {clusterSize: 50, maxReligions: 3},
+ Heresy: {clusterSize: 50, maxReligions: 43}
+};
+
+function defineOrigins(
+ religionIds: Uint16Array,
+ religions: Pick[],
+ neighbors: number[][]
+) {
+ return religions.map(religion => {
+ if (religion.type === "Folk") return [0];
+
+ const {i, type, culture: cultureId, expansion, center} = religion;
+
+ const folkReligion = religions.find(({culture, type}) => type === "Folk" || culture === cultureId);
+ const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center);
+
+ if (isFolkBased) return [folkReligion.i];
+
+ const {clusterSize, maxReligions} = religionOriginsParamsMap[type];
+ const origins = getReligionsInRadius(neighbors, center, religionIds, i, clusterSize, maxReligions);
+ return origins;
+ });
+}
+
+function getReligionsInRadius(
+ neighbors: number[][],
+ center: number,
+ religionIds: Uint16Array,
+ religionId: number,
+ clusterSize: number,
+ maxReligions: number
+) {
+ const religions = new Set();
+ const queue = [center];
+ const checked = <{[key: number]: true}>{};
+
+ for (let size = 0; queue.length && size < clusterSize; size++) {
+ const cellId = queue.pop()!;
+ checked[center] = true;
+
+ for (const neibId of neighbors[cellId]) {
+ if (checked[neibId]) continue;
+ checked[neibId] = true;
+
+ const neibReligion = religionIds[neibId];
+ if (neibReligion && neibReligion !== religionId) religions.add(neibReligion);
+ queue.push(neibId);
+ }
+ }
+
+ return religions.size ? [...religions].slice(0, maxReligions) : [0];
+}
diff --git a/src/types/common.d.ts b/src/types/common.d.ts
index 8148e129..6cc35379 100644
--- a/src/types/common.d.ts
+++ b/src/types/common.d.ts
@@ -2,20 +2,22 @@ type Logical = number & (1 | 0); // data type for logical numbers
type UnknownObject = {[key: string]: unknown};
-// extract element from array
-type Entry = T[number];
-
type noop = () => void;
interface Dict {
[key: string]: T;
}
+// extract element from array
+type Entry = T[number];
+
// element of Object.entries
type ObjectEntry = [string, T];
type UintArray = Uint8Array | Uint16Array | Uint32Array;
type IntArray = Int8Array | Int16Array | Int32Array;
+type FloatArray = Float32Array | Float64Array;
+type TypedArray = UintArray | IntArray | FloatArray;
type RGB = `rgb(${number}, ${number}, ${number})`;
type Hex = `#${string}`;
diff --git a/src/types/pack/features.d.ts b/src/types/pack/features.d.ts
index 2ae8f95a..13cd5b22 100644
--- a/src/types/pack/features.d.ts
+++ b/src/types/pack/features.d.ts
@@ -35,6 +35,6 @@ interface IPackFeatureLake extends IPackFeatureBase {
type TPackFeature = IPackFeatureOcean | IPackFeatureIsland | IPackFeatureLake;
-type FirstElement = 0;
+type TNoFeature = 0;
-type TPackFeatures = [FirstElement, ...TPackFeature[]];
+type TPackFeatures = [TNoFeature, ...TPackFeature[]];
diff --git a/src/types/pack/pack.d.ts b/src/types/pack/pack.d.ts
index d1a3302a..71bbe374 100644
--- a/src/types/pack/pack.d.ts
+++ b/src/types/pack/pack.d.ts
@@ -3,10 +3,10 @@ interface IPack extends IGraph {
features: TPackFeatures;
states: TStates;
cultures: TCultures;
- provinces: IProvince[];
+ provinces: TProvinces;
burgs: TBurgs;
- rivers: IRiver[];
- religions: IReligion[];
+ rivers: TRivers;
+ religions: TReligions;
routes: TRoutes;
}
@@ -23,9 +23,9 @@ interface IPackCells {
conf: Uint16Array; // conluence, defined by defineRivers() in river-generator.ts
biome: Uint8Array;
area: UintArray;
- state: UintArray;
+ state: Uint16Array;
culture: Uint16Array;
- religion: UintArray;
+ religion: Uint16Array;
province: UintArray;
burg: UintArray;
haven: UintArray;
@@ -38,34 +38,3 @@ interface IPackBase extends IGraph {
cells: IGraphCells & Partial;
features?: TPackFeatures;
}
-
-interface IProvince {
- i: number;
- name: string;
- fullName: string;
- removed?: boolean;
-}
-
-interface IReligion {
- i: number;
- name: string;
- type: "Folk" | "Orgamized" | "Cult" | "Heresy";
- removed?: boolean;
-}
-
-interface IRiver {
- i: number;
- name: string;
- basin: number;
- parent: number;
- type: string;
- source: number;
- mouth: number;
- sourceWidth: number;
- width: number;
- widthFactor: number;
- length: number;
- discharge: number;
- cells: number[];
- points?: number[];
-}
diff --git a/src/types/pack/provinces.d.ts b/src/types/pack/provinces.d.ts
new file mode 100644
index 00000000..502b5b80
--- /dev/null
+++ b/src/types/pack/provinces.d.ts
@@ -0,0 +1,8 @@
+interface IProvince {
+ i: number;
+ name: string;
+ fullName: string;
+ removed?: boolean;
+}
+
+type TProvinces = IProvince[];
diff --git a/src/types/pack/religions.d.ts b/src/types/pack/religions.d.ts
new file mode 100644
index 00000000..d99b652c
--- /dev/null
+++ b/src/types/pack/religions.d.ts
@@ -0,0 +1,21 @@
+interface IReligion {
+ i: number;
+ name: string;
+ type: "Folk" | "Organized" | "Cult" | "Heresy";
+ color: string;
+ culture: number;
+ form: any;
+ deity: string | null;
+ center: number;
+ origins: number[];
+ expansion?: "global" | "culture" | "state";
+ expansionism: number;
+ removed?: boolean;
+}
+
+type TNoReligion = {
+ i: 0;
+ name: "No religion";
+};
+
+type TReligions = [TNoReligion, ...IReligion[]];
diff --git a/src/types/pack/rivers.d.ts b/src/types/pack/rivers.d.ts
new file mode 100644
index 00000000..c682fcf5
--- /dev/null
+++ b/src/types/pack/rivers.d.ts
@@ -0,0 +1,18 @@
+interface IRiver {
+ i: number;
+ name: string;
+ basin: number;
+ parent: number;
+ type: string;
+ source: number;
+ mouth: number;
+ sourceWidth: number;
+ width: number;
+ widthFactor: number;
+ length: number;
+ discharge: number;
+ cells: number[];
+ points?: number[];
+}
+
+type TRivers = IRiver[];
diff --git a/src/types/pack/states.d.ts b/src/types/pack/states.d.ts
index 01c216e8..1a104e4b 100644
--- a/src/types/pack/states.d.ts
+++ b/src/types/pack/states.d.ts
@@ -9,6 +9,7 @@ interface IState {
fullName: string;
capital: Logical;
coa: ICoa | string;
+ // pole: TPoint ?
removed?: boolean;
}
diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts
index bdd05c93..4d1834a8 100644
--- a/src/utils/colorUtils.ts
+++ b/src/utils/colorUtils.ts
@@ -15,8 +15,9 @@ const cardinal12: Hex[] = [
"#eb8de7"
];
-type ColorScheme = d3.ScaleSequential;
-const colorSchemeMap: Dict = {
+export type TColorScheme = "default" | "bright" | "light" | "green" | "rainbow" | "monochrome";
+const colorSchemeMap: {[key in TColorScheme]: d3.ScaleSequential} = {
+ default: d3.scaleSequential(d3.interpolateSpectral),
bright: d3.scaleSequential(d3.interpolateSpectral),
light: d3.scaleSequential(d3.interpolateRdYlGn),
green: d3.scaleSequential(d3.interpolateGreens),
@@ -24,10 +25,10 @@ const colorSchemeMap: Dict = {
monochrome: d3.scaleSequential(d3.interpolateGreys)
};
-export function getColors(number: number) {
+export function getColors(number: number): Hex[] {
if (number <= cardinal12.length) return d3.shuffle(cardinal12.slice(0, number));
- const scheme = colorSchemeMap.bright;
+ const scheme = colorSchemeMap.default;
const colors = d3.range(number).map(index => {
if (index < 12) return cardinal12[index];
@@ -39,22 +40,31 @@ export function getColors(number: number) {
}
export function getRandomColor(): Hex {
- const scheme = colorSchemeMap.bright;
+ const scheme = d3.scaleSequential(d3.interpolateSpectral);
const rgb = scheme(Math.random())!;
return d3.color(rgb)?.formatHex() as Hex;
}
-// mix a color with a random color
-export function getMixedColor(hexColor: string, mixation = 0.2, bright = 0.3) {
- // if provided color is not hex (e.g. harching), generate random one
- const color1 = hexColor && hexColor[0] === "#" ? hexColor : getRandomColor();
+// mix a color with a random color. TODO: refactor without interpolation
+export function getMixedColor(color: Hex | CssUrl, mixation: number) {
+ const color1 = color.startsWith("#") ? color : getRandomColor();
const color2 = getRandomColor();
const mixedColor = d3.interpolate(color1, color2)(mixation);
- return d3.color(mixedColor)!.brighter(bright).hex();
+ return d3.color(mixedColor)!.formatHex() as Hex;
}
-export function getColorScheme(schemeName: string) {
+export function darker(color: Hex | CssUrl, amount = 1) {
+ if (color.startsWith("#") === false) return color;
+ return d3.color(color)!.darker(amount).formatHex() as Hex;
+}
+
+export function brighter(color: Hex | CssUrl, amount = 1) {
+ if (color.startsWith("#") === false) return color;
+ return d3.color(color)!.brighter(amount).formatHex() as Hex;
+}
+
+export function getColorScheme(schemeName: TColorScheme) {
return colorSchemeMap[schemeName] || colorSchemeMap.bright;
}
diff --git a/src/utils/debugUtils.ts b/src/utils/debugUtils.ts
index 623d66a5..f72c614f 100644
--- a/src/utils/debugUtils.ts
+++ b/src/utils/debugUtils.ts
@@ -1,5 +1,6 @@
// utils to be used for debugging (not in PROD)
+import {getColorScheme, TColorScheme} from "./colorUtils";
import {getNormal} from "./lineUtils";
export function drawPoint([x, y]: TPoint, {radius = 1, color = "red"} = {}) {
@@ -55,3 +56,49 @@ export function drawText(text: string | number, [x, y]: TPoint, {size = 6, color
.attr("stroke", "none")
.text(text);
}
+
+export function drawPolygons(
+ values: TypedArray | number[],
+ cellVertices: number[][],
+ vertexPoints: TPoints,
+ {
+ fillOpacity = 0.3,
+ stroke = "#222",
+ strokeWidth = 0.2,
+ colorScheme = "default",
+ excludeZeroes = false
+ }: {
+ fillOpacity?: number;
+ stroke?: string;
+ strokeWidth?: number;
+ colorScheme?: TColorScheme;
+ excludeZeroes?: boolean;
+ } = {}
+) {
+ const cellIds = [...Array(values.length).keys()];
+ const data = excludeZeroes ? cellIds.filter(id => values[id] !== 0) : cellIds;
+
+ const getPolygon = (id: number) => {
+ const vertices = cellVertices[id];
+ const points = vertices.map(id => vertexPoints[id]);
+ return `${points.join(" ")} ${points[0].join(",")}`;
+ };
+
+ // get fill from normalizing and interpolating values to color scheme
+ const min = Math.min(...values);
+ const max = Math.max(...values);
+ const normalized = Array.from(values).map(value => (value - min) / (max - min));
+ const scheme = getColorScheme(colorScheme);
+ const getFill = (id: number) => scheme(normalized[id])!;
+
+ debug
+ .selectAll("polyline")
+ .data(data)
+ .enter()
+ .append("polyline")
+ .attr("points", getPolygon)
+ .attr("fill", getFill)
+ .attr("fill-opacity", fillOpacity)
+ .attr("stroke", stroke)
+ .attr("stroke-width", strokeWidth);
+}
diff --git a/src/utils/lineUtils.ts b/src/utils/lineUtils.ts
index 0d837f12..9674a7ad 100644
--- a/src/utils/lineUtils.ts
+++ b/src/utils/lineUtils.ts
@@ -63,12 +63,12 @@ function getPointOffCanvasSide([x, y]: TPoint) {
// remove intermediate out-of-canvas points from polyline
export function filterOutOfCanvasPoints(points: TPoints) {
- const pointsOutSide = points.map(getPointOffCanvasSide);
+ const pointsOutside = points.map(getPointOffCanvasSide);
const SAFE_ZONE = 3;
- const fragment = (i: number) => sliceFragment(pointsOutSide, i, SAFE_ZONE);
+ const fragment = (i: number) => sliceFragment(pointsOutside, i, SAFE_ZONE);
const filterOutCanvasPoint = (i: number) => {
- const pointSide = pointsOutSide[i];
+ const pointSide = pointsOutside[i];
return !pointSide || fragment(i).some(side => !side || side !== pointSide);
};
diff --git a/src/utils/probabilityUtils.ts b/src/utils/probabilityUtils.ts
index 08695bdc..95cffd7d 100644
--- a/src/utils/probabilityUtils.ts
+++ b/src/utils/probabilityUtils.ts
@@ -42,10 +42,9 @@ export function ra(array: T[]) {
}
// return random value from weighted array
-export function rw(object: {[key: string]: number}) {
- const weightedArray = Object.entries(object)
- .map(([choise, weight]) => new Array(weight).fill(choise))
- .flat();
+export function rw(object: {[key in T]: number}) {
+ const entries = Object.entries(object);
+ const weightedArray: T[] = entries.map(([choise, weight]) => new Array(weight).fill(choise)).flat();
return ra(weightedArray);
}
diff --git a/src/utils/typeUtils.ts b/src/utils/typeUtils.ts
new file mode 100644
index 00000000..8248144b
--- /dev/null
+++ b/src/utils/typeUtils.ts
@@ -0,0 +1,16 @@
+export const isFeature = (feature: TNoFeature | TPackFeature): feature is TPackFeature => feature !== 0;
+
+export const isLake = (feature: TNoFeature | TPackFeature): feature is IPackFeatureLake =>
+ isFeature(feature) && feature.type === "lake";
+
+export const isState = (state: TNeutrals | IState): state is IState => state.i !== 0 && !(state as IState).removed;
+
+export const isNeutals = (neutrals: TNeutrals | IState): neutrals is TNeutrals => neutrals.i === 0;
+
+export const isCulture = (culture: TWilderness | ICulture): culture is ICulture =>
+ culture.i !== 0 && !(culture as ICulture).removed;
+
+export const isBurg = (burg: TNoBurg | IBurg): burg is IBurg => burg.i !== 0 && !(burg as IBurg).removed;
+
+export const isReligion = (religion: TNoReligion | IReligion): religion is IReligion =>
+ religion.i !== 0 && !(religion as IReligion).removed;
diff --git a/yarn.lock b/yarn.lock
index b0f6d4ff..e5e80b0e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -422,6 +422,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==
+"@types/polylabel@^1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043"
+ integrity sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w==
+
"@types/qs@^6.2.31":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|