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>
|
||||||
<td>Religions number</td>
|
<td>Religions number</td>
|
||||||
<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>
|
||||||
<td>
|
<td>
|
||||||
<output id="religionsOutput" data-stored="religions" value="auto"></output>
|
<output id="religionsOutput" data-stored="religions" value="auto"></output>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"@types/delaunator": "^5.0.0",
|
"@types/delaunator": "^5.0.0",
|
||||||
"@types/jquery": "^3.5.14",
|
"@types/jquery": "^3.5.14",
|
||||||
"@types/jqueryui": "^1.12.16",
|
"@types/jqueryui": "^1.12.16",
|
||||||
|
"@types/polylabel": "^1.0.5",
|
||||||
"c8": "^7.12.0",
|
"c8": "^7.12.0",
|
||||||
"happy-dom": "^6.0.4",
|
"happy-dom": "^6.0.4",
|
||||||
"rollup": "^2.75.7",
|
"rollup": "^2.75.7",
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#404040",
|
"stroke": "#404040",
|
||||||
"stroke-width": 0.7,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#404040",
|
"stroke": "#404040",
|
||||||
"stroke-width": 0.7,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.5,
|
"opacity": 0.5,
|
||||||
"stroke": "#404040",
|
"stroke": "#404040",
|
||||||
"stroke-width": 2,
|
"stroke-width": 3,
|
||||||
"filter": "url(#splotch)"
|
"filter": "url(#splotch)"
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.35,
|
"opacity": 0.35,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 2,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": "url(#splotch)"
|
"filter": "url(#splotch)"
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#404040",
|
"stroke": "#404040",
|
||||||
"stroke-width": 1,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 1.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.5,
|
"opacity": 0.5,
|
||||||
"stroke": null,
|
"stroke": null,
|
||||||
"stroke-width": 0,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.5,
|
"opacity": 0.5,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#404040",
|
"stroke": "#404040",
|
||||||
"stroke-width": 0.7,
|
"stroke-width": 3,
|
||||||
"filter": null
|
"filter": null
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": null
|
"filter": null
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"#relig": {
|
"#relig": {
|
||||||
"opacity": 0.7,
|
"opacity": 0.7,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0,
|
"stroke-width": 3,
|
||||||
"filter": "url(#bluredSplotch)"
|
"filter": "url(#bluredSplotch)"
|
||||||
},
|
},
|
||||||
"#cults": {
|
"#cults": {
|
||||||
"opacity": 0.6,
|
"opacity": 0.6,
|
||||||
"stroke": "#777777",
|
"stroke": "#777777",
|
||||||
"stroke-width": 0.5,
|
"stroke-width": 3,
|
||||||
"stroke-dasharray": null,
|
"stroke-dasharray": null,
|
||||||
"stroke-linecap": null,
|
"stroke-linecap": null,
|
||||||
"filter": "url(#splotch)"
|
"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
|
// reassign province ownership to province center owner
|
||||||
prevOwner.provinces = prevOwner.provinces.filter(province => province !== provinceId);
|
prevOwner.provinces = prevOwner.provinces.filter(province => province !== provinceId);
|
||||||
province.state = stateId;
|
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);
|
states[stateId].provinces.push(provinceId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1163,7 +1163,7 @@ function adjustProvinces(affectedProvinces) {
|
||||||
const formOptions = ["Zone", "Area", "Territory", "Province"];
|
const formOptions = ["Zone", "Area", "Territory", "Province"];
|
||||||
const formName = burgCell && oldProvince.formName ? oldProvince.formName : ra(formOptions);
|
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 kinship = nameByBurg ? 0.8 : 0.4;
|
||||||
const type = BurgsAndStates.getType(center, burg?.port);
|
const type = BurgsAndStates.getType(center, burg?.port);
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
#biomes {
|
#biomes {
|
||||||
stroke-width: 0.7;
|
stroke-width: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#landmass {
|
#landmass {
|
||||||
|
|
@ -137,7 +137,8 @@ a {
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
t,
|
/* TODO: turn on after debugging */
|
||||||
|
/* t,
|
||||||
#regions,
|
#regions,
|
||||||
#cults,
|
#cults,
|
||||||
#relig,
|
#relig,
|
||||||
|
|
@ -150,7 +151,7 @@ t,
|
||||||
#landmass,
|
#landmass,
|
||||||
#fogging {
|
#fogging {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
#armies text {
|
#armies text {
|
||||||
pointer-events: none;
|
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 {pick} from "utils/functionUtils";
|
||||||
|
import {byId} from "utils/shorthands";
|
||||||
import {getPaths} from "./utilts";
|
import {getPaths} from "./utils/getVertexPaths";
|
||||||
|
|
||||||
export function drawCultures() {
|
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({
|
||||||
const paths = getPaths(cells.c, cells.v, vertices, getType);
|
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")
|
const htmlPaths = paths.map(([index, {fill, waterGap}]) => {
|
||||||
.selectAll("path")
|
const color = getColor(index);
|
||||||
.remove()
|
|
||||||
.data(Object.entries(paths))
|
return /* html */ `
|
||||||
.enter()
|
<path d="${waterGap}" fill="none" stroke="${color}" id="culture-gap${index}" />
|
||||||
.append("path")
|
<path d="${fill}" fill="${color}" stroke="none" id="culture${index}" />
|
||||||
.attr("d", ([, path]) => path)
|
`;
|
||||||
.attr("fill", ([i]) => getColor(Number(i)))
|
});
|
||||||
.attr("id", ([i]) => "culture" + i);
|
|
||||||
|
byId("cults")!.innerHTML = htmlPaths.join("");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {filterOutOfCanvasPoints} from "utils/lineUtils";
|
||||||
import {round} from "utils/stringUtils";
|
import {round} from "utils/stringUtils";
|
||||||
|
|
||||||
export function drawFeatures() {
|
export function drawFeatures() {
|
||||||
/* uses */ const {vertices, features} = pack;
|
/* global */ const {vertices, features} = pack;
|
||||||
|
|
||||||
const landMask = defs.select("#land");
|
const landMask = defs.select("#land");
|
||||||
const waterMask = defs.select("#water");
|
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() {
|
export function drawRoutes() {
|
||||||
routes.selectAll("path").remove();
|
/* global */ const {cells, burgs} = pack;
|
||||||
|
|
||||||
const {cells, burgs} = pack;
|
|
||||||
const lineGen = d3.line();
|
const lineGen = d3.line();
|
||||||
|
|
||||||
const SHARP_ANGLE = 135;
|
const SHARP_ANGLE = 135;
|
||||||
|
|
@ -32,6 +30,7 @@ export function drawRoutes() {
|
||||||
routePaths[type].push(`<path id="${type}${i}" d="${path}"/>`);
|
routePaths[type].push(`<path id="${type}${i}" d="${path}"/>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routes.selectAll("path").remove();
|
||||||
for (const type in routePaths) {
|
for (const type in routePaths) {
|
||||||
routes.select(`[data-type=${type}]`).html(routePaths[type].join(""));
|
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);
|
const sameColored = pack.states.filter(s => s.color === c);
|
||||||
sameColored.forEach((s, d) => {
|
sameColored.forEach((s, d) => {
|
||||||
if (!d) return;
|
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);
|
const formName = rw(form);
|
||||||
form[formName] += 10;
|
form[formName] += 10;
|
||||||
const fullName = name + " " + formName;
|
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 kinship = nameByBurg ? 0.8 : 0.4;
|
||||||
const type = getType(center, burg.port);
|
const type = getType(center, burg.port);
|
||||||
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
||||||
|
|
@ -1317,7 +1317,7 @@ window.BurgsAndStates = (function () {
|
||||||
// generate "wild" province name
|
// generate "wild" province name
|
||||||
const cultureId = cells.culture[center];
|
const cultureId = cells.culture[center];
|
||||||
const f = pack.features[cells.f[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 provCells = stateNoProvince.filter(i => cells.province[i] === province);
|
||||||
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
||||||
|
|
|
||||||
|
|
@ -566,7 +566,7 @@ export function randomizeOptions() {
|
||||||
manorsInput.value = 1000;
|
manorsInput.value = 1000;
|
||||||
manorsOutput.value = "auto";
|
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("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("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
|
||||||
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
|
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`);
|
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
|
// connect vertices around feature
|
||||||
export function connectVertices({
|
export function connectVertices({
|
||||||
vertices,
|
vertices,
|
||||||
startingVertex,
|
startingVertex,
|
||||||
ofSameType,
|
ofSameType,
|
||||||
checkedCellsMutable
|
addToChecked,
|
||||||
|
closeRing
|
||||||
}: {
|
}: {
|
||||||
vertices: IGraphVertices;
|
vertices: IGraphVertices;
|
||||||
startingVertex: number;
|
startingVertex: number;
|
||||||
ofSameType: (cellId: number) => boolean;
|
ofSameType: (cellId: number) => boolean;
|
||||||
checkedCellsMutable?: Uint8Array;
|
addToChecked?: (cellId: number) => void;
|
||||||
|
closeRing?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const chain: number[] = []; // vertices chain to form a path
|
const chain: number[] = []; // vertices chain to form a path
|
||||||
|
|
||||||
const addToChecked = (cellIds: number[]) => {
|
|
||||||
if (checkedCellsMutable) {
|
|
||||||
cellIds.forEach(cellId => {
|
|
||||||
checkedCellsMutable[cellId] = 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let next = startingVertex;
|
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 previous = chain.at(-1);
|
||||||
const current = next;
|
const current = next;
|
||||||
chain.push(current);
|
chain.push(current);
|
||||||
|
|
||||||
const neibCells = vertices.c[current];
|
const neibCells = vertices.c[current];
|
||||||
addToChecked(neibCells);
|
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
|
||||||
|
|
||||||
const [c1, c2, c3] = neibCells.map(ofSameType);
|
const [c1, c2, c3] = neibCells.map(ofSameType);
|
||||||
const [v1, v2, v3] = vertices.v[current];
|
const [v1, v2, v3] = vertices.v[current];
|
||||||
|
|
@ -134,5 +128,6 @@ export function connectVertices({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (closeRing) chain.push(startingVertex);
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@ import {openDialog} from "dialogs";
|
||||||
import {tip} from "scripts/tooltips";
|
import {tip} from "scripts/tooltips";
|
||||||
import {handleMapClick} from "./onclick";
|
import {handleMapClick} from "./onclick";
|
||||||
import {onMouseMove} from "./onhover";
|
import {onMouseMove} from "./onhover";
|
||||||
// @ts-expect-error js module
|
|
||||||
import {clearLegend, dragLegendBox} from "modules/legend";
|
import {clearLegend, dragLegendBox} from "modules/legend";
|
||||||
|
|
||||||
export function setDefaultEventHandlers() {
|
export function setDefaultEventHandlers() {
|
||||||
window.Zoom.setZoomBehavior();
|
window.Zoom.setZoomBehavior();
|
||||||
|
|
||||||
viewbox.style("cursor", "default").on(".drag", null).on("click", handleMapClick);
|
viewbox
|
||||||
//.on("touchmove mousemove", onMouseMove);
|
.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"));
|
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {layerIsOn} from "layers";
|
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 {updateCellInfo} from "modules/ui/cell-info";
|
||||||
import {debounce} from "utils/functionUtils";
|
import {debounce} from "utils/functionUtils";
|
||||||
import {findCell, findGridCell, isLand} from "utils/graphUtils";
|
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("togglePopulation")) return "populationLayer";
|
||||||
if (layerIsOn("toggleTemp")) return "temperatureLayer";
|
if (layerIsOn("toggleTemp")) return "temperatureLayer";
|
||||||
if (layerIsOn("toggleBiomes") && biome[cellId]) return "biomesLayer";
|
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("toggleProvinces") || (layerIsOn("toggleStates") && state[cellId])) return "statesLayer";
|
||||||
if (layerIsOn("toggleCultures") && culture[cellId]) return "culturesLayer";
|
if (layerIsOn("toggleCultures") && culture[cellId]) return "culturesLayer";
|
||||||
if (layerIsOn("toggleHeight")) return "heightLayer";
|
if (layerIsOn("toggleHeight")) return "heightLayer";
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {createGrid} from "./grid/grid";
|
||||||
import {createPack} from "./pack/pack";
|
import {createPack} from "./pack/pack";
|
||||||
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
||||||
import {calculateMapCoordinates} from "modules/coordinates";
|
import {calculateMapCoordinates} from "modules/coordinates";
|
||||||
|
import {drawPoint} from "utils/debugUtils";
|
||||||
|
|
||||||
const {Zoom, ThreeD} = window;
|
const {Zoom, ThreeD} = window;
|
||||||
|
|
||||||
|
|
@ -69,6 +70,12 @@ async function generate(options?: IGenerationOptions) {
|
||||||
// renderLayer("biomes");
|
// renderLayer("biomes");
|
||||||
renderLayer("burgs");
|
renderLayer("burgs");
|
||||||
renderLayer("routes");
|
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`);
|
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||||
// showStatistics();
|
// showStatistics();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const {Names, COA} = window;
|
||||||
|
|
||||||
type TCapitals = ReturnType<typeof createCapitals>;
|
type TCapitals = ReturnType<typeof createCapitals>;
|
||||||
|
|
||||||
export function createStates(capitals: TCapitals, cultures: TCultures) {
|
export function createStates(capitals: TCapitals, cultures: TCultures): TStates {
|
||||||
TIME && console.time("createStates");
|
TIME && console.time("createStates");
|
||||||
|
|
||||||
const colors = getColors(capitals.length);
|
const colors = getColors(capitals.length);
|
||||||
|
|
@ -28,7 +28,7 @@ export function createStates(capitals: TCapitals, cultures: TCultures) {
|
||||||
const shield = COA.getShield(cultureShield, null);
|
const shield = COA.getShield(cultureShield, null);
|
||||||
const coa: ICoa = {...COA.generate(null, null, null, type), shield};
|
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");
|
TIME && console.timeEnd("createStates");
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import {gauss} from "utils/probabilityUtils";
|
||||||
const {Names} = window;
|
const {Names} = window;
|
||||||
|
|
||||||
export function createTowns(
|
export function createTowns(
|
||||||
capitalCells: Map<number, boolean>,
|
|
||||||
cultures: TCultures,
|
cultures: TCultures,
|
||||||
|
scoredCellIds: UintArray,
|
||||||
cells: Pick<IPack["cells"], "p" | "i" | "f" | "s" | "culture">
|
cells: Pick<IPack["cells"], "p" | "i" | "f" | "s" | "culture">
|
||||||
) {
|
) {
|
||||||
TIME && console.time("createTowns");
|
TIME && console.time("createTowns");
|
||||||
|
|
@ -17,9 +17,6 @@ export function createTowns(
|
||||||
// randomize cells score a bit for more natural towns placement
|
// randomize cells score a bit for more natural towns placement
|
||||||
const randomizeScore = (suitability: number) => suitability * gauss(1, 3, 0, 20, 3);
|
const randomizeScore = (suitability: number) => suitability * gauss(1, 3, 0, 20, 3);
|
||||||
const scores = new Int16Array(cells.s.map(randomizeScore));
|
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
|
scoredCellIds.sort((a, b) => scores[b] - scores[a]); // sort by randomized suitability score
|
||||||
|
|
||||||
const townsNumber = getTownsNumber();
|
const townsNumber = getTownsNumber();
|
||||||
|
|
@ -53,13 +50,13 @@ function placeTowns(townsNumber: number, scoredCellIds: UintArray, points: TPoin
|
||||||
const townCells: number[] = [];
|
const townCells: number[] = [];
|
||||||
const townsQuadtree = d3.quadtree();
|
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) {
|
for (const cellId of scoredCellIds) {
|
||||||
const [x, y] = points[cellId];
|
const [x, y] = points[cellId];
|
||||||
|
|
||||||
// randomize min spacing a bit to make placement not that uniform
|
// 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) {
|
if (townsQuadtree.find(x, y, currentSpacing) === undefined) {
|
||||||
townCells.push(cellId);
|
townCells.push(cellId);
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,8 @@ import FlatQueue from "flatqueue";
|
||||||
import {TIME} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
import {getInputNumber} from "utils/nodeUtils";
|
import {getInputNumber} from "utils/nodeUtils";
|
||||||
import {minmax} from "utils/numberUtils";
|
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";
|
import {ELEVATION, FOREST_BIOMES, MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
||||||
|
import {isNeutals} from "utils/typeUtils";
|
||||||
type TCapitals = ReturnType<typeof createCapitals>;
|
|
||||||
type TStates = ReturnType<typeof createStates>;
|
|
||||||
|
|
||||||
// growth algorithm to assign cells to states
|
// growth algorithm to assign cells to states
|
||||||
export function expandStates(
|
export function expandStates(
|
||||||
|
|
@ -104,13 +100,9 @@ export function expandStates(
|
||||||
|
|
||||||
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
return normalizeStates(stateIds, capitalCells, cells.c, cells.h);
|
||||||
|
|
||||||
function isNeutrals(state: Entry<TStates>): state is TNeutrals {
|
|
||||||
return state.i === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState(stateId: number) {
|
function getState(stateId: number) {
|
||||||
const state = states[stateId];
|
const state = states[stateId];
|
||||||
if (isNeutrals(state)) throw new Error("Neutrals cannot expand");
|
if (isNeutals(state)) throw new Error("Neutrals cannot expand");
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function generateBurgsAndStates(
|
||||||
vertices: IGraphVertices,
|
vertices: IGraphVertices,
|
||||||
cells: Pick<
|
cells: Pick<
|
||||||
IPack["cells"],
|
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} {
|
): {burgIds: Uint16Array; stateIds: Uint16Array; burgs: TBurgs; states: TStates} {
|
||||||
const cellsNumber = cells.i.length;
|
const cellsNumber = cells.i.length;
|
||||||
|
|
@ -34,9 +34,13 @@ export function generateBurgsAndStates(
|
||||||
|
|
||||||
const capitals = createCapitals(statesNumber, scoredCellIds, cultures, pick(cells, "p", "f", "culture"));
|
const capitals = createCapitals(statesNumber, scoredCellIds, cultures, pick(cells, "p", "f", "culture"));
|
||||||
const capitalCells = new Map(capitals.map(({cell}) => [cell, true]));
|
const capitalCells = new Map(capitals.map(({cell}) => [cell, true]));
|
||||||
|
|
||||||
const states = createStates(capitals, cultures);
|
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(
|
const stateIds = expandStates(
|
||||||
capitalCells,
|
capitalCells,
|
||||||
|
|
@ -63,11 +67,12 @@ export function generateBurgsAndStates(
|
||||||
return {burgIds, stateIds, burgs, states};
|
return {burgIds, stateIds, burgs, states};
|
||||||
|
|
||||||
function getScoredCellIds() {
|
function getScoredCellIds() {
|
||||||
// cell score for capitals placement
|
|
||||||
const score = new Int16Array(cells.s.map(s => s * Math.random()));
|
const score = new Int16Array(cells.s.map(s => s * Math.random()));
|
||||||
|
|
||||||
// filtered and sorted array of indexes
|
// filtered and sorted array of indexes: only populated cells not on map edge
|
||||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]);
|
const sorted = cells.i
|
||||||
|
.filter(i => !cells.b[i] && score[i] > 0 && cells.culture[i])
|
||||||
|
.sort((a, b) => score[b] - score[a]);
|
||||||
|
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {minmax, rn} from "utils/numberUtils";
|
||||||
import {biased, P, rand} from "utils/probabilityUtils";
|
import {biased, P, rand} from "utils/probabilityUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {defaultNameBases} from "config/namebases";
|
import {defaultNameBases} from "config/namebases";
|
||||||
|
import {isCulture} from "utils/typeUtils";
|
||||||
|
|
||||||
const {COA} = window;
|
const {COA} = window;
|
||||||
|
|
||||||
|
|
@ -284,9 +285,7 @@ export const expandCultures = function (
|
||||||
const cultureIds = new Uint16Array(cells.h.length); // cell cultures
|
const cultureIds = new Uint16Array(cells.h.length); // cell cultures
|
||||||
const queue = new FlatQueue<{cellId: number; cultureId: number}>();
|
const queue = new FlatQueue<{cellId: number; cultureId: number}>();
|
||||||
|
|
||||||
const isWilderness = (culture: ICulture | TWilderness): culture is TWilderness => culture.i === 0;
|
cultures.filter(isCulture).forEach(culture => {
|
||||||
cultures.forEach(culture => {
|
|
||||||
if (isWilderness(culture) || culture.removed) return;
|
|
||||||
queue.push({cellId: culture.center, cultureId: culture.i}, 0);
|
queue.push({cellId: culture.center, cultureId: culture.i}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -323,7 +322,7 @@ export const expandCultures = function (
|
||||||
|
|
||||||
function getCulture(cultureId: number) {
|
function getCulture(cultureId: number) {
|
||||||
const culture = cultures[cultureId];
|
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;
|
return culture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import FlatQueue from "flatqueue";
|
||||||
import {TIME} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
|
import {ELEVATION, MIN_LAND_HEIGHT, ROUTES} from "config/generation";
|
||||||
import {dist2} from "utils/functionUtils";
|
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) {
|
export function generateRoutes(burgs: TBurgs, temp: Int8Array, cells: TCellsData) {
|
||||||
const cellRoutes = new Uint8Array(cells.h.length);
|
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 capitalsByFeature: Dict<IBurg[]> = {};
|
||||||
const portsByFeature: 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) => {
|
const addBurg = (object: Dict<IBurg[]>, feature: number, burg: IBurg) => {
|
||||||
if (!object[feature]) object[feature] = [];
|
if (!object[feature]) object[feature] = [];
|
||||||
object[feature].push(burg);
|
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});
|
const segments = findPathSegments({isWater: true, cellRoutes, connections, start, exit});
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
addConnections(segment, ROUTES.MAIN_ROAD);
|
addConnections(segment, ROUTES.SEA_ROUTE);
|
||||||
mainRoads.push({feature: Number(key), cells: segment});
|
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 cellId = segment[i];
|
||||||
const nextCellId = segment[i + 1];
|
const nextCellId = segment[i + 1];
|
||||||
if (nextCellId) connections.set(`${cellId}-${nextCellId}`, true);
|
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 {generateRivers} from "./rivers";
|
||||||
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
|
import {generateBurgsAndStates} from "./burgsAndStates/generateBurgsAndStates";
|
||||||
import {generateRoutes} from "./generateRoutes";
|
import {generateRoutes} from "./generateRoutes";
|
||||||
|
import {generateReligions} from "./religions/generateReligions";
|
||||||
|
|
||||||
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
|
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
|
||||||
const {Biomes} = window;
|
const {Biomes} = window;
|
||||||
|
|
@ -103,7 +104,7 @@ export function createPack(grid: IGrid): IPack {
|
||||||
rawRivers,
|
rawRivers,
|
||||||
vertices,
|
vertices,
|
||||||
{
|
{
|
||||||
...pick(cells, "v", "c", "p", "i", "g"),
|
...pick(cells, "v", "c", "p", "b", "i", "g"),
|
||||||
h: heights,
|
h: heights,
|
||||||
f: featureIds,
|
f: featureIds,
|
||||||
t: distanceField,
|
t: distanceField,
|
||||||
|
|
@ -124,10 +125,29 @@ export function createPack(grid: IGrid): IPack {
|
||||||
h: heights,
|
h: heights,
|
||||||
t: distanceField,
|
t: distanceField,
|
||||||
biome,
|
biome,
|
||||||
state: stateIds,
|
|
||||||
burg: burgIds
|
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();
|
// Religions.generate();
|
||||||
// BurgsAndStates.defineStateForms();
|
// BurgsAndStates.defineStateForms();
|
||||||
// BurgsAndStates.generateProvinces();
|
// BurgsAndStates.generateProvinces();
|
||||||
|
|
@ -167,15 +187,17 @@ export function createPack(grid: IGrid): IPack {
|
||||||
culture: cultureIds,
|
culture: cultureIds,
|
||||||
burg: burgIds,
|
burg: burgIds,
|
||||||
state: stateIds,
|
state: stateIds,
|
||||||
route: cellRoutes
|
route: cellRoutes,
|
||||||
// religion, province
|
religion: religionIds,
|
||||||
|
province: new Uint16Array(cells.i.length)
|
||||||
},
|
},
|
||||||
features: mergedFeatures,
|
features: mergedFeatures,
|
||||||
rivers: rawRivers, // "name" | "basin" | "type"
|
rivers: rawRivers, // "name" | "basin" | "type"
|
||||||
cultures,
|
cultures,
|
||||||
states,
|
states,
|
||||||
burgs,
|
burgs,
|
||||||
routes
|
routes,
|
||||||
|
religions
|
||||||
};
|
};
|
||||||
|
|
||||||
return pack;
|
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};
|
type UnknownObject = {[key: string]: unknown};
|
||||||
|
|
||||||
// extract element from array
|
|
||||||
type Entry<T> = T[number];
|
|
||||||
|
|
||||||
type noop = () => void;
|
type noop = () => void;
|
||||||
|
|
||||||
interface Dict<T> {
|
interface Dict<T> {
|
||||||
[key: string]: T;
|
[key: string]: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extract element from array
|
||||||
|
type Entry<T> = T[number];
|
||||||
|
|
||||||
// element of Object.entries
|
// element of Object.entries
|
||||||
type ObjectEntry<T> = [string, T];
|
type ObjectEntry<T> = [string, T];
|
||||||
|
|
||||||
type UintArray = Uint8Array | Uint16Array | Uint32Array;
|
type UintArray = Uint8Array | Uint16Array | Uint32Array;
|
||||||
type IntArray = Int8Array | Int16Array | Int32Array;
|
type IntArray = Int8Array | Int16Array | Int32Array;
|
||||||
|
type FloatArray = Float32Array | Float64Array;
|
||||||
|
type TypedArray = UintArray | IntArray | FloatArray;
|
||||||
|
|
||||||
type RGB = `rgb(${number}, ${number}, ${number})`;
|
type RGB = `rgb(${number}, ${number}, ${number})`;
|
||||||
type Hex = `#${string}`;
|
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 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;
|
features: TPackFeatures;
|
||||||
states: TStates;
|
states: TStates;
|
||||||
cultures: TCultures;
|
cultures: TCultures;
|
||||||
provinces: IProvince[];
|
provinces: TProvinces;
|
||||||
burgs: TBurgs;
|
burgs: TBurgs;
|
||||||
rivers: IRiver[];
|
rivers: TRivers;
|
||||||
religions: IReligion[];
|
religions: TReligions;
|
||||||
routes: TRoutes;
|
routes: TRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,9 +23,9 @@ interface IPackCells {
|
||||||
conf: Uint16Array; // conluence, defined by defineRivers() in river-generator.ts
|
conf: Uint16Array; // conluence, defined by defineRivers() in river-generator.ts
|
||||||
biome: Uint8Array;
|
biome: Uint8Array;
|
||||||
area: UintArray;
|
area: UintArray;
|
||||||
state: UintArray;
|
state: Uint16Array;
|
||||||
culture: Uint16Array;
|
culture: Uint16Array;
|
||||||
religion: UintArray;
|
religion: Uint16Array;
|
||||||
province: UintArray;
|
province: UintArray;
|
||||||
burg: UintArray;
|
burg: UintArray;
|
||||||
haven: UintArray;
|
haven: UintArray;
|
||||||
|
|
@ -38,34 +38,3 @@ interface IPackBase extends IGraph {
|
||||||
cells: IGraphCells & Partial<IPackCells>;
|
cells: IGraphCells & Partial<IPackCells>;
|
||||||
features?: TPackFeatures;
|
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;
|
fullName: string;
|
||||||
capital: Logical;
|
capital: Logical;
|
||||||
coa: ICoa | string;
|
coa: ICoa | string;
|
||||||
|
// pole: TPoint ?
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ const cardinal12: Hex[] = [
|
||||||
"#eb8de7"
|
"#eb8de7"
|
||||||
];
|
];
|
||||||
|
|
||||||
type ColorScheme = d3.ScaleSequential<string>;
|
export type TColorScheme = "default" | "bright" | "light" | "green" | "rainbow" | "monochrome";
|
||||||
const colorSchemeMap: Dict<ColorScheme> = {
|
const colorSchemeMap: {[key in TColorScheme]: d3.ScaleSequential<string>} = {
|
||||||
|
default: d3.scaleSequential(d3.interpolateSpectral),
|
||||||
bright: d3.scaleSequential(d3.interpolateSpectral),
|
bright: d3.scaleSequential(d3.interpolateSpectral),
|
||||||
light: d3.scaleSequential(d3.interpolateRdYlGn),
|
light: d3.scaleSequential(d3.interpolateRdYlGn),
|
||||||
green: d3.scaleSequential(d3.interpolateGreens),
|
green: d3.scaleSequential(d3.interpolateGreens),
|
||||||
|
|
@ -24,10 +25,10 @@ const colorSchemeMap: Dict<ColorScheme> = {
|
||||||
monochrome: d3.scaleSequential(d3.interpolateGreys)
|
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));
|
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 => {
|
const colors = d3.range(number).map(index => {
|
||||||
if (index < 12) return cardinal12[index];
|
if (index < 12) return cardinal12[index];
|
||||||
|
|
||||||
|
|
@ -39,22 +40,31 @@ export function getColors(number: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomColor(): Hex {
|
export function getRandomColor(): Hex {
|
||||||
const scheme = colorSchemeMap.bright;
|
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||||
const rgb = scheme(Math.random())!;
|
const rgb = scheme(Math.random())!;
|
||||||
return d3.color(rgb)?.formatHex() as Hex;
|
return d3.color(rgb)?.formatHex() as Hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mix a color with a random color
|
// mix a color with a random color. TODO: refactor without interpolation
|
||||||
export function getMixedColor(hexColor: string, mixation = 0.2, bright = 0.3) {
|
export function getMixedColor(color: Hex | CssUrl, mixation: number) {
|
||||||
// if provided color is not hex (e.g. harching), generate random one
|
const color1 = color.startsWith("#") ? color : getRandomColor();
|
||||||
const color1 = hexColor && hexColor[0] === "#" ? hexColor : getRandomColor();
|
|
||||||
const color2 = getRandomColor();
|
const color2 = getRandomColor();
|
||||||
const mixedColor = d3.interpolate(color1, color2)(mixation);
|
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;
|
return colorSchemeMap[schemeName] || colorSchemeMap.bright;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// utils to be used for debugging (not in PROD)
|
// utils to be used for debugging (not in PROD)
|
||||||
|
|
||||||
|
import {getColorScheme, TColorScheme} from "./colorUtils";
|
||||||
import {getNormal} from "./lineUtils";
|
import {getNormal} from "./lineUtils";
|
||||||
|
|
||||||
export function drawPoint([x, y]: TPoint, {radius = 1, color = "red"} = {}) {
|
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")
|
.attr("stroke", "none")
|
||||||
.text(text);
|
.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
|
// remove intermediate out-of-canvas points from polyline
|
||||||
export function filterOutOfCanvasPoints(points: TPoints) {
|
export function filterOutOfCanvasPoints(points: TPoints) {
|
||||||
const pointsOutSide = points.map(getPointOffCanvasSide);
|
const pointsOutside = points.map(getPointOffCanvasSide);
|
||||||
const SAFE_ZONE = 3;
|
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 filterOutCanvasPoint = (i: number) => {
|
||||||
const pointSide = pointsOutSide[i];
|
const pointSide = pointsOutside[i];
|
||||||
return !pointSide || fragment(i).some(side => !side || side !== pointSide);
|
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
|
// return random value from weighted array
|
||||||
export function rw(object: {[key: string]: number}) {
|
export function rw<T extends string>(object: {[key in T]: number}) {
|
||||||
const weightedArray = Object.entries(object)
|
const entries = Object.entries<number>(object);
|
||||||
.map(([choise, weight]) => new Array(weight).fill(choise))
|
const weightedArray: T[] = entries.map(([choise, weight]) => new Array(weight).fill(choise)).flat();
|
||||||
.flat();
|
|
||||||
return ra(weightedArray);
|
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"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
|
||||||
integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==
|
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":
|
"@types/qs@^6.2.31":
|
||||||
version "6.9.7"
|
version "6.9.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue