diff --git a/README.md b/README.md index 17cfd7a9..20331678 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # Fantasy Map Generator -Azgaar's _Fantasy Map Generator_ is a free web application generating interactive and highly customizable svg maps based on voronoi diagram. +Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps. -Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator). +Link: [azgaar.github.io/Fantasy-Map-Generator](https://azgaar.github.io/Fantasy-Map-Generator). Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance. The current progress is tracked in [Trello](https://trello.com/b/7x832DG4/fantasy-map-generator). Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com). diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 9d270a7c..00000000 --- a/Readme.txt +++ /dev/null @@ -1,9 +0,0 @@ -Azgaar's Fantasy Map Generator - -Developed by Azgaar (azgaar.fmg@yandex.com) and contributors - -Minsk, 2017-2021. MIT License - -https://github.com/Azgaar/Fantasy-Map-Generator - -To run the tool unzip ALL files and open index.html in browser \ No newline at end of file diff --git a/heightmaps/import-rules.txt b/heightmaps/import-rules.txt index 9478b35c..69499114 100644 --- a/heightmaps/import-rules.txt +++ b/heightmaps/import-rules.txt @@ -1,8 +1,8 @@ To get heightmap with correct height scale: -1. Open tangrams.github.io +1. Open https://tangrams.github.io/heightmapper 2. Toggle off auto-exposure 3. Set max elevation to 2000 4. Set min elevation to -500 5. Find region you like 6. Render image -7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used \ No newline at end of file +7. Optionally rescale image to a smaller size (e.g. 500x300px) as high resolution is not used diff --git a/index.css b/index.css index bfd1ed67..9ec277b6 100644 --- a/index.css +++ b/index.css @@ -1082,12 +1082,16 @@ tr.battleSurvivors { font-size: 0.9em; } -#battleBody div.battlePhases, #battleBottom div.battleTypes { position: fixed; background-color: #ffffff30; } +#battleBody div.battlePhases { + position: absolute; + background-color: #ffffff30; +} + #battleBody div.battlePhases > button, #battleBottom div.battleTypes > button { width: 3.2em; @@ -2045,6 +2049,7 @@ div.textual span, } #notesLegend { + width: auto; height: 87%; outline: 0; overflow-y: auto; @@ -2239,7 +2244,6 @@ svg.button { user-select: none; } - .dontAsk { margin: 0.9em 0 0 0.6em; display: inline-flex; @@ -2338,7 +2342,7 @@ svg.button { } @media (prefers-color-scheme: dark) { - body { - background: #25252a; - } + body { + background: #25252a; + } } diff --git a/index.html b/index.html index bc9d9fc5..d0714dab 100644 --- a/index.html +++ b/index.html @@ -3,14 +3,33 @@ + Azgaar's Fantasy Map Generator - - + + + - + + + + + + + + + @@ -108,7 +127,7 @@ } - + @@ -1237,10 +1256,6 @@ - - + + @@ -2749,7 +2764,7 @@ `; @@ -109,6 +116,7 @@ function addListeners() { byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment()); byId("religionsAdd").on("click", enterAddReligionMode); byId("religionsExport").on("click", downloadReligionsCsv); + byId("religionsRecalculate").on("click", () => recalculateReligions(true)); } function refreshReligionsEditor() { @@ -166,9 +174,10 @@ function religionsEditorAddLines() { data-type="" data-form="" data-deity="" + data-expansion="" data-expansionism="" > - + -
${si(area) + unit}
+
${si(area) + unit}
-
${si(population)}
+
${si( + population + )}
`; continue; } @@ -195,6 +206,7 @@ function religionsEditorAddLines() { data-type="${r.type}" data-form="${r.form}" data-deity="${r.deity || ""}" + data-expansion="${r.expansion}" data-expansionism="${r.expansionism}" > @@ -209,13 +221,13 @@ function religionsEditorAddLines() { -
${si(area) + unit}
+
${si(area) + unit}
-
${si(population)}
- +
${si( + population + )}
+ ${getExpansionColumns(r)} + `; } @@ -245,6 +257,8 @@ function religionsEditorAddLines() { $body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity)); $body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity)); $body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation)); + $body.querySelectorAll("div > select.religionExtent").forEach(el => el.on("change", religionChangeExtent)); + $body.querySelectorAll("div > input.religionExpantion").forEach(el => el.on("change", religionChangeExpansionism)); $body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt)); $body.querySelectorAll("div > span.icon-lock").forEach($el => $el.on("click", updateLockStatus)); $body.querySelectorAll("div > span.icon-lock-open").forEach($el => $el.on("click", updateLockStatus)); @@ -253,6 +267,7 @@ function religionsEditorAddLines() { $body.dataset.type = "absolute"; togglePercentageMode(); } + applySorting(religionsHeader); $("#religionsEditor").dialog({width: fitContent()}); } @@ -264,6 +279,41 @@ function getTypeOptions(type) { return options; } +function getExpansionColumns(r) { + if (r.type === "Folk") { + const tip = + "Folk religions are not competitive and do not expand. Initially they cover all cells of their parent culture, but get ousted by organized religions when they expand"; + return /* html */ ` + + culture + + `; + } + + return /* html */ ` + + + + `; +} + +function getExtentOptions(type) { + let options = ""; + const types = ["global", "state", "culture"]; + types.forEach(t => (options += ``)); + return options; +} + const religionHighlightOn = debounce(event => { const religionId = Number(event.id || event.target.dataset.id); const $el = $body.querySelector(`div[data-id='${religionId}']`); @@ -272,20 +322,19 @@ const religionHighlightOn = debounce(event => { if (!layerIsOn("toggleReligions")) return; if (customization) return; - const animate = d3.transition().duration(1500).ease(d3.easeSinIn); + const animate = d3.transition().duration(2000).ease(d3.easeSinIn); relig .select("#religion" + religionId) .raise() .transition(animate) .attr("stroke-width", 2.5) - .attr("stroke", "#c13119"); + .attr("stroke", "#d0240f"); debug .select("#religionsCenter" + religionId) .raise() .transition(animate) - .attr("r", 8) - .attr("stroke-width", 2) - .attr("stroke", "#c13119"); + .attr("r", 3) + .attr("stroke", "#d0240f"); }, 200); function religionHighlightOff(event) { @@ -301,8 +350,7 @@ function religionHighlightOff(event) { debug .select("#religionsCenter" + religionId) .transition() - .attr("r", 4) - .attr("stroke-width", 1.2) + .attr("r", 2) .attr("stroke", null); } @@ -434,6 +482,20 @@ function changePopulation() { } } +function religionChangeExtent() { + const religion = +this.parentNode.dataset.id; + this.parentNode.dataset.expansion = this.value; + pack.religions[religion].expansion = this.value; + recalculateReligions(); +} + +function religionChangeExpansionism() { + const religion = +this.parentNode.dataset.id; + this.parentNode.dataset.expansionism = this.value; + pack.religions[religion].expansionism = +this.value; + recalculateReligions(); +} + function religionRemovePrompt() { if (customization) return; @@ -471,11 +533,14 @@ function drawReligionCenters() { const religionCenters = debug .append("g") .attr("id", "religionCenters") - .attr("stroke-width", 1.2) + .attr("stroke-width", 0.8) .attr("stroke", "#444444") .style("cursor", "move"); - const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed); + let data = pack.religions.filter(r => r.i && r.center && !r.removed); + const showExtinct = $body.dataset.extinct === "show"; + if (!showExtinct) data = data.filter(r => r.cells > 0); + religionCenters .selectAll("circle") .data(data) @@ -483,7 +548,7 @@ function drawReligionCenters() { .append("circle") .attr("id", d => "religionsCenter" + d.i) .attr("data-id", d => d.i) - .attr("r", 4) + .attr("r", 2) .attr("fill", d => d.color) .attr("cx", d => pack.cells.p[d.center][0]) .attr("cy", d => pack.cells.p[d.center][1]) @@ -499,15 +564,23 @@ function drawReligionCenters() { } function religionCenterDrag() { - const $el = d3.select(this); const religionId = +this.dataset.id; - d3.event.on("drag", () => { + const tr = parseTransform(this.getAttribute("transform")); + const x0 = +tr[0] - d3.event.x; + const y0 = +tr[1] - d3.event.y; + + function handleDrag() { const {x, y} = d3.event; - $el.attr("cx", x).attr("cy", y); + this.setAttribute("transform", `translate(${x0 + x},${y0 + y})`); const cell = findCell(x, y); if (pack.cells.h[cell] < 20) return; // ignore dragging on water + pack.religions[religionId].center = cell; - }); + recalculateReligions(); + } + + const dragDebounced = debounce(handleDrag, 50); + d3.event.on("drag", dragDebounced); } function toggleLegend() { @@ -578,13 +651,14 @@ async function showHierarchy() { function toggleExtinct() { $body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide"; religionsEditorAddLines(); + drawReligionCenters(); } function enterReligionsManualAssignent() { if (!layerIsOn("toggleReligions")) toggleReligions(); customization = 7; relig.append("g").attr("id", "temp"); - document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none")); + document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "none")); byId("religionsManuallyButtons").style.display = "inline-block"; debug.select("#religionCenters").style("display", "none"); @@ -686,7 +760,7 @@ function exitReligionsManualAssignment(close) { customization = 0; relig.select("#temp").remove(); removeCircle(); - document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block")); + document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "inline-block")); byId("religionsManuallyButtons").style.display = "none"; byId("religionsEditor") @@ -740,15 +814,15 @@ function addReligion() { function downloadReligionsCsv() { const unit = getAreaUnit("2"); - const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins`; + const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins,Potential,Expansionism`; const lines = Array.from($body.querySelectorAll(":scope > div")); const data = lines.map($line => { - const {id, name, color, type, form, deity, area, population} = $line.dataset; + const {id, name, color, type, form, deity, area, population, expansion, expansionism} = $line.dataset; const deityText = '"' + deity + '"'; const {origins} = pack.religions[+id]; const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name); const originText = '"' + originList.join(", ") + '"'; - return [id, name, color, type, form, deityText, area, population, originText].join(","); + return [id, name, color, type, form, deityText, area, population, originText, expansion, expansionism].join(","); }); const csvData = [headers].concat(data).join("\n"); @@ -773,3 +847,13 @@ function updateLockStatus() { classList.toggle("icon-lock-open"); classList.toggle("icon-lock"); } + +function recalculateReligions(must) { + if (!must && !religionsAutoChange.checked) return; + + Religions.recalculate(); + + drawReligions(); + refreshReligionsEditor(); + drawReligionCenters(); +} diff --git a/modules/dynamic/editors/states-editor.js b/modules/dynamic/editors/states-editor.js index 773d421d..41241340 100644 --- a/modules/dynamic/editors/states-editor.js +++ b/modules/dynamic/editors/states-editor.js @@ -163,8 +163,6 @@ function addListeners() { const line = $element.parentNode; const state = +line.dataset.id; if (classList.contains("stateCapital")) stateChangeCapitalName(state, line, $element.value); - else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value); - else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value); }); $body.on("change", function (ev) { @@ -173,6 +171,8 @@ function addListeners() { const line = $element.parentNode; const state = +line.dataset.id; if (classList.contains("stateCulture")) stateChangeCulture(state, line, $element.value); + else if (classList.contains("cultureType")) stateChangeType(state, line, $element.value); + else if (classList.contains("statePower")) stateChangeExpansionism(state, line, $element.value); }); } @@ -883,7 +883,6 @@ function changeStatesGrowthRate() { const growthRate = +this.value; byId("statesNeutral").value = growthRate; byId("statesNeutralNumber").value = growthRate; - statesNeutral = growthRate; tip("Growth rate: " + growthRate); recalculateStates(false); } diff --git a/modules/dynamic/supporters.js b/modules/dynamic/supporters.js index 038da4d8..c83e10ec 100644 --- a/modules/dynamic/supporters.js +++ b/modules/dynamic/supporters.js @@ -1,44 +1,481 @@ -const capitalize = text => text.charAt(0).toUpperCase() + text.slice(1); - -const format = rawList => - rawList - .replace(/(?:\r\n|\r|\n)/g, "") - .split(",") - .map(name => capitalize(name.trim())) - .sort(); - -export const supporters = format(` - Aaron Meyer,Ahmad Amerih,AstralJacks,aymeric,Billy Dean Goehring,Branndon Edwards,Chase Mayers,Curt Flood,cyninge,Dino Princip, - E.M. White,es,Fondue,Fritjof Olsson,Gatsu,Johan Fröberg,Jonathan Moore,Joseph Miranda,Kate,KC138,Luke Nelson,Markus Finster,Massimo Vella,Mikey, - Nathan Mitchell,Paavi1,Pat,Ryan Westcott,Sasquatch,Shawn Spencer,Sizz_TV,Timothée CALLET,UTG community,Vlad Tomash,Wil Sisney,William Merriott, - Xariun,Gun Metal Games,Scott Marner,Spencer Sherman,Valerii Matskevych,Alloyed Clavicle,Stewart Walsh,Ruthlyn Mollett (Javan),Benjamin Mair-Pratt, - Diagonath,Alexander Thomas,Ashley Wilson-Savoury,William Henry,Preston Brooks,JOSHUA QUALTIERI,Hilton Williams,Katharina Haase,Hisham Bedri, - Ian arless,Karnat,Bird,Kevin,Jessica Thomas,Steve Hyatt,Logicspren,Alfred García,Jonathan Killstring,John Ackley,Invad3r233,Norbert Žigmund,Jennifer, - PoliticsBuff,_gfx_,Maggie,Connor McMartin,Jared McDaris,BlastWind,Franc Casanova Ferrer,Dead & Devil,Michael Carmody,Valerie Elise,naikibens220, - Jordon Phillips,William Pucs,The Dungeon Masters,Brady R Rathbun,J,Shadow,Matthew Tiffany,Huw Williams,Joseph Hamilton,FlippantFeline,Tamashi Toh, - kms,Stephen Herron,MidnightMoon,Whakomatic x,Barished,Aaron bateson,Brice Moss,Diklyquill,PatronUser,Michael Greiner,Steven Bennett,Jacob Harrington, - Miguel C.,Reya C.,Giant Monster Games,Noirbard,Brian Drennen,Ben Craigie,Alex Smolin,Endwords,Joshua E Goodwin,SirTobit ,Allen S. Rout,Allen Bull Bear, - Pippa Mitchell,R K,G0atfather,Ryan Lege,Caner Oleas Pekgönenç,Bradley Edwards,Tertiary ,Austin Miller,Jesse Holmes,Jan Dvořák,Marten F,Erin D. Smale, - Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge, - Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ, - Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta, - Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson, - Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo, - Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau, - Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel, - Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley, - Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion, - Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius, - Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox, - PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman, - Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks, - Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel, - Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky, - Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021, - Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol, - Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce, - Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule, - Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita,Randy Ross,John Wick,RedSpaz,cameron cannon,Ian Grau-Fay,Kyle Barrett,Charlotte Wiland, - David Kaul,E. Jason Davis,Cyberate,Atenfox,Sea Wolf,Holly Loveless,Roekai,Alden Z,angel carrillo,Sam Spoerle,S A Rudy,Bird Law Expert,Mira Cyr, - Aaron Blair,Neyimadd,RLKZ1022,DerWolf,Kenji Yamada,Zion,Robert Rinne,Actual_Dio,Kyarou -`); +export const supporters = `ken burgan +Sera's Nafitlaan +Richard Rogers +Hylobate +Colin deSousa +Aurelia De La Silla +Maciej Kontny +Ricky L Cain +Iggyflare +Garrett Renner +Michael Harris +Joshua Maly +Nigel Guest +Theo Hodges +BERTHEAS Frédéric +lilMoni +Δημήτρης Μάρκογιαννακης +Lee S. +Chris Dibbs +jarrad tait +Jacen Solo +Hannes Rotestam +Preston Hicks +Лонгин +Will Fink +ControlFreq +IllAngel +John Giardina +Thiago Prado +Zhang Dijon +NoBurny +thibault tersinet +scarletsky +Nich Smith +Omegus +Karl Abrahamsson +Sara Fernandes +peetey897 +Cooper Janse +G F +Glen Aultman-Bettridge +Nathan Rogers +Benjamin Mock +CadmiumMan +Kirk Edwards +Leigh G +Thom Colyer +Frederik +C pstj +Zachary Pecora +Trevor D'Arcey +Ryan Gauvin +Shawn Moore +Jim Channon +Kyarou +Actual_Dio +Jim B Johnson +Robert Rinne +Zion +Kenji Yamada +DerWolf +RLKZ1022 +Neyimadd +Aaron Blair +Mira Cyr +Bird Law Expert +S A Rudy +Sam Spoerle +angel carrillo +Alden Z +Holly Loveless +Sea Wolf +Atenfox +Cyberate +E. Jason Davis +Caro Lyns +David Kaul +Charlotte Wiland +Kyle Barrett +Ian Grau-Fay +cameron cannon +RedSpaz +John Wick +Randy Ross +Rita +Ele +Johnathan Xavier Hutchinson +Andrew Stein +Ghettov Milan +Malke +TameMoon +Daniel Cooper +Markus Peham +The Next Level +Alexander Whittaker +Sidr Dim +William Markus +Jordan +Pleaseworkforonce +Damon Gallaty +Trentin Bergeron +Emarandzeb +Laulajatar +Dale McBane +Chris Kohut +Preston Mitchell +Andrew Kircher +Frank Fewkes +Moist mongol +Joshua Xiong +Jan Bundesmann +www15o +Game Master Pro +jason baldrick +Exp1nt +w +Shubham Jakhotiya +Braxton Istace +LesterThePossum +Rurikid +ojacid . +james +A Patreon of the Ahts +BigE0021 +Angelique Badger +Jonathan Williams +AntiBlock +Redsauz +Florian Kelber +John Patrick Callahan Jr +Alexandra Vesey +Bas Kroot +Dzmitry Malyshau +PedanticSteve +Josh Wren +BLRageQuit +Dario Spadavecchia +Neutrix +Markell +Rocky +Robert Landry +Skylar Mangum-Turner +Nick Mowry +Anjen Pai +Hope You're Well +Alexandre Boivin +Racussa +Mike Conley +Karen Blythe +Mark Sprietsma +Xavier privé +Tommy Mayfield +Václav Švec +Binks +Mackenzie +Linn Browning +Writer's Consultant Page by George J.Lekkas +Andrew Hines +Wexxler +Jason Matthew Wuerfel +Milo Cohen +Alan Buehne +Dominick Ormsby +Espen Sæverud +Rasmus Legêne +rbbalderama +Nobody679 +Prince of Morgoth +Jaryd Armstrong +Gary Smith +ThyHolyDevil +良义 金 +Andrew Pirkola +Dig +Chris Gray +Tyshaun Wise +Phoenix +Ethan Cook +Jordan Bellah +Petro Lombaard +Kass Frisson +Lazer Elf +Gavin Madrigal +Rox +PinkEvil +Martin Lorber +Emanuel Pietri +Alex Beard +Jeffrey Henning +Eric Alexander Cartaya +Dust Bunny +GameNight +Beingus +Crys Cain +Lon Varnadore +Thomas Mortensen Hansen +Drinarius +Ed Wright +Adrian Wright +Zklaus +Chris Bloom +PlayByMail.Net +Maxim Lowe +Aquelion +Tiber +Daydream1013 +Page One Project +Clonetone +Egoensis +Brad Wardell +Heaven N Lee +BarnabyJones +Paul Ingram +Lance Saba +Chad Riley +Austin +Rowland Kingman +Decimus Vitalis +Grayson McClead +Battleturtle1 +Kristin Chernoff +Justin Mcclain +Patrick Jones +Esther Busch +Chance Mena +JimmyTheBob +Antiroo +Dalton Clark +Guilherme Aguiar +Simon Drapeau +Akirsop +Radovan Zapletal +Vanessa Anjos +Rikard Wolff +Justa Badge +teco 47 +Jake +Miguel Alejandro +Blargh Blarghmoomoo +Jakob Siegel +Grant A. Murray +Jarno Hallikainen +Jan Ka +Joshua Vaught +MaxOliver +WarWizardGames +Evan-DiLeo +Eric Moore +Kyle S +Alex Debus +Uniquenameosaurus +Dean Dunakin +Jack +Bryan Brake +McNeil Atticus Inksmudge +Char +Tom Van Orden jr +Kendall Patterson +Akylos +Barna Csíkos +Nicholas Grabstas +OldFarkas +Riley Seaman +Daniel Gill +Kyle Robertson +Natasha Taylor +Pierrick Bertrand +Jared.K +Dylan Devenny +logic_error +SashaTK +Steve Johnson +MontyBoosh +Achillain +Jaden +Vito Martono +Thirty-OneR +Eric Foley +ThatGuyGW +Dee Chiu +James H. Anthony +Kevin Cossutta +MadNomadMedia +Darinius Dragonclaw Studios +Tsahyla (Triston Lightyear) +Christopher Whitney +María Martín López +Annie Rishor +Aram Sabatés +Jeppe Skov Jensen +Martin Seeger +Oneiris (Oni) +EternalDeiwos +Richard Keating +StroboWolf +Rick Falkvinge +Zewen Senpai +Adam Butler +Kassidy +Sadie Blackthorne +ErrorForever +Seth Fusion +Gus +Paul +Lucid +Allen Varney +Hannah May +Sankroh +Eliot Miller +Detocroix +Meg Ziegler +rob bee +Anoplexian +Marten F +Erin D. Smale +Johnpaul Morrow +Roekai +Drunken_Legends +Jesse Holmes +Maxwell Hill +Jan Dvořák +SirTobit +G0atfather +Allen S. Rout +Pippa Mitchell +Austin Miller +Caner Oleas Pekgönenç +Alison Bull Bear +Bradley Edwards +Tertiary +Daniel +Joshua E Goodwin +Shaun Alexander +Ryan Lege +Myrrhlin +Jesper Cockx +Noirbard +Dice +Brian Drennen +Giant Monster Games +Reya C. +Krk +Endwords +Jacob Harrington +RK +Michael Greiner +Steven Bennett +Brice Moss +Whakomatic x +Stephen Herron +kosmobius +ZizRenanim +Barished +Maur Razimtheth +Aaron bateson +Diklyquill +Shawn Taylor +Brady R Rathbun +FlippantFeline +Shadow +J +Tamashi Toh +Huw Williams +Graves +ShadeByTheSea +The Dungeon Masters +Valerie Elise +Empi3 +William Pucs +Michael Carmody +Marco Veldman +naikibens220 +Jordon Phillips +_gfx_ +F. Casanova +Jared McDaris +BlastWind +Taldonix +Connor McMartin +Nexoness +Guy +Maggie +AdvancedAzrielAngel +Alfred García +Norbert Žigmund +Jennifer +Titanium Tomes +John Ackley +Invad3r233 +Jonathan Killstring +Jessica Thomas +Nikita Kondratjuks +Steve Hyatt +PoliticsBuff +Ian arless +Karnat +Hilton Williams +Kevin +Katharina Haase +Hisham Bedri +Bird +JOSHUA QUALTIERI +Preston Brooks +Troy Schuler +DerGeisterbär +L. V. Werneck +Marcus Hellyrr +yami +Daniel Eric Crosby +Augusto Chiarle +Doug Churchman +David Roza +Alexander Thomas +Ashley Wilson-Savoury +Nathan L Myers +Theresa Walsh +JP Roberts III +William Henry +OldbeanOldboy +Javasharp +Diagonath +Gun Metal Games +Scott Marner +Alloyed Clavicle +Valerii Matskevych +Spencer Sherman +Nolan Moore +James Schellenger +Pat +Dino Princip +Shawn Spencer +Timothée CALLET +KC138 +Nylian +Kate +Markus Finster +CanadianGold +AstralJacks +Keith Marshall +Scott Davis +Joseph Miranda +Shaptarshi Joarder +Branndon +EP +Johan Fröberg +Sasquatch +Chase Mayers +Sizz_TV +Ryan Westcott +Nathan Mitchell +Curt Flood +Mikey +E.M. White +Billy +Vlad Tomash +Xariun +Luke Nelson +W Maxwell Cassity-Guilliom +Marty H +Aaron Meyer +Max Amillios +chris +cyninge +Omegavoid +Fritjof Olsson +Crazypedia +Duncan Thomson +William Merriott +Gold Tamarin +Lhoris +Jonathan +Jon +Massimo Vella +Feuver +aymeric +Eric Schumann +Rei +Fondue +Paavi1 +Wil Sisney +David Patterson`; diff --git a/modules/religions-generator.js b/modules/religions-generator.js index d823b4c6..0ebd226d 100644 --- a/modules/religions-generator.js +++ b/modules/religions-generator.js @@ -304,15 +304,34 @@ window.Religions = (function () { Heresy: {Heresy: 1} }; - const methods = { - "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 + 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 = { @@ -342,381 +361,416 @@ window.Religions = (function () { } }; - const generate = function () { + 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) + }; + + function generate() { TIME && console.time("generateReligions"); - const {cells, states, cultures} = pack; + const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || []; - const religionIds = new Uint16Array(cells.culture); // cell religion; initially based on culture - const religions = []; + const folkReligions = generateFolkReligions(); + const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions); - // add folk religions - pack.cultures.forEach(c => { - const newId = c.i; - if (!newId) return religions.push({i: 0, name: "No religion"}); + const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]); + const indexedReligions = combineReligions(namedReligions, lockedReligions); + const religionIds = expandReligions(indexedReligions); + const religions = defineOrigins(religionIds, indexedReligions); - if (c.removed) { - religions.push({ - i: c.i, - name: "Extinct religion for " + c.name, - color: getMixedColor(c.color, 0.1, 0), - removed: true - }); - return; - } - - if (pack.religions) { - const lockedFolkReligion = pack.religions.find( - r => r.culture === c.i && !r.removed && r.lock && r.type === "Folk" - ); - - if (lockedFolkReligion) { - for (const i of cells.i) { - if (cells.religion[i] === lockedFolkReligion.i) religionIds[i] = newId; - } - - lockedFolkReligion.i = newId; - religions.push(lockedFolkReligion); - return; - } - } - - const form = rw(forms.Folk); - const name = c.name + " " + rw(types[form]); - const deity = form === "Animism" ? null : getDeityName(c.i); - const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`; - religions.push({ - i: newId, - name, - color, - culture: newId, - type: "Folk", - form, - deity, - center: c.center, - origins: [0] - }); - }); - - if (religionsInput.value == 0 || pack.cultures.length < 2) - return religions.filter(r => r.i).forEach(r => (r.code = abbreviate(r.name))); - - const burgs = pack.burgs.filter(b => b.i && !b.removed); - const sorted = - burgs.length > +religionsInput.value - ? burgs.sort((a, b) => b.population - a.population).map(b => b.cell) - : cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); - - const religionsTree = d3.quadtree(); - const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns - const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value); - const count = +religionsInput.value - cultsCount + religions.length; - - function getReligionsInRadius({x, y, r, max}) { - if (max === 0) return [0]; - const cellsInRadius = findAll(x, y, r); - const religions = unique(cellsInRadius.map(i => religionIds[i]).filter(r => r)); - return religions.length ? religions.slice(0, max) : [0]; - } - - // restore locked non-folk religions - if (pack.religions) { - const lockedNonFolkReligions = pack.religions.filter(r => r.lock && !r.removed && r.type !== "Folk"); - for (const religion of lockedNonFolkReligions) { - const newId = religions.length; - for (const i of cells.i) { - if (cells.religion[i] === religion.i) religionIds[i] = newId; - } - - religion.i = newId; - religion.origins = religion.origins.filter(origin => origin < newId); - religionsTree.add(cells.p[religion.center]); - religions.push(religion); - } - } - - // generate organized religions - for (let i = 0; religions.length < count && i < 1000; i++) { - let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center - const form = rw(forms.Organized); - const state = cells.state[center]; - const culture = cells.culture[center]; - - const deity = form === "Non-theism" ? null : getDeityName(culture); - let [name, expansion] = getReligionName(form, deity, center); - if (expansion === "state" && !state) expansion = "global"; - if (expansion === "culture" && !culture) expansion = "global"; - - if (expansion === "state" && Math.random() > 0.5) center = states[state].center; - if (expansion === "culture" && Math.random() > 0.5) center = cultures[culture].center; - - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - - const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform - if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion - - // add "Old" to name of the folk religion on this culture - const isFolkBased = expansion === "culture" || P(0.5); - const folk = isFolkBased && religions.find(r => r.culture === culture && r.type === "Folk"); - if (folk && expansion === "culture" && folk.name.slice(0, 3) !== "Old") folk.name = "Old " + folk.name; - - const origins = folk ? [folk.i] : getReligionsInRadius({x, y, r: 150 / count, max: 2}); - const expansionism = rand(3, 8); - const baseColor = religions[culture]?.color || states[state]?.color || getRandomColor(); - const color = getMixedColor(baseColor, 0.3, 0); - - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Organized", - form, - deity, - expansion, - expansionism, - center, - origins - }); - religionsTree.add([x, y]); - } - - // generate cults - for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) { - const form = rw(forms.Cult); - let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - - const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform - if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion - - const culture = cells.culture[center]; - const origins = getReligionsInRadius({x, y, r: 300 / count, max: rand(0, 4)}); - - const deity = getDeityName(culture); - const name = getCultName(form, center); - const expansionism = gauss(1.1, 0.5, 0, 5); - const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)"; - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Cult", - form, - deity, - expansion: "global", - expansionism, - center, - origins - }); - religionsTree.add([x, y]); - } - - expandReligions(); - - // generate heresies - religions - .filter(r => r.type === "Organized") - .forEach(r => { - if (r.expansionism < 3) return; - const count = gauss(0, 1, 0, 3); - for (let i = 0; i < count; i++) { - let center = ra(cells.i.filter(i => religionIds[i] === r.i && cells.c[i].some(c => religionIds[c] !== r.i))); - if (!center) continue; - if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) - center = cells.c[center].find(c => cells.burg[c]); - const [x, y] = cells.p[center]; - if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other - - const culture = cells.culture[center]; - const name = getCultName("Heresy", center); - const expansionism = gauss(1.2, 0.5, 0, 5); - const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)"; - religions.push({ - i: religions.length, - name, - color, - culture, - type: "Heresy", - form: r.form, - deity: r.deity, - expansion: "global", - expansionism, - center, - origins: [r.i] - }); - religionsTree.add([x, y]); - } - }); - - expandHeresies(); + pack.religions = religions; + pack.cells.religion = religionIds; checkCenters(); - cells.religion = religionIds; - pack.religions = religions; - TIME && console.timeEnd("generateReligions"); + } - // growth algorithm to assign cells to religions - function expandReligions() { - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = []; + function generateFolkReligions() { + return pack.cultures + .filter(c => c.i && !c.removed) + .map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center})); + } - religions - .filter(r => !r.lock && (r.type === "Organized" || r.type === "Cult")) - .forEach(r => { - religionIds[r.center] = r.i; - queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture}); - cost[r.center] = 1; - }); + function generateOrganizedReligions(desiredReligionNumber, lockedReligions) { + const cells = pack.cells; + const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0; + const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount; + if (requiredReligionsNumber < 1) return []; - const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth - const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty + const candidateCells = getCandidateCells(); + const religionCores = placeReligions(); - while (queue.length) { - const {e, p, r, c, s} = queue.dequeue(); - const expansion = religions[r].expansion; + const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40% + const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30% + const organizedCount = religionCores.length - cultsCount - heresiesCount; - cells.c[e].forEach(nextCell => { - if (expansion === "culture" && c !== cells.culture[nextCell]) return; - if (expansion === "state" && s !== cells.state[nextCell]) return; - if (religions[religionIds[nextCell]]?.lock) return; + const getType = index => { + if (index < organizedCount) return "Organized"; + if (index < organizedCount + cultsCount) return "Cult"; + return "Heresy"; + }; - const cultureCost = c !== cells.culture[nextCell] ? 10 : 0; - const stateCost = s !== cells.state[nextCell] ? 10 : 0; - const biomeCost = cells.road[nextCell] ? 1 : biomesData.cost[cells.biome[nextCell]]; - const populationCost = Math.max(rn(popCost - cells.pop[nextCell]), 0); - const heightCost = Math.max(cells.h[nextCell], 20) - 20; - const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0; - const totalCost = - p + - (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism; - if (totalCost > neutral) return; + return religionCores.map((cellId, index) => { + const type = getType(index); + const form = rw(forms[type]); + const cultureId = cells.culture[cellId]; - if (!cost[nextCell] || totalCost < cost[nextCell]) { - if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell - cost[nextCell] = totalCost; - queue.queue({e: nextCell, p: totalCost, r, c, s}); - } - }); + return {type, form, culture: cultureId, center: cellId}; + }); + + function placeReligions() { + const religionCells = []; + const religionsTree = d3.quadtree(); + + // pre-populate with locked centers + lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center])); + + // min distance between religion inceptions + const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber; + + for (const cellId of candidateCells) { + const [x, y] = cells.p[cellId]; + + if (religionsTree.find(x, y, spacing) === undefined) { + religionCells.push(cellId); + religionsTree.add([x, y]); + + if (religionCells.length === requiredReligionsNumber) return religionCells; + } + } + + WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`); + return religionCells; + } + + function getCandidateCells() { + const validBurgs = pack.burgs.filter(b => b.i && !b.removed); + + if (validBurgs.length >= requiredReligionsNumber) + return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell); + return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); + } + } + + function specifyReligions(newReligions) { + const {cells, cultures} = pack; + + const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => { + const supreme = getDeityName(cultureId); + const deity = form === "Non-theism" || form === "Animism" ? null : supreme; + + const stateId = cells.state[center]; + + let [name, expansion] = generateReligionName(type, form, supreme, center); + if (expansion === "state" && !stateId) expansion = "global"; + + const expansionism = expansionismMap[type](); + const color = getReligionColor(cultures[cultureId], type); + + return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color}; + }); + + return rawReligions; + + function getReligionColor(culture, type) { + if (!culture.i) return getRandomColor(); + + if (type === "Folk") return culture.color; + if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2); + if (type === "Cult") return getMixedColor(culture.color, 0.5, 0); + return getMixedColor(culture.color, 0.25, 0.4); + } + } + + // indexes, conditionally renames, and abbreviates religions + function combineReligions(namedReligions, lockedReligions) { + const indexedReligions = [{name: "No religion", i: 0}]; + + const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions(); + const maxIndex = Math.max( + highestLockedIndex, + namedReligions.length + lockedReligions.length + 1 - numberLockedFolk + ); + + for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) { + // place locked religion back at its old index + if (index === lockedReligionQueue[0]?.i) { + const nextReligion = lockedReligionQueue.shift(); + indexedReligions.push(nextReligion); + continue; + } + + // slot the new religions + if (progress < namedReligions.length) { + const nextReligion = namedReligions[progress]; + progress++; + + if ( + nextReligion.type === "Folk" && + lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture) + ) + continue; // when there is a locked Folk religion for this culture discard duplicate + + const newName = renameOld(nextReligion); + const code = abbreviate(newName, codes); + codes.push(code); + indexedReligions.push({...nextReligion, i: index, name: newName, code}); + continue; + } + + indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true}); + } + return indexedReligions; + + function parseLockedReligions() { + // copy and sort the locked religions list + const lockedReligionQueue = lockedReligions + .map(religion => { + // and filter their origins to locked religions + let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n)); + if (newOrigin === []) newOrigin = [0]; + return {...religion, origins: newOrigin}; + }) + .sort((a, b) => a.i - b.i); + + const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i)); + const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : []; + const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length; + + return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk}; + } + + // prepend 'Old' to names of folk religions which have organized competitors + function renameOld({name, type, culture: cultureId}) { + if (type !== "Folk") return name; + + const haveOrganized = + namedReligions.some( + ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" + ) || + lockedReligions.some( + ({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture" + ); + if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`; + return name; + } + } + + // finally generate and stores origins trees + function defineOrigins(religionIds, indexedReligions) { + const religionOriginsParamsMap = { + Organized: {clusterSize: 100, maxReligions: 2}, + Cult: {clusterSize: 50, maxReligions: 3}, + Heresy: {clusterSize: 50, maxReligions: 4} + }; + + const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => { + if (i === 0) return null; // no religion + if (type === "Folk") return [0]; // folk religions originate from its parent culture only + + const folkReligion = indexedReligions.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 fallbackOrigin = folkReligion?.i || 0; + return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin); + }); + + return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]})); + } + + function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) { + const foundReligions = new Set(); + const queue = [center]; + const checked = {}; + + for (let size = 0; queue.length && size < clusterSize; size++) { + const cellId = queue.shift(); + checked[cellId] = true; + + for (const neibId of neighbors[cellId]) { + if (checked[neibId]) continue; + checked[neibId] = true; + + const neibReligion = religionIds[neibId]; + if (neibReligion && neibReligion < religionId) foundReligions.add(neibReligion); + if (foundReligions.size >= maxReligions) return [...foundReligions]; + queue.push(neibId); } } - // growth algorithm to assign cells to heresies - function expandHeresies() { - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = []; + return foundReligions.size ? [...foundReligions] : [fallbackOrigin]; + } - religions - .filter(r => !r.lock && r.type === "Heresy") - .forEach(r => { - const b = religionIds[r.center]; // "base" religion id - religionIds[r.center] = r.i; // heresy id - queue.queue({e: r.center, p: 0, r: r.i, b}); - cost[r.center] = 1; - }); + // growth algorithm to assign cells to religions + function expandReligions(religions) { + const cells = pack.cells; + const religionIds = spreadFolkReligions(religions); - const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth + const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); + const cost = []; - while (queue.length) { - const {e, p, r, b} = queue.dequeue(); + const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth - cells.c[e].forEach(nextCell => { - if (religions[religionIds[nextCell]]?.lock) return; - const religionCost = religionIds[nextCell] === b ? 0 : 2000; - const biomeCost = cells.road[nextCell] ? 0 : biomesData.cost[cells.biome[nextCell]]; - const heightCost = Math.max(cells.h[nextCell], 20) - 20; - const waterCost = cells.h[nextCell] < 20 ? (cells.road[nextCell] ? 50 : 1000) : 0; - const totalCost = - p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1); + const biomePassageCost = cellId => biomesData.cost[cells.biome[cellId]]; - if (totalCost > neutral) return; + religions + .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) + .forEach(r => { + religionIds[r.center] = r.i; + queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}); + cost[r.center] = 1; + }); - if (!cost[nextCell] || totalCost < cost[nextCell]) { - if (cells.h[nextCell] >= 20 && cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell - cost[nextCell] = totalCost; - queue.queue({e: nextCell, p: totalCost, r}); - } - }); - } - } + const religionsMap = new Map(religions.map(r => [r.i, r])); - function checkCenters() { - const codes = religions.map(r => r.code); - religions.forEach(r => { - if (!r.i) return; - r.code = abbreviate(r.name, codes); + const isMainRoad = cellId => cells.road[cellId] - cells.crossroad[cellId] > 4; + const isTrail = cellId => cells.h[cellId] > 19 && cells.road[cellId] - cells.crossroad[cellId] === 1; + const isSeaRoute = cellId => cells.h[cellId] < 20 && cells.road[cellId]; + const isWater = cellId => cells.h[cellId] < 20; - // move religion center if it's not within religion area after expansion - if (religionIds[r.center] === r.i) return; // in area - const firstCell = cells.i.find(i => religionIds[i] === r.i); - if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion + while (queue.length) { + const {e: cellId, p, r, s: state} = queue.dequeue(); + const {culture, expansion, expansionism} = religionsMap.get(r); + + cells.c[cellId].forEach(nextCell => { + if (expansion === "culture" && culture !== cells.culture[nextCell]) return; + if (expansion === "state" && state !== cells.state[nextCell]) return; + if (religionsMap.get(religionIds[nextCell])?.lock) return; + + const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0; + const stateCost = state !== cells.state[nextCell] ? 10 : 0; + const passageCost = getPassageCost(nextCell); + + const cellCost = cultureCost + stateCost + passageCost; + const totalCost = p + 10 + cellCost / expansionism; + if (totalCost > maxExpansionCost) return; + + if (!cost[nextCell] || totalCost < cost[nextCell]) { + if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell + cost[nextCell] = totalCost; + + queue.queue({e: nextCell, p: totalCost, r, s: state}); + } }); } - }; + + return religionIds; + + function getPassageCost(cellId) { + if (isWater(cellId)) return isSeaRoute ? 50 : 500; + if (isMainRoad(cellId)) return 1; + const biomeCost = biomePassageCost(cellId); + return isTrail(cellId) ? biomeCost / 1.5 : biomeCost; + } + } + + // folk religions initially get all cells of their culture, and locked religions are retained + function spreadFolkReligions(religions) { + const cells = pack.cells; + const hasPrior = cells.religion && true; + const religionIds = new Uint16Array(cells.i.length); + + const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed); + const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i])); + + for (const cellId of cells.i) { + const oldId = (hasPrior && cells.religion[cellId]) || 0; + if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) { + religionIds[cellId] = oldId; + continue; + } + const cultureId = cells.culture[cellId]; + religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0; + } + + return religionIds; + } + + function checkCenters() { + const cells = pack.cells; + pack.religions.forEach(r => { + if (!r.i) return; + // move religion center if it's not within religion area after expansion + if (cells.religion[r.center] === r.i) return; // in area + const firstCell = cells.i.find(i => cells.religion[i] === r.i); + const cultureHome = pack.cultures[r.culture]?.center; + if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion + else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers + }); + } + + function recalculate() { + const newReligionIds = expandReligions(pack.religions); + pack.cells.religion = newReligionIds; + + checkCenters(); + } const add = function (center) { - const {cells, religions} = pack; + const {cells, cultures, religions} = pack; const religionId = cells.religion[center]; + const i = religions.length; - const culture = cells.culture[center]; - const color = getMixedColor(religions[religionId].color, 0.3, 0); + const cultureId = cells.culture[center]; + const missingFolk = + cultureId !== 0 && + !religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed); + const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0); - const type = - religions[religionId].type === "Organized" ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2}); + const type = missingFolk + ? "Folk" + : religions[religionId].type === "Organized" + ? rw({Organized: 4, Cult: 1, Heresy: 2}) + : rw({Organized: 5, Cult: 2}); const form = rw(forms[type]); const deity = - type === "Heresy" ? religions[religionId].deity : form === "Non-theism" ? null : getDeityName(culture); + type === "Heresy" + ? religions[religionId].deity + : form === "Non-theism" || form === "Animism" + ? null + : getDeityName(cultureId); - let name, expansion; - if (type === "Organized") [name, expansion] = getReligionName(form, deity, center); - else { - name = getCultName(form, center); - expansion = "global"; - } + const [name, expansion] = generateReligionName(type, form, deity, center); const formName = type === "Heresy" ? religions[religionId].form : form; const code = abbreviate( name, religions.map(r => r.code) ); + const influences = getReligionsInRadius(cells.c, center, cells.religion, i, 25, 3, 0); + const origins = type === "Folk" ? [0] : influences; - const i = religions.length; religions.push({ i, name, color, - culture, + culture: cultureId, type, form: formName, deity, expansion, - expansionism: 0, + expansionism: expansionismMap[type](), center, cells: 0, area: 0, rural: 0, urban: 0, - origins: [religionId], + origins, code }); cells.religion[center] = i; }; function updateCultures() { - TIME && console.time("updateCulturesForReligions"); pack.religions = pack.religions.map((religion, index) => { - if (index === 0) { - return religion; - } + if (index === 0) return religion; return {...religion, culture: pack.cells.culture[religion.center]}; }); - TIME && console.timeEnd("updateCulturesForReligions"); } // get supreme deity name @@ -735,22 +789,24 @@ window.Religions = (function () { if (a === "Number") return ra(base.number); if (a === "Being") return ra(base.being); if (a === "Adjective") return ra(base.adjective); - if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal); - if (a === "Adjective + Animal") return ra(base.adjective) + " " + ra(base.animal); - if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being); - if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive); - if (a === "Color + Being") return ra(base.color) + " " + ra(base.being); - if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive); - if (a === "Being + of + Genitive") return ra(base.being) + " of " + ra(base.genitive); - if (a === "Being + of the + Genitive") return ra(base.being) + " of the " + ra(base.theGenitive); - if (a === "Animal + of + Genitive") return ra(base.animal) + " of " + ra(base.genitive); + if (a === "Color + Animal") return `${ra(base.color)} ${ra(base.animal)}`; + if (a === "Adjective + Animal") return `${ra(base.adjective)} ${ra(base.animal)}`; + if (a === "Adjective + Being") return `${ra(base.adjective)} ${ra(base.being)}`; + if (a === "Adjective + Genitive") return `${ra(base.adjective)} ${ra(base.genitive)}`; + if (a === "Color + Being") return `${ra(base.color)} ${ra(base.being)}`; + if (a === "Color + Genitive") return `${ra(base.color)} ${ra(base.genitive)}`; + if (a === "Being + of + Genitive") return `${ra(base.being)} of ${ra(base.genitive)}`; + if (a === "Being + of the + Genitive") return `${ra(base.being)} of the ${ra(base.theGenitive)}`; + if (a === "Animal + of + Genitive") return `${ra(base.animal)} of ${ra(base.genitive)}`; if (a === "Adjective + Being + of + Genitive") - return ra(base.adjective) + " " + ra(base.being) + " of " + ra(base.genitive); + return `${ra(base.adjective)} ${ra(base.being)} of ${ra(base.genitive)}`; if (a === "Adjective + Animal + of + Genitive") - return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive); + return `${ra(base.adjective)} ${ra(base.animal)} of ${ra(base.genitive)}`; + + ERROR && console.error("Unkown generation approach"); } - function getReligionName(form, deity, center) { + function generateReligionName(variety, form, deity, center) { const {cells, cultures, burgs, states} = pack; const random = () => Names.getCulture(cells.culture[center], null, null, "", 0); @@ -766,7 +822,7 @@ window.Religions = (function () { return adj ? getAdjective(name) : name; }; - const m = rw(methods); + const m = rw(namingMethods[variety]); if (m === "Random + type") return [random() + " " + type(), "global"]; if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"]; if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"]; @@ -776,24 +832,11 @@ window.Religions = (function () { if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"]; if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"]; if (m === "Culture + type") return [culture() + " " + type(), "culture"]; + if (m === "Burg + ian + type") return [`${place("adj")} ${type()}`, "global"]; + if (m === "Random + ian + type") return [`${getAdjective(random())} ${type()}`, "global"]; + if (m === "Type + of the + meaning") return [`${type()} of the ${generateMeaning()}`, "global"]; return [trimVowels(random()) + "ism", "global"]; // else } - function getCultName(form, center) { - const cells = pack.cells; - const type = function () { - return rw(types[form]); - }; - const random = function () { - return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]); - }; - const burg = function () { - return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]); - }; - if (cells.burg[center]) return burg() + "ian " + type(); - if (Math.random() > 0.5) return random() + "ian " + type(); - return type() + " of the " + generateMeaning(); - } - - return {generate, add, getDeityName, updateCultures}; + return {generate, add, getDeityName, updateCultures, recalculate}; })(); diff --git a/modules/river-generator.js b/modules/river-generator.js index 957fe6fc..fd8a95b4 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -48,7 +48,9 @@ window.Rivers = (function () { cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation // create lake outlet if lake is not in deep depression and flux > evaporation - const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; + const lakes = lakeOutCells[i] + ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) + : []; for (const lake of lakes) { const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet @@ -191,7 +193,18 @@ window.Rivers = (function () { const length = getApproximateLength(meanderedPoints); const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0)); - pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells}); + pack.rivers.push({ + i: riverId, + source, + mouth, + discharge, + length, + width, + widthFactor, + sourceWidth: 0, + parent, + cells: riverCells + }); } } @@ -479,6 +492,10 @@ window.Rivers = (function () { return getBasin(parent); }; + const getNextId = function (rivers) { + return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1; + }; + return { generate, alterHeights, @@ -493,6 +510,7 @@ window.Rivers = (function () { getOffset, getApproximateLength, getRiverPoints, - remove + remove, + getNextId }; })(); diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 74ed5906..49a525eb 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1176,18 +1176,18 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=12062022"); + const Editor = await import("../dynamic/editors/states-editor.js?v=1.89.05"); Editor.open(); } async function editCultures() { if (customization) return; - const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.88.06"); + const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.89.09"); Editor.open(); } async function editReligions() { if (customization) return; - const Editor = await import("../dynamic/editors/religions-editor.js?v=1.88.06"); + const Editor = await import("../dynamic/editors/religions-editor.js?v=1.89.10"); Editor.open(); } diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 07216f90..bd41cefe 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -204,6 +204,13 @@ function editHeightmap(options) { INFO && console.group("Edit Heightmap"); TIME && console.time("regenerateErasedData"); + // remove data + pack.cultures = []; + pack.burgs = []; + pack.states = []; + pack.provinces = []; + pack.religions = []; + const erosionAllowed = allowErosion.checked; markFeatures(); markupGridOcean(); @@ -231,8 +238,10 @@ function editHeightmap(options) { Lakes.defineGroup(); defineBiomes(); rankCells(); + Cultures.generate(); Cultures.expand(); + BurgsAndStates.generate(); Religions.generate(); BurgsAndStates.defineStateForms(); diff --git a/modules/ui/ice-editor.js b/modules/ui/ice-editor.js index f07cb6f9..a9e6ff28 100644 --- a/modules/ui/ice-editor.js +++ b/modules/ui/ice-editor.js @@ -67,11 +67,11 @@ function editIce() { function addIcebergOnClick() { const [x, y] = d3.mouse(this); const i = findGridCell(x, y, grid); - const c = grid.points[i]; - const s = +document.getElementById("iceSize").value; + const [cx, cy] = grid.points[i]; + const size = +document.getElementById("iceSize")?.value || 1; - const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) / s) | 0, (p[1] + (c[1] - p[1]) / s) | 0]); - const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s); + const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]); + const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size); iceberg.call(d3.drag().on("drag", dragElement)); if (d3.event.shiftKey === false) toggleAdd(); } diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 28b6ef57..3ea64e72 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -671,11 +671,10 @@ function toggleIce(event) { } function drawIce() { - const cells = grid.cells, - vertices = grid.vertices, - n = cells.i.length, - temp = cells.temp, - h = cells.h; + const {cells, vertices} = grid; + const {temp, h} = cells; + const n = cells.i.length; + const used = new Uint8Array(cells.i.length); Math.random = aleaPRNG(seed); @@ -700,23 +699,22 @@ function drawIce() { continue; } + const tNormalized = normalize(t, -8, 2); + const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1; + // mildly cold: iceberd - if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells + if (P(tNormalized ** 0.5 * randomFactor)) continue; // cold: skip some cells if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers - let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size - if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers - size = Math.min(size * (0.4 + rand() * 1.2), 0.95); // randomize iceberg size - resizePolygon(i, size); + + let size = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size + if (cells.t[i] === -1) size /= 1.3; // coasline: smaller icebers + resizePolygon(i, minmax(rn(size * randomFactor, 2), 0.08, 1)); } - function resizePolygon(i, s) { - const c = grid.points[i]; - const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) * s) | 0, (p[1] + (c[1] - p[1]) * s) | 0]); - ice - .append("polygon") - .attr("points", points) - .attr("cell", i) - .attr("size", rn(1 - s, 2)); + function resizePolygon(i, size) { + const [cx, cy] = grid.points[i]; + const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]); + ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size); } // connect vertices to chain diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js index 95499433..a28885b9 100644 --- a/modules/ui/notes-editor.js +++ b/modules/ui/notes-editor.js @@ -42,12 +42,11 @@ function editNotes(id, name) { $("#notesEditor").dialog({ title: "Notes Editor", - width: "minmax(80vw, 540px)", + width: window.innerWidth * 0.8, height: window.innerHeight * 0.75, position: {my: "center", at: "center", of: "svg"}, close: removeEditor }); - $("[aria-describedby='notesEditor']").css("top", "10vh"); if (modules.editNotes) return; modules.editNotes = true; diff --git a/modules/ui/options.js b/modules/ui/options.js index f8ccfb38..ea7f596e 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -77,12 +77,15 @@ document // show popup with a list of Patreon supportes (updated manually) async function showSupporters() { const {supporters} = await import("../dynamic/supporters.js?v=19062022"); + const list = supporters.split("\n").sort(); + const columns = window.innerWidth < 800 ? 2 : 5; + alertMessage.innerHTML = - ""; + `"; $("#alert").dialog({ resizable: false, title: "Patreon Supporters", - width: "54vw", + width: "min-width", position: {my: "center", at: "center", of: "svg"} }); } @@ -157,9 +160,20 @@ optionsContent.addEventListener("click", function (event) { }); function mapSizeInputChange() { + const $mapWidthInput = byId("mapWidthInput"); + const $mapHeightInput = byId("mapHeightInput"); + changeMapSize(); - localStorage.setItem("mapWidth", mapWidthInput.value); - localStorage.setItem("mapHeight", mapHeightInput.value); + localStorage.setItem("mapWidth", $mapWidthInput.value); + localStorage.setItem("mapHeight", $mapHeightInput.value); + + const tooWide = +$mapWidthInput.value > window.innerWidth; + const tooHigh = +$mapHeightInput.value > window.innerHeight; + + if (tooWide || tooHigh) { + const message = `Canvas size is larger than actual window size (${window.innerWidth} x ${window.innerHeight}). It can affect the performance if you are going to create a new map`; + tip(message, false, "warn", 4000); + } } // change svg size on manual size change or window resize, do not change graph size @@ -534,7 +548,7 @@ function applyStoredOptions() { options.stateLabelsMode = stateLabelsModeInput.value; } -// randomize options if randomization is allowed (not locked or options='default') +// randomize options if randomization is allowed (not locked or queryParam options='default') function randomizeOptions() { const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options @@ -546,7 +560,7 @@ function randomizeOptions() { manorsInput.value = 1000; manorsOutput.value = "auto"; } - if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(5, 2, 2, 10); + if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10); if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2); if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1); if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30); @@ -602,17 +616,17 @@ function randomizeCultureSet() { function setRendering(value) { viewbox.attr("shape-rendering", value); - if (value === "optimizeSpeed") { - // block some styles - coastline.select("#sea_island").style("filter", "none"); - statesHalo.style("display", "none"); - emblems.style("opacity", 1); - } else { - // remove style block - coastline.select("#sea_island").style("filter", null); - statesHalo.style("display", null); - emblems.style("opacity", null); - } + // if (value === "optimizeSpeed") { + // // block some styles + // coastline.select("#sea_island").style("filter", "none"); + // statesHalo.style("display", "none"); + // emblems.style("opacity", 1); + // } else { + // // remove style block + // coastline.select("#sea_island").style("filter", null); + // statesHalo.style("display", null); + // emblems.style("opacity", null); + // } } // generate current year and era name diff --git a/modules/ui/rivers-creator.js b/modules/ui/rivers-creator.js index 83a4d1b9..a8600917 100644 --- a/modules/ui/rivers-creator.js +++ b/modules/ui/rivers-creator.js @@ -74,12 +74,13 @@ function createRiver() { function addRiver() { const {rivers, cells} = pack; - const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers; + const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} = + Rivers; const riverCells = createRiver.cells; if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error"); - const riverId = rivers.length ? last(rivers).i + 1 : 1; + const riverId = getNextId(rivers); const parent = cells.r[last(riverCells)] || riverId; riverCells.forEach(cell => { @@ -100,12 +101,30 @@ function createRiver() { const name = getName(mouth); const basin = getBasin(parent); - rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"}); + rivers.push({ + i: riverId, + source, + mouth, + discharge, + length, + width, + widthFactor, + sourceWidth, + parent, + cells: riverCells, + basin, + name, + type: "River" + }); const id = "river" + riverId; // render river lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - viewbox.select("#rivers").append("path").attr("id", id).attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth)); + viewbox + .select("#rivers") + .append("path") + .attr("id", id) + .attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth)); editRiver(id); } diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 785c22a9..ca52b036 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -1,4 +1,7 @@ "use strict"; + +const CONTROL_POINST_DISTANCE = 10; + function editRoute(onClick) { if (customization) return; if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return; @@ -47,13 +50,13 @@ function editRoute(onClick) { } function drawControlPoints(node) { - const l = node.getTotalLength(); - const increment = l / Math.ceil(l / 4); - for (let i = 0; i <= l; i += increment) { + const totalLength = node.getTotalLength(); + const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE); + for (let i = 0; i <= totalLength; i += increment) { const point = node.getPointAtLength(i); addControlPoint([point.x, point.y]); } - routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value; + routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value; } function addControlPoint(point, before = null) { diff --git a/modules/ui/stylePresets.js b/modules/ui/stylePresets.js index 7353b067..20e1612c 100644 --- a/modules/ui/stylePresets.js +++ b/modules/ui/stylePresets.js @@ -1,14 +1,27 @@ // UI module to control the style presets "use strict"; -const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"]; +const systemPresets = [ + "default", + "ancient", + "gloom", + "pale", + "light", + "watercolor", + "clean", + "atlas", + "cyberpunk", + "monochrome" +]; const customPresetPrefix = "fmgStyle_"; // add style presets to list { const systemOptions = systemPresets.map(styleName => ``); const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix)); - const customOptions = storedStyles.map(styleName => ``); + const customOptions = storedStyles.map( + styleName => `` + ); const options = systemOptions.join("") + customOptions.join(""); document.getElementById("stylePreset").innerHTML = options; } @@ -37,7 +50,8 @@ async function getStylePreset(desiredPreset) { const isValid = JSON.isValid(storedStyleJSON); if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)]; - ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`); + ERROR && + console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`); presetToLoad = "default"; } } @@ -145,8 +159,31 @@ function addStylePreset() { "#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], "#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"], - "#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"], - "#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"], + "#gridOverlay": [ + "opacity", + "scale", + "dx", + "dy", + "type", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap", + "transform", + "filter", + "mask" + ], + "#coordinates": [ + "opacity", + "data-size", + "font-size", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap", + "filter", + "mask" + ], "#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"], "#rose": ["transform"], "#relig": ["opacity", "stroke", "stroke-width", "filter"], @@ -174,7 +211,17 @@ function addStylePreset() { "#statesBody": ["opacity", "filter"], "#statesHalo": ["opacity", "data-width", "stroke-width", "filter"], "#provs": ["opacity", "fill", "font-size", "font-family", "filter"], - "#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"], + "#temperature": [ + "opacity", + "font-size", + "fill", + "fill-opacity", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap", + "filter" + ], "#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"], "#emblems": ["opacity", "stroke-width", "filter"], "#texture": ["opacity", "filter", "mask"], @@ -184,16 +231,65 @@ function addStylePreset() { "#oceanBase": ["fill"], "#oceanicPattern": ["href", "opacity"], "#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"], - "#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"], + "#legend": [ + "data-size", + "font-size", + "font-family", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap", + "data-x", + "data-y", + "data-columns" + ], "#legendBox": ["fill", "fill-opacity"], "#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], - "#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"], + "#burgIcons > #cities": [ + "opacity", + "fill", + "fill-opacity", + "size", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap" + ], "#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"], "#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"], - "#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"], + "#burgIcons > #towns": [ + "opacity", + "fill", + "fill-opacity", + "size", + "stroke", + "stroke-width", + "stroke-dasharray", + "stroke-linecap" + ], "#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"], - "#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"], - "#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"], + "#labels > #states": [ + "opacity", + "fill", + "stroke", + "stroke-width", + "text-shadow", + "data-size", + "font-size", + "font-family", + "filter" + ], + "#labels > #addedLabels": [ + "opacity", + "fill", + "stroke", + "stroke-width", + "text-shadow", + "data-size", + "font-size", + "font-family", + "filter" + ], "#fogging": ["opacity", "fill", "filter"] }; @@ -238,7 +334,8 @@ function addStylePreset() { if (!styleJSON) return tip("Please provide a style JSON", false, "error"); if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error"); if (!desiredName) return tip("Please provide a preset name", false, "error"); - if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error"); + if (styleSaverTip.innerHTML === "default") + return tip("You cannot overwrite default preset, please change the name", false, "error"); const presetName = customPresetPrefix + desiredName; applyOption(stylePreset, presetName, desiredName + " [custom]"); diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 12d76bb0..7fbc1ebf 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -74,10 +74,8 @@ toolsContent.addEventListener("click", function (event) { }); function processFeatureRegeneration(event, button) { - if (button === "regenerateStateLabels") { - BurgsAndStates.drawStateLabels(); - if (!layerIsOn("toggleLabels")) toggleLabels(); - } else if (button === "regenerateReliefIcons") { + if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels(); + else if (button === "regenerateReliefIcons") { ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief(); } else if (button === "regenerateRoutes") { @@ -628,10 +626,11 @@ function addRiverOnClick() { getType, getWidth, getOffset, - getApproximateLength + getApproximateLength, + getNextId } = Rivers; const riverCells = []; - let riverId = rivers.length ? last(rivers).i + 1 : 1; + let riverId = getNextId(rivers); let parent = riverId; const initialFlux = grid.cells.prec[cells.g[i]]; diff --git a/styles/default.json b/styles/default.json index eb4acb69..4c684dc8 100644 --- a/styles/default.json +++ b/styles/default.json @@ -226,7 +226,7 @@ "opacity": 0.4, "data-width": 10, "stroke-width": 10, - "filter": "blur(5px)" + "filter": "blur(3.5px)" }, "#provs": { "opacity": 0.7, diff --git a/styles/light.json b/styles/light.json index 5663d6c8..fb9b838c 100644 --- a/styles/light.json +++ b/styles/light.json @@ -192,18 +192,18 @@ "filter": null }, "#roads": { - "opacity": 0.9, - "stroke": "#3c1d0b", - "stroke-width": 1.37, + "opacity": 0.8, + "stroke": "#95481a", + "stroke-width": 0.8, "stroke-dasharray": 2, "stroke-linecap": "inherit", "filter": null, "mask": null }, "#trails": { - "opacity": 0.9, + "opacity": 0.8, "stroke": "#95481a", - "stroke-width": 0.88, + "stroke-width": 0.5, "stroke-dasharray": ".8 1.6", "stroke-linecap": "butt", "filter": null, diff --git a/styles/pale.json b/styles/pale.json new file mode 100644 index 00000000..e6acdafd --- /dev/null +++ b/styles/pale.json @@ -0,0 +1,389 @@ +{ + "#map": { + "background-color": "#000000", + "filter": null, + "data-filter": null + }, + "#armies": { + "font-size": 9, + "box-size": 4.5, + "stroke": "#000", + "stroke-width": 0, + "fill-opacity": 1, + "filter": "url(#dropShadow05)" + }, + "#biomes": { + "opacity": 0.6, + "filter": null, + "mask": "url(#land)" + }, + "#stateBorders": { + "opacity": 0.6, + "stroke": "#4c483e", + "stroke-width": 0.8, + "stroke-dasharray": "1 2.5", + "stroke-linecap": "square", + "filter": null + }, + "#provinceBorders": { + "opacity": 0.6, + "stroke": "#56566d", + "stroke-width": 0.2, + "stroke-dasharray": 0.5, + "stroke-linecap": "butt", + "filter": null + }, + "#cells": { + "opacity": null, + "stroke": "#808080", + "stroke-width": 0.1, + "filter": null, + "mask": null + }, + "#gridOverlay": { + "opacity": 0.5, + "scale": 1, + "dx": 0, + "dy": 0, + "type": "pointyHex", + "stroke": "#808080", + "stroke-width": 1, + "stroke-dasharray": null, + "stroke-linecap": null, + "transform": null, + "filter": null, + "mask": null + }, + "#coordinates": { + "opacity": 0.7, + "data-size": 15, + "font-size": 15, + "stroke": "#734d37", + "stroke-width": 1.5, + "stroke-dasharray": 5, + "stroke-linecap": "square", + "filter": null, + "mask": "" + }, + "#compass": { + "opacity": 0.6, + "transform": null, + "filter": null, + "mask": "url(#water)", + "shape-rendering": "optimizespeed" + }, + "#rose": { + "transform": null + }, + "#relig": { + "opacity": 0.5, + "stroke": null, + "stroke-width": 0, + "filter": null + }, + "#cults": { + "opacity": 0.5, + "stroke": "#777777", + "stroke-width": 0, + "stroke-dasharray": null, + "stroke-linecap": null, + "filter": null + }, + "#landmass": { + "opacity": 1, + "fill": "#f4f2f0", + "filter": null + }, + "#markers": { + "opacity": null, + "rescale": 1, + "filter": null + }, + "#prec": { + "opacity": null, + "stroke": "#000000", + "stroke-width": 0.1, + "fill": "#2554ef", + "filter": null + }, + "#population": { + "opacity": null, + "stroke-width": 1.6, + "stroke-dasharray": null, + "stroke-linecap": "butt", + "filter": null + }, + "#rural": { + "stroke": "#0000ff" + }, + "#urban": { + "stroke": "#ff0000" + }, + "#freshwater": { + "opacity": 0.8, + "fill": "#98b6cd", + "stroke": "#718798", + "stroke-width": 0.5, + "filter": "url(#dropShadow05)" + }, + "#salt": { + "opacity": 0.5, + "fill": "#409b8a", + "stroke": "#388985", + "stroke-width": 0.7, + "filter": null + }, + "#sinkhole": { + "opacity": 1, + "fill": "#5bc9fd", + "stroke": "#53a3b0", + "stroke-width": 0.7, + "filter": null + }, + "#frozen": { + "opacity": 0.95, + "fill": "#cdd4e7", + "stroke": "#cfe0eb", + "stroke-width": 0, + "filter": null + }, + "#lava": { + "opacity": 0.7, + "fill": "#90270d", + "stroke": "#f93e0c", + "stroke-width": 2, + "filter": "url(#crumpled)" + }, + "#dry": { + "opacity": 1, + "fill": "#c9bfa7", + "stroke": "#8e816f", + "stroke-width": 0.7, + "filter": null + }, + "#sea_island": { + "opacity": 1, + "stroke": "#242424", + "stroke-width": 0.1, + "filter": "url(#dropShadow)", + "auto-filter": 1 + }, + "#lake_island": { + "opacity": 1, + "stroke": "#7c8eaf", + "stroke-width": 0.1, + "filter": null + }, + "#terrain": { + "opacity": 0.8, + "set": "simple", + "size": 0.7, + "density": 0.3, + "filter": null, + "mask": "" + }, + "#rivers": { + "opacity": 1, + "filter": null, + "fill": "#6dabba" + }, + "#ruler": { + "opacity": null, + "filter": null + }, + "#roads": { + "opacity": 0.9, + "stroke": "#d06324", + "stroke-width": 0.6, + "stroke-dasharray": "1 2", + "stroke-linecap": "round", + "filter": null, + "mask": null + }, + "#trails": { + "opacity": 0.9, + "stroke": "#d06324", + "stroke-width": 0.5, + "stroke-dasharray": ".5 2", + "stroke-linecap": "round", + "filter": null, + "mask": null + }, + "#searoutes": { + "opacity": 1, + "stroke": "#e5edff", + "stroke-width": 0.5, + "stroke-dasharray": "2 3", + "stroke-linecap": "round", + "filter": null, + "mask": null + }, + "#statesBody": { + "opacity": 0.15, + "filter": null + }, + "#statesHalo": { + "opacity": 0.3, + "data-width": 10, + "stroke-width": 10, + "filter": "blur(3.5px)" + }, + "#provs": { + "opacity": 0.4, + "fill": "#000000", + "font-size": 8, + "font-family": "Arima Madurai", + "filter": null + }, + "#temperature": { + "opacity": null, + "font-size": "8px", + "fill": "#000000", + "fill-opacity": 0.3, + "stroke": null, + "stroke-width": 1.8, + "stroke-dasharray": null, + "stroke-linecap": null, + "filter": null + }, + "#ice": { + "opacity": 0.9, + "fill": "#e8f0f6", + "stroke": "#e8f0f6", + "stroke-width": 0.1, + "filter": "url(#dropShadow05)" + }, + "#emblems": { + "opacity": 0.9, + "stroke-width": 1, + "filter": null + }, + "#texture": { + "opacity": 0.39, + "filter": null, + "mask": "url(#land)" + }, + "#textureImage": { + "x": 0, + "y": 0 + }, + "#zones": { + "opacity": 0.6, + "stroke": "#333333", + "stroke-width": 0, + "stroke-dasharray": null, + "stroke-linecap": "butt", + "filter": null, + "mask": null + }, + "#oceanLayers": { + "filter": "url(#dropShadow05)", + "layers": "-6,-3,-1" + }, + "#oceanBase": { + "fill": "#7ca4b6" + }, + "#oceanicPattern": { + "href": "./images/kiwiroo.png", + "opacity": 0.3 + }, + "#terrs": { + "opacity": 0.7, + "scheme": "bright", + "terracing": 0, + "skip": 2, + "relax": 1, + "curve": 0, + "filter": "", + "mask": "url(#land)" + }, + "#legend": { + "data-size": 13, + "font-size": 13, + "font-family": "Arima Madurai", + "stroke": "#812929", + "stroke-width": 2.5, + "stroke-dasharray": "0 4 10 4", + "stroke-linecap": "round", + "data-x": 54.73, + "data-y": 62.98, + "data-columns": 8 + }, + "#burgLabels > #cities": { + "opacity": 0.8, + "fill": "#3a3a3a", + "text-shadow": "white 0px 0px 4px", + "data-size": 7, + "font-size": 7, + "font-family": "Arima Madurai" + }, + "#burgIcons > #cities": { + "opacity": 1, + "fill": "#ffffff", + "fill-opacity": 0.7, + "size": 1.5, + "stroke": "#4f4f4f", + "stroke-width": 0.2, + "stroke-dasharray": "", + "stroke-linecap": "butt" + }, + "#anchors > #cities": { + "opacity": 1, + "fill": "#ffffff", + "size": 3, + "stroke": "#3e3e4b", + "stroke-width": 1.2 + }, + "#burgLabels > #towns": { + "opacity": 0.8, + "fill": "#3e3e4b", + "text-shadow": "white 0px 0px 4px", + "data-size": 4, + "font-size": 4, + "font-family": "Arima Madurai" + }, + "#burgIcons > #towns": { + "opacity": 1, + "fill": "#ffffff", + "fill-opacity": 0.7, + "size": 0.6, + "stroke": "#4f4f4f", + "stroke-width": 0.12, + "stroke-dasharray": "", + "stroke-linecap": "butt" + }, + "#anchors > #towns": { + "opacity": 1, + "fill": "#ffffff", + "size": 1.2, + "stroke": "#3e3e4b", + "stroke-width": 1 + }, + "#labels > #states": { + "opacity": 0.8, + "fill": "#3e3e3e", + "stroke": "#000000", + "stroke-width": 0, + "text-shadow": "white 0px 0px 6px", + "data-size": 14, + "font-size": 14, + "font-family": "Arima Madurai", + "filter": null + }, + "#labels > #addedLabels": { + "opacity": 1, + "fill": "#f24706", + "stroke": "#701b05", + "stroke-width": 0.1, + "text-shadow": "white 0px 0px 4px", + "data-size": 6, + "font-size": 6, + "font-family": "Arima Madurai", + "filter": null + }, + "#fogging": { + "opacity": 1, + "fill": "#30426f", + "filter": null + } +} diff --git a/sw.js b/sw.js index ce803d5b..4e214e8e 100644 --- a/sw.js +++ b/sw.js @@ -8,7 +8,10 @@ const {ExpirationPlugin} = workbox.expiration; const DAY = 24 * 60 * 60; const getPolitics = ({entries, days}) => { - return [new CacheableResponsePlugin({statuses: [0, 200]}), new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY})]; + return [ + new CacheableResponsePlugin({statuses: [0, 200]}), + new ExpirationPlugin({maxEntries: entries, maxAgeSeconds: days * DAY}) + ]; }; registerRoute( @@ -21,7 +24,8 @@ registerRoute( ); registerRoute( - ({request, url}) => request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"), + ({request, url}) => + request.destination === "script" && !url.pathname.endsWith("min.js") && !url.pathname.includes("versioning.js"), new CacheFirst({ cacheName: "fmg-scripts", plugins: getPolitics({entries: 100, days: 30}) diff --git a/utils/numberUtils.js b/utils/numberUtils.js index e3f143a5..ada7c284 100644 --- a/utils/numberUtils.js +++ b/utils/numberUtils.js @@ -20,3 +20,7 @@ function lim(v) { function normalize(val, min, max) { return minmax((val - min) / (max - min), 0, 1); } + +function lerp(a, b, t) { + return a + (b - a) * t; +} diff --git a/versioning.js b/versioning.js index 89816315..64ad10bd 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.00"; // generator version, update each time +const version = "1.89.15"; // generator version, update each time { document.title += " v" + version; @@ -28,6 +28,7 @@ const version = "1.89.00"; // generator version, update each time