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"