mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
commit
60e69348a9
58 changed files with 1365 additions and 507 deletions
|
|
@ -1639,7 +1639,7 @@
|
|||
</td>
|
||||
<td>Religions number</td>
|
||||
<td>
|
||||
<input id="religionsInput" data-stored="religions" type="range" min="0" max="50" step="1" value="15" />
|
||||
<input id="religionsInput" data-stored="religions" type="range" min="0" max="50" step="1" />
|
||||
</td>
|
||||
<td>
|
||||
<output id="religionsOutput" data-stored="religions" value="auto"></output>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
358
src/config/religionsData.ts
Normal file
358
src/config/religionsData.ts
Normal file
|
|
@ -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<number>(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};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
29
src/layers/renderers/drawBiomes.ts
Normal file
29
src/layers/renderers/drawBiomes.ts
Normal file
|
|
@ -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 */ `
|
||||
<path d="${waterGap}" fill="none" stroke="${color}" id="biome-gap${index}" />
|
||||
<path d="${fill}" fill="${color}" stroke="none" id="biome${index}" />
|
||||
`;
|
||||
});
|
||||
|
||||
byId("biomes")!.innerHTML = htmlPaths.join("");
|
||||
}
|
||||
|
|
@ -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 */ `
|
||||
<path d="${waterGap}" fill="none" stroke="${color}" id="culture-gap${index}" />
|
||||
<path d="${fill}" fill="${color}" stroke="none" id="culture${index}" />
|
||||
`;
|
||||
});
|
||||
|
||||
byId("cults")!.innerHTML = htmlPaths.join("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
28
src/layers/renderers/drawReligions.ts
Normal file
28
src/layers/renderers/drawReligions.ts
Normal file
|
|
@ -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 */ `
|
||||
<path d="${waterGap}" fill="none" stroke="${color}" id="religion-gap${index}" />
|
||||
<path d="${fill}" fill="${color}" stroke="none" id="religion${index}" />
|
||||
`;
|
||||
});
|
||||
|
||||
byId("relig")!.innerHTML = htmlPaths.join("");
|
||||
}
|
||||
|
|
@ -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(`<path id="${type}${i}" d="${path}"/>`);
|
||||
}
|
||||
|
||||
routes.selectAll("path").remove();
|
||||
for (const type in routePaths) {
|
||||
routes.select(`[data-type=${type}]`).html(routePaths[type].join(""));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join("");
|
||||
const gapString = gapData.map(d => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join("");
|
||||
const clipString = bodyData
|
||||
.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`)
|
||||
.join("");
|
||||
const haloString = haloData
|
||||
.map(
|
||||
d =>
|
||||
`<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${
|
||||
d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666"
|
||||
}"/>`
|
||||
)
|
||||
.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();
|
||||
}
|
||||
48
src/layers/renderers/drawStates.ts
Normal file
48
src/layers/renderers/drawStates.ts
Normal file
|
|
@ -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 */ `
|
||||
<path d="${waterGap}" fill="none" stroke="${color}" id="state-gap${index}" />
|
||||
<path d="${fill}" fill="${color}" stroke="none" id="state${index}" />
|
||||
`);
|
||||
|
||||
clipPaths.push(/* html */ `
|
||||
<clipPath id="state-clip${index}"><use href="#state${index}"/></clipPath>
|
||||
`);
|
||||
|
||||
haloPaths.push(/* html */ `
|
||||
<path id="state-border${index}" d="${halo}" clip-path="url(#state-clip${index})" stroke="${haloColor}"/>
|
||||
`);
|
||||
}
|
||||
|
||||
byId("statesBody")!.innerHTML = bodyPaths.join("");
|
||||
byId("statePaths")!.innerHTML = clipPaths.join("");
|
||||
byId("statesHalo")!.innerHTML = haloPaths.join("");
|
||||
|
||||
/* global */ window.Zoom.invoke();
|
||||
}
|
||||
106
src/layers/renderers/utils/getVertexPaths.ts
Normal file
106
src/layers/renderers/utils/getVertexPaths.ts
Normal file
|
|
@ -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<IPack["cells"], "c" | "v" | "b" | "h" | "f">;
|
||||
options: {[key in keyof TPath]: boolean};
|
||||
}) {
|
||||
const paths: Dict<TPath> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -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<string> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const {Names, COA} = window;
|
|||
|
||||
type TCapitals = ReturnType<typeof createCapitals>;
|
||||
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import {gauss} from "utils/probabilityUtils";
|
|||
const {Names} = window;
|
||||
|
||||
export function createTowns(
|
||||
capitalCells: Map<number, boolean>,
|
||||
cultures: TCultures,
|
||||
scoredCellIds: UintArray,
|
||||
cells: Pick<IPack["cells"], "p" | "i" | "f" | "s" | "culture">
|
||||
) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<typeof createCapitals>;
|
||||
type TStates = ReturnType<typeof createStates>;
|
||||
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<TStates>): 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IPack["cells"], "c" | "p" | "g" | "h" | "t" | "haven" | "biome" | "state" | "burg">;
|
||||
type TCellsData = Pick<IPack["cells"], "c" | "p" | "g" | "h" | "t" | "biome" | "burg">;
|
||||
|
||||
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<IBurg[]> = {};
|
||||
const portsByFeature: Dict<IBurg[]> = {};
|
||||
|
||||
const isBurg = (burg: IBurg | TNoBurg): burg is IBurg => burg.i !== 0;
|
||||
const addBurg = (object: Dict<IBurg[]>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
88
src/scripts/generation/pack/religions/expandReligions.ts
Normal file
88
src/scripts/generation/pack/religions/expandReligions.ts
Normal file
|
|
@ -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<IReligion, "i" | "type" | "center" | "culture" | "expansion" | "expansionism">;
|
||||
type TCellsData = Pick<IPack["cells"], "i" | "c" | "h" | "biome" | "culture" | "state" | "route">;
|
||||
|
||||
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<number, TReligionData>(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<number, number>(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;
|
||||
}
|
||||
38
src/scripts/generation/pack/religions/generateDeityName.ts
Normal file
38
src/scripts/generation/pack/religions/generateDeityName.ts
Normal file
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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<IReligion, "type" | "form" | "culture" | "center">[] {
|
||||
return cultures.filter(isCulture).map(culture => {
|
||||
const {i: cultureId, center} = culture;
|
||||
const form = rw<string>(forms.Folk);
|
||||
|
||||
return {type: "Folk", form, culture: cultureId, center};
|
||||
});
|
||||
}
|
||||
|
|
@ -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<IPack["cells"], "i" | "p" | "pop" | "culture">
|
||||
): Pick<IReligion, "type" | "form" | "culture" | "center">[] {
|
||||
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<string>(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]);
|
||||
}
|
||||
}
|
||||
120
src/scripts/generation/pack/religions/generateReligionName.ts
Normal file
120
src/scripts/generation/pack/religions/generateReligionName.ts
Normal file
|
|
@ -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<string>(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};
|
||||
}
|
||||
38
src/scripts/generation/pack/religions/generateReligions.ts
Normal file
38
src/scripts/generation/pack/religions/generateReligions.ts
Normal file
|
|
@ -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};
|
||||
}
|
||||
146
src/scripts/generation/pack/religions/specifyReligions.ts
Normal file
146
src/scripts/generation/pack/religions/specifyReligions.ts
Normal file
|
|
@ -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<IReligion, "type" | "form" | "culture" | "center">;
|
||||
type TCellsData = Pick<IPack["cells"], "i" | "c" | "h" | "biome" | "culture" | "burg" | "state" | "route">;
|
||||
|
||||
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<IReligion, "name" | "culture" | "type" | "expansion">[]) {
|
||||
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<IReligion, "i" | "culture" | "type" | "expansion" | "center">[],
|
||||
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<number>();
|
||||
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];
|
||||
}
|
||||
8
src/types/common.d.ts
vendored
8
src/types/common.d.ts
vendored
|
|
@ -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> = T[number];
|
||||
|
||||
type noop = () => void;
|
||||
|
||||
interface Dict<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
// extract element from array
|
||||
type Entry<T> = T[number];
|
||||
|
||||
// element of Object.entries
|
||||
type ObjectEntry<T> = [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}`;
|
||||
|
|
|
|||
4
src/types/pack/features.d.ts
vendored
4
src/types/pack/features.d.ts
vendored
|
|
@ -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[]];
|
||||
|
|
|
|||
41
src/types/pack/pack.d.ts
vendored
41
src/types/pack/pack.d.ts
vendored
|
|
@ -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<IPackCells>;
|
||||
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[];
|
||||
}
|
||||
|
|
|
|||
8
src/types/pack/provinces.d.ts
vendored
Normal file
8
src/types/pack/provinces.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
interface IProvince {
|
||||
i: number;
|
||||
name: string;
|
||||
fullName: string;
|
||||
removed?: boolean;
|
||||
}
|
||||
|
||||
type TProvinces = IProvince[];
|
||||
21
src/types/pack/religions.d.ts
vendored
Normal file
21
src/types/pack/religions.d.ts
vendored
Normal file
|
|
@ -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[]];
|
||||
18
src/types/pack/rivers.d.ts
vendored
Normal file
18
src/types/pack/rivers.d.ts
vendored
Normal file
|
|
@ -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[];
|
||||
1
src/types/pack/states.d.ts
vendored
1
src/types/pack/states.d.ts
vendored
|
|
@ -9,6 +9,7 @@ interface IState {
|
|||
fullName: string;
|
||||
capital: Logical;
|
||||
coa: ICoa | string;
|
||||
// pole: TPoint ?
|
||||
removed?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ const cardinal12: Hex[] = [
|
|||
"#eb8de7"
|
||||
];
|
||||
|
||||
type ColorScheme = d3.ScaleSequential<string>;
|
||||
const colorSchemeMap: Dict<ColorScheme> = {
|
||||
export type TColorScheme = "default" | "bright" | "light" | "green" | "rainbow" | "monochrome";
|
||||
const colorSchemeMap: {[key in TColorScheme]: d3.ScaleSequential<string>} = {
|
||||
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<ColorScheme> = {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,9 @@ export function ra<T>(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<T extends string>(object: {[key in T]: number}) {
|
||||
const entries = Object.entries<number>(object);
|
||||
const weightedArray: T[] = entries.map(([choise, weight]) => new Array(weight).fill(choise)).flat();
|
||||
return ra(weightedArray);
|
||||
}
|
||||
|
||||
|
|
|
|||
16
src/utils/typeUtils.ts
Normal file
16
src/utils/typeUtils.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue