diff --git a/LICENSE b/LICENSE index 1ffcb0f2..c755d218 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Max Ganiev (Azgaar) +Copyright (c) 2018-2019 Max Ganiev (Azgaar) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index dd152562..32e889e8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Azgaar's _Fantasy Map Generator_. Online tool generating interactive and highly customizable svg maps based on voronoi diagram. -Project is under development, check out the beta version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_. +Project is under development, check out the current version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_. Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator). diff --git a/icons.css b/icons.css index 2bce5a2c..5a8b22f9 100644 --- a/icons.css +++ b/icons.css @@ -209,6 +209,7 @@ .icon-smooth:before {font-weight: bold; content: '∼'; } .icon-disrupt:before {font-weight: bold; content: '෴'; } .icon-if:before {font-style: italic; font-weight: bold; content: 'if'; } +.icon-fleur:before {content: '⚜'; font-size: 11px; margin: -2px; } .icon-ruler:before {content: 'I'; } .icon-curve:before {content: 'C'; } @@ -223,4 +224,4 @@ margin-left: 1px; width: 10px; font-family: monospace; -} +} \ No newline at end of file diff --git a/images/textures/antique-big.jpg b/images/textures/antique-big.jpg deleted file mode 100644 index 5222bd6e..00000000 Binary files a/images/textures/antique-big.jpg and /dev/null differ diff --git a/images/textures/antique-small.jpg b/images/textures/antique-small.jpg deleted file mode 100644 index fa46101f..00000000 Binary files a/images/textures/antique-small.jpg and /dev/null differ diff --git a/images/textures/iran-small.jpg b/images/textures/iran-small.jpg deleted file mode 100644 index 91d369f8..00000000 Binary files a/images/textures/iran-small.jpg and /dev/null differ diff --git a/images/textures/marble-big.jpg b/images/textures/marble-big.jpg deleted file mode 100644 index 1ec033f1..00000000 Binary files a/images/textures/marble-big.jpg and /dev/null differ diff --git a/images/textures/marble-blue-big.jpg b/images/textures/marble-blue-big.jpg deleted file mode 100644 index 33c9fd79..00000000 Binary files a/images/textures/marble-blue-big.jpg and /dev/null differ diff --git a/images/textures/marble-blue-small.jpg b/images/textures/marble-blue-small.jpg deleted file mode 100644 index 539e2aed..00000000 Binary files a/images/textures/marble-blue-small.jpg and /dev/null differ diff --git a/images/textures/marble-small.jpg b/images/textures/marble-small.jpg deleted file mode 100644 index eca86953..00000000 Binary files a/images/textures/marble-small.jpg and /dev/null differ diff --git a/images/textures/mars-big.jpg b/images/textures/mars-big.jpg deleted file mode 100644 index b3654a9b..00000000 Binary files a/images/textures/mars-big.jpg and /dev/null differ diff --git a/images/textures/mars-small.jpg b/images/textures/mars-small.jpg deleted file mode 100644 index 017a7315..00000000 Binary files a/images/textures/mars-small.jpg and /dev/null differ diff --git a/images/textures/mauritania-small.jpg b/images/textures/mauritania-small.jpg deleted file mode 100644 index 9752dea3..00000000 Binary files a/images/textures/mauritania-small.jpg and /dev/null differ diff --git a/images/textures/mercury-big.jpg b/images/textures/mercury-big.jpg deleted file mode 100644 index eebaed96..00000000 Binary files a/images/textures/mercury-big.jpg and /dev/null differ diff --git a/images/textures/mercury-small.jpg b/images/textures/mercury-small.jpg deleted file mode 100644 index 885215b1..00000000 Binary files a/images/textures/mercury-small.jpg and /dev/null differ diff --git a/images/textures/pergamena-small.jpg b/images/textures/pergamena-small.jpg deleted file mode 100644 index 4d7d10ad..00000000 Binary files a/images/textures/pergamena-small.jpg and /dev/null differ diff --git a/images/textures/spain-small.jpg b/images/textures/spain-small.jpg deleted file mode 100644 index 4244d3ff..00000000 Binary files a/images/textures/spain-small.jpg and /dev/null differ diff --git a/images/textures/stone-big.jpg b/images/textures/stone-big.jpg deleted file mode 100644 index 0a431f9e..00000000 Binary files a/images/textures/stone-big.jpg and /dev/null differ diff --git a/images/textures/stone-small.jpg b/images/textures/stone-small.jpg deleted file mode 100644 index 7d616eed..00000000 Binary files a/images/textures/stone-small.jpg and /dev/null differ diff --git a/images/textures/textures-attribution.txt b/images/textures/textures-attribution.txt deleted file mode 100644 index b0033a8d..00000000 --- a/images/textures/textures-attribution.txt +++ /dev/null @@ -1,12 +0,0 @@ -All used textures should be distrubuted under free license -marble.jpg https://www.rawpixel.com/image/327647/closeup-marble-textured-background by Teddy Rawpixel -marble-blue.jpg https://www.pexels.com/photo/gray-and-blue-surface-988871/ -timbercut.jpg https://www.pexels.com/photo/brown-close-up-hd-wallpaper-surface-172289 -antique.jpg https://www.pexels.com/photo/abstract-ancient-antique-art-235985/ -pergamena.jpg https://www.freepik.com/free-photo/old-paper-texture-background_1007802.htm -stone.jpg https://www.wildtextures.com/free-textures/grungy-yet-elegant-elevation-stone-ii -mars.jpg https://www.solarsystemscope.com/textures/download/2k_mars.jpg -mercury.jpg https://www.solarsystemscope.com/textures/download/2k_mercury.jpg -mauritania.jpg https://go.nasa.gov/2Ugu9M8 NASA Worldview (Jan 24, 2019) -iran.jpg https://go.nasa.gov/2FNmiT5 NASA Worldview (Jan 24, 2019) -spain.jpg https://go.nasa.gov/2FMDPuu NASA Worldview (Jan 24, 2019) \ No newline at end of file diff --git a/images/textures/timbercut-big.jpg b/images/textures/timbercut-big.jpg deleted file mode 100644 index 6c4b2794..00000000 Binary files a/images/textures/timbercut-big.jpg and /dev/null differ diff --git a/images/textures/timbercut-small.jpg b/images/textures/timbercut-small.jpg deleted file mode 100644 index cf3c8ac5..00000000 Binary files a/images/textures/timbercut-small.jpg and /dev/null differ diff --git a/index.css b/index.css index 4c8668cc..08e5d07e 100644 --- a/index.css +++ b/index.css @@ -41,6 +41,10 @@ button { left: 1em; } +#pickerContainer { + position: absolute; +} + input, button, select, a { outline: none; } @@ -92,11 +96,11 @@ button, select, a { stroke-linejoin: round; } -#regions, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass { +#regions, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass { pointer-events: none; } -#statesBody { +#statesBody, #provincesBody { stroke-width: 2; fill-rule: evenodd; mask: url(#land); @@ -113,6 +117,13 @@ button, select, a { fill: none; } +#provinceLabels { + text-anchor: middle; + fill: "#18181a"; + font-family: "Georgia"; + font-size: 9px; +} + @keyframes hideshow { 0% {stroke-width: 1;} 50% {stroke-width: 10;} @@ -183,7 +194,7 @@ i.icon-lock { } #labels { - text-anchor: middle; + text-anchor: start; dominant-baseline: central; text-shadow: 0 0 4px white; cursor: pointer; @@ -191,6 +202,7 @@ i.icon-lock { #burgLabels { dominant-baseline: alphabetic; + text-anchor: middle; } #routeLength, #riverLength { @@ -538,8 +550,8 @@ fieldset { background-color: #997b89; cursor: pointer; padding: 4px; - margin: 2px 0; - display: inline-block; + margin: 2px 3px; + float: left; width: 28%; text-align: center; } @@ -643,6 +655,77 @@ fieldset { text-align: center; } +.matrix-table { + width: 100%; + font-size: smaller; + text-align: center; + border-collapse: collapse; +} + +table.matrix-table th, table.matrix-table td { + border: 1px solid #5d4651; + height: 2em; + padding: .2em; + position: relative; +} + +table.matrix-table th { + background-color: #302a2a; + color: #ffffff; +} + +table.matrix-table tr:hover th { + background: #3e3636; +} + +table.matrix-table td:hover { + outline: 2px solid #5d4651; + outline-offset: -1px; + z-index: 1; +} + +table.matrix-table td.Ally { + background-color: #73ec73; + color: #000000; +} + +table.matrix-table td.Sympathy { + background-color:#d4f8aa; +} + +table.matrix-table td.Neutral { + background-color:#d8d9d3; +} + +table.matrix-table td.Suspicion { + background-color:#f3c7c4; +} + +table.matrix-table td.Enemy { + background-color:#ffa39c; + color: #af0d23; +} + +table.matrix-table td.Unknown { + background-color:#c1bfbf; +} + +table.matrix-table td.Rival { + background-color:#bd845c; +} + +table.matrix-table td.Vassal { + background-color:#87CEFA; +} + +table.matrix-table td.Suzerain { + background-color:#8f8fe1; +} + +table.matrix-table td.x { + background-color:#d4ca94; +} + #sizeOutput { color: green; } @@ -706,26 +789,43 @@ body button.noicon { float: right; } -#templateBody input { - width: 36px; - height: 12px; - border: 0; +#templateBody input, +#templateBody select { + width: 4em; + height: 1em; + border: 0; + font-size: .95em; + background-color: #ffffff95; + color: #05044d; + font-style: italic; font-family: monospace; } #templateBody select { - border: 0; - width: 79px; + width: 8em; cursor: pointer; - font-family: monospace; - height: 12px; font-size: .9em; } +#templateBody .icon-resize-vertical { + cursor: row-resize; + font-size: .9em; + color: #555555; + margin: 1px 1px; +} + +#templateBody .icon-check-empty, +#templateBody .icon-check { + width: 1.1em; + cursor: pointer; + color: #575957; + font-size: .9em; +} + #controlPoints { fill: #ff0000; stroke: #841f1f; - stroke-width: .2; + stroke-width: .3; cursor: move; opacity: .8; } @@ -733,8 +833,8 @@ body button.noicon { #controlPoints > path { fill: none; stroke: #000000; - stroke-width: 1; - opacity: .3; + stroke-width: 2; + opacity: .4; cursor: pointer; } @@ -927,12 +1027,24 @@ div.slider .ui-slider-handle { display: none !important; } +.burgs-table { + max-height: 75vh; + overflow-x: hidden; + overflow-y: scroll; +} + .table { max-height: 75vh; overflow-x: hidden; overflow-y: auto; } +div.header > div { + font-weight: bold; + font-size: .9em; + display: inline-block; +} + .sortable { font-weight: bold; font-size: .9em; @@ -957,6 +1069,7 @@ div.states { margin: 1px 0; padding: 0 2px; font-size: .9em; + line-height: 15px; } div.states:hover { @@ -1010,18 +1123,24 @@ div.states>.statePopulation { width: 30px; } -div.states .icon-trash-empty { +div.states .icon-trash-empty, +div.states .icon-eye, +div.states .icon-pin { cursor: pointer; } +div.states .icon-resize-vertical { + cursor: row-resize; + font-size: .9em; +} + div.states>[class^="icon-"] { color: #6e5e66; padding: 0; } -div.states>[class="icon-arrows-cw"] { +div.states > .icon-arrows-cw { color: #67575c; - padding: 0 2px 0 0; font-size: 9px; cursor: pointer; } @@ -1037,15 +1156,17 @@ div.states>.small { div.states>.cultureName { width: 50px; + white-space: nowrap; } div.states>.culturePopulation { width: 40px; } -div.states > .cultureBase, +div.states > .cultureBase, div.states > .cultureType, -div.states > .stateCulture { +div.states > .stateCulture, +div.states > .diplomacyRelations { width: 46px; cursor: pointer; border: 0; @@ -1055,6 +1176,10 @@ div.states > .stateCulture { appearance: textfield; } +div.states > .cultureBase { + width: 6em; +} + div.states > .burgName, div.states > .burgState, div.states > .burgCulture { @@ -1075,12 +1200,20 @@ div.states .burgType > span { transition: 0.2s; } -div.states .burgType > span.inactive { - color: #dfdbdb; +div.states span.inactive { + color: #c6c2c2; } -div.states .burgType > span.inactive:hover { - color: #d1d0d0; +div.states span.inactive:hover { + color: #abaaaa; +} + +#diplomacyBodySection > div { + cursor: pointer; +} + +div.states > div.stateName { + width: 12em; } #burgsFooterPopulation { @@ -1091,6 +1224,28 @@ div.states .burgType > span.inactive:hover { line-height: 14px; } +div.states>.religionName { + width: 11em; +} + +div.states>.religionType { + width: 5em; + cursor: pointer; + border: 0; + background-color: transparent; + -webkit-appearance: textfield; + -moz-appearance: none; + appearance: textfield; +} + +div.states>.religionForm { + width: 6em; +} + +div.states>.religionDeidy { + width: 15em; +} + .placeholder { opacity: 0; cursor: default; @@ -1103,8 +1258,16 @@ span.ui-dialog-title>input.stateColor { } div.states.selected { - border: 1px solid #b28585; - background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%); + border-color: #b28585; + background-image: linear-gradient(to right, #f2f2f2 0%, #ebe7e7 50%, #E5DADB 100%); +} + +div.states.Self { + border-color: #858b8e; + background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%); + font-style: italic; + margin-bottom: .2em; + cursor: default !important; } div.states button.selectCapital { @@ -1120,6 +1283,64 @@ div.states > div.biomeArea { width: 50px; } +.zoneFill { + stroke: #666666; + stroke-width: 2; + cursor: pointer; +} + +#pickerHeader { + fill: #916e7f; + cursor: move; +} + +#pickerLabel { + fill: #f8ffff; + font-size: 1.2em; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + cursor: move !important; +} + +#picker text { + cursor: default; +} + +#pickerControls line { + stroke: #999999; + stroke-width: 2; +} + +#pickerControls circle { + fill: #ffeb3b; + stroke: #666666; + cursor: ew-resize; +} + +#pickerControls circle:hover { + fill: #eca116; + stroke: #000000; +} + +#pickerColors rect, #pickerHatches rect { + cursor: pointer; +} + +#picker rect.selected { + outline: 2px dashed #b90c0c; + stroke-width: 0; +} + +.hoverButton { + position: sticky; + margin-left: -1.8em; + margin-top: 1px; + background-color: #dedede; + font-size: 8px; + cursor: pointer; + padding: 0px 3px !important; +} + #unitsBody>div>* { display: inline-block; } @@ -1150,6 +1371,15 @@ div.states > div.biomeArea { border: 1px solid #e9e9e9; } +#unitsEditor i.icon-lock-open, +#unitsEditor i.icon-lock { + color: #626573; + font-size: .8em; + cursor: pointer; + position: fixed; + margin: .4em 0 0 -.9em; +} + #distanceUnitOutput { width: 0; margin-left: -2.5em; @@ -1218,19 +1448,12 @@ div.states > div.biomeArea { fill: none; } -#coordinates text { +#coordinateLabels { fill: #333333; - stroke: none; font-family: monospace; text-shadow: 0 0 4px white; -} - -#lalitude text { + stroke-width: 0; dominant-baseline: central; -} - -#longitude text { - dominant-baseline: hanging; text-anchor: middle; } @@ -1338,25 +1561,24 @@ input[type="checkbox"] { } .checkbox+.checkbox-label:before { - content: ''; - background: #ece6eb; - border-radius: 1px; - display: inline-block; - vertical-align: text-top; - width: 7px; - height: 7px; - padding: 2px; - margin-right: 3px; + content: ''; + display: inline-block; + vertical-align: text-top; + width: 7px; + height: 7px; + padding: 2px; + margin-right: 3px; + border: 1px solid darkgrey; + border-radius: 15%; + background: white; } .checkbox:checked+.checkbox-label:before { - line-height: 8px; - font-size: 12px; - font-weight: bold; - content: '✓'; - background: #c6b4bb; - color: #1c181a; - box-shadow: inset 0px 0px 0px 2px #ece6ea; + line-height: 8px; + font-size: 12px; + font-weight: bold; + content: '✓'; + color: #333333; } .shadowed { @@ -1387,12 +1609,6 @@ input[type="checkbox"] { height: 100%; } -#cultureCenters circle { - stroke-width: 2; - stroke: #00000080; - cursor: pointer; -} - div.textual select, div.textual textarea { font-family: Copperplate, monospace; @@ -1458,33 +1674,32 @@ div.textual span, .textual legend { fill: none; } -div#legend { +div#notes { display: none; - position: fixed; - width: 25vw; - right: 1vw; - top: 1vw; - font-size: 1em; - border: 1px solid #5e4fa2; - background: #cdb99040; - box-shadow: 2px 2px 5px -3px #3a2804; - white-space: pre-line; - -moz-user-select: none; - user-select: none; + position: fixed; + width: 28vw; + right: 1vw; + top: 1vw; + font-size: 1.2em; + border: 1px solid #5e4fa2; + background: rgba(255, 250, 228, 0.7); + box-shadow: 2px 2px 5px -3px #3a2804; + white-space: pre-line; + -moz-user-select: none; + user-select: none; } -div#legendHeader { +div#notesHeader { font-weight: bold; font-size: 1.3em; padding: 0 0 4px 14px; border-bottom: 1px solid #5e4fa2; } -div#legendBody { +div#notesBody { padding: 0 10px; } - svg.button { position: relative; background-color: transparent; @@ -1493,13 +1708,16 @@ svg.button { } #reliefEditor > div > div { - width: 4em; font-style: italic; display: inline-block; } +#reliefEditor div.reliefEditorLabel { + width: 4em; +} + #reliefEditor input[type="range"] { - width: 15em; + width: 16em; } #reliefEditor input[type="number"] { @@ -1512,22 +1730,24 @@ svg.button { max-width: 30vw; } -#reliefIconsDiv > svg { +#reliefIconsDiv svg { width: 40px; height: 40px; - border-radius: 15%; + background-color: #e7e6e4; border: 1px solid #a9a9a9; cursor: pointer; } -#reliefIconsDiv > svg:hover { +#reliefIconsDiv svg:hover { border-color: #5c5c5c; background-color: #eef6fb; + transition: all .3s ease-out 3s; + transform: scale(2); } -#reliefIconsDiv > svg.pressed { +#reliefIconsDiv svg.pressed { border: 1px solid #b3352c; - background-color: #eef6fb; + background-color: #f2f2f2; } #reliefIconsSeletionAny { @@ -1540,6 +1760,7 @@ svg.button { -moz-user-select: text; user-select: text; max-height: 75vh; + max-width: 75vw; } #alertMessage ul { @@ -1558,27 +1779,28 @@ svg.button { } #worldControls { - width: 190px; + width: 16em; display: inline-block; - vertical-align: top; + vertical-align: top; } -#worldControls > label { +#worldControls > div { display: block; margin: 1px 0; - font-size: 11px; padding: 2px 0; } #worldControls input[type="number"] { border: 1px solid #e5e5e5; padding: 0px; + width: 3.2em; } #worldControls i.icon-lock-open, #worldControls i.icon-lock { color: #626573; - font-size: 9px; + font-size: .8em; + cursor: pointer; } #globe { @@ -1649,6 +1871,17 @@ svg.button { stroke-width: 1.4; } +#legend { + cursor: move; + -moz-user-select: none; + user-select: none; +} + +.dontAsk { + display: inline-block; + margin: 10px 0 0 7px; +} + #debug { font-size: 1px; opacity: 0.8; diff --git a/index.html b/index.html index 32d95839..7fcfd41d 100644 --- a/index.html +++ b/index.html @@ -16,8 +16,8 @@ - - + +
@@ -30,9 +30,15 @@LOADING...
Displayed layers:
User Interface:
@@ -1253,10 +1642,10 @@Fantasy Map Generator is a free open source tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a new map from scratch. Check out the quick start tutorial and project wiki for guidance.
-Join our Reddit Community if you have questions, need help, have a suggestion or want to share a created map. You may support the project on Patreon.
+Join our Discord server and Reddit community to ask questions, get help and share created maps. You may support the project on Patreon.
The project is under active development. For older versions see the changelog. To track the development progress see the devboard. Please report bugs here. You can also contact me directly via email.
A special thanks to all supporters!
-Supporters: 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, Doug Churchman, Daniel Eric Crosby, DerGeisterbГär, 魏, Marcus Hellyrr and many others!
+Supporters: 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 and many others!
Drop a .map file to open
Join our Reddit community and @@ -132,7 +155,7 @@ function showWelcomeMessage() {
Thanks for all supporters on Patreon!
`; $("#alert").dialog( - {resizable: false, title: "Fantasy Map Generator update", width: 330, + {resizable: false, title: "Fantasy Map Generator update", width: 310, buttons: { OK: function() { localStorage.clear(); @@ -220,16 +243,14 @@ function applyDefaultNamesData() { // apply default biomes data function applyDefaultBiomesSystem() { - const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rain forest","Temperate rain forest","Taiga","Tundra","Glacier"]; - const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#45b348","#4b6b32","#96784b","#d5e7eb"]; - - const i = new Uint8Array(d3.range(0, name.length)); - const habitability = new Uint16Array([0,2,5,15,25,50,100,80,90,10,2,0]); - const iconsDensity = new Uint8Array([0,3,2,120,120,120,120,150,150,100,5,0]); - //const icons = [{},{dune:1},{dune:1},{acacia:1, grass:9},{grass:1},{acacia:1, palm:1},{deciduous:1},{acacia:7, palm:2, deciduous:1},{deciduous:7, swamp:3},{conifer:1},{grass:1},{}]; - const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:2},{deciduous:5, swamp:3},{conifer:1},{grass:1},{}]; - const cost = new Uint8Array([10,200,150,60,50,70,70,80,90,80,100,255]); // biome movement cost + const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rainforest","Temperate rainforest","Taiga","Tundra","Glacier","Wetland"]; + const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#409c43","#4b6b32","#96784b","#d5e7eb","#0b9131"]; + const habitability = [0,2,5,20,30,50,100,80,90,10,2,0,12]; + const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150]; + const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}]; + const cost = [10,200,150,60,50,70,70,80,90,80,100,255,150]; // biome movement cost const biomesMartix = [ + // hot ↔ cold; dry ↕ wet new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]), new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,9,10,10]), new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]), @@ -237,8 +258,8 @@ function applyDefaultBiomesSystem() { new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10]) ]; - // parse icons 'weighted array' into a simple array - for (let i = 0; i < icons.length; i++) { + // parse icons weighted array into a simple array + for (let i=0; i < icons.length; i++) { const parsed = []; for (const icon in icons[i]) { for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);} @@ -246,22 +267,24 @@ function applyDefaultBiomesSystem() { icons[i] = parsed; } - return {i, name, color, biomesMartix, habitability, iconsDensity, icons, cost}; + return {i:d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost}; } // restore initial style function applyDefaultStyle() { biomes.attr("opacity", null).attr("filter", null); - borders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .7).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt").attr("filter", null); + stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null); + provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .2).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt").attr("filter", null); cells.attr("opacity", null).attr("stroke", "#808080").attr("stroke-width", .1).attr("filter", null).attr("mask", null); gridOverlay.attr("opacity", .8).attr("stroke", "#808080").attr("stroke-width", .5).attr("stroke-dasharray", null).attr("transform", null).attr("filter", null).attr("mask", null); - coordinates.attr("opacity", 1).attr("data-size", 10).attr("font-size", 10).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null); - compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)"); + coordinates.attr("opacity", 1).attr("data-size", 12).attr("font-size", 12).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null); + compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)").attr("shape-rendering", "optimizespeed"); if (!d3.select("#initial").size()) d3.select("#rose").attr("transform", "translate(80 80) scale(.25)"); coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)"); styleCoastlineAuto.checked = true; + relig.attr("opacity", .8).attr("stroke", "#777777").attr("stroke-width", 0).attr("filter", null).attr("fill-rule", "evenodd"); cults.attr("opacity", .6).attr("stroke", "#777777").attr("stroke-width", .5).attr("filter", null).attr("fill-rule", "evenodd"); icons.selectAll("g").attr("opacity", null).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("filter", null).attr("mask", null); landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null); @@ -277,17 +300,18 @@ function applyDefaultStyle() { terrain.attr("opacity", null).attr("filter", null).attr("mask", null); rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", null); - roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .45).attr("stroke-dasharray", "1.5").attr("stroke-linecap", "butt").attr("filter", null); + roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .7).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null); ruler.attr("opacity", null).attr("filter", null); searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .45).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round").attr("filter", null); - - statesBody.attr("opacity", .4).attr("filter", null); - statesHalo.attr("stroke-width", 10).attr("opacity", .4); + + regions.attr("opacity", .4).attr("filter", null); + statesHalo.attr("stroke-width", 10).attr("opacity", 1); + provs.attr("opacity", .6).attr("filter", null); temperature.attr("opacity", null).attr("fill", "#000000").attr("stroke-width", 1.8).attr("fill-opacity", .3).attr("font-size", "8px").attr("stroke-dasharray", null).attr("filter", null).attr("mask", null); texture.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)"); texture.select("image").attr("x", 0).attr("y", 0); - + zones.attr("opacity", .6).attr("stroke", "#333333").attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null); trails.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .25).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt").attr("filter", null); // ocean and svg default style @@ -316,6 +340,13 @@ function applyDefaultStyle() { styleHeightmapCurveInput.value = 0; if (changed) drawHeightmap(); + // legend + legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round"); + styleLegendBack.value = "#ffffff"; + styleLegendOpacity.value = styleLegendOpacityOutput.value = .8; + styleLegendColItems.value = styleLegendColItemsOutput.value = 8; + if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend(); + const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3); burgLabels.select("#cities").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", citiesSize).attr("data-size", citiesSize); burgIcons.select("#cities").attr("opacity", 1).attr("size", 1).attr("stroke-width", .24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-dasharray", "").attr("stroke-linecap", "butt"); @@ -329,6 +360,8 @@ function applyDefaultStyle() { labels.select("#states").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", stateLabelSize).attr("data-size", stateLabelSize).attr("filter", null); labels.select("#addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null); invokeActiveZooming(); + + fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5); } // focus on coordinates, cell or burg provided in searchParams @@ -342,7 +375,7 @@ function focusOn() { params.set("burg", params.get("seed").slice(-4)); } else { // select burg for MFCG - findBurgForMFCG(params); + findBurgForMFCG(params); return; } } @@ -463,7 +496,7 @@ function invokeActiveZooming() { const desired = +this.dataset.size; const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1); this.getAttribute("font-size", relative); - const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 100); + const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50); if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden"); }); } @@ -497,15 +530,15 @@ function invokeActiveZooming() { } // Pull request from @evyatron -function addDragToUpload() { +void function addDragToUpload() { document.addEventListener('dragover', function(e) { - e.stopPropagation(); - e.preventDefault(); - $('#map-dragged').show(); + e.stopPropagation(); + e.preventDefault(); + $('#map-dragged').show(); }); document.addEventListener('dragleave', function(e) { - $('#map-dragged').hide(); + $('#map-dragged').hide(); }); document.addEventListener('drop', function(e) { @@ -532,9 +565,10 @@ function addDragToUpload() { $("#map-dragged > p").text("Drop to upload"); }); }); -} +}() function generate() { + const timeStart = performance.now(); console.time("TOTAL"); invokeActiveZooming(); generateSeed(); @@ -562,13 +596,17 @@ function generate() { Cultures.generate(); Cultures.expand(); BurgsAndStates.generate(); - BurgsAndStates.drawStateLabels(); - console.timeEnd("TOTAL"); + Religions.generate(); - window.setTimeout(() => { - showStatistics(); - console.groupEnd("Map " + seed); - }, 300); // wait for rendering + drawStates(); + drawBorders(); + BurgsAndStates.drawStateLabels(); + addZone(); + addMarkers(); + + console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`); + showStatistics(); + console.groupEnd("Map " + seed); } // generate map seed (string!) or get it from URL searchParams @@ -695,14 +733,15 @@ function openNearSeaLakes() { console.timeEnd("openLakes"); } -// calculate map position on globe based on equator position and length to poles +// calculate map position on globe function calculateMapCoordinates() { - const eqY = +document.getElementById("equatorInput").value; - const eqD = +document.getElementById("equidistanceInput").value; - const latT = graphHeight / 2 / eqD * 180; - const eqMod = eqY / graphHeight; - const latN = latT * eqMod; + const size = +document.getElementById("mapSizeOutput").value; + const latShift = +document.getElementById("latitudeOutput").value; + + const latT = size / 100 * 180; + const latN = 90 - (180 - latT) * latShift / 100; const latS = latN - latT; + const lon = Math.min(graphWidth / graphHeight * latT / 2, 180); mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon}; } @@ -712,23 +751,24 @@ function calculateTemperatures() { console.time('calculateTemperatures'); const cells = grid.cells; cells.temp = new Int8Array(cells.i.length); // temperature array + const tEq = +temperatureEquatorInput.value; const tPole = +temperaturePoleInput.value; - const eqY = +document.getElementById("equatorInput").value; - const eqD = +document.getElementById("equidistanceInput").value; + const tDelta = Math.abs(tEq) + Math.abs(tPole); d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) { const y = grid.points[r][1]; - const initTemp = tEq - Math.abs(y - eqY) / eqD * (tEq - tPole); + const deg = mapCoordinates.latN - y / graphHeight * mapCoordinates.latT; + const initTemp = tEq - Math.abs(deg) / 90 * tDelta; for (let i = r; i < r+grid.cellsX; i++) { cells.temp[i] = initTemp - convertToFriendly(cells.h[i]); } }); - // temperature decreases by 6.5�C per 1km + // temperature decreases by 6.5 degree C per 1km function convertToFriendly(h) { if (h < 20) return 0; - const exponent = +heightExponent.value; + const exponent = +heightExponentInput.value; const height = Math.pow(h - 18, exponent); return rn(height / 1000 * 6.5); } @@ -909,7 +949,7 @@ function drawCoastline() { const used = new Uint8Array(features.length); // store conneted features const largestLand = d3.scan(features.map(f => f.land ? f.cells : 0), (a, b) => b - a); const landMask = defs.select("#land"); - const waterMask = defs.select("#water"); + const waterMask = defs.select("#water"); lineGen.curve(d3.curveBasisClosed); for (const i of cells.i) { @@ -1060,14 +1100,15 @@ function defineBiomes() { let moist = grid.cells.prec[cells.g[i]]; if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2); const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]); - moist = rn(d3.mean(n)); + moist = rn(4 + d3.mean(n)); const temp = grid.cells.temp[cells.g[i]]; // flux from precipitation - cells.biome[i] = getBiomeId(moist, temp); + cells.biome[i] = getBiomeId(moist, temp, cells.h[i]); } - function getBiomeId(moisture, temperature) { + function getBiomeId(moisture, temperature, height) { if (temperature < -5) return 11; // permafrost biome - const m = Math.min((moisture + 4) / 5 | 0, 4); // moisture band from 0 to 4 + if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome + const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4 const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25 return biomesData.biomesMartix[m][t]; } @@ -1111,6 +1152,225 @@ function rankCells() { console.timeEnd('rankCells'); } +// add a zone as an example: rebels along one border +function addZone() { + const cells = pack.cells, states = pack.states; + const state = states.find(s => s.i && s.neighbors.size > 0 && s.neighbors.values().next().value); + if (!state) return; + + const neib = state.neighbors.values().next().value; + const data = cells.i.filter(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib)); + + const rebels = rw({Rebels:5, Insurgents:2, Recusants:1, Mutineers:1, Rioters:1, Dissenters:1, Secessionists:1, Insurrection:2, Rebellion:1, Conspiracy:2}); + const name = getAdjective(states[neib].name) + " " + rebels; + + const zone = zones.append("g").attr("id", "zone0").attr("data-description", name).attr("data-cells", data).attr("fill", "url(#hatch3)"); + zone.selectAll("polygon").data(data).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("id", d => "zone0_"+d); +} + +// add some markers as an example +function addMarkers() { + console.time("addMarkers"); + const cells = pack.cells; + + void function addVolcanoes() { + let mounts = Array.from(cells.i).filter(i => cells.h[i] > 70).sort((a, b) => cells.h[b] - cells.h[a]); + let count = mounts.length < 10 ? 0 : Math.ceil(mounts.length / 300); + if (count) addMarker("volcano", "🌋", 52, 52, 17.5); + + while (count) { + const cell = mounts.splice(biased(0, mounts.length, 5), 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_volcano").attr("data-id", "#marker_volcano") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + const height = getFriendlyHeight(cells.h[cell]); + const proper = Names.getCulture(cells.culture[cell]); + const name = Math.random() < .3 ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper; + notes.push({id, name, legend:`Active volcano. Height: ${height}`}); + count--; + } + }() + + void function addHotSprings() { + let springs = Array.from(cells.i).filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b]-cells.h[a]); + let count = springs.length < 30 ? 0 : Math.ceil(springs.length / 1000); + if (count) addMarker("hot_springs", "♨", 50, 50, 19.5); + + while (count) { + const cell = springs.splice(biased(1, springs.length, 3), 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_hot_springs").attr("data-id", "#marker_hot_springs") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const proper = Names.getCulture(cells.culture[cell]); + const temp = convertTemperature(gauss(25,15,20,100)); + notes.push({id, name: proper + " Hot Springs", legend:`A hot springs area. Temperature: ${temp}`}); + count--; + } + }() + + void function addMines() { + let hills = Array.from(cells.i).filter(i => cells.h[i] > 47 && cells.burg[i]); + let count = !hills.length ? 0 : Math.ceil(hills.length / 7); + if (!count) return; + + addMarker("mine", "⚒", 50, 50, 20); + const resources = {"salt":5, "gold":2, "silver":4, "copper":2, "iron":3, "lead":1, "tin":1}; + + while (count) { + const cell = hills.splice(Math.floor(Math.random() * hills.length), 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_mine").attr("data-id", "#marker_mine") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + const resource = rw(resources); + const burg = pack.burgs[cells.burg[cell]]; + const name = `${burg.name} - ${resource} mining town`; + const population = rn(burg.population * populationRate.value * urbanization.value); + const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`; + notes.push({id, name, legend}); + count--; + } + }() + + void function addBridges() { + const meanRoad = d3.mean(cells.road.filter(r => r)); + const meanFlux = d3.mean(cells.fl.filter(fl => fl)); + + let bridges = Array.from(cells.i) + .filter(i => cells.burg[i] && cells.h[i] >= 20 && cells.r[i] && cells.fl[i] > meanFlux && cells.road[i] > meanRoad) + .sort((a, b) => (cells.road[b] + cells.fl[b] / 10) - (cells.road[a] + cells.fl[a] / 10)); + + let count = !bridges.length ? 0 : Math.ceil(bridges.length / 12); + if (count) addMarker("bridge", "🌉", 50, 50, 16.5); + + while (count) { + const cell = bridges.splice(0, 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_bridge").attr("data-id", "#marker_bridge") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const burg = pack.burgs[cells.burg[cell]]; + const river = Names.getCulture(cells.culture[cell]); // river name + const name = Math.random() < .2 ? river : burg.name; + notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${river} River near ${burg.name}`}); + count--; + } + }() + + void function addInns() { + const maxRoad = d3.max(cells.road) * .9; + let taverns = Array.from(cells.i).filter(i => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad); + if (!taverns.length) return; + addMarker("inn", "🍻", 50, 50, 17.5); + + const color = ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]; + const animal = ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox"]; + const adj = ["New", "Good", "High", "Old", "Great", "Big", "Major", "Happy", "Main", "Huge", "Far", "Beautiful", "Fair", "Prime", "Ancient", "Golden", "Proud", "Lucky", "Fat", "Honest", "Giant", "Distant", "Friendly", "Loud", "Hungry", "Magical", "Superior", "Peaceful", "Frozen", "Divine", "Favorable", "Brave", "Sunny", "Flying"]; + + + for (let i=0; i < taverns.length && i < 4; i++) { + const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_inn").attr("data-id", "#marker_inn") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const type = Math.random() > .7 ? "inn" : "tavern"; + const name = Math.random() < .5 ? ra(color) + " " + ra(animal) : Math.random() < .6 ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type); + notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`}); + } + }() + + void function addLighthouses() { + const lands = cells.i.filter(i => cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c])); + const lighthouses = Array.from(lands).map(i => [i, cells.v[i][cells.c[i].findIndex(c => cells.h[c] < 20 && cells.road[c])]]); + if (lighthouses.length) addMarker("lighthouse", "🚨", 50, 50, 16); + + for (let i=0; i < lighthouses.length && i < 4; i++) { + const cell = lighthouses[i][0], vertex = lighthouses[i][1]; + const x = pack.vertices.p[vertex][0], y = pack.vertices.p[vertex][1]; + const id = getNextId("markerElement"); + + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_lighthouse").attr("data-id", "#marker_lighthouse") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]); + notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend:`A lighthouse to keep the navigation safe`}); + } + }() + + void function addWaterfalls() { + const waterfalls = cells.i.filter(i => cells.r[i] && cells.h[i] > 70); + if (waterfalls.length) addMarker("waterfall", "⟱", 50, 54, 16.5); + + for (let i=0; i < waterfalls.length && i < 3; i++) { + const cell = waterfalls[i]; + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_waterfall").attr("data-id", "#marker_waterfall") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]); + notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend:`An extremely beautiful waterfall`}); + } + }() + + void function addBattlefields() { + let battlefields = Array.from(cells.i).filter(i => cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25); + let count = battlefields.length < 100 ? 0 : Math.ceil(battlefields.length / 500); + const era = Names.getCulture(0, 3, 7, "", 0) + " Era"; + if (count) addMarker("battlefield", "⚔", 50, 50, 20); + + while (count) { + const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1); + const x = cells.p[cell][0], y = cells.p[cell][1]; + const id = getNextId("markerElement"); + markers.append("use").attr("id", id) + .attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield") + .attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30) + .attr("data-size", 1).attr("width", 30).attr("height", 30); + + const name = Names.getCulture(cells.culture[cell]) + " Battlefield"; + const date = new Date(rand(100, 1000),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'}) + " " + era; + notes.push({id, name, legend:`A historical battlefield spot. \r\nDate: ${date}`}); + count--; + } + }() + + function addMarker(id, icon, x, y, size) { + const markers = svg.select("#defs-markers"); + if (markers.select("#marker_"+id).size()) return; + + const symbol = markers.append("symbol").attr("id", "marker_"+id).attr("viewBox", "0 0 30 30"); + symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none"); + symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1); + symbol.append("text").attr("x", x+"%").attr("y", y+"%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0) + .attr("font-size", size+"px").attr("dominant-baseline", "central").text(icon); + } + + console.timeEnd("addMarkers"); +} + // show map stats on generation complete function showStatistics() { const template = templateInput.value; @@ -1121,7 +1381,9 @@ function showStatistics() { Points: ${grid.points.length} Cells: ${pack.cells.i.length} States: ${pack.states.length-1} - Burgs: ${pack.burgs.length-1}`; + Provinces: ${pack.provinces.length-1} + Burgs: ${pack.burgs.length-1} + Religions: ${pack.religions.length-1}`; mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created: Date.now()}); console.log(stats); } @@ -1138,6 +1400,8 @@ const regenerateMap = debounce(function() { // Clear the map function undraw() { - viewbox.selectAll("path, circle, polygon, line, text, use, #ruler > g").remove(); + viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #ruler > g").remove(); defs.selectAll("path, clipPath").remove(); + notes = []; + unfog(); } diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 244a0693..365e0c7d 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -5,11 +5,11 @@ }(this, (function () { 'use strict'; const generate = function() { - console.time("generateBurgsAndStates"); const cells = pack.cells, cultures = pack.cultures, n = cells.i.length; cells.burg = new Uint16Array(n); // cell burg cells.road = new Uint16Array(n); // cell road power + cells.crossroad = new Uint16Array(n); // cell crossroad power const burgs = pack.burgs = placeCapitals(); pack.states = createStates(); @@ -18,11 +18,17 @@ placeTowns(); const townRoutes = Routes.getTrails(); specifyBurgs(); - + const oceanRoutes = Routes.getSearoutes(); expandStates(); normalizeStates(); + collectStatistics(); + assignColors(); + + generateDiplomacy(); + defineStateForms(); + generateProvinces(); Routes.draw(capitalRoutes, townRoutes, oceanRoutes); drawBurgs(); @@ -37,18 +43,14 @@ if (sorted.length < count * 10) { count = Math.floor(sorted.length / 10); - if (!count) { - console.error(`There is no populated cells. Cannot generate states`); - return burgs; - } else { - console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`); - } + if (!count) {console.warn(`There is no populated cells. Cannot generate states`); return burgs;} + else {console.warn(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);} } let burgsTree = d3.quadtree(); let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals - for (let i = 0; burgs.length <= count; i++) { + for (let i=0; burgs.length <= count; i++) { const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1]; if (burgsTree.find(x, y, spacing) === undefined) { @@ -57,7 +59,7 @@ } if (i === sorted.length - 1) { - console.error("Cannot place capitals with current spacing. Trying again with reduced spacing"); + console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing"); burgsTree = d3.quadtree(); i = -1, burgs = [0], spacing /= 1.2; } @@ -80,18 +82,16 @@ // burgs data b.i = b.state = i; b.culture = cells.culture[b.cell]; - const base = cultures[b.culture].base; - const min = nameBases[base].min-1; - const max = Math.max(nameBases[base].max-2, min); - b.name = Names.getCulture(b.culture, min, max, "", 0); + b.name = Names.getCultureShort(b.culture); b.feature = cells.f[b.cell]; b.capital = true; // states data - const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1); - const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0); + const expansionism = rn(Math.random() * powerInput.value + 1, 1); + const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture); const name = Names.getState(basename, b.culture); - const type = cultures[b.culture].type; + const nomadic = [1, 2, 3, 4].includes(cells.biome[b.cell]); + const type = nomadic ? "Nomadic" : cultures[b.culture].type === "Nomadic" ? "Generic" : cultures[b.culture].type; states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture}); cells.burg[b.cell] = i; }); @@ -103,38 +103,43 @@ // place secondary settlements based on geo and economical evaluation function placeTowns() { console.time('placeTowns'); - const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for towns placement + const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // cell score for towns placement const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes - let burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) : +manorsInput.value; - burgsCount += burgs.length; - const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns - const burgsTree = burgs[0]; + const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber; + const burgsNumber = Math.min(desiredNumber, sorted.length); + let burgsAdded = 0; - for (let i = 0; burgs.length < burgsCount && i < sorted.length; i++) { - const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1]; - const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform - if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg - const burg = burgs.length; - const culture = cells.culture[id]; - const name = Names.getCulture(culture); - const feature = cells.f[id]; - burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature}); - burgsTree.add([x, y]); - cells.burg[id] = burg; + const burgsTree = burgs[0]; + let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns + + while (burgsAdded < burgsNumber) { + for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) { + const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1]; + const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform + if (cells.burg[cell] || burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg + const burg = burgs.length; + const culture = cells.culture[cell]; + const name = Names.getCulture(culture); + burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: false, feature:cells.f[cell]}); + burgsTree.add([x, y]); + cells.burg[cell] = burg; + burgsAdded++; + } + spacing *= .5; } - if (burgs.length < burgsCount) console.error(`Cannot place all burgs. Requested ${burgsCount}, placed ${burgs.length-1}`); + if (manorsInput.value != 1000 && burgsAdded < desiredNumber) { + console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`); + } //const min = d3.min(score.filter(s => s)), max = d3.max(score); //terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max))); //labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2); burgs[0] = {name:undefined}; - console.timeEnd('placeTowns'); + console.timeEnd('placeTowns'); } - - console.timeEnd("generateBurgsAndStates"); } // define burg coordinates and define details @@ -151,17 +156,20 @@ b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on // define burg population (keep urbanization at about 10% rate) - b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3); + b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3); if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population if (port) { - b.population = rn(b.population * 1.3, 3); // increase port population + b.population *= b.population * 1.3; // increase port population const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2); b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2); continue; } + // add random factor + b.population = rn(b.population * gauss(2,3,.6,20,3), 3); + // shift burgs on rivers semi-randomly and just a bit if (cells.r[i]) { const shift = Math.min(cells.fl[i]/150, 1); @@ -241,7 +249,7 @@ console.time("expandStates"); const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs; - cells.state = new Uint8Array(cells.i.length); // cell state + cells.state = new Uint16Array(cells.i.length); // cell state const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const cost = []; states.filter(s => s.i && !s.removed).forEach(function(s) { @@ -257,21 +265,17 @@ const type = states[s].type; cells.c[n].forEach(function(e) { - const biome = cells.biome[e]; - const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100; - const biomeCost = getBiomeCost(cells.road[e], b, biome, type); - const heightCost = getHeightCost(cells.h[e], type); + const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700; + const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type); + const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type); const riverCost = getRiverCost(cells.r[e], e, type); const typeCost = getTypeCost(cells.t[e], type); - const totalCost = p + (cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism; + const totalCost = p + (10 + cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism; if (totalCost > neutral) return; if (!cost[e] || totalCost < cost[e]) { - if (cells.h[e] >= 20) { - cells.state[e] = s; // assign state to cell - if (cells.burg[e]) burgs[cells.burg[e]].state = s; - } + if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell cost[e] = totalCost; queue.queue({e, p:totalCost, s, b}); @@ -280,10 +284,10 @@ //debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6); } }); - } //debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2); + burgs.filter(b => b.i && !b.removed).forEach(b => b.state = cells.state[b.cell]); // assign state to burgs function getBiomeCost(r, b, biome, type) { if (r > 5) return 0; // no penalty if there is a road; @@ -293,19 +297,20 @@ return biomesData.cost[biome]; // general non-native biome penalty } - function getHeightCost(h, type) { - if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals - if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals + function getHeightCost(f, h, type) { + if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures + if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals + if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads if (h < 20) return 1000; // general sea crossing penalty - if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands + if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands if (type === "Highland") return 0; // no penalty for highlanders on highlands - if (h >= 70) return 100; // general mountains crossing penalty - if (h >= 50) return 30; // general hills crossing penalty + if (h >= 67) return 2200; // general mountains crossing penalty + if (h >= 44) return 300; // general hills crossing penalty return 0; } function getRiverCost(r, i, type) { - if (type === "River") return r ? 0 : 50; // penalty for river cultures + if (type === "River") return r ? 0 : 100; // penalty for river cultures if (!r) return 0; // no penalty for others if there is no river return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux } @@ -322,44 +327,41 @@ const normalizeStates = function() { console.time("normalizeStates"); - const cells = pack.cells; - const burgs = pack.burgs; + const cells = pack.cells, burgs = pack.burgs; for (const i of cells.i) { - if (cells.h[i] < 20) continue; - const adversaries = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== cells.state[i]); - const buddies = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] === cells.state[i]); - if (adversaries.length <= buddies.length) continue; + if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital - if (burgs[cells.burg[i]].capital) continue; // do not overwrite capital - const newState = cells.state[adversaries[0]]; - cells.state[i] = newState; - if (cells.burg[i]) burgs[cells.burg[i]].state = newState; + const neibs = cells.c[i].filter(c => cells.h[c] >= 20); + const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]); + if (adversaries.length < 2) continue; + const buddies = neibs.filter(c => cells.state[c] === cells.state[i]); + if (buddies.length > 2) continue; + if (adversaries.length <= buddies.length) continue; + cells.state[i] = cells.state[adversaries[0]]; + //debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5).attr("fill", "red"); } console.timeEnd("normalizeStates"); } - // calculate and draw curved state labels - const drawStateLabels = function() { + // calculate and draw curved state labels for a list of states + const drawStateLabels = function(list) { console.time("drawStateLabels"); const cells = pack.cells, features = pack.features, states = pack.states; const paths = []; // text paths lineGen.curve(d3.curveBundle.beta(1)); for (const s of states) { - if (!s.i || s.removed) continue; + if (!s.i || s.removed || (list && !list.includes(s.i))) continue; const used = []; - const hull = getHull(s.center, s.i); + const visualCenter = findCell(s.pole[0], s.pole[1]); + const start = cells.state[visualCenter] === s.i ? visualCenter : s.center; + const hull = getHull(start, s.i, s.cells / 10); const points = [...hull].map(v => pack.vertices.p[v]); - - //const poly = polylabel([points], 1.0); // pole of inaccessibility - //debug.append("circle").attr("r", 3).attr("cx", poly[0]).attr("cy", poly[1]); - const delaunay = Delaunator.from(points); const voronoi = Voronoi(delaunay, points, points.length); - const c = voronoi.vertices; - const chain = connectCenters(c, s.i); - const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length); + const chain = connectCenters(voronoi.vertices, s.pole[1]); + const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length); paths.push([s.i, relaxed]); // if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red"); @@ -367,19 +369,19 @@ // if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5); // if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1); - function getHull(start, state) { + function getHull(start, state, maxLake) { const queue = [start], hull = new Set(); while (queue.length) { const q = queue.pop(); const nQ = cells.c[q].filter(c => cells.state[c] === state); + cells.c[q].forEach(function(c, d) { - if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes - if (cells.b[c]) {hull.add(cells.v[q][d]); return;} - if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;} + const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake; + if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;} const nC = cells.c[c].filter(n => cells.state[n] === state); const intersected = intersect(nQ, nC).length - if (hull.size > 20 && !intersected) {hull.add(cells.v[q][d]); return;} + if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;} if (used[c]) return; used[c] = 1; queue.push(c); @@ -389,25 +391,28 @@ return hull; } - function connectCenters(c, state) { + function connectCenters(c, y) { // check if vertex is inside the area const inside = c.p.map(function(p) { if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen return used[findCell(p[0], p[1])]; }); - //if (state == 13) debug.selectAll(".circle").data(c.p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", (d, i) => inside[i] ? "green" : "blue"); - - const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]); - const left = sorted[0] || 0, right = sorted.pop() || 0; + + const pointsInside = d3.range(c.p.length).filter(i => inside[i]); + if (!pointsInside.length) return [0]; + const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .5 : 1; // power of horyzontality shift + const end = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[a][0] - c.p[b][0]) + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point + const start = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[b][0] - c.p[a][0]) - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point + //debug.append("line").attr("x1", c.p[start][0]).attr("y1", c.p[start][1]).attr("x2", c.p[end][0]).attr("y2", c.p[end][1]).attr("stroke", "#00dd00"); // connect leftmost and rightmost points with shortest path const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const cost = [], from = []; - queue.queue({e: right, p: 0}); - + queue.queue({e: start, p: 0}); + while (queue.length) { const next = queue.dequeue(), n = next.e, p = next.p; - if (n === left) break; + if (n === end) break; for (const v of c.v[n]) { if (v === -1) continue; @@ -420,9 +425,9 @@ } // restore path - const chain = [left]; - let cur = left; - while (cur !== right) { + const chain = [end]; + let cur = end; + while (cur !== start) { cur = from[cur]; if (inside[cur]) chain.push(cur); } @@ -432,56 +437,556 @@ } void function drawLabels() { - const g = labels.select("#states"), p = defs.select("#textPaths"); - g.selectAll("text").remove(); - p.selectAll("path[id*='stateLabel']").remove(); + const g = labels.select("#states"), t = defs.select("#textPaths"); - const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]); - p.selectAll(".path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]); + if (!list) { + g.selectAll("text").remove(); + t.selectAll("path[id*='stateLabel']").remove(); + } - g.selectAll("text").data(data).enter() - .append("text").attr("id", d => d[1]) - .append("textPath").attr("xlink:href", d => "#textPath_"+d[1]) - .attr("startOffset", "50%").text(d => d[2]); + const example = g.append("text").attr("x", 0).attr("x", 0).text("Average"); + const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter - // resize label based on its length - g.selectAll("text").each(function(e) { - const textPath = document.getElementById("textPath_"+e[1]) - const pathLength = textPath.getTotalLength(); + paths.forEach(p => { + const id = p[0]; + const s = states[p[0]]; - // if area is too small to get a path and length is 0 - if (pathLength === 0) { - const x = e[3][0][0], y = e[3][0][1]; - textPath.setAttribute("d", `M${x-50},${y}h${100}`); - this.firstChild.setAttribute("font-size", "60%"); - return; + if (list) { + t.select("#textPath_stateLabel"+id).remove(); + g.select("#stateLabel"+id).remove(); } - const copy = g.append("text").text(this.textContent); - const textLength = copy.node().getComputedTextLength(); - copy.remove(); + const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0]-50},${p[1][0][1]}h${100}`; + const textPath = t.append("path").attr("d", path).attr("id", "textPath_stateLabel"+id); + const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters - const size = Math.max(Math.min(rn(pathLength / textLength * 60), 175), 60); - this.firstChild.setAttribute("font-size", size+"%"); + let lines = [], ratio = 100; - // prolongate textPath to not trim labels - if (pathLength < 100) { - const mod = 25 / pathLength; - const points = e[3]; + if (pathLength < s.name.length) { + // only short name will fit + lines = splitInTwo(s.name); + ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 50); + } else if (pathLength > s.fullName.length * 2.5) { + // full name will fit in one line + lines = [s.fullName]; + ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 70), 170), 70); + } else { + // try miltilined label + lines = splitInTwo(s.fullName); + ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 70); + } + + // prolongate path if it's too short + if (pathLength && pathLength < lines[0].length) { + const points = p[1]; const f = points[0], l = points[points.length-1]; const dx = l[0] - f[0], dy = l[1] - f[1]; + const mod = Math.abs(letterLength * lines[0].length / dx) / 2; points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)]; points[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)]; - textPath.setAttribute("d", round(lineGen(points))); - //debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red"); + textPath.attr("d", round(lineGen(points))); } - + + example.attr("font-size", ratio+"%"); + const top = (lines.length - 1) / -2; // y offset + const spans = lines.map((l, d) => { + example.text(l); + const left = example.node().getBBox().width / -2; // x offset + return `| Id | Current name | New Name |
|---|
| Id | Current name | New Name |
|---|---|---|
| ${i} | ${pack.burgs[i].name} | ${v} |
| ${i+1} | ${pack.burgs[i+1].name} | ${v} |
| `; + message += states.map(s => ` | ${s.name} | `).join("") + `
|---|---|
| ${s.name} | ` + s.diplomacy.filter((v, i) => valid.includes(i)).map(r => `${r} | `).join("") + "
Heightmap is a core element on which all other data (rivers, burgs, states etc) is based. So the best edit approach is to erase the secondary data and let the system automatically regenerate it on edit completion.
-You can also keep all the data as is, but you won't be able to change the coastline.
+You can also keep all the data, but you won't be able to change the coastline.
If you need to change the coastline and keep the data, you may try the risk edit option. - The secondary data will be kept with burgs placed on water being removed, - but the landmass change can cause unexpected data fluctuation and errors.
`; + The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors. + +Check out wiki for guidance.
`; $("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300, buttons: { @@ -40,7 +41,9 @@ function editHeightmap() { document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1)); function enterHeightmapEditMode(type) { - editHeightmap.layers = getLayersState(); + editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset + editHeightmap.layers.forEach(l => document.getElementById(l).click()); // turn off all layers + customization = 1; closeDialogs(); tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true); @@ -74,15 +77,6 @@ function editHeightmap() { viewbox.on("touchmove mousemove", moveCursor); } - function getLayersState() { - const layers = []; - mapLayers.querySelectorAll("li").forEach(l => { - if (l.id === "toggleScaleBar") return; - if (!l.classList.contains("buttonoff")) {layers.push(l.id); l.click();} - }); - return layers; - } - function moveCursor() { const p = d3.mouse(this), cell = findGridCell(p[0], p[1]); heightmapInfoX.innerHTML = rn(p[0]); @@ -108,6 +102,7 @@ function editHeightmap() { customization = 0; customizationMenu.style.display = "none"; toolsContent.style.display = "block"; + layersPreset.disabled = false; restoreDefaultEvents(); clearMainTip(); closeDialogs(); @@ -121,11 +116,14 @@ function editHeightmap() { else if (mode === "keep") restoreKeptData(); else if (mode === "risk") restoreRiskedData(); + // restore initial layers terrs.selectAll("*").remove(); turnButtonOff("toggleHeight"); - changePreset("landmass"); - editHeightmap.layers.forEach(l => document.getElementById(l).click()); - layersPreset.disabled = false; + document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) { + if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on + else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off + }); + getCurrentPreset(); } function regenerateErasedData() { @@ -158,7 +156,12 @@ function editHeightmap() { Cultures.generate(); Cultures.expand(); BurgsAndStates.generate(); + Religions.generate(); + drawStates(); + drawBorders(); BurgsAndStates.drawStateLabels(); + addZone(); + addMarkers(); console.timeEnd("regenerateErasedData"); console.groupEnd("Edit Heightmap"); } @@ -180,14 +183,17 @@ function editHeightmap() { const l = grid.cells.i.length; const biome = new Uint8Array(l); const conf = new Uint8Array(l); - const culture = new Int8Array(l); const fl = new Uint16Array(l); const pop = new Uint16Array(l); const r = new Uint16Array(l); const road = new Uint16Array(l); + const crossroad = new Uint16Array(l); const s = new Uint16Array(l); - const state = new Uint8Array(l); - const burg = new Uint8Array(l); + const burg = new Uint16Array(l); + const state = new Uint16Array(l); + const province = new Uint16Array(l); + const culture = new Uint16Array(l); + const religion = new Uint16Array(l); for (const i of pack.cells.i) { const g = pack.cells.g[i]; @@ -198,9 +204,12 @@ function editHeightmap() { pop[g] = pack.cells.pop[i]; r[g] = pack.cells.r[i]; road[g] = pack.cells.road[i]; + crossroad[g] = pack.cells.crossroad[i]; s[g] = pack.cells.s[i]; state[g] = pack.cells.state[i]; + province[g] = pack.cells.province[i]; burg[g] = pack.cells.burg[i]; + religion[g] = pack.cells.religion[i]; } // do not allow to remove land with burgs @@ -224,12 +233,15 @@ function editHeightmap() { // assign saved pack data from grid back to pack const n = pack.cells.i.length; - pack.cells.burg = new Uint16Array(n); - pack.cells.culture = new Int8Array(n); pack.cells.pop = new Uint16Array(n); pack.cells.road = new Uint16Array(n); + pack.cells.crossroad = new Uint16Array(n); pack.cells.s = new Uint16Array(n); - pack.cells.state = new Uint8Array(n); + pack.cells.burg = new Uint16Array(n); + pack.cells.state = new Uint16Array(n); + pack.cells.province = new Uint16Array(n); + pack.cells.culture = new Uint16Array(n); + pack.cells.religion = new Uint16Array(n); if (!change) { pack.cells.r = new Uint16Array(n); @@ -255,12 +267,15 @@ function editHeightmap() { pack.cells.culture[i] = culture[g]; pack.cells.pop[i] = pop[g]; pack.cells.road[i] = road[g]; + pack.cells.crossroad[i] = crossroad[g]; pack.cells.s[i] = s[g]; pack.cells.state[i] = state[g]; + pack.cells.province[i] = province[g]; + pack.cells.religion[i] = religion[g]; } for (const b of pack.burgs) { - if (!b.i) continue; + if (!b.i || b.removed) continue; b.cell = findCell(b.x, b.y); b.feature = pack.cells.f[b.cell]; pack.cells.burg[b.cell] = b.i; @@ -268,6 +283,26 @@ function editHeightmap() { if (b.capital) pack.states[b.state].center = b.cell; } + for (const p of pack.provinces) { + if (!p.i || p.removed) continue; + const provCells = pack.cells.i.filter(i => pack.cells.province[i] === p.i); + if (!provCells.length) { + const state = p.state; + const stateProvs = pack.states[state].provinces; + if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1); + + p.removed = true; + continue; + } + + if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell; + else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];} + } + + BurgsAndStates.drawStateLabels(); + drawStates(); + drawBorders(); + console.timeEnd("restoreRiskedData"); console.groupEnd("Edit Heightmap"); } @@ -417,9 +452,11 @@ function editHeightmap() { d3.event.on("drag", () => { const p = d3.mouse(this); moveCircle(p[0], p[1], r, "#333"); + if (~~d3.event.sourceEvent.timeStamp % 5 != 0) return; // slow down the edit + const inRadius = findGridAll(p[0], p[1], r); const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius; - if (selection && selection.length) changeHeightForSelection(selection, start); + if (selection && selection.length) changeHeightForSelection(selection, start); }); d3.event.on("end", updateHeightmap); @@ -497,6 +534,8 @@ function editHeightmap() { function openTemplateEditor() { if ($("#templateEditor").is(":visible")) return; + const body = document.getElementById("templateBody"); + $("#templateEditor").dialog({ title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false, position: {my: "right top", at: "right-10 top+10", of: "svg"} @@ -505,21 +544,40 @@ function editHeightmap() { if (modules.openTemplateEditor) return; modules.openTemplateEditor = true; - $("#templateBody").sortable({items: "div:not(.elType)"}); + $("#templateBody").sortable({items: "div", handle: ".icon-resize-vertical", containment: "parent", axis: "y"}); // add listeners + body.addEventListener("click", function(ev) { + const el = ev.target; + if (el.classList.contains("icon-check")) { + el.classList.remove("icon-check"); + el.classList.add("icon-check-empty"); + el.parentElement.style.opacity = .5; + body.dataset.changed = 1; + return; + } + if (el.classList.contains("icon-check-empty")) { + el.classList.add("icon-check"); + el.classList.remove("icon-check-empty"); + el.parentElement.style.opacity = 1; + return; + } + if (el.classList.contains("icon-trash-empty")) { + el.parentElement.remove(); return; + } + }); + document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e)); - document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e)); - document.getElementById("templateRun").addEventListener("click", executeTemplate); - document.getElementById("templateSave").addEventListener("click", downloadTemplate); - document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click()); - document.getElementById("templateToLoad").addEventListener("change", uploadTemplate); + document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e)); + document.getElementById("templateRun").addEventListener("click", executeTemplate); + document.getElementById("templateSave").addEventListener("click", downloadTemplate); + document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click()); + document.getElementById("templateToLoad").addEventListener("change", uploadTemplate); function addStepOnClick(e) { if (e.target.tagName !== "BUTTON") return; const type = e.target.id.replace("template", ""); - const body = document.getElementById("templateBody"); - body.setAttribute("data-changed", 1); + document.getElementById("templateBody").dataset.changed = 1; addStep(type); } @@ -540,19 +598,22 @@ function editHeightmap() { } function getStepHTML(type, count, arg3, arg4, arg5) { - const Trash = ``; + const Trash = ``; + const Hide = ``; + const Reorder = ``; + const common = `