Merge pull request #863 from Azgaar/vite-religions

Vite religions
This commit is contained in:
Azgaar 2022-09-01 22:09:14 +03:00 committed by GitHub
commit 60e69348a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1365 additions and 507 deletions

View file

@ -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>

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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};

View file

@ -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);

View file

@ -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;

View file

@ -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");
}

View 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("");
}

View file

@ -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("");
}

View file

@ -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");

View file

@ -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;
}
}

View 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("");
}

View file

@ -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(""));
}

View file

@ -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();
}

View 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();
}

View 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);
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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;
}

View file

@ -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"));

View file

@ -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";

View file

@ -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();

View file

@ -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");

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;

View 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;
}

View 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";
}

View file

@ -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};
});
}

View file

@ -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]);
}
}

View 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};
}

View 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};
}

View 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];
}

View file

@ -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}`;

View file

@ -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[]];

View file

@ -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
View 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
View 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
View 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[];

View file

@ -9,6 +9,7 @@ interface IState {
fullName: string;
capital: Logical;
coa: ICoa | string;
// pole: TPoint ?
removed?: boolean;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
};

View file

@ -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
View 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;

View file

@ -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"