Compare commits

...

134 commits

Author SHA1 Message Date
Federico Busetti
9a16e06223
Fix Markers GeoJSON export (#1248)
* Fix the retrieval of the marker node on GeoJSON export

* Versioning update
2025-11-28 13:15:51 +01:00
Copilot
f73a8906ce
Add comprehensive GitHub Copilot instructions for Fantasy Map Generator (#1233)
* Initial plan

* Add comprehensive GitHub Copilot instructions for Fantasy Map Generator

Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>

* chore: copilot instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2025-08-29 17:20:03 +02:00
Azgaar
538cc3423a fix: 1227 2025-08-29 14:36:34 +02:00
Ender-Emp
ab08dc9429
fix regex of 'and' adjectivization rule (#1225) 2025-08-02 13:48:10 +02:00
Azgaar
d06ebe5ac8 fix(v1.108.11): add external icons to export in base64 format 2025-07-18 02:31:10 +02:00
Azgaar
738732364e fix(ui): correct marker note lookup by adding prefix 2025-07-02 00:52:41 +02:00
Azgaar
c26827bfe5 fix(markers-overview): correct note lookup by marker id 2025-07-01 23:16:07 +02:00
Nekomantikku
a3dcc8d2c4
Merge pull request #1212 from Nekomantikku/master
Add shell script to launch local HTTP server and open browser (GNU/Linux)
2025-06-20 00:18:57 +02:00
Azgaar
d9391d6d97
Merge pull request #1211 from Azgaar/ollama
Ollama: local AI text generation
2025-06-14 16:13:59 +02:00
Azgaar
c891689796 feat(ai-generator): update supported AI models list 2025-06-14 15:24:23 +02:00
Azgaar
d96e339043 chore: update version to 1.108.8 2025-06-14 15:20:41 +02:00
Azgaar
bba3587e50 refactor: ollama generation 2025-06-14 15:20:01 +02:00
Krory
fe2fa6d6b8
Ollama integration as a new AI provider (#1208)
* ollama implementation

* ollama implementation

* Update ai-generator.js

* Update README.md

* Create OLLAMAREADME.MD

* Update OLLAMAREADME.MD

* Update notes-editor.js

* Update index.html

* Update OLLAMAREADME.MD

* Update ai-generator.js
2025-06-14 14:03:06 +02:00
Greger
004097ef93
Make id field in exports more consistent. (#1210)
The id field for geojson export was not consistent with csv exports.
Removes the prefix on routes, rivers and markers geojson, and on
markers csv, to make them all use only an integer as id.

This makes it easier to import and do joins in other software.
2025-06-11 00:42:31 +02:00
Azgaar
a6f66e9828 fix: grid layer to not be clickable 2025-03-09 13:29:45 +01:00
Ruichka
8131f25456
Allow data URI scheme for custom images (#1196)
* Allow data URL external images

* fix

* removed inconsistency
2025-03-08 14:51:48 +01:00
Azgaar
f859439fc8 refactor: drawReliefIcons, v1.108.4 2025-02-15 18:13:45 +01:00
Azgaar
4dd34e13d1 refactor: drawReliefIcons, v1.108.4 2025-02-15 18:03:54 +01:00
Azgaar
791347b1ee feat: generate less water ice, v1.108.3 2025-02-15 17:45:16 +01:00
Azgaar
12b8b941e3 fix: remove old lake paths on drawFeatures, v1.108.2 2025-02-15 15:09:39 +01:00
Azgaar
d98ef5717e perf: set text-rendering to optimizeSpeed, v1.108.1 2025-02-15 14:43:51 +01:00
Azgaar
764993b680 fix: remove old feature masks, v1.108.0 2025-02-15 13:06:14 +01:00
Azgaar
e39ca793f2 feat: add growthRate to safe file 2025-02-13 02:54:37 +01:00
Azgaar
e526646076 fix: notes editor size to be relative to canvas size 2025-02-10 12:41:14 +01:00
Azgaar
51c47a18d2 fix: external icons - battle screen 2025-02-10 01:39:21 +01:00
Azgaar
01a69fd40b Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator 2025-02-08 15:13:23 +01:00
Azgaar
22636b1b62 fix: data integrity checks - better Stripping issue detection, v1.107.1 2025-02-08 15:12:57 +01:00
Azgaar
92b0b4d306
Merge pull request #1187 from Azgaar/linked-icons
Custom images for Markers and Regiments
2025-02-08 14:21:44 +01:00
Azgaar
0be14790d2 feat: rerender affected layers on auto-update 2025-02-08 14:15:59 +01:00
Azgaar
33a02aeea0 chore: cleanup 2025-02-08 14:06:33 +01:00
Azgaar
d51deffdac feat: make lined icons work for all elements, v1.107.0 2025-02-08 14:05:28 +01:00
Issac411
7b8ffd025f
custom pictures for regiments (#1183)
* forms and ajustements

* variable size for style as requested
2025-01-19 23:29:27 +01:00
Azgaar
5bb33311fb fix: 1.106.7 - rivers starting width calc 2024-12-17 17:11:13 +01:00
Azgaar
04c6fb3ee7 feat: like temp likeness, 1.106.6 2024-12-17 12:48:41 +01:00
Azgaar
f15bccd610 style: increase dialog buttons size 2024-12-16 14:30:11 +01:00
Azgaar
6d4c9f6b18 fix: sumap - clip routes by bbox 2024-12-14 15:12:38 +01:00
Azgaar
488f51a0f4 fix: don't mask routes 2024-12-13 19:53:57 +01:00
Azgaar
ced7b88054 fix: submap - generate in current canvas size 2024-12-13 13:15:01 +01:00
Azgaar
50ee5150c1 fix: #1174 2024-12-13 11:58:53 +01:00
Azgaar
66d22f26c0
[Draft] Submap refactoring (#1153)
* refactor: submap - start

* refactor: submap - continue

* Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into submap-refactoring

* refactor: submap - relocate burgs

* refactor: submap - restore routes

* refactor: submap - restore lake names

* refactor: submap - UI update

* refactor: submap - restore river and biome data

* refactor: submap - simplify options

* refactor: submap - restore rivers

* refactor: submap - recalculateMapSize

* refactor: submap - add middle points

* refactor: submap - don't add middle points, unified findPath fn

* chore: update version

* feat: submap - relocate out of map regiments

* feat: submap - fix route gen

* feat: submap - allow custom number of cells

* feat: submap - add checkbox submapRescaleBurgStyles

* feat: submap - update version hash

* chore: supporters update

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-12-12 13:11:54 +01:00
Azgaar
23f36c3210 Merge remote-tracking branch 'refs/remotes/origin/master' 2024-11-28 12:19:28 +01:00
Azgaar
610a2d9ae6 chore: update supporters 2024-11-28 12:19:05 +01:00
Azgaar
fd8fd28ab1
Update FUNDING.yml 2024-11-28 12:02:43 +01:00
Azgaar
03c7db32ef fix: #1172 2024-11-27 12:21:36 +01:00
Azgaar
fa03b2d705 fix: #1170 2024-11-24 15:02:56 +01:00
Azgaar
8db9dc9bed chore: supporters update 2024-11-15 15:53:41 +01:00
Azgaar
8d621ba9ce
AI Claude support (#1167)
* Add Claude AI support (#1165)

* feat: ai generator - add support for claude

* feat: ai generator - add claude support

* refactor: clean up API calls

---------

Co-authored-by: Azgaar <maxganiev@yandex.com>

* feat: ai - claude support

---------

Co-authored-by: aesli <37640637+aesliva@users.noreply.github.com>
Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-11-14 02:05:34 +01:00
Dyxang
ca8e723006
Temperature parameters can be customized (#1162)
* Temperature parameters can be customized

* fix typo

* update to 1.105.22

* Update index.html
2024-11-07 14:23:38 +01:00
Ángel Montero Lamas
91dc16878e
Stroke dash to cells (#1159)
* style.js sorted items alphabetically

* style.js added strokeDash to "cells"
2024-10-26 14:40:43 +02:00
Azgaar
1706fa0981 fix: add p to priority queue 2024-10-26 14:29:52 +02:00
Ryan D. Guild
d7f5cae229
Removed priority queue in favor of FlatQueue (#1157)
* removed priority queue in favor of simple array extension as it will be easier to migrate to esm

* patch: bump version

* spacing

* moved references to globalThis

* demonstrate module interop

* added version to priority-queue and moved to utils to follow dom loading pattern

* removed PriorityQueue in favor of FlatQueue

* update index.html

* never mind that force push I don't know how to amend commits right

* missing capitalization

* priority set to 0 on 541

---------

Co-authored-by: RyanGuild <ryan.guild@us-ignite.org>
2024-10-26 14:26:59 +02:00
Azgaar
54491cfd09 feat: zones editor - don't close other editors on open 2024-10-22 23:04:57 +02:00
Azgaar
2ec2c9f773 chore: update 1.105.19 hash 2024-10-22 15:10:33 +02:00
Azgaar
5ac99d180d chore: parse DEBUG setting as an object 2024-10-22 14:45:25 +02:00
Azgaar
6d69eb855f fix: zones editor - legend to be toggable 2024-10-19 14:02:04 +02:00
Azgaar
8a4f28b321 fix: CRLF issue 2024-10-19 13:32:59 +02:00
Azgaar
87e1dc2c5d
Draw state labels improvement (#1155)
* chore: render debug elements

* feat: redo draw state labels algo

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-10-19 13:25:34 +02:00
Azgaar
efbe0373b0 fix: lock all burgs 2024-10-16 15:20:44 +02:00
Azgaar
c447afb829 fix: features rendering - close the ring 2024-10-13 20:32:37 +02:00
Azgaar
6c37c7babf fix: #1152, don't keep removed locked cultures on regenerate 2024-10-13 15:10:31 +02:00
Azgaar
f0ff23a119 fix: #1152, don't keep removed locked cultures on regenerate 2024-10-13 15:08:40 +02:00
Azgaar
26b659a59e fix #1152 2024-10-11 12:17:33 +02:00
Azgaar
c795ac6c30 fix: allow to load smaller namesbase without issues on regeneration 2024-10-09 01:08:47 +02:00
Azgaar
56597d961d fix: remove unwanted states styling 2024-10-03 13:05:10 +02:00
Azgaar
2d0030e3d4 feat: allow to crean data in case of load error 2024-10-01 21:20:29 +02:00
Azgaar
80da2f0cda fix: gap path to not omit the M path sign 2024-09-29 15:09:29 +02:00
Azgaar
c04fb2bfca refactor: burg types 2024-09-27 13:32:22 +02:00
Azgaar
84c326e347 fix: issue with feature vertex being out of bound 2024-09-25 17:05:31 +02:00
Azgaar
949a486bf8 fix: redraw features on load 2024-09-25 13:18:22 +02:00
Azgaar
879cf6b692 fix: typo 2024-09-25 12:13:16 +02:00
Azgaar
ba3a9d1598 chore: set libs version 2024-09-23 12:52:56 +02:00
Azgaar
b127607811 chore: supporters update 2024-09-23 12:35:16 +02:00
Azgaar
ea27276558 fix: disable double-click on heightmap edit 2024-09-22 20:07:55 +02:00
Azgaar
b66874ddda feat: battles - move Regiments back to init position after the battle 2024-09-22 18:20:22 +02:00
Azgaar
e25f231697
AI Assistant widget (#1115)
* feat: add assistan widget

* feat: remove gtm

* feat: assistant - minify js, add option UI

* feat: assistant - ability to toggle assistant

* chore: update version to 1.102.00

* chore: resolve version conflict

* chore: cleanup

* chore: cleanup

* feat: ai widget - improve style

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-09-22 14:59:53 +02:00
Azgaar
97e504d2aa fix: features - define first cell 2024-09-22 13:04:22 +02:00
Azgaar
6d3b88b36f fix: split regiment v1.104.14 2024-09-22 12:14:30 +02:00
Azgaar
330eb62024 fix: draw military fn reference 2024-09-21 16:33:35 +02:00
Azgaar
861b219e6e fix: don't hide armies layer 2024-09-21 14:23:18 +02:00
Azgaar
1a61a433b7 fix: loose format requirements for old files to show correct message 2024-09-21 13:58:11 +02:00
Azgaar
877afa546d fix: remove route if it has <2 points 2024-09-21 13:43:46 +02:00
Azgaar
601e71b846 fix: heightmap edit in Risk mode 2024-09-21 02:09:16 +02:00
Azgaar
5964657a16 fix: layers - show emblems on render 2024-09-21 02:01:55 +02:00
Azgaar
8be55eae51 fix: heightmap edit in Erase mode 2024-09-21 01:41:14 +02:00
Azgaar
62805dc1a6 fix: slow load 2024-09-21 01:16:28 +02:00
Azgaar
18b9f604e9 fix: #1136 2024-09-21 00:33:24 +02:00
Azgaar
e9113730b9 fix: service worker fn 2024-09-20 15:40:10 +02:00
Azgaar
5904e9e7c6 fix: routes (v1.104.3) 2024-09-20 14:16:07 +02:00
Azgaar
3d1f268003 feat: use StaleWhileRevalidate for scripts poloicy; v1.104.2 2024-09-20 13:04:47 +02:00
Azgaar
d3ba6dd95b style: reduce submap dialog width - v1.104.1 2024-09-20 12:21:51 +02:00
Azgaar
05de284e02
Refactor layers rendering (#1120)
* feat: render states - use global fn

* feat: render states - separate pole detection from layer render

* feat: render provinces

* chore: unify drawFillWithGap

* refactor: drawIce

* refactor: drawBorders

* refactor: drawHeightmap

* refactor: drawTemperature

* refactor: drawBiomes

* refactor: drawPrec

* refactor: drawPrecipitation

* refactor: drawPopulation

* refactor: drawCells

* refactor: geColor

* refactor: drawMarkers

* refactor: drawScaleBar

* refactor: drawScaleBar

* refactor: drawMilitary

* refactor: pump version to 1.104.00

* refactor: pump version to 1.104.00

* refactor: drawCoastline and createDefaultRuler

* refactor: drawCoastline

* refactor: Features module start

* refactor: features - define distance fields

* feat: drawFeatures

* feat: drawIce don't hide

* feat: detect coastline - fix issue with border feature

* feat: separate labels rendering from generation process

* feat: auto-update and restore layers

* refactor - change layers

* refactor - sort layers

* fix: regenerate burgs to re-render layers

* fix: getColor is not defined

* fix: burgs overview - don't auto-show labels on hover

* fix: redraw population on change

* refactor: improve tooltip logic for burg labels and icons

* chore: pump version to 1.104.0

* fefactor: edit coastline and lake

* fix: minot fixes

* fix: submap

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-09-20 12:20:27 +02:00
Azgaar
ec993d1a9b fix: #1129 2024-09-12 12:55:31 +02:00
Azgaar
0187bba76c hotfix: 1.103.7 2024-09-12 11:25:53 +02:00
Ángel Montero Lamas
95c6af8993
added names to the random names of zones (#1128)
* added names of zones

added names for rebels, invasion and animals.

* deleted suggested names

- deleted siege and subjugation from zones.generator.js const subtype = rw

* fixed invationCells spelling

Fixed invationCells to invasionCells.
Is invasion with s.

* update versioning and index.html
2024-09-12 01:23:50 +02:00
Azgaar
cea9b1a48a fix: locking options 2024-09-06 02:41:14 +02:00
Azgaar
4c6c5288a1 refactor: load.js formatting 2024-09-06 01:11:25 +02:00
Azgaar
dd35947ecd fix: version detection on load 2024-09-06 00:32:32 +02:00
Azgaar
0b8d3c63fc fix: 1.103.02 - parse old .map - markers data fix 2024-09-04 22:30:24 +02:00
Azgaar
637aa398bb fix: 1.103.01 - parse old .map - add patch version 2024-09-04 22:20:56 +02:00
Ángel Montero Lamas
168203f7da
Friendly text on latitude cell info (#1125)
* Friendly text on latitude cell info

- Added function getLatitudeDescription(latitude)
- Added the text to infoLat on cell info.

* refactored, renamed to getGeozone

* v1.103 on commonUtils.js
2024-09-04 19:18:35 +02:00
Ángel Montero Lamas
473b62b3eb
Update grid overlay to add tilings (#1030)
* Update grid overlay to add tilings

Added square rotated 45 degrees, truncated square tiling, tetrakis square, triangular tiling, trihexagonal tiling and rhombille tiling with rhombus. Inspired by these uniform tilings: https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings

* Fixed stroke width of grid tilings

The stroke width of:
- Tetrakis square.
- Truncated square.
- Triangle tiling.
- Rhombille tiling.
Now has all the same stroke width.

* Updated grid to 25 px regular

Fixed triangular and rhombic, rhombille.

* fix typo on pattern triangle index.html

fix

* added wiki link and info icon

- icon-info-circled
- wiki article scale and distance

* Center to center distance

Updated svg to adjust distance to center to center cell instead of svg side.
- Square 45 degrees
- Triangular
- Rhombille
2024-09-04 02:36:15 +02:00
Ángel Montero Lamas
b273c77166
added icon-dot-circled to locate burg in burg editor menu (#1123)
* added icon-dot-circled to locate burg

- icon dot-circled
- function zoomIntoBurg()

* pack.burgs[id] and icons

pack.burgs[id].x and y
icon target and map pin

* Update versioning.js to 1.102.0

* Update index.html burg-editor.js to 1.102.00
2024-09-04 02:34:48 +02:00
Azgaar
d42fd5cf92 fix: year and era - unlock both on lock icon click 2024-09-02 12:16:51 +02:00
Azgaar
23e2484526 chore: supporters list update 2024-09-02 01:56:32 +02:00
Azgaar
59462a4f15 fix: #1118 - get lake shoreline if missing 2024-09-01 21:42:34 +02:00
Azgaar
d1fcdf20f7 chore: update version to 1.101.00 2024-09-01 14:29:18 +02:00
Azgaar
424077c7eb
Merge pull request #1117 from Azgaar/letter-spacing
Letter spacing
2024-09-01 14:21:01 +02:00
Azgaar
baf7a5c3b9 chore: update version 2024-09-01 14:20:06 +02:00
Azgaar
4f066c6dc1 feat: letter-spacing - improve UI, refactor 2024-09-01 14:14:06 +02:00
Azgaar
2fea87344b feat: letter-spacing - update style files 2024-09-01 14:07:18 +02:00
Azgaar
dbe6ef1854 chore: label letter spacing - update version 2024-09-01 13:46:42 +02:00
Azgaar
6e64912e27 Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into letter-spacing 2024-09-01 13:03:23 +02:00
Oriolowsky
6ffc5a0cc5
Added the option to set letter-spacing size to individual labels. (#1116)
* Added the option to set letter-spacing size to individual labels.

* Allowed to set letter-spacing for label groups from the Style tab.
2024-09-01 13:02:07 +02:00
Azgaar
eb29c5ec5d
Zones generator update (#1113)
* feat: style - store emblem size mod in style (v1.99.10)

* fix the isOutdated function for versions past 1.99

* fix: showUploadMessage function not called correctly for isUpdated case

* feat: load - improve version detection

* feat: improve version detection and update process

* feat: Update version and use constant for VERSION in multiple files

* Update versioning.js to fix incorrect message display for stored version

* feat: zones editor - update to work with pack data

* feat: zones editor - update editor

* feat: zones editor - update editor

* chore: update version

* feat: zones - regenerate

* feat: zones - render zones as continuius line

* feat: zones - editot changes

* feat: zones - auto-update

* feat: zones - generation fixes

* feat: zones - generation fixes

* feat: zones - restore layer

* feat: zones - proselytism - check population

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-09-01 12:48:39 +02:00
Azgaar
e77202a08a feat: remove ALT horkeys as unused 2024-08-29 12:14:49 +02:00
Azgaar
19f7f2508e fix: geoJSON export - fix array level 2024-08-27 14:04:02 +02:00
Azgaar
bf41ad1b70 fix: #1114 - saveGeoJSON_Routes 2024-08-27 12:07:49 +02:00
Azgaar
33fbfc2e48 feat: style - allow clip setting for prec and population layers 2024-08-26 19:21:15 +02:00
Azgaar
15aa7f98e1 fix: options - firefox - hide arrows for number input 2024-08-26 13:04:03 +02:00
Azgaar
f129ff5573 feat: style - store emblem size mod in style (v1.99.10) 2024-08-26 02:12:30 +02:00
Azgaar
634ad6cd8e feat: ai-generation - stream results 2024-08-25 15:21:45 +02:00
Azgaar
63496a651f fix: style update - get value from event 2024-08-24 01:12:48 +02:00
Azgaar
efbab14d11 Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator 2024-08-23 18:26:30 +02:00
Azgaar
b54f758350 fix: routes - don't render route with <2 points 2024-08-23 18:26:12 +02:00
Azgaar
6df54d1ef6
AI generation for notes (#1112)
* feat: ai generation for notes

* feat - ai generation -default to gpt-4o-mini

* feat - ai generation - change update text

* feat - ai generation - improve prompt

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-08-23 18:08:50 +02:00
Azgaar
1f280133be fix: recreateStates 2024-08-23 12:43:15 +02:00
Azgaar
dfa3813f04 feat: set min brush size to 1 2024-08-23 02:33:45 +02:00
Azgaar
cfc603edc1 style: burg editor - minor change 2024-08-22 13:51:58 +02:00
Azgaar
d4aef4920c
Slider-input web component (#1109)
* feat: slider-input web component

* feat: slider-input web component - Brush size

* feat: slider-input - statesGrowthRate

* feat: slider-input - units editor

* feat: slider-input - dissalow invalid numbers

* chore: pump version to v1.99.05

* chore: pump version to v1.99.05

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
2024-08-22 13:35:36 +02:00
Azgaar
da8c4f1e4a fix: rotues - isCrossroad 2024-08-19 14:09:38 +02:00
Azgaar
147014f0ff fix: styles - dark seas - change scale bar style 2024-08-17 14:25:21 +02:00
Azgaar
7c82a99900 feat: new style - DarkSeas 2024-08-17 14:16:01 +02:00
Azgaar
9c97711a99 fix: routes id after removing all 2024-08-16 13:15:00 +02:00
Azgaar
106d5edc78 fix: spelling 2024-08-15 17:30:15 +02:00
113 changed files with 7700 additions and 6153 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,3 +1,3 @@
# These are supported funding model platforms
github: Azgaar
patreon: Azgaar

89
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,89 @@
# Fantasy Map Generator
Azgaar's Fantasy Map Generator is a client-side JavaScript web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
- Run the application using HTTP server (required - cannot run with file:// protocol):
- `python3 -m http.server 8000` - takes 2-3 seconds to start
- Access at: `http://localhost:8000`
## Validation
- Always manually validate any changes by:
1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
2. Navigate to the application in browser
3. Click the "►" button to open the menu and generate a new map
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
5. Test UI interaction: click "Layers" button, verify layer controls work
6. Test regeneration: click "New Map!" button, verify new map generates correctly
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
## Repository Structure
### Core Files
- `index.html` - Main application entry point
- `main.js` - Core application logic
- `versioning.js` - Version management and update handling
### Key Directories
- `modules/` - core functionality modules:
- `modules/ui/` - UI components (editors, tools, style management)
- `modules/dynamic/` - runtime modules (export, installation)
- `modules/renderers/` - drawing and rendering logic
- `utils/` - utility libraries (math, arrays, strings, etc.)
- `styles/` - visual style presets (JSON files)
- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
- `images/` - backgrounds, UI elements
- `charges/` - heraldic symbols and coat of arms elements
- `config/` - Heightmap templates and configurations
- `heightmaps/` - Terrain generation data
## Common Tasks
### Making Code Changes
1. Edit JavaScript files directly (no compilation needed)
2. Refresh browser to see changes immediately
3. **ALWAYS test map generation** after making changes
4. Update version in `versioning.js` for all changes
5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
### Debugging Map Generation
- Open browser developer tools console
- Look for timing logs, e.g. "TOTAL: ~0.76s"
- Map generation logs show each step (heightmap, rivers, states, etc.)
- Error messages will indicate specific generation failures
### Testing Different Map Types
- Use "New Map!" button for quick regeneration
- Access "Layers" menu to change map visualization
- Available presets: Political, Cultural, Religions, Biomes, Heightmap, Physical, Military
## Troubleshooting
### Application Won't Load
- Ensure using HTTP server (not file://)
- Check console for JavaScript errors
- Verify all files are present in repository
### Map Generation Fails
- Check browser console for error messages
- Look for specific module failures in generation logs
- Try refreshing page and generating new map
### Performance Issues
- Map generation should complete in ~1 second for standard configurations
- If slower, check browser console for errors
Remember: This is a sophisticated client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality.

View file

@ -14,8 +14,8 @@
# Versioning
<!-- Update the version if you want the PR to be merged fast. Currently it's a manual 3-steps process:
* update version in `versioning.js` using semver principle. Just set the next patch (for fixes) or minor version (for new features)
<!-- Update the VERSION if you want the PR to be merged. Currently it's a manual 3-steps process:
* update VERSION in `versioning.js` using semver principle
* for all changed files update hash (the part after `?`) in place where file is requested (usually it's `index.html`)
* if the change can be really interesting for end-users, describe it inside the `showUpdateWindow()` function in `versioning.js` -->

View file

@ -0,0 +1,78 @@
{
const style = /* css */ `
slider-input {
display: flex;
align-items: center;
gap: .4em;
}
`;
const styleElement = document.createElement("style");
styleElement.setAttribute("type", "text/css");
styleElement.innerHTML = style;
document.head.appendChild(styleElement);
}
{
const template = document.createElement("template");
template.innerHTML = /* html */ `
<input type="range" />
<input type="number" />
`;
class SliderInput extends HTMLElement {
constructor() {
super();
this.appendChild(template.content.cloneNode(true));
const range = this.querySelector("input[type=range]");
const number = this.querySelector("input[type=number]");
range.value = number.value = this.value || this.getAttribute("value") || 50;
range.min = number.min = this.getAttribute("min") || 0;
range.max = number.max = this.getAttribute("max") || 100;
range.step = number.step = this.getAttribute("step") || 1;
range.addEventListener("input", this.handleEvent.bind(this));
number.addEventListener("input", this.handleEvent.bind(this));
range.addEventListener("change", this.handleEvent.bind(this));
number.addEventListener("change", this.handleEvent.bind(this));
}
handleEvent(e) {
const value = e.target.value;
const isNaN = Number.isNaN(Number(value));
if (isNaN || value === "") return e.stopPropagation();
const range = this.querySelector("input[type=range]");
const number = this.querySelector("input[type=number]");
this.value = range.value = number.value = value;
this.dispatchEvent(
new CustomEvent(e.type, {
detail: {value},
bubbles: true,
composed: true
})
);
}
set value(value) {
const range = this.querySelector("input[type=range]");
const number = this.querySelector("input[type=number]");
range.value = number.value = value;
}
get value() {
const number = this.querySelector("input[type=number]");
return number.value;
}
get valueAsNumber() {
const number = this.querySelector("input[type=number]");
return number.valueAsNumber;
}
}
customElements.define("slider-input", SliderInput);
}

View file

@ -253,7 +253,7 @@
.icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */
.icon-half:before {font-weight: bold;content:'½';}
.icon-voice:before {content:'🔊';}
.icon-robot:before {content:'🤖';}
.icon-die:before {content:'🎲';}
.icon-button-die:before {content:'🎲'; padding-right: .4em;}
.icon-button-power:before {content:'💪'; padding-right: .6em;}

207
index.css
View file

@ -122,10 +122,6 @@ a {
fill: none;
}
#biomes {
stroke-width: 0.7;
}
#landmass {
mask: url(#land);
fill-rule: evenodd;
@ -170,6 +166,7 @@ t,
#texture,
#landmass,
#vignette,
#gridOverlay,
#fogging {
pointer-events: none;
}
@ -190,20 +187,12 @@ t,
font-size: 0.8em;
}
#statesBody {
stroke-width: 3;
}
#statesHalo {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
#provincesBody {
stroke-width: 0.2;
}
#statesBody,
#provincesBody,
#relig,
@ -356,6 +345,14 @@ text.drag {
font-weight: bold;
}
button.ui-button:disabled {
filter: brightness(0.95);
}
button.ui-button:disabled:hover {
cursor: default;
}
.ui-dialog,
#optionsContainer {
user-select: none;
@ -525,7 +522,53 @@ input[type="color"]::-webkit-color-swatch-wrapper {
font-size: smaller;
}
#options input[type="text"] {
border: 0px;
width: 62%;
font-size: smaller;
}
#options output {
text-align: right;
font-size: smaller;
}
#options input[type="number"] {
font-size: 0.8em;
border: 0;
text-align: right;
background-color: transparent;
width: 3.3em;
}
#options input[type="number"]::-webkit-inner-spin-button,
#options input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
#options input[type="number"] {
appearance: textfield;
-moz-appearance: textfield;
}
#options input[type="number"]:hover {
outline: 1px solid var(--dark-solid);
}
#options input.paired {
text-align: center;
background-color: white;
}
#options input.long {
width: 100%;
background-color: white;
text-align: left;
}
#options input[type="range"] {
width: 100%;
height: 8px;
background: 0;
appearance: none;
@ -568,55 +611,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
height: 2px;
}
#options input[type="number"] {
font-size: 0.8em;
}
#options input[type="text"] {
border: 0px;
width: 62%;
font-size: smaller;
}
#optionsContent output {
text-align: right;
font-size: smaller;
}
#optionsContent input[type="number"] {
border: 0;
text-align: right;
background-color: transparent;
width: 3.3em;
-moz-appearance: textfield;
}
#optionsContent input[type="number"]::-webkit-inner-spin-button,
#optionsContent input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
#optionsContent input[type="number"]:hover {
outline: 1px solid var(--dark-solid);
}
#optionsContent input.paired {
text-align: center;
background-color: white;
}
#optionsContent input.long {
width: 100%;
background-color: white;
text-align: left;
}
#optionsContent input[type="range"] {
width: 100%;
}
#optionsContent select {
#options select {
width: 100%;
}
@ -641,19 +636,6 @@ input[type="color"]::-webkit-color-swatch-wrapper {
transform: translate(0px, 1px);
}
#styleElements input[type="range"] {
width: 64%;
}
#styleElements select {
width: 64%;
}
#styleElements input[type="number"] {
width: 6em;
border: 0;
}
#styleSelectFont > option {
font-size: 2em;
}
@ -692,6 +674,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
border: none;
padding: 0.45em 0.75em;
margin: 0.4em 0;
white-space: nowrap;
font-family: var(--monospace);
animation: glowing 2s infinite;
}
@ -724,9 +707,6 @@ input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0.45em 0.75em;
margin: 0.35em 0;
transition: 0.1s;
font-size: 1em;
text-transform: capitalize;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@ -743,7 +723,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
#toolsContent > .grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
margin: 0.2em 0;
}
@ -790,7 +770,7 @@ fieldset {
text-overflow: ellipsis;
}
.tabcontent .buttonoff {
.tabcontent li.buttonoff {
background-color: var(--bg-disabled);
color: #444444aa;
}
@ -1268,7 +1248,6 @@ i.resetButton:active {
padding: 0;
height: 2px;
background: #d4d4d4;
top: -0.35em;
position: relative;
appearance: none;
-webkit-appearance: none;
@ -1533,20 +1512,6 @@ div.states > .burgCulture {
width: 6em;
}
div.states .burgPopulation {
width: 4.8em;
}
div.states .burgType {
width: 3em;
}
div.states .burgType > span {
padding: 0 1px;
color: #6e5e66;
transition: 0.2s;
}
div.states span.inactive {
color: #c6c2c2;
}
@ -1844,11 +1809,6 @@ div.editorLine {
padding: 0px 3px !important;
}
#unitsBody > div > * {
display: inline-block;
margin-bottom: 0.2em;
}
.unitsHeader {
margin: 0.8em 0 0 -1.1em;
font-weight: bold;
@ -1860,28 +1820,21 @@ div.editorLine {
margin: 6px 0 0 6px;
}
#unitsBody > div > div {
#unitsBody label {
display: inline-block;
width: 9em;
}
#unitsBody > div > input[type="range"] {
width: 7em;
}
#unitsBody > div > select,
#unitsBody > div > input[type="text"] {
width: 12em;
}
#unitsBody > div > input[type="number"] {
width: 4.35em;
}
#unitsBody > div > input,
#unitsBody > div > select {
width: 14.4em;
border: 1px solid #e9e9e9;
}
#unitsBody input[type="range"] {
width: 9em;
}
#unitsEditor i.icon-lock-open,
#unitsEditor i.icon-lock {
color: #626573;
@ -2414,6 +2367,34 @@ svg.button {
margin-left: 0.25em;
}
@keyframes clockwiseBorderPulse {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#chat-widget-container {
user-select: none;
}
#chat-widget-minimized {
animation: fadeIn 1s ease-in;
transform: scale(0.65);
opacity: var(--bg-opacity);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: var(--bg-opacity);
}
}
@media print {
div,
canvas {

1091
index.html

File diff suppressed because it is too large Load diff

32
libs/jquery-ui.css vendored
View file

@ -314,30 +314,44 @@ body .ui-dialog {
}
.ui-dialog .ui-dialog-titlebar {
display: flex;
padding: 0.4em 0.3em;
justify-content: space-evenly;
padding: 0.3em 0.8em;
justify-content: space-between;
align-items: center;
font-size: 1.2em;
min-width: 150px;
}
.ui-dialog .ui-dialog-title {
float: left;
margin: 0.1em 0;
white-space: nowrap;
width: 90%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-dialog .ui-dialog-titlebar button {
padding: 0;
width: 1.8em;
height: 1.8em;
padding: 3px;
margin-left: 5px;
width: 19px;
height: 18px;
color: #ffffff;
background: none;
font-size: 0.75em;
font-size: 0.8em;
border: 1px solid #c5c5c5;
}
@media (max-width: 600px) {
.ui-dialog .ui-dialog-title {
font-size: 1.6em;
}
.ui-dialog .ui-dialog-titlebar button {
padding: 3px;
margin-left: 10px;
width: 40px;
height: 32px;
font-size: 1.6em;
}
}
.ui-dialog .ui-dialog-titlebar button:active {
border: 1px solid #5d4651;
color: #5d4651;

1
libs/openwidget.min.js vendored Normal file
View file

@ -0,0 +1 @@
window.__ow=window.__ow||{},window.__ow.organizationId="7bb02e70-bcef-4861-a4e6-d259b0d10e24",window.__ow.integration_name="manual_settings",window.__ow.product_name="openwidget",function(n,e,t){function o(n){return c._h?c._h.apply(null,n):c._q.push(n)}var c={_q:[],_h:null,_v:"2.0",on:function(){o(["on",t.call(arguments)])},once:function(){o(["once",t.call(arguments)])},off:function(){o(["off",t.call(arguments)])},get:function(){if(!c._h)throw Error("[OpenWidget] You can't use getters before load.");return o(["get",t.call(arguments)])},call:function(){o(["call",t.call(arguments)])},init:function(){var n=e.createElement("script");n.async=!0,n.type="text/javascript",n.src="https://cdn.openwidget.com/openwidget.js",e.head.appendChild(n)}};n.__ow.asyncInit||c.init(),n.OpenWidget=n.OpenWidget||c}(window,document,[].slice);

File diff suppressed because one or more lines are too long

103
libs/simplify.js Normal file
View file

@ -0,0 +1,103 @@
/*
(c) 2017, Vladimir Agafonkin
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
{
// square distance between 2 points
function getSqDist([x1, y1], [x2, y2]) {
const dx = x1 - x2;
const dy = y1 - y2;
return dx * dx + dy * dy;
}
// square distance from a point to a segment
function getSqSegDist([x1, y1], [x, y], [x2, y2]) {
let dx = x2 - x;
let dy = y2 - y;
if (dx !== 0 || dy !== 0) {
const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = x2;
y = y2;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = x1 - x;
dy = y1 - y;
return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format
// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {
let prevPoint = points[0];
const newPoints = [prevPoint];
let point;
for (let i = 1; i < points.length; i++) {
point = points[i];
if (!point) continue;
if (getSqDist(point, prevPoint) > sqTolerance) {
newPoints.push(point);
prevPoint = point;
}
}
if (prevPoint !== point) newPoints.push(point);
return newPoints;
}
function simplifyDPStep(points, first, last, sqTolerance, simplified) {
let maxSqDist = sqTolerance;
let index = first;
for (let i = first + 1; i < last; i++) {
const sqDist = getSqSegDist(points[i], points[first], points[last]);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
simplified.push(points[index]);
if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
}
}
// simplification using Ramer-Douglas-Peucker algorithm
function simplifyDouglasPeucker(points, sqTolerance) {
const last = points.length - 1;
const simplified = [points[0]];
simplifyDPStep(points, 0, last, sqTolerance, simplified);
simplified.push(points[last]);
return simplified;
}
// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality = false) {
if (points.length <= 2) return points;
const sqTolerance = tolerance * tolerance;
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
points = simplifyDouglasPeucker(points, sqTolerance);
return points;
}
window.simplify = simplify;
}

842
main.js

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@ window.BurgsAndStates = (() => {
placeTowns();
expandStates();
normalizeStates();
getPoles();
specifyBurgs();
collectStatistics();
@ -20,11 +22,10 @@ window.BurgsAndStates = (() => {
generateCampaigns();
generateDiplomacy();
drawBurgs();
function placeCapitals() {
TIME && console.time("placeCapitals");
let count = +regionsOutput.value;
let count = +byId("statesNumber").value;
let burgs = [0];
const rand = () => 0.5 + Math.random() * 0.5;
@ -85,7 +86,7 @@ window.BurgsAndStates = (() => {
b.capital = 1;
// states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1);
const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
@ -238,16 +239,23 @@ window.BurgsAndStates = (() => {
return [x, y];
}
const getType = (i, port) => {
const cells = pack.cells;
if (port) return "Naval";
if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === "lake") return "Lake";
if (cells.h[i] > 60) return "Highland";
if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return "River";
const getType = (cellId, port) => {
const {cells, features, burgs} = pack;
if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) {
if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
if (port) return "Naval";
const haven = cells.haven[cellId];
if (haven !== undefined && features[cells.f[haven]].type === "lake") return "Lake";
if (cells.h[cellId] > 60) return "Highland";
if (cells.r[cellId] && cells.fl[cellId] >= 100) return "River";
const biome = cells.biome[cellId];
const population = cells.pop[cellId];
if (!cells.burg[cellId] || population <= 5) {
if (population < 5 && [1, 2, 3, 4].includes(biome)) return "Nomadic";
if (biome > 4 && biome < 10) return "Hunting";
}
return "Generic";
@ -272,115 +280,19 @@ window.BurgsAndStates = (() => {
});
};
const drawBurgs = () => {
TIME && console.time("drawBurgs");
// remove old data
burgIcons.selectAll("circle").remove();
burgLabels.selectAll("text").remove();
icons.selectAll("use").remove();
// capitals
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalIcons = burgIcons.select("#cities");
const capitalLabels = burgLabels.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
const capitalAnchors = anchors.selectAll("#cities");
const caSize = capitalAnchors.attr("size") || 2;
capitalIcons
.selectAll("circle")
.data(capitals)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", capitalSize);
capitalLabels
.selectAll("text")
.data(capitals)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${capitalSize * -1.5}px`)
.text(d => d.name);
capitalAnchors
.selectAll("use")
.data(capitals.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - caSize * 0.47, 2))
.attr("y", d => rn(d.y - caSize * 0.47, 2))
.attr("width", caSize)
.attr("height", caSize);
// towns
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townIcons = burgIcons.select("#towns");
const townLabels = burgLabels.select("#towns");
const townSize = townIcons.attr("size") || 0.5;
const townsAnchors = anchors.selectAll("#towns");
const taSize = townsAnchors.attr("size") || 1;
townIcons
.selectAll("circle")
.data(towns)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", townSize);
townLabels
.selectAll("text")
.data(towns)
.enter()
.append("text")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${townSize * -1.5}px`)
.text(d => d.name);
townsAnchors
.selectAll("use")
.data(towns.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - taSize * 0.47, 2))
.attr("y", d => rn(d.y - taSize * 0.47, 2))
.attr("width", taSize)
.attr("height", taSize);
TIME && console.timeEnd("drawBurgs");
};
// expand cultures across the map (Dijkstra-like algorithm)
const expandStates = () => {
TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack;
cells.state = cells.state || new Uint16Array(cells.i.length);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const queue = new FlatQueue();
const cost = [];
const globalNeutralRate = byId("neutralInput")?.valueAsNumber || 1;
const statesNeutralRate = byId("statesNeutral")?.valueAsNumber || 1;
const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1;
const growthRate = (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth
// remove state from all cells except of locked
for (const cellId of cells.i) {
@ -396,12 +308,13 @@ window.BurgsAndStates = (() => {
cells.state[capitalCell] = state.i;
const cultureCenter = cultures[state.culture].center;
const b = cells.biome[cultureCenter]; // state native biome
queue.queue({e: state.center, p: 0, s: state.i, b});
queue.push({e: state.center, p: 0, s: state.i, b}, 0);
cost[state.center] = 1;
}
while (queue.length) {
const next = queue.dequeue();
const next = queue.pop();
const {e, p, s, b} = next;
const {type, culture} = states[s];
@ -419,12 +332,12 @@ window.BurgsAndStates = (() => {
const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
const totalCost = p + 10 + cellCost / states[s].expansionism;
if (totalCost > neutral) return;
if (totalCost > growthRate) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, s, b});
queue.push({e, p: totalCost, s, b}, totalCost);
}
});
}
@ -468,8 +381,7 @@ window.BurgsAndStates = (() => {
const normalizeStates = () => {
TIME && console.time("normalizeStates");
const cells = pack.cells,
burgs = pack.burgs;
const {cells, burgs} = pack;
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
@ -486,26 +398,30 @@ window.BurgsAndStates = (() => {
TIME && console.timeEnd("normalizeStates");
};
// Resets the cultures of all burgs and states to their
// cell or center cell's (respectively) culture.
// calculate pole of inaccessibility for each state
const getPoles = () => {
const getType = cellId => pack.cells.state[cellId];
const poles = getPolesOfInaccessibility(pack, getType);
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.pole = poles[s.i] || [0, 0];
});
};
// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
const updateCultures = () => {
TIME && console.time("updateCulturesForBurgsAndStates");
// Assign the culture associated with the burgs cell.
// Assign the culture associated with the burgs cell
pack.burgs = pack.burgs.map((burg, index) => {
// Ignore metadata burg
if (index === 0) {
return burg;
}
if (index === 0) return burg;
return {...burg, culture: pack.cells.culture[burg.cell]};
});
// Assign the culture associated with the states' center cell.
// Assign the culture associated with the states' center cell
pack.states = pack.states.map((state, index) => {
// Ignore neutrals state
if (index === 0) {
return state;
}
if (index === 0) return state;
return {...state, culture: pack.cells.culture[state.center]};
});
@ -611,8 +527,7 @@ window.BurgsAndStates = (() => {
// generate Diplomatic Relationships
const generateDiplomacy = () => {
TIME && console.time("generateDiplomacy");
const cells = pack.cells,
states = pack.states;
const {cells, states} = pack;
const chronicle = (states[0].diplomacy = []);
const valid = states.filter(s => s.i && !states.removed);
@ -696,21 +611,23 @@ window.BurgsAndStates = (() => {
const defender = ra(
ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d)
);
let ap = states[attacker].area * states[attacker].expansionism,
dp = states[defender].area * states[defender].expansionism;
let ap = states[attacker].area * states[attacker].expansionism;
let dp = states[defender].area * states[defender].expansionism;
if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
const an = states[attacker].name,
dn = states[defender].name; // names
const attackers = [attacker],
defenders = [defender]; // attackers and defenders array
const an = states[attacker].name;
const dn = states[defender].name; // names
const attackers = [attacker];
const defenders = [defender]; // attackers and defenders array
const dd = states[defender].diplomacy; // defender relations;
// start a war
const war = [`${an}-${trimVowels(dn)}ian War`, `${an} declared a war on its rival ${dn}`];
const end = options.year;
const start = end - gauss(2, 2, 0, 5);
states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end});
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end});
// start an ongoing war
const name = `${an}-${trimVowels(dn)}ian War`;
const start = options.year - gauss(2, 3, 0, 10);
const war = [name, `${an} declared a war on its rival ${dn}`];
const campaign = {name, start, attacker, defender};
states[attacker].campaigns.push(campaign);
states[defender].campaigns.push(campaign);
// attacker vassals join the war
ad.forEach((r, d) => {
@ -790,7 +707,6 @@ window.BurgsAndStates = (() => {
}
TIME && console.timeEnd("generateDiplomacy");
//console.table(states.map(s => s.diplomacy));
};
// select a forms for listed or all valid states
@ -949,254 +865,12 @@ window.BurgsAndStates = (() => {
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
};
const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => {
TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack;
const provinces = [0];
const provinceIds = new Uint16Array(cells.i.length);
const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
if (regenerate) {
pack.provinces.forEach(province => {
if (!province.i || province.removed || !isProvinceLocked(province)) return;
const newId = provinces.length;
for (const i of cells.i) {
if (cells.province[i] === province.i) provinceIds[i] = newId;
}
province.i = newId;
provinces.push(province);
});
}
const percentage = +provincesInput.value;
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
const forms = {
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
};
// generate provinces for selected burgs
states.forEach(s => {
s.provinces = [];
if (!s.i || s.removed) return;
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
const stateBurgs = burgs
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
.sort((a, b) => b.capital - a.capital);
if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * percentage) / 100), 2);
const form = Object.assign({}, forms[s.form]);
for (let i = 0; i < provincesNumber; i++) {
const provinceId = provinces.length;
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const nameByBurg = P(0.5);
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form);
form[formName] += 10;
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i);
s.provinces.push(provinceId);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
}
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
provinces.forEach(p => {
if (!p.i || p.removed || isProvinceLocked(p)) return;
provinceIds[p.center] = p.i;
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
cost[p.center] = 1;
});
while (queue.length) {
const {e, p, province, state} = queue.dequeue();
cells.c[e].forEach(e => {
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
const totalCost = p + evevation;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land) provinceIds[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, province, state});
}
});
}
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
const neibs = cells.c[i]
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
.map(c => provinceIds[c]);
const adversaries = neibs.filter(c => c !== provinceIds[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === provinceIds[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors);
if (buddies >= max) continue;
provinceIds[i] = adversaries[competitors.indexOf(max)];
}
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
states.forEach(s => {
if (!s.i || s.removed) return;
if (s.lock && !regenerateInLockedStates) return;
if (!s.provinces.length) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
const getColonyName = () => {
if (colonyNamePool.length < 1) return null;
const index = rand(colonyNamePool.length - 1);
const spliced = colonyNamePool.splice(index, 1);
return spliced[0] ? `New ${spliced[0]}` : null;
};
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
while (stateNoProvince.length) {
// add new province
const provinceId = provinces.length;
const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
provinceIds[center] = provinceId;
// expand province
const cost = [];
cost[center] = 1;
queue.queue({e: center, p: 0});
while (queue.length) {
const {e, p} = queue.dequeue();
cells.c[e].forEach(nextCellId => {
if (provinceIds[nextCellId]) return;
const land = cells.h[nextCellId] >= 20;
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
const totalCost = p + ter;
if (totalCost > max) return;
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
cost[nextCellId] = totalCost;
queue.queue({e: nextCellId, p: totalCost});
}
});
}
// generate "wild" province name
const c = cells.culture[center];
const f = pack.features[cells.f[center]];
const color = getMixedColor(s.color);
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const name = (() => {
const colonyName = colony && P(0.8) && getColonyName();
if (colonyName) return colonyName;
if (burgCell && P(0.5)) return burgs[burg].name;
return Names.getState(Names.getCultureShort(c), c);
})();
const formName = (() => {
if (singleIsle) return "Island";
if (isleGroup) return "Islands";
if (colony) return "Colony";
return rw(forms["Wild"]);
})();
const fullName = name + " " + formName;
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
const kinship = dominion ? 0 : 0.4;
const type = getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(provinceId);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from],
used = new Uint8Array(cells.i.length),
state = cells.state[from];
while (queue.length) {
const current = queue.pop();
if (current === to) return true; // way is found
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
});
}
return false; // way is not found
}
// re-check
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
}
});
cells.province = provinceIds;
pack.provinces = provinces;
TIME && console.timeEnd("generateProvinces");
};
return {
generate,
expandStates,
normalizeStates,
getPoles,
assignColors,
drawBurgs,
specifyBurgs,
defineBurgFeatures,
getType,
@ -1206,7 +880,7 @@ window.BurgsAndStates = (() => {
generateDiplomacy,
defineStateForms,
getFullName,
generateProvinces,
updateCultures
updateCultures,
getCloseToEdgePoint
};
})();

View file

@ -8,7 +8,10 @@ window.Cultures = (function () {
cells = pack.cells;
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
const culturesInputNumber = +byId("culturesInput").value;
const culturesInSetNumber = +byId("culturesSet").selectedOptions[0].dataset.max;
let count = Math.min(culturesInputNumber, culturesInSetNumber);
const populated = cells.i.filter(i => cells.s[i]); // populated cells
if (populated.length < count * 25) {
@ -120,26 +123,26 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(culturesNumber) {
let def = getDefault(culturesNumber);
let defaultCultures = getDefault(culturesNumber);
const cultures = [];
pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture);
if (culture.lock && !culture.removed) cultures.push(culture);
});
if (!cultures.length) {
if (culturesNumber === def.length) return def;
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
if (culturesNumber === defaultCultures.length) return defaultCultures;
if (defaultCultures.every(d => d.odd === 1)) return defaultCultures.splice(0, culturesNumber);
}
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0; ) {
for (let culture, rnd, i = 0; cultures.length < culturesNumber && defaultCultures.length > 0; ) {
do {
rnd = rand(def.length - 1);
culture = def[rnd];
rnd = rand(defaultCultures.length - 1);
culture = defaultCultures[rnd];
i++;
} while (i < 200 && !P(culture.odd));
cultures.push(culture);
def.splice(rnd, 1);
defaultCultures.splice(rnd, 1);
}
return cultures;
}
@ -169,7 +172,7 @@ window.Cultures = (function () {
else if (type === "Nomadic") base = 1.5;
else if (type === "Hunting") base = 0.7;
else if (type === "Highland") base = 1.2;
return rn(((Math.random() * powerInput.value) / 2 + 1) * base, 1);
return rn(((Math.random() * byId("sizeVariety").value) / 2 + 1) * base, 1);
}
TIME && console.timeEnd("generateCultures");
@ -515,7 +518,7 @@ window.Cultures = (function () {
TIME && console.time("expandCultures");
const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority});
const queue = new FlatQueue();
const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
@ -535,11 +538,11 @@ window.Cultures = (function () {
for (const culture of cultures) {
if (!culture.i || culture.removed || culture.lock) continue;
queue.queue({cellId: culture.center, cultureId: culture.i, priority: 0});
queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0);
}
while (queue.length) {
const {cellId, priority, cultureId} = queue.dequeue();
const {cellId, priority, cultureId} = queue.pop();
const {type, expansionism} = cultures[cultureId];
cells.c[cellId].forEach(neibCellId => {
@ -563,7 +566,7 @@ window.Cultures = (function () {
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
cost[neibCellId] = totalCost;
queue.queue({cellId: neibCellId, cultureId, priority: totalCost});
queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
}
});
}

View file

@ -1,8 +1,10 @@
"use strict";
// update old map file to the current version
export function resolveVersionConflicts(version) {
if (version < 1) {
export function resolveVersionConflicts(mapVersion) {
const isOlderThan = tagVersion => compareVersions(mapVersion, tagVersion).isOlder;
if (isOlderThan("1.0.0")) {
// v1.0 added a new religions layer
relig = viewbox.insert("g", "#terrain").attr("id", "relig");
Religions.generate();
@ -49,9 +51,8 @@ export function resolveVersionConflicts(version) {
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
Provinces.generate();
Provinces.getPoles();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
@ -63,7 +64,7 @@ export function resolveVersionConflicts(version) {
.attr("stroke-width", 0)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", "butt");
addZones();
Zones.generate();
if (!markers.selectAll("*").size()) {
Markers.generate();
turnButtonOn("toggleMarkers");
@ -107,11 +108,11 @@ export function resolveVersionConflicts(version) {
biomesData.habitability.push(12);
}
if (version < 1.1) {
// v1.0 initial code had a bug with religion layer id
if (isOlderThan("1.1.0")) {
// v1.0 code had a bug with religion layer id
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig");
// v1.0 initially has Sympathy status then relaced with Friendly
// v1.0 had Sympathy status then relaced with Friendly
for (const s of pack.states) {
if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
@ -200,10 +201,12 @@ export function resolveVersionConflicts(version) {
defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove();
lakes.selectAll("path").remove();
drawCoastline();
Features.markupPack();
createDefaultRuler();
}
if (version < 1.11) {
if (isOlderThan("1.11.0")) {
// v1.11 added new attributes
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
svg.select("#oceanic > *").attr("id", "oceanicPattern");
@ -229,7 +232,7 @@ export function resolveVersionConflicts(version) {
if (!terrain.attr("density")) terrain.attr("density", 0.4);
}
if (version < 1.21) {
if (isOlderThan("1.21.0")) {
// v1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function () {
if (this.hasAttribute("display")) {
@ -255,12 +258,12 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.22) {
if (isOlderThan("1.22.0")) {
// v1.22 changed state neighbors from Set object to array
BurgsAndStates.collectStatistics();
}
if (version < 1.3) {
if (isOlderThan("1.3.0")) {
// v1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000);
@ -285,7 +288,7 @@ export function resolveVersionConflicts(version) {
Military.generate();
}
if (version < 1.4) {
if (isOlderThan("1.4.0")) {
// v1.35 added dry lakes
if (!lakes.select("#dry").size()) {
lakes.append("g").attr("id", "dry");
@ -329,7 +332,7 @@ export function resolveVersionConflicts(version) {
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
}
if (version < 1.5) {
if (isOlderThan("1.5.0")) {
// not need to store default styles from v 1.5
localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom");
@ -367,7 +370,7 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.6) {
if (isOlderThan("1.6.0")) {
// v1.6 changed rivers data
for (const river of pack.rivers) {
const el = document.getElementById("river" + river.i);
@ -399,7 +402,7 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.61) {
if (isOlderThan("1.61.0")) {
// v1.61 changed rulers data
ruler.style("display", null);
rulers = new Rulers();
@ -453,12 +456,12 @@ export function resolveVersionConflicts(version) {
pattern.innerHTML = /* html */ `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
}
if (version < 1.62) {
if (isOlderThan("1.62.0")) {
// v1.62 changed grid data
gridOverlay.attr("size", null);
}
if (version < 1.63) {
if (isOlderThan("1.63.0")) {
// v1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById("oceanPattern");
if (oceanPattern) oceanPattern.removeAttribute("opacity");
@ -472,7 +475,7 @@ export function resolveVersionConflicts(version) {
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
}
if (version < 1.64) {
if (isOlderThan("1.64.0")) {
// v1.64 change states style
const opacity = regions.attr("opacity");
const filter = regions.attr("filter");
@ -481,7 +484,7 @@ export function resolveVersionConflicts(version) {
regions.attr("opacity", null).attr("filter", null);
}
if (version < 1.65) {
if (isOlderThan("1.65.0")) {
// v1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
@ -523,13 +526,13 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.652) {
if (isOlderThan("1.652.0")) {
// remove style to unhide layers
rivers.attr("style", null);
borders.attr("style", null);
}
if (version < 1.7) {
if (isOlderThan("1.7.0")) {
// v1.7 changed markers data
const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("markers");
@ -587,7 +590,7 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.72) {
if (isOlderThan("1.72.0")) {
// v1.72 renamed custom style presets
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style"));
storedStyles.forEach(styleName => {
@ -598,7 +601,7 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.73) {
if (isOlderThan("1.73.0")) {
// v1.73 moved the hatching patterns out of the user's SVG
document.getElementById("hatching")?.remove();
@ -609,17 +612,17 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.84) {
if (isOlderThan("1.84.0")) {
// v1.84.0 added grid.cellsDesired to stored data
if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
}
if (version < 1.85) {
if (isOlderThan("1.85.0")) {
// v1.84.0 moved intial screen out of maon svg
svg.select("#initial").remove();
}
if (version < 1.86) {
if (isOlderThan("1.86.0")) {
// v1.86.0 added multi-origin culture and religion hierarchy trees
for (const culture of pack.cultures) {
culture.origins = [culture.origin];
@ -632,14 +635,14 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.88) {
if (isOlderThan("1.88.0")) {
// v1.87 may have incorrect shield for some reason
pack.states.forEach(({coa}) => {
if (coa?.shield === "state") delete coa.shield;
});
}
if (version < 1.91) {
if (isOlderThan("1.91.0")) {
// from 1.91.00 custom coa is moved to coa object
pack.states.forEach(state => {
if (state.coa === "custom") state.coa = {custom: true};
@ -688,14 +691,14 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.92) {
if (isOlderThan("1.92.0")) {
// v1.92 change labels text-anchor from 'start' to 'middle'
labels.selectAll("tspan").each(function () {
this.setAttribute("x", 0);
});
}
if (version < 1.94) {
if (isOlderThan("1.94.0")) {
// from v1.94.00 texture image is removed when layer is off
texture.style("display", null);
@ -713,7 +716,7 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.95) {
if (isOlderThan("1.95.0")) {
// v1.95.00 added vignette visual layer
const mask = defs.append("mask").attr("id", "vignette-mask");
mask.append("rect").attr("fill", "white").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
@ -739,7 +742,7 @@ export function resolveVersionConflicts(version) {
vignette.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
}
if (version < 1.96) {
if (isOlderThan("1.96.0")) {
// v1.96 added ocean rendering for heightmap
terrs.selectAll("*").remove();
@ -833,7 +836,7 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.97) {
if (isOlderThan("1.97.0")) {
// v1.97.00 changed MFCG link to an arbitrary preview URL
options.villageMaxPopulation = 2000;
options.showBurgPreview = options.showMFCGMap;
@ -849,7 +852,7 @@ export function resolveVersionConflicts(version) {
});
}
if (version < 1.98) {
if (isOlderThan("1.98.0")) {
// v1.98.00 changed compass layer and rose element id
const rose = compass.select("use");
rose.attr("xlink:href", "#defs-compass-rose");
@ -861,7 +864,7 @@ export function resolveVersionConflicts(version) {
}
}
if (version < 1.99) {
if (isOlderThan("1.99.0")) {
// v1.99.00 changed routes generation algorithm and data format
routes.attr("display", null).attr("style", null);
@ -923,4 +926,72 @@ export function resolveVersionConflicts(version) {
}
}
}
if (isOlderThan("1.100.0")) {
// v1.100.00 added zones to pack data
pack.zones = [];
zones.selectAll("g").each(function () {
const i = pack.zones.length;
const name = this.dataset.description;
const type = this.dataset.type;
const color = this.getAttribute("fill");
const cells = this.dataset.cells.split(",").map(Number);
pack.zones.push({i, name, type, cells, color});
});
zones.style("display", null).selectAll("*").remove();
if (layerIsOn("toggleZones")) drawZones();
}
if (isOlderThan("1.104.0")) {
// v1.104.00 separated pole of inaccessibility detection from layer rendering
BurgsAndStates.getPoles();
Provinces.getPoles();
}
if (isOlderThan("1.105.0")) {
// v1.104.0 introduced some bugs with layers visibility
viewbox.select("#icons").style("display", null);
viewbox.select("#ice").style("display", null);
viewbox.select("#regions").style("display", null);
viewbox.select("#armies").style("display", null);
}
if (isOlderThan("1.106.0")) {
// v1.104.0 introduced bugs with coastlines. Redraw features
defs.select("#featurePaths").remove();
defs.append("g").attr("id", "featurePaths");
defs.select("#land").selectAll("path, use").remove();
defs.select("#water").selectAll("path, use").remove();
viewbox.select("#coastline").selectAll("path, use").remove();
// v1.104.0 introduced bugs with state borders
regions
.attr("opacity", null)
.attr("stroke-width", null)
.attr("letter-spacing", null)
.attr("fill", null)
.attr("stroke", null);
// pole can be missing for some states/provinces
BurgsAndStates.getPoles();
Provinces.getPoles();
}
if (isOlderThan("1.107.0")) {
// v1.107.0 allowed custom images for markers and regiments
if (layerIsOn("toggleMarkers")) drawMarkers();
if (layerIsOn("toggleMilitary")) drawMilitary();
}
if (isOlderThan("1.108.0")) {
// v1.108.0 changed features rendering method
pack.features.forEach(f => {
// fix lakes with missing group
if (f?.type === "lake" && !f.group) f.group = "freshwater";
});
drawFeatures();
// some old maps has incorrect "heights" groups
viewbox.selectAll("#heights").remove();
}
}

View file

@ -51,24 +51,9 @@ function insertEditorHtml() {
<button id="culturesHeirarchy" data-tip="Show cultures hierarchy tree" class="icon-sitemap"></button>
<button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button>
<div id="culturesManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic">Brush size:
<input
id="culturesManuallyBrush"
oninput="tip('Brush size: '+this.value); culturesManuallyBrushNumber.value = this.value"
type="range"
min="5"
max="99"
value="15"
style="width: 7em"
/>
<input
id="culturesManuallyBrushNumber"
oninput="tip('Brush size: '+this.value); culturesManuallyBrush.value = this.value"
type="number"
min="5"
max="99"
value="15"
/> </label><br />
<div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
<slider-input id="culturesBrush" min="1" max="100" value="15">Brush size:</slider-input>
</div>
<button id="culturesManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="culturesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div>
@ -281,6 +266,7 @@ function getTypeOptions(type) {
function getBaseOptions(base) {
let options = "";
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`));
if (!nameBases[base]) options += `<option selected value="${base}">removed</option>`; // in case namesbase was removed
return options;
}
@ -359,10 +345,13 @@ function cultureChangeName() {
}
function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture);
const cultureId = +this.parentNode.dataset.id;
const base = pack.cultures[cultureId].base;
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
const name = Names.getCultureShort(cultureId);
this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name;
pack.cultures[cultureId].name = name;
}
function cultureChangeExpansionism() {
@ -500,6 +489,7 @@ function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture)
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshCulturesEditor();
}
@ -507,12 +497,15 @@ function cultureRegenerateBurgs() {
if (customization === 4) return;
const cultureId = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock);
cBurgs.forEach(b => {
const base = pack.cultures[cultureId].base;
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
const cultureBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.removed && !b.lock);
cultureBurgs.forEach(b => {
b.name = Names.getCulture(cultureId);
labels.select("[data-id='" + b.i + "']").text(b.name);
});
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
tip(`Names for ${cultureBurgs.length} burgs are regenerated`, false, "success");
}
function removeCulture(cultureId) {
@ -718,7 +711,7 @@ function selectCultureOnMapClick() {
}
function dragCultureBrush() {
const radius = +culturesManuallyBrush.value;
const radius = +culturesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
@ -759,7 +752,7 @@ function changeCultureForSelection(selection) {
function moveCultureBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +culturesManuallyBrush.value;
const radius = +culturesBrush.value;
moveCircle(point[0], point[1], radius);
}
@ -862,14 +855,15 @@ async function uploadCulturesData() {
this.value = "";
const csv = await file.text();
const data = d3.csvParse(csv, d => ({
i: +d.Id,
name: d.Name,
i: +d.Id,
color: d.Color,
expansionism: +d.Expansionism,
type: d.Type,
population: +d.Population,
emblemsShape: d["Emblems Shape"],
origins: d.Origins
origins: d.Origins,
namesbase: d.Namesbase
}));
const {cultures, cells} = pack;
@ -896,7 +890,7 @@ async function uploadCulturesData() {
culture.i
);
} else {
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origins: [0], rural: 0, urban: 0};
cultures.push(current);
}
@ -916,6 +910,10 @@ async function uploadCulturesData() {
else current.type = "Generic";
}
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
current.base = nameBases.findIndex(n => n.name == culture.namesbase); // can be -1 if namesbase is not found
function restoreOrigins(originsString) {
const originNames = originsString
.replaceAll('"', "")
@ -931,12 +929,6 @@ async function uploadCulturesData() {
current.origins = originIds.filter(id => id !== null);
if (!current.origins.length) current.origins = [0];
}
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
const nameBaseIndex = nameBases.findIndex(n => n.name == culture.namesbase);
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
}
cultures.filter(c => c.removed).forEach(c => removeCulture(c.i));

View file

@ -26,7 +26,7 @@ function insertEditorHtml() {
<div id="religionsHeader" class="header" style="grid-template-columns: 13em 6em 7em 18em 6em 7em 6em 7em">
<div data-tip="Click to sort by religion name" class="sortable alphabetically" data-sortby="name">Religion&nbsp;</div>
<div data-tip="Click to sort by religion type" class="sortable alphabetically icon-sort-name-down" data-sortby="type">Type&nbsp;</div>
<div data-tip="Click to sort by religion form" class="sortable alphabetically hide" data-sortby="form">Form&nbsp;</div>
<div data-tip="Click to sort by religion form" class="sortable alphabetically" data-sortby="form">Form&nbsp;</div>
<div data-tip="Click to sort by supreme deity" class="sortable alphabetically hide" data-sortby="deity">Supreme Deity&nbsp;</div>
<div data-tip="Click to sort by religion area" class="sortable hide" data-sortby="area">Area&nbsp;</div>
<div data-tip="Click to sort by number of believers (religion area population)" class="sortable hide" data-sortby="population">Believers&nbsp;</div>
@ -66,25 +66,9 @@ function insertEditorHtml() {
<button id="religionsManually" data-tip="Manually re-assign religions" class="icon-brush"></button>
<div id="religionsManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic">Brush size:
<input
id="religionsManuallyBrush"
oninput="tip('Brush size: '+this.value); religionsManuallyBrushNumber.value = this.value"
type="range"
min="5"
max="99"
value="15"
style="width: 7em"
/>
<input
id="religionsManuallyBrushNumber"
oninput="tip('Brush size: '+this.value); religionsManuallyBrush.value = this.value"
type="number"
min="5"
max="99"
value="15"
/> </label
><br />
<div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
<slider-input id="religionsBrush" min="1" max="100" value="15">Brush size:</slider-input>
</div>
<button id="religionsManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="religionsManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div>
@ -183,7 +167,7 @@ function religionsEditorAddLines() {
<select data-tip="Religion type" class="religionType placeholder" style="width: 5em">
${getTypeOptions(r.type)}
</select>
<input data-tip="Religion form" class="religionForm placeholder hide" style="width: 6em" value="" autocorrect="off" spellcheck="false" />
<input data-tip="Religion form" class="religionForm placeholder" style="width: 6em" value="" autocorrect="off" spellcheck="false" />
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw placeholder hide"></span>
<input data-tip="Religion supreme deity" class="religionDeity placeholder hide" style="width: 17em" value="" autocorrect="off" spellcheck="false" />
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
@ -215,7 +199,7 @@ function religionsEditorAddLines() {
<select data-tip="Religion type" class="religionType" style="width: 5em">
${getTypeOptions(r.type)}
</select>
<input data-tip="Religion form" class="religionForm hide" style="width: 6em"
<input data-tip="Religion form" class="religionForm" style="width: 6em"
value="${r.form}" autocorrect="off" spellcheck="false" />
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw hide"></span>
<input data-tip="Religion supreme deity" class="religionDeity hide" style="width: 17em"
@ -478,6 +462,7 @@ function changePopulation() {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshReligionsEditor();
}
}
@ -696,7 +681,7 @@ function selectReligionOnMapClick() {
}
function dragReligionBrush() {
const radius = +byId("religionsManuallyBrushNumber").value;
const radius = +byId("religionsBrush").value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
@ -736,7 +721,7 @@ function changeReligionForSelection(selection) {
function moveReligionBrush() {
showMainTip();
const [x, y] = d3.mouse(this);
const radius = +byId("religionsManuallyBrushNumber").value;
const radius = +byId("religionsBrush").value;
moveCircle(x, y, radius);
}

View file

@ -24,7 +24,7 @@ function insertEditorHtml() {
<div id="statesHeader" class="header" style="grid-template-columns: 11em 8em 7em 7em 6em 6em 8em 6em 7em 6em">
<div data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State&nbsp;</div>
<div data-tip="Click to sort by state form name" class="sortable alphabetically" data-sortby="form">Form&nbsp;</div>
<div data-tip="Click to sort by capital name" class="sortable alphabetically hide" data-sortby="capital">Capital&nbsp;</div>
<div data-tip="Click to sort by capital name" class="sortable alphabetically" data-sortby="capital">Capital&nbsp;</div>
<div data-tip="Click to sort by state dominant culture" class="sortable alphabetically hide" data-sortby="culture">Culture&nbsp;</div>
<div data-tip="Click to sort by state burgs count" class="sortable hide" data-sortby="burgs">Burgs&nbsp;</div>
<div data-tip="Click to sort by state area" class="sortable hide icon-sort-number-down" data-sortby="area">Area&nbsp;</div>
@ -55,60 +55,25 @@ function insertEditorHtml() {
<div id="statesRegenerateButtons" style="display: none">
<button id="statesRegenerateBack" data-tip="Hide the regeneration menu" class="icon-cog-alt"></button>
<button id="statesRandomize" data-tip="Randomize states Expansion value and re-calculate states and provinces" class="icon-shuffle"></button>
<span data-tip="Additional growth rate. Defines how many lands will stay neutral">
<label class="italic">Growth rate:</label>
<input
id="statesNeutral"
type="range"
min=".1"
max="3"
step=".05"
value="1"
style="width: 12em"
/>
<input
id="statesNeutralNumber"
type="number"
min=".1"
max="3"
step=".05"
value="1"
style="width: 4em"
/>
</span>
<div data-tip="Additional growth rate. Defines how many land cells remain neutral" style="display: inline-block">
<slider-input id="statesGrowthRate" min=".1" max="3" step=".05" value="1">Growth rate:</slider-input>
</div>
<button id="statesRecalculate" data-tip="Recalculate states based on current values of growth-related attributes" class="icon-retweet"></button>
<span data-tip="Allow states neutral distance, expansion and type changes to take an immediate effect">
<div data-tip="Allow states neutral distance, expansion and type changes to take an immediate effect" style="display: inline-block">
<input id="statesAutoChange" class="checkbox" type="checkbox" />
<label for="statesAutoChange" class="checkbox-label"><i>auto-apply changes</i></label>
</span>
<span data-tip="Allow system to change state labels when states data is change">
</div>
<div data-tip="Allow system to change state labels when states data is change" style="display: inline-block">
<input id="adjustLabels" class="checkbox" type="checkbox" />
<label for="adjustLabels" class="checkbox-label"><i>auto-change labels</i></label>
</span>
</div>
</div>
<button id="statesManually" data-tip="Manually re-assign states" class="icon-brush"></button>
<div id="statesManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic"
>Brush size:
<input
id="statesManuallyBrush"
oninput="tip('Brush size: '+this.value); statesManuallyBrushNumber.value = this.value"
type="range"
min="5"
max="99"
value="15"
style="width: 5em"
/>
<input
id="statesManuallyBrushNumber"
oninput="tip('Brush size: '+this.value); statesManuallyBrush.value = this.value"
type="number"
min="5"
max="99"
value="15"
/> </label
><br />
<div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
<slider-input id="statesBrush" min="1" max="100" value="15">Brush size:</slider-input>
</div>
<button id="statesManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="statesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div>
@ -135,8 +100,7 @@ function addListeners() {
byId("statesRegenerateBack").on("click", exitRegenerationMenu);
byId("statesRecalculate").on("click", () => recalculateStates(true));
byId("statesRandomize").on("click", randomizeStatesExpansion);
byId("statesNeutral").on("input", changeStatesGrowthRate);
byId("statesNeutralNumber").on("change", changeStatesGrowthRate);
byId("statesGrowthRate").on("input", () => recalculateStates(false));
byId("statesManually").on("click", enterStatesManualAssignent);
byId("statesManuallyApply").on("click", applyStatesManualAssignent);
byId("statesManuallyCancel").on("click", () => exitStatesManualAssignment(false));
@ -228,10 +192,10 @@ function statesEditorAddLines() {
<input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${
s.name
}" readonly />
<svg class="coaIcon placeholder hide"></svg>
<svg class="coaIcon placeholder"></svg>
<input class="stateForm placeholder" value="none" />
<span class="icon-star-empty placeholder hide"></span>
<input class="stateCapital placeholder hide" />
<span class="icon-star-empty placeholder"></span>
<input class="stateCapital placeholder" />
<select class="stateCulture placeholder hide">${getCultureOptions(0)}</select>
<span data-tip="Click to overview neutral burgs" class="icon-dot-circled pointer hide" style="padding-right: 1px"></span>
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
@ -267,14 +231,14 @@ function statesEditorAddLines() {
>
<fill-box fill="${s.color}"></fill-box>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly />
<svg data-tip="Click to show and edit state emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#stateCOA${
<svg data-tip="Click to show and edit state emblem" class="coaIcon pointer" viewBox="0 0 200 200"><use href="#stateCOA${
s.i
}"></use></svg>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${
s.formName
}" readonly />
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false" />
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false" />
<select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(
s.culture
)}</select>
@ -579,6 +543,7 @@ function changePopulation(stateId) {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshStatesEditor();
}
}
@ -678,11 +643,11 @@ function stateRemove(stateId) {
pack.states[stateId] = {i: stateId, removed: true};
debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor();
}
@ -728,7 +693,7 @@ function showStatesChart() {
.sum(d => d.area)
.sort((a, b) => b.value - a.value);
const size = 150 + 200 * uiSizeOutput.value;
const size = 150 + 200 * uiSize.value;
const margin = {top: 0, right: -50, bottom: 0, left: -50};
const w = size - margin.left - margin.right;
const h = size - margin.top - margin.bottom;
@ -778,6 +743,7 @@ function showStatesChart() {
node
.append("text")
.attr("text-rendering", "optimizeSpeed")
.style("font-size", d => rn((d.r ** 0.97 * 4) / lp(d.data.name), 2) + "px")
.selectAll("tspan")
.data(d => d.data.name.split(exp))
@ -875,22 +841,16 @@ function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates();
BurgsAndStates.generateProvinces();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
Provinces.generate();
Provinces.getPoles();
BurgsAndStates.getPoles();
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) drawStateLabels();
refreshStatesEditor();
}
function changeStatesGrowthRate() {
const growthRate = +this.value;
byId("statesNeutral").value = growthRate;
byId("statesNeutralNumber").value = growthRate;
tip("Growth rate: " + growthRate);
recalculateStates(false);
refreshStatesEditor();
}
function randomizeStatesExpansion() {
@ -959,7 +919,7 @@ function selectStateOnMapClick() {
}
function dragStateBrush() {
const r = +statesManuallyBrush.value;
const r = +statesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
@ -1002,7 +962,7 @@ function changeStateForSelection(selection) {
function moveStateBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +statesManuallyBrush.value;
const radius = +statesBrush.value;
moveCircle(point[0], point[1], radius);
}
@ -1025,6 +985,7 @@ function applyStatesManualAssignent() {
if (affectedStates.length) {
refreshStatesEditor();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
@ -1240,7 +1201,6 @@ function addState() {
const basename = center % 5 === 0 ? burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture);
const color = getRandomColor();
const pole = cells.p[center];
// generate emblem
const cultureType = pack.cultures[culture].type;
@ -1290,38 +1250,21 @@ function addState() {
culture,
military: [],
alert: 1,
coa,
pole
coa
});
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
adjustProvinces([cells.province[center]]);
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
// add label
defs
.select("#textPaths")
.append("path")
.attr("d", `M${pole[0] - 50},${pole[1] + 6}h${100}`)
.attr("id", "textPath_stateLabel" + newState);
labels
.select("#states")
.append("text")
.attr("id", "stateLabel" + newState)
.append("textPath")
.attr("xlink:href", "#textPath_stateLabel" + newState)
.attr("startOffset", "50%")
.attr("font-size", "50%")
.append("tspan")
.attr("x", name.length * -3)
.text(name);
drawStateLabels([newState]);
COArenderer.add("state", newState, coa, states[newState].pole[0], states[newState].pole[1]);
layerIsOn("toggleProvinces") && toggleProvinces();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
statesEditorAddLines();
}
@ -1459,6 +1402,7 @@ function openStateMergeDialog() {
unfog();
debug.selectAll(".highlight").remove();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
layerIsOn("toggleProvinces") && drawProvinces();

View file

@ -53,7 +53,8 @@ function getMinimalDataJson() {
religions: pack.religions,
rivers: pack.rivers,
markers: pack.markers,
routes: pack.routes
routes: pack.routes,
zones: pack.zones
};
return JSON.stringify({info, settings, mapCoordinates, pack: packData, biomesData, notes, nameBases});
}
@ -72,7 +73,7 @@ function getGridDataJson() {
function getMapInfo() {
return {
version,
version: VERSION,
description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator",
exportedAt: new Date().toISOString(),
mapName: mapName.value,
@ -172,7 +173,8 @@ function getPackCellsData() {
religions: pack.religions,
rivers: pack.rivers,
markers: pack.markers,
routes: pack.routes
routes: pack.routes,
zones: pack.zones
};
}

View file

@ -580,4 +580,13 @@ MisterPete
Johanna Martin
Marmalade_MacGuffin
James Benware
FortunesFaded`;
FortunesFaded
breadsticks
Murderbits
Ben Jones
Marco Faltracco
L
silentArtifact
Keith Potter
Morgan Gilbert
Alengork Gamer`;

271
modules/features.js Normal file
View file

@ -0,0 +1,271 @@
"use strict";
window.Features = (function () {
const DEEPER_LAND = 3;
const LANDLOCKED = 2;
const LAND_COAST = 1;
const UNMARKED = 0;
const WATER_COAST = -1;
const DEEP_WATER = -2;
// calculate distance to coast for every cell
function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
marked = 0;
const prevDistance = distance - increment;
for (let cellId = 0; cellId < neighbors.length; cellId++) {
if (distanceField[cellId] !== prevDistance) continue;
for (const neighborId of neighbors[cellId]) {
if (distanceField[neighborId] !== UNMARKED) continue;
distanceField[neighborId] = distance;
marked++;
}
}
}
}
// mark Grid features (ocean, lakes, islands) and calculate distance field
function markupGrid() {
TIME && console.time("markupGrid");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
const cellsNumber = i.length;
const distanceField = new Int8Array(cellsNumber); // gird.cells.t
const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
const features = [0];
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = heights[firstCell] >= 20;
let border = false; // set true if feature touches map edge
while (queue.length) {
const cellId = queue.pop();
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = heights[neighborId] >= 20;
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
featureIds[neighborId] = featureId;
queue.push(neighborId);
} else if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
}
}
}
const type = land ? "island" : border ? "ocean" : "lake";
features.push({i: featureId, land, border, type});
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
// markup deep ocean cells
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
grid.cells.t = distanceField;
grid.cells.f = featureIds;
grid.features = features;
TIME && console.timeEnd("markupGrid");
}
// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
function markupPack() {
TIME && console.time("markupPack");
const {cells, vertices} = pack;
const {c: neighbors, b: borderCells, i} = cells;
const packCellsNumber = i.length;
if (!packCellsNumber) return; // no cells -> there is nothing to do
const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
const features = [0];
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell);
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
let totalCells = 1; // count cells in a feature
while (queue.length) {
const cellId = queue.pop();
if (borderCells[cellId]) border = true;
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = isLand(neighborId);
if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
if (!haven[cellId]) defineHaven(cellId);
} else if (land && isNeibLand) {
if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
distanceField[neighborId] = LANDLOCKED;
else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
distanceField[cellId] = LANDLOCKED;
}
if (!featureIds[neighborId] && land === isNeibLand) {
queue.push(neighborId);
featureIds[neighborId] = featureId;
totalCells++;
}
}
}
features.push(addFeature({firstCell, land, border, featureId, totalCells}));
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
pack.cells.t = distanceField;
pack.cells.f = featureIds;
pack.cells.haven = haven;
pack.cells.harbor = harbor;
pack.features = features;
TIME && console.timeEnd("markupPack");
function defineHaven(cellId) {
const waterCells = neighbors[cellId].filter(isWater);
const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
haven[cellId] = waterCells[closest];
harbor[cellId] = waterCells.length;
}
function addFeature({firstCell, land, border, featureId, totalCells}) {
const type = land ? "island" : border ? "ocean" : "lake";
const [startCell, featureVertices] = getCellsData(type, firstCell);
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
const area = d3.polygonArea(points); // feature perimiter area
const absArea = Math.abs(rn(area));
const feature = {
i: featureId,
type,
land,
border,
cells: totalCells,
firstCell: startCell,
vertices: featureVertices,
area: absArea
};
if (type === "lake") {
if (area > 0) feature.vertices = feature.vertices.reverse();
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
feature.height = Lakes.getHeight(feature);
}
return feature;
function getCellsData(featureType, firstCell) {
if (featureType === "ocean") return [firstCell, []];
const getType = cellId => featureIds[cellId];
const type = getType(firstCell);
const ofSameType = cellId => getType(cellId) === type;
const ofDifferentType = cellId => getType(cellId) !== type;
const startCell = findOnBorderCell(firstCell);
const featureVertices = getFeatureVertices(startCell);
return [startCell, featureVertices];
function findOnBorderCell(firstCell) {
const isOnBorder = cellId => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
if (isOnBorder(firstCell)) return firstCell;
const startCell = cells.i.filter(ofSameType).find(isOnBorder);
if (startCell === undefined)
throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
return startCell;
}
function getFeatureVertices(startCell) {
const startingVertex = cells.v[startCell].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined)
throw new Error(`Markup: startingVertex for cell ${startCell} is not found`);
return connectVertices({vertices, startingVertex, ofSameType, closeRing: false});
}
}
}
}
// add properties to pack features
function specify() {
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
feature.group = defineGroup(feature);
if (feature.type === "lake") {
feature.height = Lakes.getHeight(feature);
feature.name = Lakes.getName(feature);
}
}
function defineGroup(feature) {
if (feature.type === "island") return defineIslandGroup(feature);
if (feature.type === "ocean") return defineOceanGroup();
if (feature.type === "lake") return defineLakeGroup(feature);
throw new Error(`Markup: unknown feature type ${feature.type}`);
}
function defineOceanGroup(feature) {
if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
if (feature.cells > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup(feature) {
const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
if (feature.cells > ISLAND_MIN_SIZE) return "island";
return "isle";
}
function defineLakeGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
return {markupGrid, markupPack, specify};
})();

View file

@ -69,6 +69,12 @@ const fonts = [
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Eagle Lake",
src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Faster One",
src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
@ -129,6 +135,12 @@ const fonts = [
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Lugrasimo",
src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Kaushan Script",
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",

View file

@ -60,7 +60,7 @@ window.Cloud = (function () {
async save(fileName, contents) {
const resp = await this.call("filesUpload", {path: "/" + fileName, contents});
DEBUG && console.info("Dropbox response:", resp);
DEBUG.cloud && console.info("Dropbox response:", resp);
return true;
},
@ -104,7 +104,7 @@ window.Cloud = (function () {
// Callback function for auth window
async setDropBoxToken(token) {
DEBUG && console.info("Access token:", token);
DEBUG.cloud && console.info("Access token:", token);
setToken(this.name, token);
await this.connect(token);
this.authWindow.close();
@ -131,7 +131,7 @@ window.Cloud = (function () {
allow_download: true
};
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings});
DEBUG && console.info("Dropbox link object:", resp.result);
DEBUG.cloud && console.info("Dropbox link object:", resp.result);
return resp.result.url;
}
};

View file

@ -175,6 +175,7 @@ async function getMapURL(type, options) {
noWater = false,
noScaleBar = false,
noIce = false,
noVignette = false,
fullMap = false
} = options || {};
@ -199,6 +200,7 @@ async function getMapURL(type, options) {
clone.select("#oceanPattern").attr("opacity", 0);
}
if (noIce) clone.select("#ice")?.remove();
if (noVignette) clone.select("#vignette")?.remove();
if (fullMap) {
// reset transform to show the whole map
clone.attr("width", graphWidth).attr("height", graphHeight);
@ -318,6 +320,40 @@ async function getMapURL(type, options) {
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
{
// replace external marker icons
const externalMarkerImages = cloneEl.querySelectorAll('#markers image[href]:not([href=""])');
const imageHrefs = Array.from(externalMarkerImages).map(img => img.getAttribute("href"));
for (const url of imageHrefs) {
await new Promise(resolve => {
getBase64(url, base64 => {
externalMarkerImages.forEach(img => {
if (img.getAttribute("href") === url) img.setAttribute("href", base64);
});
resolve();
});
});
}
}
{
// replace external regiment icons
const externalRegimentImages = cloneEl.querySelectorAll('#armies image[href]:not([href=""])');
const imageHrefs = Array.from(externalRegimentImages).map(img => img.getAttribute("href"));
for (const url of imageHrefs) {
await new Promise(resolve => {
getBase64(url, base64 => {
externalRegimentImages.forEach(img => {
if (img.getAttribute("href") === url) img.setAttribute("href", base64);
});
resolve();
});
});
}
}
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
@ -440,14 +476,24 @@ function inlineStyle(clone) {
emptyG.remove();
}
function saveGeoJSON_Cells() {
function saveGeoJsonCells() {
const {cells, vertices} = pack;
const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
const getPopulation = i => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
const getHeight = i => parseInt(getFriendlyHeight([...cells.p[i]]));
function getCellCoordinates(cellVertices) {
const coordinates = cellVertices.map(vertex => {
const [x, y] = vertices.p[vertex];
return getCoordinates(x, y, 4);
});
return [[...coordinates, coordinates[0]]];
}
cells.i.forEach(i => {
const coordinates = getCellCoordinates(cells.v[i]);
@ -470,20 +516,13 @@ function saveGeoJSON_Cells() {
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Routes() {
const {cells, burgs} = pack;
let points = cells.p.map(([x, y], cellId) => {
const burgId = cells.burg[cellId];
if (burgId) return [burgs[burgId].x, burgs[burgId].y];
return [x, y];
});
const features = pack.routes.map(route => {
const coordinates = route.points || getRoutePoints(route, points);
function saveGeoJsonRoutes() {
const features = pack.routes.map(({i, points, group, name = null}) => {
const coordinates = points.map(([x, y]) => getCoordinates(x, y, 4));
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
properties: {id: route.id, group: route.group}
properties: {id: i, group, name}
};
});
const json = {type: "FeatureCollection", features};
@ -492,30 +531,31 @@ function saveGeoJSON_Routes() {
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Rivers() {
const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function () {
const river = pack.rivers.find(r => r.i === +this.id.slice(5));
if (!river) return;
const coordinates = getRiverPoints(this);
const properties = {...river, id: this.id};
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties};
json.features.push(feature);
});
function saveGeoJsonRivers() {
const features = pack.rivers.map(
({i, cells, points, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}) => {
if (!cells || cells.length < 2) return;
const meanderedPoints = Rivers.addMeandering(cells, points);
const coordinates = meanderedPoints.map(([x, y]) => getCoordinates(x, y, 4));
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
properties: {id: i, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}
};
}
);
const json = {type: "FeatureCollection", features};
const fileName = getFileName("Rivers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJSON_Markers() {
function saveGeoJsonMarkers() {
const features = pack.markers.map(marker => {
const {i, type, icon, x, y, size, fill, stroke} = marker;
const coordinates = getCoordinates(x, y, 4);
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const properties = {id, type, icon, x, y, ...note, size, fill, stroke};
const note = notes.find(note => note.id === "marker" + i);
const properties = {id: i, type, icon, x, y, ...note, size, fill, stroke};
return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
});
@ -524,22 +564,3 @@ function saveGeoJSON_Markers() {
const fileName = getFileName("Markers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function getCellCoordinates(vertices) {
const p = pack.vertices.p;
const coordinates = vertices.map(n => getCoordinates(p[n][0], p[n][1], 2));
return [coordinates.concat([coordinates[0]])];
}
function getRiverPoints(node) {
let points = [];
const l = node.getTotalLength() / 2; // half-length
const increment = 0.25; // defines density of points
for (let i = l, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const [x, y] = getCoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, 4);
points.push([x, y]);
}
return points;
}

View file

@ -1,4 +1,5 @@
"use strict";
// Functions to load and parse .map/.gz files
async function quickLoad() {
const blob = await ldb.get("lastMap");
@ -12,7 +13,7 @@ async function quickLoad() {
async function loadFromDropbox() {
const mapPath = byId("loadFromDropboxSelect")?.value;
DEBUG && console.info("Loading map from Dropbox:", mapPath);
console.info("Loading map from Dropbox:", mapPath);
const blob = await Cloud.providers.dropbox.load(mapPath);
uploadMap(blob);
}
@ -95,6 +96,7 @@ function showUploadErrorMessage(error, URL, random) {
title: "Loading error",
width: "32em",
buttons: {
"Clear cache": () => cleanupData(),
OK: function () {
$(this).dialog("close");
}
@ -104,26 +106,28 @@ function showUploadErrorMessage(error, URL, random) {
function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
const fileReader = new FileReader();
fileReader.onloadend = async function (fileLoadedEvent) {
if (callback) callback();
byId("coas").innerHTML = ""; // remove auto-generated emblems
const result = fileLoadedEvent.target.result;
const [mapData, mapVersion] = await parseLoadedResult(result);
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = mapVersion === currentVersion;
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
const isNewer = mapVersion > currentVersion;
const isOutdated = mapVersion < currentVersion;
const {mapData, mapVersion} = await parseLoadedResult(result);
const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 10 || !mapData[5];
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
if (isUpdated) return parseLoadedData(mapData);
const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
if (isUpdated) return showUploadMessage("updated", mapData, mapVersion);
const isAncient = compareVersions(mapVersion, "0.70.0").isOlder;
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
};
@ -149,53 +153,65 @@ async function uncompress(compressedData) {
async function parseLoadedResult(result) {
try {
const resultAsString = new TextDecoder().decode(result);
// data can be in FMG internal format or base64 encoded
const isDelimited = resultAsString.substring(0, 10).includes("|");
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
let content = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
const mapData = decoded.split("\r\n");
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
return [mapData, mapVersion];
// fix if svg part has CRLF line endings instead of LF
const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
const svgContent = svgMatch[0];
const hasCrlfEndings = svgContent.includes("\r\n");
if (hasCrlfEndings) {
const correctedSvgContent = svgContent.replace(/\r\n/g, "\n");
content = content.replace(svgContent, correctedSvgContent);
}
const mapData = content.split("\r\n"); // split by CRLF
const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
return {mapData, mapVersion};
} catch (error) {
// map file can be compressed with gzip
const uncompressedData = await uncompress(result);
const uncompressedData = await uncompress(result); // file can be gzip compressed
if (uncompressedData) return parseLoadedResult(uncompressedData);
ERROR && console.error(error);
return [null, null];
return {mapData: null, mapVersion: null};
}
}
function showUploadMessage(type, mapData, mapVersion) {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
let message, title, canBeLoaded;
let message, title;
if (type === "invalid") {
message = `The file does not look like a valid save file.<br>Please check the data format`;
message = "The file does not look like a valid save file.<br>Please check the data format";
title = "Invalid file";
canBeLoaded = false;
} else if (type === "updated") {
parseLoadedData(mapData, mapVersion);
return;
} else if (type === "ancient") {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
title = "Ancient file";
canBeLoaded = false;
} else if (type === "newer") {
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
title = "Newer file";
canBeLoaded = false;
} else if (type === "outdated") {
INFO && console.info(`Loading map. Auto-update from ${mapVersion} to ${version}`);
INFO && console.info(`Loading map. Auto-updating from ${mapVersion} to ${VERSION}`);
parseLoadedData(mapData, mapVersion);
return;
}
alertMessage.innerHTML = message;
const buttons = {
OK: function () {
$(this).dialog("close");
if (canBeLoaded) parseLoadedData(mapData, mapVersion);
$("#alert").dialog({
title,
buttons: {
"Clear cache": () => cleanupData(),
OK: function () {
$(this).dialog("close");
}
}
};
$("#alert").dialog({title, buttons});
});
}
async function parseLoadedData(data, mapVersion) {
@ -205,31 +221,29 @@ async function parseLoadedData(data, mapVersion) {
customization = 0;
if (customizationMenu.offsetParent) styleTab.click();
const params = data[0].split("|");
void (function parseParameters() {
{
const params = data[0].split("|");
if (params[3]) {
seed = params[3];
optionsSeed.value = seed;
}
INFO && console.group("Loaded Map " + seed);
} else INFO && console.group("Loaded Map");
if (params[4]) graphWidth = +params[4];
if (params[5]) graphHeight = +params[5];
mapId = params[6] ? +params[6] : Date.now();
})();
}
INFO && console.group("Loaded Map " + seed);
// TODO: move all to options object
void (function parseSettings() {
{
const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScale = distanceScaleInput.value = distanceScaleOutput.value = settings[1];
if (settings[1]) distanceScale = distanceScaleInput.value = settings[1];
if (settings[2]) areaUnit.value = settings[2];
if (settings[3]) applyOption(heightUnit, settings[3]);
if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4];
if (settings[4]) heightExponentInput.value = settings[4];
if (settings[5]) temperatureScale.value = settings[5];
// setting 6-11 (scaleBar) are part of style now, kept as "" in newer versions for compatibility
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
if (settings[12]) populationRate = populationRateInput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
if (settings[18]) precInput.value = precOutput.value = settings[18];
@ -241,18 +255,19 @@ async function parseLoadedData(data, mapVersion) {
if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
if (settings[23]) rescaleLabels.checked = +settings[23];
if (settings[24]) urbanDensity = urbanDensityInput.value = urbanDensityOutput.value = +settings[24];
if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24];
if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100);
})();
if (settings[26]) growthRate.value = settings[26];
}
void (function applyOptionsToUI() {
{
stateLabelsModeInput.value = options.stateLabelsMode;
yearInput.value = options.year;
eraInput.value = options.era;
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
})();
}
void (function parseConfiguration() {
{
if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]);
@ -268,13 +283,14 @@ async function parseLoadedData(data, mapVersion) {
declareFont(usedFont);
});
}
}
{
const biomes = data[3].split("|");
biomesData = Biomes.getDefault();
biomesData.color = biomes[0].split(",");
biomesData.habitability = biomes[1].split(",").map(h => +h);
biomesData.name = biomes[2].split(",");
// push custom biomes if any
for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
biomesData.i.push(biomesData.i.length);
@ -282,14 +298,14 @@ async function parseLoadedData(data, mapVersion) {
biomesData.icons.push([]);
biomesData.cost.push(50);
}
})();
}
void (function replaceSVG() {
{
svg.remove();
document.body.insertAdjacentHTML("afterbegin", data[5]);
})();
}
void (function redefineElements() {
{
svg = d3.select("#map");
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox");
@ -339,38 +355,33 @@ async function parseLoadedData(data, mapVersion) {
fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug");
burgLabels = labels.select("#burgLabels");
})();
void (function addMissingElements() {
if (!texture.size()) {
texture = viewbox
.insert("g", "#landmass")
.attr("id", "texture")
.attr("data-href", "./images/textures/plaster.jpg");
}
if (!emblems.size()) {
emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none");
}
})();
}
void (function parseGridData() {
{
grid = JSON.parse(data[6]);
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
grid.cells = cells;
grid.vertices = vertices;
grid.cells.h = Uint8Array.from(data[7].split(","));
grid.cells.prec = Uint8Array.from(data[8].split(","));
grid.cells.f = Uint16Array.from(data[9].split(","));
grid.cells.t = Int8Array.from(data[10].split(","));
grid.cells.temp = Int8Array.from(data[11].split(","));
})();
}
void (function parsePackData() {
{
reGraph();
reMarkFeatures();
Features.markupPack();
pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]);
@ -380,22 +391,21 @@ async function parseLoadedData(data, mapVersion) {
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(","));
cells.burg = Uint16Array.from(data[17].split(","));
cells.conf = Uint8Array.from(data[18].split(","));
cells.culture = Uint16Array.from(data[19].split(","));
cells.fl = Uint16Array.from(data[20].split(","));
cells.pop = Float32Array.from(data[21].split(","));
cells.r = Uint16Array.from(data[22].split(","));
// data[23] for deprecated cells.road
cells.s = Uint16Array.from(data[24].split(","));
cells.state = Uint16Array.from(data[25].split(","));
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
// data[28] for deprecated cells.crossroad
cells.routes = data[36] ? JSON.parse(data[36]) : {};
pack.zones = data[38] ? JSON.parse(data[38]) : [];
pack.cells.biome = Uint8Array.from(data[16].split(","));
pack.cells.burg = Uint16Array.from(data[17].split(","));
pack.cells.conf = Uint8Array.from(data[18].split(","));
pack.cells.culture = Uint16Array.from(data[19].split(","));
pack.cells.fl = Uint16Array.from(data[20].split(","));
pack.cells.pop = Float32Array.from(data[21].split(","));
pack.cells.r = Uint16Array.from(data[22].split(","));
// data[23] had deprecated cells.road
pack.cells.s = Uint16Array.from(data[24].split(","));
pack.cells.state = Uint16Array.from(data[25].split(","));
pack.cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(pack.cells.i.length);
pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
// data[28] had deprecated cells.crossroad
pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
if (data[31]) {
const namesDL = data[31].split("/");
@ -406,9 +416,9 @@ async function parseLoadedData(data, mapVersion) {
nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
});
}
})();
}
void (function restoreLayersState() {
{
const isVisible = selection => selection.node() && selection.style("display") !== "none";
const isVisibleNode = node => node && node.style.display !== "none";
const hasChildren = selection => selection.node()?.hasChildNodes();
@ -422,7 +432,7 @@ async function parseLoadedData(data, mapVersion) {
// turn on active layers
if (hasChild(texture, "image")) turnOn("toggleTexture");
if (hasChildren(terrs)) turnOn("toggleHeight");
if (hasChildren(terrs.select("#landHeights"))) turnOn("toggleHeight");
if (hasChildren(biomes)) turnOn("toggleBiomes");
if (hasChildren(cells)) turnOn("toggleCells");
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
@ -437,13 +447,13 @@ async function parseLoadedData(data, mapVersion) {
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
if (hasChildren(temperature)) turnOn("toggleTemp");
if (hasChildren(temperature)) turnOn("toggleTemperature");
if (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChildren(ice)) turnOn("toggleIce");
if (hasChild(prec, "circle")) turnOn("togglePrec");
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels");
if (isVisible(icons)) turnOn("toggleIcons");
if (isVisible(icons)) turnOn("toggleBurgIcons");
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
if (hasChildren(markers)) turnOn("toggleMarkers");
if (isVisible(ruler)) turnOn("toggleRulers");
@ -451,20 +461,19 @@ async function parseLoadedData(data, mapVersion) {
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
getCurrentPreset();
})();
}
void (function restoreEvents() {
{
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
.on("click", () => clearLegend());
})();
}
{
// dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.01");
resolveVersionConflicts(versionNumber);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.108.0");
resolveVersionConflicts(mapVersion);
}
// add custom heightmap color scheme if any
@ -481,19 +490,23 @@ async function parseLoadedData(data, mapVersion) {
if (textureHref) updateTextureSelectValue(textureHref);
}
void (function checkDataIntegrity() {
const cells = pack.cells;
// data integrity checks
{
const {cells, vertices} = pack;
if (pack.cells.i.length !== pack.cells.state.length) {
const message = "Data integrity check. Striping issue detected. To fix edit the heightmap in ERASE mode";
ERROR && console.error(message);
const cellsMismatch = cells.i.length !== cells.state.length;
const featureVerticesMismatch = pack.features.some(f => f?.vertices?.some(vertex => !vertices.p[vertex]));
if (cellsMismatch || featureVerticesMismatch) {
const message = "[Data integrity] Striping issue detected. To fix try to edit the heightmap in ERASE mode";
throw new Error(message);
}
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
invalidStates.forEach(s => {
const invalidCells = cells.i.filter(i => cells.state[i] === s);
invalidCells.forEach(i => (cells.state[i] = 0));
ERROR && console.error("Data integrity check. Invalid state", s, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid state", s, "is assigned to cells", invalidCells);
});
const invalidProvinces = [...new Set(cells.province)].filter(
@ -502,14 +515,14 @@ async function parseLoadedData(data, mapVersion) {
invalidProvinces.forEach(p => {
const invalidCells = cells.i.filter(i => cells.province[i] === p);
invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data integrity check. Invalid province", p, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid province", p, "is assigned to cells", invalidCells);
});
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
invalidCultures.forEach(c => {
const invalidCells = cells.i.filter(i => cells.culture[i] === c);
invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data integrity check. Invalid culture", c, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid culture", c, "is assigned to cells", invalidCells);
});
const invalidReligions = [...new Set(cells.religion)].filter(
@ -518,14 +531,14 @@ async function parseLoadedData(data, mapVersion) {
invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => (cells.religion[i] = 0));
ERROR && console.error("Data integrity check. Invalid religion", r, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid religion", r, "is assigned to cells", invalidCells);
});
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
invalidFeatures.forEach(f => {
const invalidCells = cells.i.filter(i => cells.f[i] === f);
// No fix as for now
ERROR && console.error("Data integrity check. Invalid feature", f, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid feature", f, "is assigned to cells", invalidCells);
});
const invalidBurgs = [...new Set(cells.burg)].filter(
@ -534,7 +547,7 @@ async function parseLoadedData(data, mapVersion) {
invalidBurgs.forEach(burgId => {
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
invalidCells.forEach(i => (cells.burg[i] = 0));
ERROR && console.error("Data integrity check. Invalid burg", burgId, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid burg", burgId, "is assigned to cells", invalidCells);
});
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
@ -542,21 +555,20 @@ async function parseLoadedData(data, mapVersion) {
const invalidCells = cells.i.filter(i => cells.r[i] === r);
invalidCells.forEach(i => (cells.r[i] = 0));
rivers.select("river" + r).remove();
ERROR && console.error("Data integrity check. Invalid river", r, "is assigned to cells", invalidCells);
ERROR && console.error("[Data integrity] Invalid river", r, "is assigned to cells", invalidCells);
});
pack.burgs.forEach(burg => {
if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);
if (!burg.i && burg.lock) {
ERROR && console.error(`Data integrity check. Burg 0 is marked as locked, removing the status`);
ERROR && console.error(`[Data integrity] Burg 0 is marked as locked, removing the status`);
delete burg.lock;
return;
}
if (burg.removed && burg.lock) {
ERROR &&
console.error(`Data integrity check. Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
ERROR && console.error(`[Data integrity] Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
delete burg.lock;
return;
}
@ -565,36 +577,34 @@ async function parseLoadedData(data, mapVersion) {
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
ERROR &&
console.error(
`Data integrity check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
);
console.error(`[Data integrity] Burg ${burg.i} is missing cell info or coordinates. Removing the burg`);
burg.removed = true;
}
if (burg.port < 0) {
ERROR && console.error("Data integrity check. Burg", burg.i, "has invalid port value", burg.port);
ERROR && console.error("[Data integrity] Burg", burg.i, "has invalid port value", burg.port);
burg.port = 0;
}
if (burg.cell >= cells.i.length) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid cell", burg.cell);
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid cell", burg.cell);
burg.cell = findCell(burg.x, burg.y);
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
cells.burg[burg.cell] = burg.i;
}
if (burg.state && !pack.states[burg.state]) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to invalid state", burg.state);
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid state", burg.state);
burg.state = 0;
}
if (burg.state && pack.states[burg.state].removed) {
ERROR && console.error("Data integrity check. Burg", burg.i, "is linked to removed state", burg.state);
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to removed state", burg.state);
burg.state = 0;
}
if (burg.state === undefined) {
ERROR && console.error("Data integrity check. Burg", burg.i, "has no state data");
ERROR && console.error("[Data integrity] Burg", burg.i, "has no state data");
burg.state = 0;
}
});
@ -608,7 +618,7 @@ async function parseLoadedData(data, mapVersion) {
if (!state.i && capitalBurgs.length) {
ERROR &&
console.error(
`Data integrity check. Neutral burgs (${capitalBurgs
`[Data integrity] Neutral burgs (${capitalBurgs
.map(b => b.i)
.join(", ")}) marked as capitals. Moving them to towns`
);
@ -622,7 +632,7 @@ async function parseLoadedData(data, mapVersion) {
}
if (capitalBurgs.length > 1) {
const message = `Data integrity check. State ${state.i} has multiple capitals (${capitalBurgs
const message = `[Data integrity] State ${state.i} has multiple capitals (${capitalBurgs
.map(b => b.i)
.join(", ")}) assigned. Keeping the first as capital and moving others to towns`;
ERROR && console.error(message);
@ -638,7 +648,7 @@ async function parseLoadedData(data, mapVersion) {
if (state.i && stateBurgs.length && !capitalBurgs.length) {
ERROR &&
console.error(`Data integrity check. State ${state.i} has no capital. Assigning the first burg as capital`);
console.error(`[Data integrity] State ${state.i} has no capital. Assigning the first burg as capital`);
stateBurgs[0].capital = 1;
moveBurgToGroup(stateBurgs[0].i, "cities");
}
@ -647,17 +657,48 @@ async function parseLoadedData(data, mapVersion) {
pack.provinces.forEach(p => {
if (!p.i || p.removed) return;
if (pack.states[p.state] && !pack.states[p.state].removed) return;
ERROR && console.error("Data integrity check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province
ERROR &&
console.error(
`[Data integrity] Province ${p.i} is linked to removed state ${p.state}. Removing the province`
);
p.removed = true;
});
pack.routes.forEach(route => {
if (!route.points || route.points.length < 2) {
ERROR && console.error(`[Data integrity] Route ${route.i} has less than 2 points. Removing the route`);
Routes.remove(route);
}
});
for (const from in pack.cells.routes) {
const value = pack.cells.routes[from];
if (!value) continue;
if (Object.keys(value).length === 0) {
// remove empty object
delete pack.cells.routes[from];
continue;
}
for (const to in value) {
const routeId = value[to];
const route = pack.routes.find(r => r.i === routeId);
if (!route) {
ERROR &&
console.error(`[Data integrity] Route ${routeId} from ${from} to ${to} is missing. Removing the route`);
delete pack.cells.routes[from][to];
}
}
}
{
const markerIds = [];
let nextId = last(pack.markers)?.i + 1 || 0;
pack.markers.forEach(marker => {
if (markerIds[marker.i]) {
ERROR && console.error("Data integrity check. Marker", marker.i, "has non-unique id. Changing to", nextId);
ERROR && console.error("[Data integrity] Marker", marker.i, "has non-unique id. Changing to", nextId);
const domElements = document.querySelectorAll("#marker" + marker.i);
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
@ -675,20 +716,25 @@ async function parseLoadedData(data, mapVersion) {
// sort markers by index
pack.markers.sort((a, b) => a.i - b.i);
}
})();
}
fitMapToScreen();
{
// remove href from emblems, to trigger rendering on load
emblems.selectAll("use").attr("href", null);
}
// remove href from emblems, to trigger rendering on load
emblems.selectAll("use").attr("href", null);
{
// draw data layers (not kept in svg)
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
if (layerIsOn("toggleGrid")) drawGrid();
}
// draw data layers (no kept in svg)
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
if (layerIsOn("toggleGrid")) drawGrid();
if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
{
if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
fitMapToScreen();
}
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
showStatistics();
@ -698,14 +744,15 @@ async function parseLoadedData(data, mapVersion) {
ERROR && console.error(error);
clearMainTip();
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${version}.
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Loading error",
maxWidth: "50em",
maxWidth: "40em",
buttons: {
"Clear cache": () => cleanupData(),
"Select file": function () {
$(this).dialog("close");
mapToLoad.click();

View file

@ -41,7 +41,7 @@ function prepareMapData() {
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const params = [VERSION, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [
distanceUnitInput.value,
distanceScale,
@ -68,7 +68,8 @@ function prepareMapData() {
stylePreset.value,
+rescaleLabels.checked,
urbanDensity,
longitudeOutput.value
longitudeOutput.value,
growthRate.value
].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
@ -100,6 +101,7 @@ function prepareMapData() {
const markers = JSON.stringify(pack.markers);
const cellRoutes = JSON.stringify(pack.cells.routes);
const routes = JSON.stringify(pack.routes);
const zones = JSON.stringify(pack.zones);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
@ -152,7 +154,8 @@ function prepareMapData() {
fonts,
markers,
cellRoutes,
routes
routes,
zones
].join("\r\n");
return mapData;
}

View file

@ -1,98 +1,87 @@
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
const LAKE_ELEVATION_DELTA = 0.1;
pack.features.forEach(f => {
if (f.type !== "lake") return;
// check if lake can be potentially open (not in deep depression)
const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
// default flux: sum of precipitation around lake
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
delete feature.closed;
// temperature and evaporation to detect closed lakes
f.temp =
f.cells < 6
? grid.cells.temp[cells.g[f.firstCell]]
: rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
// no outlet for lakes in depressed areas
if (f.closed) return;
// lake outlet cell
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
});
return lakeOutCells;
};
// get array of land cells aroound lake
const getShoreline = function (lake) {
const uniqueCells = new Set();
if (!lake.vertices) lake.vertices = [];
lake.vertices.forEach(v => pack.vertices.c[v].forEach(c => pack.cells.h[c] >= 20 && uniqueCells.add(c)));
lake.shoreline = [...uniqueCells];
};
const prepareLakeData = h => {
const cells = pack.cells;
const ELEVATION_LIMIT = +document.getElementById("lakeElevationLimitOutput").value;
pack.features.forEach(f => {
if (f.type !== "lake") return;
delete f.flux;
delete f.inlets;
delete f.outlet;
delete f.height;
delete f.closed;
!f.shoreline && Lakes.getShoreline(f);
// lake surface height is as lowest land cells around
const min = f.shoreline.sort((a, b) => h[a] - h[b])[0];
f.height = h[min] - 0.1;
// check if lake can be open (not in deep depression)
if (ELEVATION_LIMIT === 80) {
f.closed = false;
const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
if (MAX_ELEVATION > 99) {
feature.closed = false;
return;
}
let deep = true;
const threshold = f.height + ELEVATION_LIMIT;
const queue = [min];
let isDeep = true;
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [lowestShorelineCell];
const checked = [];
checked[min] = true;
checked[lowestShorelineCell] = true;
// check if elevated lake can potentially pour to another water body
while (deep && queue.length) {
const q = queue.pop();
while (queue.length && isDeep) {
const cellId = queue.pop();
for (const n of cells.c[q]) {
if (checked[n]) continue;
if (h[n] >= threshold) continue;
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[n] < 20) {
const nFeature = pack.features[cells.f[n]];
if (nFeature.type === "ocean" || f.height > nFeature.height) {
deep = false;
break;
}
if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
}
checked[n] = true;
queue.push(n);
checked[neibCellId] = true;
queue.push(neibCellId);
}
}
f.closed = deep;
feature.closed = isDeep;
});
};
const defineClimateData = function (heights) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell] = feature.i;
});
return lakeOutCells;
function getFlux(lake) {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
function getLakeTemp(lake) {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
}
function getLakeEvaporation(lake) {
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
function getLowestShoreCell(lake) {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
@ -111,23 +100,10 @@ window.Lakes = (function () {
}
};
const defineGroup = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = getGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
};
const generateName = function () {
Math.random = aleaPRNG(seed);
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
feature.name = getName(feature);
}
const getHeight = function (feature) {
const heights = pack.cells.h;
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
};
const getName = function (feature) {
@ -136,19 +112,5 @@ window.Lakes = (function () {
return Names.getCulture(culture);
};
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
})();

View file

@ -11,7 +11,7 @@ window.Markers = (function () {
/*
Default markers config:
type - short description (snake-case)
icon - unicode character, make sure it's supported by most of the browsers. Source: emojipedia.org
icon - unicode character or url to image
dx: icon offset in x direction, in pixels
dy: icon offset in y direction, in pixels
min: minimum number of candidates to add at least 1 marker
@ -117,6 +117,7 @@ window.Markers = (function () {
while (quantity && candidates.length) {
const [cell] = extractAnyElement(candidates);
const marker = addMarker({icon, type, dx, dy, px}, {cell});
if (!marker) continue;
add("marker" + marker.i, cell);
quantity--;
}
@ -150,6 +151,7 @@ window.Markers = (function () {
}
function addMarker(base, marker) {
if (marker.cell === undefined) return;
const i = last(pack.markers)?.i + 1 || 0;
const [x, y] = getMarkerCoordinates(marker.cell);
marker = {...base, x, y, ...marker, i};

View file

@ -2,7 +2,7 @@
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
TIME && console.time("generateMilitary");
const {cells, states} = pack;
const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states
@ -252,8 +252,6 @@ window.Military = (function () {
delete s.temp; // do not store temp data
});
redraw();
function createRegiments(nodes, s) {
if (!nodes.length) return [];
@ -312,19 +310,9 @@ window.Military = (function () {
return regiments;
}
TIME && console.timeEnd("generateMilitaryForces");
TIME && console.timeEnd("generateMilitary");
};
function redraw() {
const validStates = pack.states.filter(s => s.i && !s.removed);
armies.selectAll("g > g").each(function () {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
});
armies.selectAll("g").remove();
validStates.forEach(s => drawRegiments(s.military, s.i));
}
const getDefaultOptions = function () {
return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
@ -335,122 +323,6 @@ window.Military = (function () {
];
};
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
const h = size * 2;
const x = d => rn(d.x - w(d) / 2, 2);
const y = d => rn(d.y - size, 2);
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
const army = armies
.append("g")
.attr("id", "army" + s)
.attr("fill", baseColor)
.attr("color", darkerColor);
const g = army
.selectAll("g")
.data(regiments)
.enter()
.append("g")
.attr("id", d => "regiment" + s + "-" + d.i)
.attr("data-name", d => d.name)
.attr("data-state", s)
.attr("data-id", d => d.i)
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
g.append("rect")
.attr("x", d => x(d))
.attr("y", d => y(d))
.attr("width", d => w(d))
.attr("height", h);
g.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.text(d => getTotal(d));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", d => x(d) - size)
.attr("y", d => d.y)
.text(d => d.icon);
};
const drawRegiment = function (reg, stateId) {
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = rn(reg.x - w / 2, 2);
const y1 = rn(reg.y - size, 2);
let army = armies.select("g#army" + stateId);
if (!army.size()) {
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
army = armies
.append("g")
.attr("id", "army" + stateId)
.attr("fill", baseColor)
.attr("color", darkerColor);
}
const g = army
.append("g")
.attr("id", "regiment" + stateId + "-" + reg.i)
.attr("data-name", reg.name)
.attr("data-state", stateId)
.attr("data-id", reg.i)
.attr("transform", `rotate(${reg.angle || 0})`)
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", x1 - h)
.attr("y", y1)
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("x", x1 - size)
.attr("y", reg.y)
.text(reg.icon);
};
// move one regiment to another
const moveRegiment = function (reg, x, y) {
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
if (!el.size()) return;
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
reg.x = x;
reg.y = y;
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
el.select("text").transition(move).attr("x", x).attr("y", y);
el.selectAll("rect:nth-of-type(2)")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y));
el.select(".regimentIcon")
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y);
};
// utilize si function to make regiment total text fit regiment box
const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
@ -503,21 +375,19 @@ window.Military = (function () {
: "";
const campaign = s.campaigns ? ra(s.campaigns) : null;
const year = campaign ? rand(campaign.start, campaign.end) : gauss(options.year - 100, 150, 1, options.year - 6);
const year = campaign
? rand(campaign.start, campaign.end || options.year)
: gauss(options.year - 100, 150, 1, options.year - 6);
const conflict = campaign ? ` during the ${campaign.name}` : "";
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
};
return {
generate,
redraw,
getDefaultOptions,
getName,
generateNote,
drawRegiments,
drawRegiment,
moveRegiment,
getTotal,
getEmblem
};

View file

@ -48,18 +48,28 @@ window.Names = (function () {
return chain;
};
// update chain for specific base
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null);
const updateChain = i => {
chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
};
// update chains for all used bases
const clearChains = () => (chains = []);
const clearChains = () => {
chains = [];
};
// generate name using Markov's chain
const getBase = function (base, min, max, dupl) {
if (base === undefined) {
ERROR && console.error("Please define a base");
return;
if (base === undefined) return ERROR && console.error("Please define a base");
if (nameBases[base] === undefined) {
if (nameBases[0]) {
WARN && console.warn("Namebase " + base + " is not found. First available namebase will be used");
base = 0;
} else {
ERROR && console.error("Namebase " + base + " is not found");
return "ERROR";
}
}
if (!chains[base]) updateChain(base);
const data = chains[base];
@ -141,16 +151,8 @@ window.Names = (function () {
// generate short name for base
const getBaseShort = function (base) {
if (nameBases[base] === undefined) {
tip(
`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`,
false,
"error"
);
base = 1;
}
const min = nameBases[base].min - 1;
const max = Math.max(nameBases[base].max - 2, min);
const min = nameBases[base] ? nameBases[base].min - 1 : null;
const max = min ? Math.max(nameBases[base].max - 2, min) : null;
return getBase(base, min, max, "", 0);
};
@ -286,7 +288,7 @@ window.Names = (function () {
{name: "Basque", i: 20, min: 4, max: 11, d: "r", m: .1, b: "Agurain,Aia,Aiara,Albiztur,Alkiza,Altzaga,Amorebieta,Amurrio,Andoain,Anoeta,Antzuola,Arakaldo,Arantzazu,Arbatzegi,Areatza,Arratzua,Arrieta,Artea,Artziniega,Asteasu,Astigarraga,Ataun,Atxondo,Aulesti,Azkoitia,Azpeitia,Bakio,Baliarrain,Barakaldo,Barrika,Barrundia,Basauri,Beasain,Bedia,Beizama,Belauntza,Berastegi,Bergara,Bermeo,Bernedo,Berriatua,Berriz,Bidania,Bilar,Bilbao,Busturia,Deba,Derio,Donostia,Dulantzi,Durango,Ea,Eibar,Elantxobe,Elduain,Elgeta,Elgoibar,Elorrio,Erandio,Ergoitia,Ermua,Errenteria,Errezil,Eskoriatza,Eskuernaga,Etxebarri,Etxebarria,Ezkio,Forua,Gabiria,Gaintza,Galdakao,Gamiz,Garai,Gasteiz,Gatzaga,Gaubea,Gautegiz,Gaztelu,Gernika,Gerrikaitz,Getaria,Getxo,Gizaburuaga,Goiatz,Gorliz,Gorriaga,Harana,Hernani,Hondarribia,Ibarra,Ibarrangelu,Idiazabal,Iekora,Igorre,Ikaztegieta,Irun,Irura,Iruraiz,Itsaso,Itsasondo,Iurreta,Izurtza,Jatabe,Kanpezu,Karrantza,Kortezubi,Kripan,Kuartango,Lanestosa,Lantziego,Larrabetzu,Lasarte,Laukiz,Lazkao,Leaburu,Legazpi,Legorreta,Legutio,Leintz,Leioa,Lekeitio,Lemoa,Lemoiz,Leza,Lezama,Lezo,Lizartza,Maeztu,Mallabia,Manaria,Markina,Maruri,Menaka,Mendaro,Mendata,Mendexa,Morga,Mundaka,Mungia,Munitibar,Murueta,Muskiz,Mutiloa,Mutriku,Nabarniz,Oiartzun,Oion,Okondo,Olaberria,Onati,Ondarroa,Ordizia,Orendain,Orexa,Oria,Orio,Ormaiztegi,Orozko,Ortuella,Otegi,Otxandio,Pasaia,Plentzia,Santurtzi,Sestao,Sondika,Soraluze,Sukarrieta,Tolosa,Trapagaran,Turtzioz,Ubarrundia,Ubide,Ugao,Urdua,Urduliz,Urizaharra,Urkabustaiz,Urnieta,Urretxu,Usurbil,Xemein,Zabaleta,Zaia,Zaldibar,Zambrana,Zamudio,Zaratamo,Zarautz,Zeberio,Zegama,Zerain,Zestoa,Zierbena,Zigoitia,Ziortza,Zuia,Zumaia,Zumarraga"},
{name: "Nigerian", i: 21, min: 4, max: 10, d: "", m: .3, b: "Abadogo,Abafon,Adealesu,Adeto,Adyongo,Afaga,Afamju,Agigbigi,Agogoke,Ahute,Aiyelaboro,Ajebe,Ajola,Akarekwu,Akunuba,Alawode,Alkaijji,Amangam,Amgbaye,Amtasa,Amunigun,Animahun,Anyoko,Arapagi,Asande,Awgbagba,Awhum,Awodu,Babateduwa,Bandakwai,Bangdi,Bilikani,Birnindodo,Braidu,Bulakawa,Buriburi,Cainnan,Chakum,Chondugh,Dagwarga,Darpi,Dokatofa,Dozere,Ebelibri,Efem,Ekoku,Ekpe,Ewhoeviri,Galea,Gamen,Ganjin,Gantetudu,Gargar,Garinbode,Gbure,Gerti,Gidan,Gitabaremu,Giyagiri,Giyawa,Gmawa,Golakochi,Golumba,Gunji,Gwambula,Gwodoti,Hayinlere,Hayinmaialewa,Hirishi,Hombo,Ibefum,Iberekodo,Icharge,Idofin,Idofinoka,Igbogo,Ijoko,Ijuwa,Ikawga,Ikhin,Ikpakidout,Ikpeoniong,Imuogo,Ipawo,Ipinlerere,Isicha,Itakpa,Jangi,Jare,Jataudakum,Jaurogomki,Jepel,Kafinmalama,Katab,Katanga,Katinda,Katirije,Kaurakimba,Keffinshanu,Kellumiri,Kiagbodor,Kirbutu,Kita,Kogogo,Kopje,Korokorosei,Kotoku,Kuata,Kujum,Kukau,Kunboon,Kuonubogbene,Kurawe,Kushinahu,Kwaramakeri,Ladimeji,Lafiaro,Lahaga,Laindebajanle,Laindegoro,Lakati,Litenswa,Maba,Madarzai,Maianita,Malikansaa,Mata,Megoyo,Meku,Miama,Modi,Mshi,Msugh,Muduvu,Murnachehu,Namnai,Ndamanma,Ndiwulunbe,Ndonutim,Ngbande,Nguengu,Ntoekpe,Nyajo,Nyior,Odajie,Ogbaga,Ogultu,Ogunbunmi,Ojopode,Okehin,Olugunna,Omotunde,Onipede,Onma,Orhere,Orya,Otukwang,Otunade,Rampa,Rimi,Rugan,Rumbukawa,Sabiu,Sangabama,Sarabe,Seboregetore,Shafar,Shagwa,Shata,Shengu,Sokoron,Sunnayu,Tafoki,Takula,Talontan,Tarhemba,Tayu,Ter,Timtim,Timyam,Tindirke,Tokunbo,Torlwam,Tseakaadza,Tseanongo,Tsebeeve,Tsepaegh,Tuba,Tumbo,Tungalombo,Tunganyakwe,Uhkirhi,Umoru,Umuabai,Umuajuju,Unchida,Ungua,Unguwar,Unongo,Usha,Utongbo,Vembera,Wuro,Yanbashi,Yanmedi,Yoku,Zarunkwari,Zilumo,Zulika"},
{name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Anfosadh,Antinbhearmor,Ardenna,Attacon,Banwen,Beira,Bhrura,Bleddfa,Boioduro,Bona,Boskyny,Boslowenpolbrogh,Boudobriga,Bravon,Brigant,Briganta,Briva,Brosnach,Caersws,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Chearbhallain,Chlasaigh,Chormaic,Cuileannach,Dinn,Diwa,Dubingen,Duibhidighe,Duro,Ebora,Ebruac,Eburodunum,Eccles,Egloskuri,Eighe,Eireann,Elerghi,Ferkunos,Fhlaithnin,Gallbhuaile,Genua,Ghrainnse,Gwyles,Heartsease,Hebron,Hordh,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Iubhrach,Karardhek,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Lanngorrow,Latense,Leming,Lindomagos,Llanaber,Llandidiwg,Llandyrnog,Llanfarthyn,Llangadwaldr,Llansanwyr,Lochinver,Lugduno,Magoduro,Mheara,Monmouthshire,Nanshiryarth,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ponsmeur,Raithin,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Tobargeal,Trealaw,Trefesgob,Trewedhenek,Trewythelan,Tuaisceart,Uige,Vitodurum,Windobona"},
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Adamndun,Adma,Admatum,Agrab,Akkad,Akshak,Amnanum,Andarig,Anshan,Apiru,Apum,Arantu,Arbid,Arpachiyah,Arpad,Arrapha,Ashlakka,Assur,Awan,Babilim,Bad-Tibira,Balawat,Barsip,Birtu,Bit-Bunakki,Borsippa,Chuera,Dashrah,Der,Dilbat,Diniktum,Doura,Dur-Kurigalzu,Dur-Sharrukin,Dur-Untash,Dûr-gurgurri,Ebla,Ekallatum,Ekalte,Emar,Erbil,Eresh,Eridu,Eshnunn,Eshnunna,Gargamish,Gasur,Gawra,Gibil,Girsu,Gizza,Habirun,Habur,Hadatu,Hakkulan,Halab,Halabit,Hamazi,Hamoukar,Haradum,Harbidum,Harran,Harranu,Hassuna,Hatarikka,Hatra,Hissar,Hiyawa,Hormirzad,Ida-Maras,Idamaraz,Idu,Imerishu,Imgur-Enlil,Irisagrig,Irnina,Irridu,Isin,Issinnitum,Iturungal,Izubitum,Jarmo,Jemdet,Kabnak,Kadesh,Kahat,Kalhu,Kar-Shulmanu-Asharedu,Kar-Tukulti-Ninurta,Kar-Šulmānu-ašarēdu,Karana,Karatepe,Kartukulti,Kazallu,Kesh,Kidsha,Kinza,Kish,Kisiga,Kisurra,Kuara,Kurda,Kurruhanni,Kutha,Lagaba,Lagash,Larak,Larsa,Leilan,Malgium,Marad,Mardaman,Mari,Marlik,Mashkan,Mashkan-shapir,Matutem,Me-Turan,Meliddu,Mumbaqat,Nabada,Nagar,Nanagugal,Nerebtum,Nigin,Nimrud,Nina,Nineveh,Ninua,Nippur,Niru,Niya,Nuhashe,Nuhasse,Nuzi,Puzrish-Dagan,Qalatjarmo,Qatara,Qatna,Qattunan,Qidshu,Rapiqum,Rawda,Sagaz,Shaduppum,Shaggaratum,Shalbatu,Shanidar,Sharrukin,Shawwan,Shehna,Shekhna,Shemshara,Shibaniba,Shubat-Enlil,Shurkutir,Shuruppak,Shusharra,Shushin,Sikan,Sippar,Sippar-Amnanum,Sippar-sha-Annunitum,Subatum,Susuka,Tadmor,Tarbisu,Telul,Terqa,Tirazish,Tisbon,Tuba,Tushhan,Tuttul,Tutub,Ubaid,Umma,Ur,Urah,Urbilum,Urkesh,Ursa'um,Uruk,Urum,Uzarlulu,Warka,Washukanni,Zabalam,Zarri-Amnan"},
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Adamndun,Adma,Admatum,Agrab,Akkad,Akshak,Amnanum,Andarig,Anshan,Apiru,Apum,Arantu,Arbid,Arpachiyah,Arpad,Arrapha,Ashlakka,Assur,Awan,Babilim,Bad-Tibira,Balawat,Barsip,Birtu,Bit-Bunakki,Borsippa,Chuera,Dashrah,Der,Dilbat,Diniktum,Doura,Dur-Kurigalzu,Dur-Sharrukin,Dur-Untash,Dûr-gurgurri,Ebla,Ekallatum,Ekalte,Emar,Erbil,Eresh,Eridu,Eshnunn,Eshnunna,Gargamish,Gasur,Gawra,Gibil,Girsu,Gizza,Habirun,Habur,Hadatu,Hakkulan,Halab,Halabit,Hamazi,Hamoukar,Haradum,Harbidum,Harran,Harranu,Hassuna,Hatarikka,Hatra,Hissar,Hiyawa,Hormirzad,Ida-Maras,Idamaraz,Idu,Imerishu,Imgur-Enlil,Irisagrig,Irnina,Irridu,Isin,Issinnitum,Iturungal,Izubitum,Jarmo,Jemdet,Kabnak,Kadesh,Kahat,Kalhu,Kar-Shulmanu-Asharedu,Kar-Tukulti-Ninurta,Kar-shulmanu-asharedu,Karana,Karatepe,Kartukulti,Kazallu,Kesh,Kidsha,Kinza,Kish,Kisiga,Kisurra,Kuara,Kurda,Kurruhanni,Kutha,Lagaba,Lagash,Larak,Larsa,Leilan,Malgium,Marad,Mardaman,Mari,Marlik,Mashkan,Mashkan-shapir,Matutem,Me-Turan,Meliddu,Mumbaqat,Nabada,Nagar,Nanagugal,Nerebtum,Nigin,Nimrud,Nina,Nineveh,Ninua,Nippur,Niru,Niya,Nuhashe,Nuhasse,Nuzi,Puzrish-Dagan,Qalatjarmo,Qatara,Qatna,Qattunan,Qidshu,Rapiqum,Rawda,Sagaz,Shaduppum,Shaggaratum,Shalbatu,Shanidar,Sharrukin,Shawwan,Shehna,Shekhna,Shemshara,Shibaniba,Shubat-Enlil,Shurkutir,Shuruppak,Shusharra,Shushin,Sikan,Sippar,Sippar-Amnanum,Sippar-sha-Annunitum,Subatum,Susuka,Tadmor,Tarbisu,Telul,Terqa,Tirazish,Tisbon,Tuba,Tushhan,Tuttul,Tutub,Ubaid,Umma,Ur,Urah,Urbilum,Urkesh,Ursa'um,Uruk,Urum,Uzarlulu,Warka,Washukanni,Zabalam,Zarri-Amnan"},
{name: "Iranian", i: 24, min: 5, max: 11, d: "", m: .1, b: "Abali,Abrisham,Absard,Abuzeydabad,Afus,Alavicheh,Alikosh,Amol,Anarak,Anbar,Andisheh,Anshan,Aran,Ardabil,Arderica,Ardestan,Arjomand,Asgaran,Asgharabad,Ashian,Awan,Babajan,Badrud,Bafran,Baghestan,Baghshad,Bahadoran,Baharan Shahr,Baharestan,Bakun,Bam,Baqershahr,Barzok,Bastam,Behistun,Bitistar,Bumahen,Bushehr,Chadegan,Chahardangeh,Chamgardan,Chermahin,Choghabonut,Chugan,Damaneh,Damavand,Darabgard,Daran,Dastgerd,Dehaq,Dehaqan,Dezful,Dizicheh,Dorcheh,Dowlatabad,Duruntash,Ecbatana,Eslamshahr,Estakhr,Ezhiyeh,Falavarjan,Farrokhi,Fasham,Ferdowsieh,Fereydunshahr,Ferunabad,Firuzkuh,Fuladshahr,Ganjdareh,Ganzak,Gaz,Geoy,Godin,Goldasht,Golestan,Golpayegan,Golshahr,Golshan,Gorgab,Guged,Habibabad,Hafshejan,Hajjifiruz,Hana,Harand,Hasanabad,Hasanlu,Hashtgerd,Hecatompylos,Hormirzad,Imanshahr,Isfahan,Jandaq,Javadabad,Jiroft,Jowsheqan ,Jowzdan,Kabnak,Kahrizak,Kahriz Sang,Kangavar,Karaj,Karkevand,Kashan,Kelishad,Kermanshah,Khaledabad,Khansar,Khorramabad,Khur,Khvorzuq,Kilan,Komeh,Komeshcheh,Konar,Kuhpayeh,Kul,Kushk,Lavasan,Laybid,Liyan,Lyan,Mahabad,Mahallat,Majlesi,Malard,Manzariyeh,Marlik,Meshkat,Meymeh,Miandasht,Mish,Mobarakeh,Nahavand,Nain,Najafabad,Naqshe,Narezzash,Nasimshahr,Nasirshahr,Nasrabad,Natanz,Neyasar,Nikabad,Nimvar,Nushabad,Pakdasht,Parand,Pardis,Parsa,Pasargadai,Patigrabana,Pir Bakran,Pishva,Qahderijan,Qahjaverestan,Qamsar,Qarchak,Qods,Rabat,Ray-shahr,Rezvanshahr,Rhages,Robat Karim,Rozveh,Rudehen,Sabashahr,Safadasht,Sagzi,Salehieh,Sandal,Sarvestan,Sedeh,Sefidshahr,Semirom,Semnan,Shadpurabad,Shah,Shahdad,Shahedshahr,Shahin,Shahpour,Shahr,Shahreza,Shahriar,Sharifabad,Shemshak,Shiraz,Shushan,Shushtar,Sialk,Sin,Sukhteh,Tabas,Tabriz,Takhte,Talkhuncheh,Talli,Tarq,Temukan,Tepe,Tiran,Tudeshk,Tureng,Urmia,Vahidieh,Vahrkana,Vanak,Varamin,Varnamkhast,Varzaneh,Vazvan,Yahya,Yarim,Yasuj,Zarrin Shahr,Zavareh,Zayandeh,Zazeran,Ziar,Zibashahr,Zranka"},
{name: "Hawaiian", i: 25, min: 5, max: 10, d: "auo", m: 1, b: "Aapueo,Ahoa,Ahuakaio,Ahupau,Alaakua,Alae,Alaeloa,Alamihi,Aleamai,Alena,Alio,Aupokopoko,Halakaa,Haleu,Haliimaile,Hamoa,Hanakaoo,Hanaulu,Hanawana,Hanehoi,Haou,Hikiaupea,Hokuula,Honohina,Honokahua,Honokeana,Honokohau,Honolulu,Honomaele,Hononana,Honopou,Hoolawa,Huelo,Kaalaea,Kaapahu,Kaeo,Kahalehili,Kahana,Kahuai,Kailua,Kainehe,Kakalahale,Kakanoni,Kalenanui,Kaleoaihe,Kalialinui,Kalihi,Kalimaohe,Kaloi,Kamani,Kamehame,Kanahena,Kaniaula,Kaonoulu,Kapaloa,Kapohue,Kapuaikini,Kapunakea,Kauau,Kaulalo,Kaulanamoa,Kauluohana,Kaumakani,Kaumanu,Kaunauhane,Kaupakulua,Kawaloa,Keaa,Keaaula,Keahua,Keahuapono,Kealahou,Keanae,Keauhou,Kelawea,Keokea,Keopuka,Kikoo,Kipapa,Koakupuna,Koali,Kolokolo,Kopili,Kou,Kualapa,Kuhiwa,Kuholilea,Kuhua,Kuia,Kuikui,Kukoae,Kukohia,Kukuiaeo,Kukuipuka,Kukuiula,Kulahuhu,Lapakea,Lapueo,Launiupoko,Lole,Maalo,Mahinahina,Mailepai,Makaakini,Makaalae,Makaehu,Makaiwa,Makaliua,Makapipi,Makapuu,Maluaka,Manawainui,Mehamenui,Moalii,Moanui,Mohopili,Mokae,Mokuia,Mokupapa,Mooiki,Mooloa,Moomuku,Muolea,Nakaaha,Nakalepo,Nakaohu,Nakapehu,Nakula,Napili,Niniau,Nuu,Oloewa,Olowalu,Omaopio,Onau,Onouli,Opaeula,Opana,Opikoula,Paakea,Paeahu,Paehala,Paeohi,Pahoa,Paia,Pakakia,Palauea,Palemo,Paniau,Papaaea,Papaanui,Papaauhau,Papaka,Papauluana,Pauku,Paunau,Pauwalu,Pauwela,Pohakanele,Polaiki,Polanui,Polapola,Poopoo,Poponui,Poupouwela,Puahoowali,Puakea,Puako,Pualaea,Puehuehu,Pueokauiki,Pukaauhuhu,Pukuilua,Pulehu,Puolua,Puou,Puuhaehae,Puuiki,Puuki,Puulani,Puunau,Puuomaile,Uaoa,Uhao,Ukumehame,Ulaino,Ulumalu,Wahikuli,Waianae,Waianu,Waiawa,Waiehu,Waieli,Waikapu,Wailamoa,Wailaulau,Wainee,Waiohole,Waiohonu,Waiohuli,Waiokama,Waiokila,Waiopai,Waiopua,Waipao,Waipionui,Waipouli"},
{name: "Karnataka", i: 26, min: 5, max: 11, d: "tnl", m: 0, b: "Adityapatna,Adyar,Afzalpur,Aland,Alnavar,Alur,Ambikanagara,Anekal,Ankola,Annigeri,Arkalgud,Arsikere,Athni,Aurad,Badami,Bagalkot,Bagepalli,Bail,Bajpe,Bangalore,Bangarapet,Bankapura,Bannur,Bantval,Basavakalyan,Basavana,Belgaum,Beltangadi,Belur,Bhadravati,Bhalki,Bhatkal,Bhimarayanagudi,Bidar,Bijapur,Bilgi,Birur,Bommasandra,Byadgi,Challakere,Chamarajanagar,Channagiri,Channapatna,Channarayapatna,Chik,Chikmagalur,Chiknayakanhalli,Chikodi,Chincholi,Chintamani,Chitapur,Chitgoppa,Chitradurga,Dandeli,Dargajogihalli,Devadurga,Devanahalli,Dod,Donimalai,Gadag,Gajendragarh,Gangawati,Gauribidanur,Gokak,Gonikoppal,Gubbi,Gudibanda,Gulbarga,Guledgudda,Gundlupet,Gurmatkal,Haliyal,Hangal,Harapanahalli,Harihar,Hassan,Hatti,Haveri,Hebbagodi,Heggadadevankote,Hirekerur,Holalkere,Hole,Homnabad,Honavar,Honnali,Hoovina,Hosakote,Hosanagara,Hosdurga,Hospet,Hubli,Hukeri,Hungund,Hunsur,Ilkal,Indi,Jagalur,Jamkhandi,Jevargi,Jog,Kadigenahalli,Kadur,Kalghatgi,Kamalapuram,Kampli,Kanakapura,Karkal,Karwar,Khanapur,Kodiyal,Kolar,Kollegal,Konnur,Koppa,Koppal,Koratagere,Kotturu,Krishnarajanagara,Krishnarajasagara,Krishnarajpet,Kudchi,Kudligi,Kudremukh,Kumta,Kundapura,Kundgol,Kunigal,Kurgunta,Kushalnagar,Kushtagi,Lakshmeshwar,Lingsugur,Londa,Maddur,Madhugiri,Madikeri,Mahalingpur,Malavalli,Mallar,Malur,Mandya,Mangalore,Manvi,Molakalmuru,Mudalgi,Mudbidri,Muddebihal,Mudgal,Mudhol,Mudigere,Mulbagal,Mulgund,Mulki,Mulur,Mundargi,Mundgod,Munirabad,Mysore,Nagamangala,Nanjangud,Narasimharajapura,Naregal,Nargund,Navalgund,Nipani,Pandavapura,Pavagada,Piriyapatna,Pudu,Puttur,Rabkavi,Raichur,Ramanagaram,Ramdurg,Ranibennur,Raybag,Robertson,Ron,Sadalgi,Sagar,Sakleshpur,Saligram,Sandur,Sankeshwar,Saundatti,Savanur,Sedam,Shahabad,Shahpur,Shaktinagar,Shiggaon,Shikarpur,Shirhatti,Shorapur,Shrirangapattana,Siddapur,Sidlaghatta,Sindgi,Sindhnur,Sira,Siralkoppa,Sirsi,Siruguppa,Somvarpet,Sorab,Sringeri,Srinivaspur,Sulya,Talikota,Tarikere,Tekkalakote,Terdal,Thumbe,Tiptur,Tirthahalli,Tirumakudal,Tumkur,Turuvekere,Udupi,Vijayapura,Wadi,Yadgir,Yelandur,Yelbarga,Yellapur,Yenagudde"},

View file

@ -0,0 +1,257 @@
"use strict";
window.Provinces = (function () {
const forms = {
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
Theocracy: {Parish: 3, Deanery: 1},
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
};
const generate = (regenerate = false, regenerateLockedStates = false) => {
TIME && console.time("generateProvinces");
const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack;
const provinces = [0]; // 0 index is reserved for "no province"
const provinceIds = new Uint16Array(cells.i.length);
const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
if (regenerate) {
pack.provinces.forEach(province => {
if (!province.i || province.removed || !isProvinceLocked(province)) return;
const newId = provinces.length;
for (const i of cells.i) {
if (cells.province[i] === province.i) provinceIds[i] = newId;
}
province.i = newId;
provinces.push(province);
});
}
const provincesRatio = +byId("provincesRatio").value;
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
// generate provinces for selected burgs
states.forEach(s => {
s.provinces = [];
if (!s.i || s.removed) return;
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
const stateBurgs = burgs
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
.sort((a, b) => b.capital - a.capital);
if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
const form = Object.assign({}, forms[s.form]);
for (let i = 0; i < provincesNumber; i++) {
const provinceId = provinces.length;
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const nameByBurg = P(0.5);
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
const formName = rw(form);
form[formName] += 10;
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = BurgsAndStates.getType(center, burg.port);
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
coa.shield = COA.getShield(c, s.i);
s.provinces.push(provinceId);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
}
});
// expand generated provinces
const queue = new FlatQueue();
const cost = [];
provinces.forEach(p => {
if (!p.i || p.removed || isProvinceLocked(p)) return;
provinceIds[p.center] = p.i;
queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
cost[p.center] = 1;
});
while (queue.length) {
const {e, p, province, state} = queue.pop();
cells.c[e].forEach(e => {
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
const totalCost = p + evevation;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land) provinceIds[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.push({e, province, state, p: totalCost}, totalCost);
}
});
}
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
const neibs = cells.c[i]
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
.map(c => provinceIds[c]);
const adversaries = neibs.filter(c => c !== provinceIds[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === provinceIds[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
const max = d3.max(competitors);
if (buddies >= max) continue;
provinceIds[i] = adversaries[competitors.indexOf(max)];
}
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
states.forEach(s => {
if (!s.i || s.removed) return;
if (s.lock && !regenerateLockedStates) return;
if (!s.provinces.length) return;
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
const getColonyName = () => {
if (colonyNamePool.length < 1) return null;
const index = rand(colonyNamePool.length - 1);
const spliced = colonyNamePool.splice(index, 1);
return spliced[0] ? `New ${spliced[0]}` : null;
};
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
while (stateNoProvince.length) {
// add new province
const provinceId = provinces.length;
const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
provinceIds[center] = provinceId;
// expand province
const cost = [];
cost[center] = 1;
queue.push({e: center, p: 0}, 0);
while (queue.length) {
const {e, p} = queue.pop();
cells.c[e].forEach(nextCellId => {
if (provinceIds[nextCellId]) return;
const land = cells.h[nextCellId] >= 20;
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
const totalCost = p + ter;
if (totalCost > max) return;
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
cost[nextCellId] = totalCost;
queue.push({e: nextCellId, p: totalCost}, totalCost);
}
});
}
// generate "wild" province name
const c = cells.culture[center];
const f = pack.features[cells.f[center]];
const color = getMixedColor(s.color);
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
const name = (() => {
const colonyName = colony && P(0.8) && getColonyName();
if (colonyName) return colonyName;
if (burgCell && P(0.5)) return burgs[burg].name;
return Names.getState(Names.getCultureShort(c), c);
})();
const formName = (() => {
if (singleIsle) return "Island";
if (isleGroup) return "Islands";
if (colony) return "Colony";
return rw(forms["Wild"]);
})();
const fullName = name + " " + formName;
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
const kinship = dominion ? 0 : 0.4;
const type = BurgsAndStates.getType(center, burgs[burg]?.port);
const coa = COA.generate(s.coa, kinship, dominion, type);
coa.shield = COA.getShield(c, s.i);
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
s.provinces.push(provinceId);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands
const passableQueue = [from],
used = new Uint8Array(cells.i.length),
state = cells.state[from];
while (passableQueue.length) {
const current = passableQueue.pop();
if (current === to) return true; // way is found
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
passableQueue.push(c);
used[c] = 1;
});
}
return false; // way is not found
}
// re-check
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
}
});
cells.province = provinceIds;
pack.provinces = provinces;
TIME && console.timeEnd("generateProvinces");
};
// calculate pole of inaccessibility for each province
const getPoles = () => {
const getType = cellId => pack.cells.province[cellId];
const poles = getPolesOfInaccessibility(pack, getType);
pack.provinces.forEach(province => {
if (!province.i || province.removed) return;
province.pole = poles[province.i] || [0, 0];
});
};
return {generate, getPoles};
})();

View file

@ -1,128 +0,0 @@
"use strict";
window.ReliefIcons = (function () {
const ReliefIcons = function () {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
const cells = pack.cells;
const density = terrain.attr("density") || 0.4;
const size = 2 * (terrain.attr("size") || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
for (const i of cells.i) {
const height = cells.h[i];
if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers
const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
const polygon = getPackPolygon(i);
const [minX, maxX] = d3.extent(polygon, p => p[0]);
const [minY, maxY] = d3.extent(polygon, p => p[1]);
if (height < 50) placeBiomeIcons(i, biome);
else placeReliefIcons(i);
function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return;
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.2;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function placeReliefIcons(i) {
const radius = 2 / density;
const [icon, h] = getReliefIcon(i, height);
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}
// sort relief icons by y+size
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
let reliefHTML = "";
for (const r of relief) {
reliefHTML += `<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`;
}
terrain.html(reliefHTML);
TIME && console.timeEnd("drawRelief");
};
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow";
return getIcon(type);
}
function getVariant(type) {
switch (type) {
case "mount":
return rand(2, 7);
case "mountSnow":
return rand(1, 6);
case "hill":
return rand(2, 5);
case "conifer":
return 2;
case "coniferSnow":
return 1;
case "swamp":
return rand(2, 3);
case "cactus":
return rand(1, 3);
case "deadTree":
return rand(1, 2);
default:
return 2;
}
}
function getOldIcon(type) {
switch (type) {
case "mountSnow":
return "mount";
case "vulcan":
return "mount";
case "coniferSnow":
return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
}
}
function getIcon(type) {
const set = terrain.attr("set") || "simple";
if (set === "simple") return "#relief-" + getOldIcon(type) + "-1";
if (set === "colored") return "#relief-" + type + "-" + getVariant(type);
if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
return "#relief-" + getOldIcon(type) + "-1"; // simple
}
return ReliefIcons;
})();

View file

@ -457,7 +457,7 @@ window.Religions = (function () {
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
const folkReligions = generateFolkReligions();
const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions);
const organizedReligions = generateOrganizedReligions(+religionsNumber.value, lockedReligions);
const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
const indexedReligions = combineReligions(namedReligions, lockedReligions);
@ -695,23 +695,24 @@ window.Religions = (function () {
const {cells, routes} = pack;
const religionIds = spreadFolkReligions(religions);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const queue = new FlatQueue();
const cost = [];
const maxExpansionCost = (cells.i.length / 20) * neutralInput.value; // limit cost for organized religions growth
// limit cost for organized religions growth
const maxExpansionCost = (cells.i.length / 20) * byId("growthRate").valueAsNumber;
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]});
queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0);
cost[r.center] = 1;
});
const religionsMap = new Map(religions.map(r => [r.i, r]));
while (queue.length) {
const {e: cellId, p, r, s: state} = queue.dequeue();
const {e: cellId, p, r, s: state} = queue.pop();
const {culture, expansion, expansionism} = religionsMap.get(r);
cells.c[cellId].forEach(nextCell => {
@ -731,7 +732,7 @@ window.Religions = (function () {
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost;
queue.queue({e: nextCell, p: totalCost, r, s: state});
queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
}
});
}

View file

@ -0,0 +1,120 @@
"use strict";
function drawBorders() {
TIME && console.time("drawBorders");
const {cells, vertices} = pack;
const statePath = [];
const provincePath = [];
const checked = {};
const isLand = cellId => cells.h[cellId] >= 20;
for (let cellId = 0; cellId < cells.i.length; cellId++) {
if (!cells.state[cellId]) continue;
const provinceId = cells.province[cellId];
const stateId = cells.state[cellId];
// bordering cell of another province
if (provinceId) {
const provToCell = cells.c[cellId].find(neibId => {
const neibProvinceId = cells.province[neibId];
return (
neibProvinceId &&
provinceId > neibProvinceId &&
!checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] &&
cells.state[neibId] === stateId
);
});
if (provToCell !== undefined) {
const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked});
if (border) {
provincePath.push(border);
cellId--; // check the same cell again
continue;
}
}
}
// if cell is on state border
const stateToCell = cells.c[cellId].find(neibId => {
const neibStateId = cells.state[neibId];
return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`];
});
if (stateToCell !== undefined) {
const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked});
if (border) {
statePath.push(border);
cellId--; // check the same cell again
continue;
}
}
}
svg.select("#borders").selectAll("path").remove();
svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
function getBorder({type, fromCell, toCell, addToChecked}) {
const getType = cellId => cells[type][cellId];
const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell);
const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell);
addToChecked(fromCell);
const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i)));
if (startingVertex === undefined) return null;
const checkVertex = vertex =>
vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c));
const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
return null;
}
// connect vertices to chain to form a border
function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) {
let chain = []; // vertices chain to form a path
let next = startingVertex;
const MAX_ITERATIONS = vertices.c.length;
for (let run = 0; run < 2; run++) {
// first run: from any vertex to a border edge
// second run: from found border edge to another edge
chain = [];
for (let i = 0; i < MAX_ITERATIONS; i++) {
const previous = chain.at(-1);
const current = next;
chain.push(current);
const neibCells = vertices.c[current];
neibCells.map(addToChecked);
const [c1, c2, c3] = neibCells.map(checkCell);
const [v1, v2, v3] = vertices.v[current].map(checkVertex);
const [vertex1, vertex2, vertex3] = vertices.v[current];
if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
if (next === current || next === startingVertex) {
if (next === startingVertex) chain.push(startingVertex);
startingVertex = next;
break;
}
}
}
return chain;
}
TIME && console.timeEnd("drawBorders");
}

View file

@ -0,0 +1,69 @@
"use strict";
function drawBurgIcons() {
TIME && console.time("drawBurgIcons");
icons.selectAll("circle, use").remove(); // cleanup
// capitals
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalIcons = burgIcons.select("#cities");
const capitalSize = capitalIcons.attr("size") || 1;
const capitalAnchors = anchors.selectAll("#cities");
const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
capitalIcons
.selectAll("circle")
.data(capitals)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", capitalSize);
capitalAnchors
.selectAll("use")
.data(capitals.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
.attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
.attr("width", capitalAnchorsSize)
.attr("height", capitalAnchorsSize);
// towns
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townIcons = burgIcons.select("#towns");
const townSize = townIcons.attr("size") || 0.5;
const townsAnchors = anchors.selectAll("#towns");
const townsAnchorsSize = townsAnchors.attr("size") || 1;
townIcons
.selectAll("circle")
.data(towns)
.enter()
.append("circle")
.attr("id", d => "burg" + d.i)
.attr("data-id", d => d.i)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", townSize);
townsAnchors
.selectAll("use")
.data(towns.filter(c => c.port))
.enter()
.append("use")
.attr("xlink:href", "#icon-anchor")
.attr("data-id", d => d.i)
.attr("x", d => rn(d.x - townsAnchorsSize * 0.47, 2))
.attr("y", d => rn(d.y - townsAnchorsSize * 0.47, 2))
.attr("width", townsAnchorsSize)
.attr("height", townsAnchorsSize);
TIME && console.timeEnd("drawBurgIcons");
}

View file

@ -0,0 +1,41 @@
"use strict";
function drawBurgLabels() {
TIME && console.time("drawBurgLabels");
burgLabels.selectAll("text").remove(); // cleanup
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
const capitalSize = burgIcons.select("#cities").attr("size") || 1;
burgLabels
.select("#cities")
.selectAll("text")
.data(capitals)
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${capitalSize * -1.5}px`)
.text(d => d.name);
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgLabels
.select("#towns")
.selectAll("text")
.data(towns)
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", `${townSize * -2}px`)
.text(d => d.name);
TIME && console.timeEnd("drawBurgLabels");
}

View file

@ -0,0 +1,129 @@
"use strict";
function drawEmblems() {
TIME && console.time("drawEmblems");
const {states, provinces, burgs} = pack;
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
const getStateEmblemsSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
};
const sizeBurgs = getBurgEmblemSize();
const burgCOAs = validBurgs.map(burg => {
const {x, y} = burg;
const size = burg.coa.size || 1;
const shift = (sizeBurgs * size) / 2;
return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
});
const sizeProvinces = getProvinceEmblemsSize();
const provinceCOAs = validProvinces.map(province => {
const [x, y] = province.pole || pack.cells.p[province.center];
const size = province.coa.size || 1;
const shift = (sizeProvinces * size) / 2;
return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
});
const sizeStates = getStateEmblemsSize();
const stateCOAs = validStates.map(state => {
const [x, y] = state.pole || pack.cells.p[state.center];
const size = state.coa.size || 1;
const shift = (sizeStates * size) / 2;
return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
});
const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
const simulation = d3
.forceSimulation(nodes)
.alphaMin(0.6)
.alphaDecay(0.2)
.velocityDecay(0.6)
.force(
"collision",
d3.forceCollide().radius(d => d.shift)
)
.stop();
d3.timeout(function () {
const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
for (let i = 0; i < n; ++i) {
simulation.tick();
}
const burgNodes = nodes.filter(node => node.type === "burg");
const burgString = burgNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
const provinceNodes = nodes.filter(node => node.type === "province");
const provinceString = provinceNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
const stateNodes = nodes.filter(node => node.type === "state");
const stateString = stateNodes
.map(
d =>
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
d.size
}em"/>`
)
.join("");
emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
invokeActiveZooming();
});
TIME && console.timeEnd("drawEmblems");
}
const getDataAndType = id => {
if (id === "burgEmblems") return [pack.burgs, "burg"];
if (id === "provinceEmblems") return [pack.provinces, "province"];
if (id === "stateEmblems") return [pack.states, "state"];
throw new Error(`Unknown emblem type: ${id}`);
};
async function renderGroupCOAs(g) {
const [data, type] = getDataAndType(g.id);
for (let use of g.children) {
const i = +use.dataset.i;
const id = type + "COA" + i;
COArenderer.trigger(id, data[i].coa);
use.setAttribute("href", "#" + id);
}
}

View file

@ -0,0 +1,66 @@
"use strict";
function drawFeatures() {
TIME && console.time("drawFeatures");
const html = {
paths: [],
landMask: [],
waterMask: ['<rect x="0" y="0" width="100%" height="100%" fill="white" />'],
coastline: {},
lakes: {}
};
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
html.paths.push(`<path d="${getFeaturePath(feature)}" id="feature_${feature.i}" data-f="${feature.i}"></path>`);
if (feature.type === "lake") {
html.landMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="black"></use>`);
const lakeGroup = feature.group || "freshwater";
if (!html.lakes[lakeGroup]) html.lakes[lakeGroup] = [];
html.lakes[lakeGroup].push(`<use href="#feature_${feature.i}" data-f="${feature.i}"></use>`);
} else {
html.landMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="white"></use>`);
html.waterMask.push(`<use href="#feature_${feature.i}" data-f="${feature.i}" fill="black"></use>`);
const coastlineGroup = feature.group === "lake_island" ? "lake_island" : "sea_island";
if (!html.coastline[coastlineGroup]) html.coastline[coastlineGroup] = [];
html.coastline[coastlineGroup].push(`<use href="#feature_${feature.i}" data-f="${feature.i}"></use>`);
}
}
defs.select("#featurePaths").html(html.paths.join(""));
defs.select("#land").html(html.landMask.join(""));
defs.select("#water").html(html.waterMask.join(""));
coastline.selectAll("g").each(function () {
const paths = html.coastline[this.id] || [];
d3.select(this).html(paths.join(""));
});
lakes.selectAll("g").each(function () {
const paths = html.lakes[this.id] || [];
d3.select(this).html(paths.join(""));
});
TIME && console.timeEnd("drawFeatures");
}
function getFeaturePath(feature) {
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
if (points.some(point => point === undefined)) {
ERROR && console.error("Undefined point in getFeaturePath");
return "";
}
const simplifiedPoints = simplify(points, 0.3);
const clippedPoints = clipPoly(simplifiedPoints, 1);
const lineGen = d3.line().curve(d3.curveBasisClosed);
const path = round(lineGen(clippedPoints)) + "Z";
return path;
}

View file

@ -0,0 +1,144 @@
"use strict";
function drawHeightmap() {
TIME && console.time("drawHeightmap");
const ocean = terrs.select("#oceanHeights");
const land = terrs.select("#landHeights");
ocean.selectAll("*").remove();
land.selectAll("*").remove();
const paths = new Array(101);
const {cells, vertices} = grid;
const used = new Uint8Array(cells.i.length);
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
// ocean cells
const renderOceanCells = Boolean(+ocean.attr("data-render"));
if (renderOceanCells) {
const skip = +ocean.attr("skip") + 1 || 1;
const relax = +ocean.attr("relax") || 0;
lineGen.curve(d3[ocean.attr("curve") || "curveBasisClosed"]);
let currentLayer = 0;
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer >= 20) break;
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
const chain = connectVertices(cells, vertices, vertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
// land cells
{
const skip = +land.attr("skip") + 1 || 1;
const relax = +land.attr("relax") || 0;
lineGen.curve(d3[land.attr("curve") || "curveBasisClosed"]);
let currentLayer = 20;
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer > 100) break; // no layers possible with height > 100
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const startVertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
const chain = connectVertices(cells, vertices, startVertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
// render paths
for (const height of d3.range(0, 101)) {
const group = height < 20 ? ocean : land;
const scheme = getColorScheme(group.attr("scheme"));
if (height === 0 && renderOceanCells) {
// draw base ocean layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(1));
}
if (height === 20) {
// draw base land layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(0.8));
}
if (paths[height] && paths[height].length >= 10) {
const terracing = group.attr("terracing") / 10 || 0;
const color = getColor(height, scheme);
if (terracing) {
group
.append("path")
.attr("d", paths[height])
.attr("transform", "translate(.7,1.4)")
.attr("fill", d3.color(color).darker(terracing))
.attr("data-height", height);
}
group.append("path").attr("d", paths[height]).attr("fill", color).attr("data-height", height);
}
}
// connect vertices to chain: specific case for heightmap
function connectVertices(cells, vertices, start, h, used) {
const MAX_ITERATIONS = vertices.c.length;
const n = cells.i.length;
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.h[c] === h).forEach(c => (used[c] = 1));
const c0 = c[0] >= n || cells.h[c[0]] < h;
const c1 = c[1] >= n || cells.h[c[1]] < h;
const c2 = c[2] >= n || cells.h[c[2]] < h;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
return chain;
}
function simplifyLine(chain, simplification) {
if (!simplification) return chain;
const n = simplification + 1; // filter each nth element
return chain.filter((d, i) => i % n === 0);
}
TIME && console.timeEnd("drawHeightmap");
}

View file

@ -0,0 +1,53 @@
"use strict";
function drawMarkers() {
TIME && console.time("drawMarkers");
const rescale = +markers.attr("rescale");
const pinned = +markers.attr("pinned");
const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
const html = markersData.map(marker => drawMarker(marker, rescale));
markers.html(html.join(""));
TIME && console.timeEnd("drawMarkers");
}
// prettier-ignore
const pinShapes = {
bubble: (fill, stroke) => `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`,
pin: (fill, stroke) => `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`,
square: (fill, stroke) => `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`,
squarish: (fill, stroke) => `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`,
diamond: (fill, stroke) => `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`,
hex: (fill, stroke) => `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`,
hexy: (fill, stroke) => `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`,
shieldy: (fill, stroke) => `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`,
shield: (fill, stroke) => `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`,
pentagon: (fill, stroke) => `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`,
heptagon: (fill, stroke) => `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`,
circle: (fill, stroke) => `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`,
no: () => ""
};
const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
const shapeFunction = pinShapes[shape] || pinShapes.bubble;
return shapeFunction(fill, stroke);
};
function drawMarker(marker, rescale = 1) {
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
const id = `marker${i}`;
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
const viewX = rn(x - zoomSize / 2, 1);
const viewY = rn(y - zoomSize, 1);
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
return /* html */ `
<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}">
<g>${getPin(pin, fill, stroke)}</g>
<text x="${dx}%" y="${dy}%" font-size="${px}px" >${isExternal ? "" : icon}</text>
<image x="${dx / 2}%" y="${dy / 2}%" width="${px}px" height="${px}px" href="${isExternal ? icon : ""}" />
</svg>`;
}

View file

@ -0,0 +1,155 @@
"use strict";
function drawMilitary() {
TIME && console.time("drawMilitary");
armies.selectAll("g").remove();
pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i));
TIME && console.timeEnd("drawMilitary");
}
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
const h = size * 2;
const x = d => rn(d.x - w(d) / 2, 2);
const y = d => rn(d.y - size, 2);
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
const army = armies
.append("g")
.attr("id", "army" + s)
.attr("fill", baseColor)
.attr("color", darkerColor);
const g = army
.selectAll("g")
.data(regiments)
.enter()
.append("g")
.attr("id", d => "regiment" + s + "-" + d.i)
.attr("data-name", d => d.name)
.attr("data-state", s)
.attr("data-id", d => d.i)
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
g.append("rect")
.attr("x", d => x(d))
.attr("y", d => y(d))
.attr("width", d => w(d))
.attr("height", h);
g.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("text-rendering", "optimizeSpeed")
.text(d => Military.getTotal(d));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("text-rendering", "optimizeSpeed")
.attr("x", d => x(d) - size)
.attr("y", d => d.y)
.text(d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? "" : d.icon));
g.append("image")
.attr("class", "regimentImage")
.attr("x", d => x(d) - h)
.attr("y", d => y(d))
.attr("height", h)
.attr("width", h)
.attr("href", d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? d.icon : ""));
};
const drawRegiment = function (reg, stateId) {
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = rn(reg.x - w / 2, 2);
const y1 = rn(reg.y - size, 2);
let army = armies.select("g#army" + stateId);
if (!army.size()) {
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
const darkerColor = d3.color(baseColor).darker().hex();
army = armies
.append("g")
.attr("id", "army" + stateId)
.attr("fill", baseColor)
.attr("color", darkerColor);
}
const g = army
.append("g")
.attr("id", "regiment" + stateId + "-" + reg.i)
.attr("data-name", reg.name)
.attr("data-state", stateId)
.attr("data-id", reg.i)
.attr("transform", `rotate(${reg.angle || 0})`)
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
g.append("text")
.attr("x", reg.x)
.attr("y", reg.y)
.attr("text-rendering", "optimizeSpeed")
.text(Military.getTotal(reg));
g.append("rect")
.attr("fill", "currentColor")
.attr("x", x1 - h)
.attr("y", y1)
.attr("width", h)
.attr("height", h);
g.append("text")
.attr("class", "regimentIcon")
.attr("text-rendering", "optimizeSpeed")
.attr("x", x1 - size)
.attr("y", reg.y)
.text(reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? "" : reg.icon);
g.append("image")
.attr("class", "regimentImage")
.attr("x", x1 - h)
.attr("y", y1)
.attr("height", h)
.attr("width", h)
.attr("href", reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? reg.icon : "");
};
// move one regiment to another
const moveRegiment = function (reg, x, y) {
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
if (!el.size()) return;
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
reg.x = x;
reg.y = y;
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
el.select("text").transition(move).attr("x", x).attr("y", y);
el.selectAll("rect:nth-of-type(2)")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y));
el.select(".regimentIcon")
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y)
.attr("height", "6")
.attr("width", "6");
el.select(".regimentImage")
.transition(move)
.attr("x", x1(x) - h)
.attr("y", y1(y))
.attr("height", "6")
.attr("width", "6");
};

View file

@ -0,0 +1,124 @@
"use strict";
function drawReliefIcons() {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
const cells = pack.cells;
const density = terrain.attr("density") || 0.4;
const size = 2 * (terrain.attr("size") || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
for (const i of cells.i) {
const height = cells.h[i];
if (height < 20) continue; // no icons on water
if (cells.r[i]) continue; // no icons on rivers
const biome = cells.biome[i];
if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
const polygon = getPackPolygon(i);
const [minX, maxX] = d3.extent(polygon, p => p[0]);
const [minY, maxY] = d3.extent(polygon, p => p[1]);
if (height < 50) placeBiomeIcons(i, biome);
else placeReliefIcons(i);
function placeBiomeIcons() {
const iconsDensity = biomesData.iconsDensity[biome] / 100;
const radius = 2 / iconsDensity / density;
if (Math.random() > iconsDensity * 10) return;
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biomesData.icons[biome]);
if (icon === "#relief-grass-1") h *= 1.2;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function placeReliefIcons(i) {
const radius = 2 / density;
const [icon, h] = getReliefIcon(i, height);
for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
}
}
function getReliefIcon(i, h) {
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
return [getIcon(type), size];
}
}
// sort relief icons by y+size
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
const reliefHTML = new Array(relief.length);
for (const r of relief) {
reliefHTML.push(`<use href="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`);
}
terrain.html(reliefHTML.join(""));
TIME && console.timeEnd("drawRelief");
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow";
return getIcon(type);
}
function getVariant(type) {
switch (type) {
case "mount":
return rand(2, 7);
case "mountSnow":
return rand(1, 6);
case "hill":
return rand(2, 5);
case "conifer":
return 2;
case "coniferSnow":
return 1;
case "swamp":
return rand(2, 3);
case "cactus":
return rand(1, 3);
case "deadTree":
return rand(1, 2);
default:
return 2;
}
}
function getOldIcon(type) {
switch (type) {
case "mountSnow":
return "mount";
case "vulcan":
return "mount";
case "coniferSnow":
return "conifer";
case "cactus":
return "dune";
case "deadTree":
return "dune";
default:
return type;
}
}
function getIcon(type) {
const set = terrain.attr("set") || "simple";
if (set === "simple") return "#relief-" + getOldIcon(type) + "-1";
if (set === "colored") return "#relief-" + type + "-" + getVariant(type);
if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
return "#relief-" + getOldIcon(type) + "-1"; // simple
}
}

View file

@ -0,0 +1,103 @@
"use strict";
function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
const unit = distanceUnitInput.value;
const size = +scaleBar.attr("data-bar-size");
const length = getLength(scaleLevel, size);
scaleBar.select("#scaleBarContent").remove(); // redraw content every time
const content = scaleBar.append("g").attr("id", "scaleBarContent");
const lines = content.append("g");
lines
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", length + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
lines
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", length + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
lines
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", length + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", size + " " + rn(length / 5 - size, 2))
.attr("stroke", "#3d3d3d");
const texts = content.append("g").attr("text-anchor", "middle").attr("font-family", "var(--serif)");
texts
.selectAll("text")
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.6em")
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
const label = scaleBar.attr("data-label");
if (label) {
texts
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", (length + 1) / 2)
.attr("dy", ".6em")
.attr("dominant-baseline", "text-before-edge")
.text(label);
}
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (scaleBarBack.size()) {
const bbox = content.node().getBBox();
const paddingTop = +scaleBarBack.attr("data-top") || 0;
const paddingLeft = +scaleBarBack.attr("data-left") || 0;
const paddingRight = +scaleBarBack.attr("data-right") || 0;
const paddingBottom = +scaleBarBack.attr("data-bottom") || 0;
scaleBar
.select("#scaleBarBack")
.attr("x", -paddingLeft)
.attr("y", -paddingTop)
.attr("width", bbox.width + paddingRight)
.attr("height", bbox.height + paddingBottom);
}
}
function getLength(scaleLevel) {
const init = 100;
const size = +scaleBar.attr("data-bar-size");
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
if (val > 900) val = rn(val, -3); // round to 1000
else if (val > 90) val = rn(val, -2); // round to 100
else if (val > 9) val = rn(val, -1); // round to 10
else val = rn(val); // round to 1
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
return length;
}
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
const posX = +scaleBar.attr("data-x") || 99;
const posY = +scaleBar.attr("data-y") || 99;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn((fullWidth * posX) / 100 - bbox.width + 10);
const y = rn((fullHeight * posY) / 100 - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}

View file

@ -2,7 +2,7 @@
// list - an optional array of stateIds to regenerate
function drawStateLabels(list) {
console.time("drawStateLabels");
TIME && console.time("drawStateLabels");
// temporary make the labels visible
const layerDisplay = labels.style("display");
@ -14,11 +14,11 @@ function drawStateLabels(list) {
// increase step to 15 or 30 to make it faster and more horyzontal
// decrease step to 5 to improve accuracy
const ANGLE_STEP = 9;
const raycast = precalculateAngles(ANGLE_STEP);
const angles = precalculateAngles(ANGLE_STEP);
const INITIAL_DISTANCE = 10;
const DISTANCE_STEP = 15;
const MAX_ITERATIONS = 100;
const LENGTH_START = 5;
const LENGTH_STEP = 5;
const LENGTH_MAX = 300;
const labelPaths = getLabelPaths();
const letterLength = checkExampleLetterLength();
@ -35,87 +35,27 @@ function drawStateLabels(list) {
if (list && !list.includes(state.i)) continue;
const offset = getOffsetWidth(state.cells);
const maxLakeSize = state.cells / 50;
const maxLakeSize = state.cells / 20;
const [x0, y0] = state.pole;
const offsetPoints = new Map(
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
return [angle, {x, y}];
})
);
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
let distanceMin;
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
if (offset) {
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90);
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
distanceMin = Math.min(distance1, distance2, distance3);
} else {
distanceMin = distance1;
}
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
return {angle, distance: distanceMin * modifier, x, y};
const rays = angles.map(({angle, dx, dy}) => {
const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
return {angle, length, x, y};
});
const [ray1, ray2] = findBestRayPair(rays);
const {
angle,
x: x1,
y: y1
} = distances.reduce(
(acc, {angle, distance, x, y}) => {
if (distance > acc.distance) return {angle, distance, x, y};
return acc;
},
{angle: 0, distance: 0, x: 0, y: 0}
);
const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
if (ray1.x > ray2.x) pathPoints.reverse();
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
const {x: x2, y: y2} = distances.reduce(
(acc, {angle, distance, x, y}) => {
const angleDif = getAnglesDif(angle, oppositeAngle);
const score = distance * getAngleModifier(angleDif);
if (score > acc.score) return {angle, score, x, y};
return acc;
},
{angle: 0, score: 0, x: 0, y: 0}
);
if (DEBUG.stateLabels) {
drawPoint(state.pole, {color: "black", radius: 1});
drawPath(pathPoints, {color: "black", width: 0.2});
}
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
if (x1 > x2) pathPoints.reverse();
labelPaths.push([state.i, pathPoints]);
}
return labelPaths;
function getMaxDistance(stateId, point, dx, dy, maxLakeSize) {
let distance = INITIAL_DISTANCE;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
const cellId = findCell(x, y, DISTANCE_STEP);
// drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
if (!cellId || !isPassable(cellId)) break;
distance += DISTANCE_STEP;
}
return distance;
function isPassable(cellId) {
const feature = features[cells.f[cellId]];
if (feature.type === "lake") return feature.cells <= maxLakeSize;
return stateIds[cellId] === stateId;
}
}
}
function checkExampleLetterLength() {
@ -129,7 +69,7 @@ function drawStateLabels(list) {
function drawLabelPath(letterLength) {
const mode = options.stateLabelsMode || "auto";
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
const lineGen = d3.line().curve(d3.curveNatural);
const textGroup = d3.select("g#labels > g#states");
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
@ -166,6 +106,7 @@ function drawStateLabels(list) {
const textElement = textGroup
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "stateLabel" + stateId)
.append("textPath")
.attr("startOffset", "50%")
@ -192,35 +133,15 @@ function drawStateLabels(list) {
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130);
textElement.setAttribute("font-size", correctedRatio + "%");
}
}
// point offset to reduce label overlap with state borders
function getOffsetWidth(cellsNumber) {
if (cellsNumber < 80) return 0;
if (cellsNumber < 140) return 5;
if (cellsNumber < 200) return 15;
if (cellsNumber < 300) return 20;
if (cellsNumber < 500) return 25;
return 30;
}
// difference between two angles in range [0, 180]
function getAnglesDif(angle1, angle2) {
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
}
// score multiplier based on angle difference betwee left and right sides
function getAngleModifier(angleDif) {
if (angleDif === 0) return 1;
if (angleDif <= 15) return 0.95;
if (angleDif <= 30) return 0.9;
if (angleDif <= 45) return 0.6;
if (angleDif <= 60) return 0.3;
if (angleDif <= 90) return 0.1;
return 0; // >90
if (cellsNumber < 40) return 0;
if (cellsNumber < 200) return 5;
return 10;
}
function precalculateAngles(step) {
@ -228,37 +149,135 @@ function drawStateLabels(list) {
const RAD = Math.PI / 180;
for (let angle = 0; angle < 360; angle += step) {
const x = Math.cos(angle * RAD);
const y = Math.sin(angle * RAD);
const angleDif = 90 - Math.abs((angle % 180) - 90);
const modifier = 1 - angleDif / 120; // [0.25, 1]
angles.push({angle, modifier, x, y});
const dx = Math.cos(angle * RAD);
const dy = Math.sin(angle * RAD);
angles.push({angle, dx, dy});
}
return angles;
}
function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) {
let ray = {length: 0, x: x0, y: y0};
for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) {
const [x, y] = [x0 + length * dx, y0 + length * dy];
// offset points are perpendicular to the ray
const offset1 = [x + -dy * offset, y + dx * offset];
const offset2 = [x + dy * offset, y + -dx * offset];
if (DEBUG.stateLabels) {
drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
}
const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
if (!inState) break;
ray = {length, x, y};
}
return ray;
function isInsideState(x, y) {
if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false;
const cellId = findCell(x, y);
const feature = features[cells.f[cellId]];
if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature);
return stateIds[cellId] === stateId;
}
function isInnerLake(feature) {
return feature.shoreline.every(cellId => stateIds[cellId] === stateId);
}
function isSmallLake(feature) {
return feature.cells <= maxLakeSize;
}
}
function findBestRayPair(rays) {
let bestPair = null;
let bestScore = -Infinity;
for (let i = 0; i < rays.length; i++) {
const score1 = rays[i].length * scoreRayAngle(rays[i].angle);
for (let j = i + 1; j < rays.length; j++) {
const score2 = rays[j].length * scoreRayAngle(rays[j].angle);
const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle);
if (pairScore > bestScore) {
bestScore = pairScore;
bestPair = [rays[i], rays[j]];
}
}
}
return bestPair;
}
function scoreRayAngle(angle) {
const normalizedAngle = Math.abs(angle % 180); // [0, 180]
const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1]
if (horizontality === 1) return 1; // Best: horizontal
if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted
if (horizontality >= 0.5) return 0.6; // Good: moderate slant
if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted
if (horizontality >= 0.15) return 0.2; // Poor: almost vertical
return 0.1; // Very poor: almost vertical
}
function scoreCurvature(angle1, angle2) {
const delta = getAngleDelta(angle1, angle2);
const similarity = evaluateArc(angle1, angle2);
if (delta === 180) return 1; // straight line: best
if (delta < 90) return 0; // acute: not allowed
if (delta < 120) return 0.6 * similarity;
if (delta < 140) return 0.7 * similarity;
if (delta < 160) return 0.8 * similarity;
return similarity;
}
function getAngleDelta(angle1, angle2) {
let delta = Math.abs(angle1 - angle2) % 360;
if (delta > 180) delta = 360 - delta; // [0, 180]
return delta;
}
// compute arc similarity towards x-axis
function evaluateArc(angle1, angle2) {
const proximity1 = Math.abs((angle1 % 180) - 90);
const proximity2 = Math.abs((angle2 % 180) - 90);
return 1 - Math.abs(proximity1 - proximity2) / 90;
}
function getLinesAndRatio(mode, name, fullName, pathLength) {
// short name
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
const lines = splitInTwo(name);
if (mode === "short") return getShortOneLine();
if (pathLength > fullName.length * 2) return getFullOneLine();
return getFullTwoLines();
function getShortOneLine() {
const ratio = pathLength / name.length;
return [[name], minmax(rn(ratio * 60), 50, 150)];
}
function getFullOneLine() {
const ratio = pathLength / fullName.length;
return [[fullName], minmax(rn(ratio * 70), 70, 170)];
}
function getFullTwoLines() {
const lines = splitInTwo(fullName);
const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength;
return [lines, minmax(rn(ratio * 60), 50, 150)];
return [lines, minmax(rn(ratio * 60), 70, 150)];
}
// full name: one line
if (pathLength > fullName.length * 2) {
const lines = [fullName];
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 70), 70, 170)];
}
// full name: two lines
const lines = splitInTwo(fullName);
const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength;
return [lines, minmax(rn(ratio * 60), 70, 150)];
}
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
@ -289,5 +308,5 @@ function drawStateLabels(list) {
return false;
}
console.timeEnd("drawStateLabels");
TIME && console.timeEnd("drawStateLabels");
}

View file

@ -0,0 +1,104 @@
"use strict";
function drawTemperature() {
TIME && console.time("drawTemperature");
temperature.selectAll("*").remove();
lineGen.curve(d3.curveBasisClosed);
const scheme = d3.scaleSequential(d3.interpolateSpectral);
const tMax = +byId("temperatureEquatorOutput").max;
const tMin = +byId("temperatureEquatorOutput").min;
const delta = tMax - tMin;
const {cells, vertices} = grid;
const n = cells.i.length;
const checkedCells = new Uint8Array(n);
const addToChecked = cellId => (checkedCells[cellId] = 1);
const min = d3.min(cells.temp);
const max = d3.max(cells.temp);
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
const isolines = d3.range(min + step, max, step);
const chains = [];
const labels = []; // store label coordinates
for (const cellId of cells.i) {
const t = cells.temp[cellId];
if (checkedCells[cellId] || !isolines.includes(t)) continue;
const startingVertex = findStart(cellId, t);
if (!startingVertex) continue;
checkedCells[cellId] = 1;
const ofSameType = cellId => cells.temp[cellId] >= t;
const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
if (relaxed.length < 6) continue;
const points = relaxed.map(v => vertices.p[v]);
chains.push([t, points]);
addLabel(points, t);
}
// min temp isoline covers all graph
temperature
.append("path")
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
.attr("fill", scheme(1 - (min - tMin) / delta))
.attr("stroke", "none");
for (const t of isolines) {
const path = chains
.filter(c => c[0] === t)
.map(c => round(lineGen(c[1])))
.join("");
if (!path) continue;
const fill = scheme(1 - (t - tMin) / delta),
stroke = d3.color(fill).darker(0.2);
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
}
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
tempLabels
.selectAll("text")
.data(labels)
.enter()
.append("text")
.attr("x", d => d[0])
.attr("y", d => d[1])
.text(d => convertTemperature(d[2]));
// find cell with temp < isotherm and find vertex to start path detection
function findStart(i, t) {
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
}
function addLabel(points, t) {
const xCenter = svgWidth / 2;
// add label on isoline top center
const tc =
points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
pushLabel(tc[0], tc[1], t);
// add label on isoline bottom center
if (points.length > 20) {
const bc =
points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
}
}
function pushLabel(x, y, t) {
if (x < 20 || x > svgWidth - 20) return;
if (y < 20 || y > svgHeight - 20) return;
labels.push([x, y, t]);
}
TIME && console.timeEnd("drawTemperature");
}

367
modules/resample.js Normal file
View file

@ -0,0 +1,367 @@
"use strict";
window.Resample = (function () {
/*
generate new map based on an existing one (resampling parentMap)
parentMap: {grid, pack, notes} from original map
projection: f(Number, Number) -> [Number, Number]
inverse: f(Number, Number) -> [Number, Number]
scale: Number
*/
function process({projection, inverse, scale}) {
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
const riversData = saveRiversData(pack.rivers);
grid = generateGrid();
pack = {};
notes = parentMap.notes;
resamplePrimaryGridData(parentMap, inverse, scale);
Features.markupGrid();
addLakesInDeepDepressions();
openNearSeaLakes();
OceanLayers();
calculateMapCoordinates();
calculateTemperatures();
reGraph();
Features.markupPack();
createDefaultRuler();
restoreCellData(parentMap, inverse, scale);
restoreRivers(riversData, projection, scale);
restoreCultures(parentMap, projection);
restoreBurgs(parentMap, projection, scale);
restoreStates(parentMap, projection);
restoreRoutes(parentMap, projection);
restoreReligions(parentMap, projection);
restoreProvinces(parentMap);
restoreFeatureDetails(parentMap, inverse);
restoreMarkers(parentMap, projection);
restoreZones(parentMap, projection, scale);
showStatistics();
}
function resamplePrimaryGridData(parentMap, inverse, scale) {
grid.cells.h = new Uint8Array(grid.points.length);
grid.cells.temp = new Int8Array(grid.points.length);
grid.cells.prec = new Uint8Array(grid.points.length);
grid.points.forEach(([x, y], newGridCell) => {
const [parentX, parentY] = inverse(x, y);
const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
const parentGridCell = parentMap.pack.cells.g[parentPackCell];
grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
});
if (scale >= 2) smoothHeightmap();
}
function smoothHeightmap() {
grid.cells.h.forEach((height, newGridCell) => {
const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
const meanHeight = d3.mean(heights);
grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
});
}
function restoreCellData(parentMap, inverse, scale) {
pack.cells.biome = new Uint8Array(pack.cells.i.length);
pack.cells.fl = new Uint16Array(pack.cells.i.length);
pack.cells.s = new Int16Array(pack.cells.i.length);
pack.cells.pop = new Float32Array(pack.cells.i.length);
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cells.state = new Uint16Array(pack.cells.i.length);
pack.cells.burg = new Uint16Array(pack.cells.i.length);
pack.cells.religion = new Uint16Array(pack.cells.i.length);
pack.cells.province = new Uint16Array(pack.cells.i.length);
const parentPackCellGroups = groupCellsByType(parentMap.pack);
const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
for (const newPackCell of pack.cells.i) {
const [x, y] = inverse(...pack.cells.p[newPackCell]);
if (isWater(pack, newPackCell)) continue;
const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
const parentCellArea = parentMap.pack.cells.area[parentPackCell];
const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
const scaleRatio = areaRatio / scale;
pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
}
}
function saveRiversData(parentRivers) {
return parentRivers.map(river => {
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
return {...river, meanderedPoints};
});
}
function restoreRivers(riversData, projection, scale) {
pack.cells.r = new Uint16Array(pack.cells.i.length);
pack.cells.conf = new Uint8Array(pack.cells.i.length);
pack.rivers = riversData
.map(river => {
let wasInMap = true;
const points = [];
river.meanderedPoints.forEach(([parentX, parentY]) => {
const [x, y] = projection(parentX, parentY);
const inMap = isInMap(x, y);
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
wasInMap = inMap;
});
if (points.length < 2) return null;
const cells = points.map(point => findCell(...point));
cells.forEach(cellId => {
if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
pack.cells.r[cellId] = river.i;
});
const widthFactor = river.widthFactor * scale;
return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
})
.filter(Boolean);
pack.rivers.forEach(river => {
river.basin = Rivers.getBasin(river.i);
river.length = Rivers.getApproximateLength(river.points);
});
}
function restoreCultures(parentMap, projection) {
const validCultures = new Set(pack.cells.culture);
const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
pack.cultures = parentMap.pack.cultures.map(culture => {
if (!culture.i || culture.removed) return culture;
if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
const [x, y] = [rn(xp, 2), rn(yp, 2)];
const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
const center = findCell(...centerCoords);
return {...culture, center};
});
}
function restoreBurgs(parentMap, projection, scale) {
const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
pack.burgs = parentMap.pack.burgs.map(burg => {
if (!burg.i || burg.removed) return burg;
burg.population *= scale; // adjust for populationRate change
const [xp, yp] = projection(burg.x, burg.y);
if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
const closestCell = findCell(xp, yp);
const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
if (pack.cells.burg[cell]) {
WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
return {...burg, removed: true, lock: false};
}
pack.cells.burg[cell] = burg.i;
const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
return {...burg, cell, x, y};
});
function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
const haven = pack.cells.haven[cell];
if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
if (closestCell !== cell) return pack.cells.p[cell];
return [rn(xp, 2), rn(yp, 2)];
}
}
function restoreStates(parentMap, projection) {
const validStates = new Set(pack.cells.state);
pack.states = parentMap.pack.states.map(state => {
if (!state.i || state.removed) return state;
if (validStates.has(state.i)) return state;
return {...state, removed: true, lock: false};
});
BurgsAndStates.getPoles();
const regimentCellsMap = {};
const VERTICAL_GAP = 8;
pack.states = pack.states.map(state => {
if (!state.i || state.removed) return state;
const capital = pack.burgs[state.capital];
state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
const military = state.military.map(regiment => {
const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
const [xPos, yPos] = projection(regiment.x, regiment.y);
const [xBase, yBase] = projection(regiment.bx, regiment.by);
const [xCell, yCell] = pack.cells.p[cell];
const regsOnCell = regimentCellsMap[cell] || 0;
regimentCellsMap[cell] = regsOnCell + 1;
const name =
isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
const pos = isInMap(xPos, yPos)
? {x: rn(xPos, 2), y: rn(yPos, 2)}
: {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
return {...regiment, cell, name, ...base, ...pos};
});
const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
return {...state, neighbors, military};
});
}
function restoreRoutes(parentMap, projection) {
pack.routes = parentMap.pack.routes
.map(route => {
let wasInMap = true;
const points = [];
route.points.forEach(([parentX, parentY]) => {
const [x, y] = projection(parentX, parentY);
const inMap = isInMap(x, y);
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
wasInMap = inMap;
});
if (points.length < 2) return null;
const bbox = [0, 0, graphWidth, graphHeight];
const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
const firstCell = clipped[0][2];
const feature = pack.cells.f[firstCell];
return {...route, feature, points: clipped};
})
.filter(Boolean);
pack.cells.routes = Routes.buildLinks(pack.routes);
}
function restoreReligions(parentMap, projection) {
const validReligions = new Set(pack.cells.religion);
const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
pack.religions = parentMap.pack.religions.map(religion => {
if (!religion.i || religion.removed) return religion;
if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
const [x, y] = [rn(xp, 2), rn(yp, 2)];
const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
const center = findCell(...centerCoords);
return {...religion, center};
});
}
function restoreProvinces(parentMap) {
const validProvinces = new Set(pack.cells.province);
pack.provinces = parentMap.pack.provinces.map(province => {
if (!province.i || province.removed) return province;
if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
return province;
});
Provinces.getPoles();
pack.provinces.forEach(province => {
if (!province.i || province.removed) return;
const capital = pack.burgs[province.burg];
province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
});
}
function restoreMarkers(parentMap, projection) {
pack.markers = parentMap.pack.markers;
pack.markers.forEach(marker => {
const [x, y] = projection(marker.x, marker.y);
if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
const cell = findCell(x, y);
marker.x = rn(x, 2);
marker.y = rn(y, 2);
marker.cell = cell;
});
}
function restoreZones(parentMap, projection, scale) {
const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
pack.zones = parentMap.pack.zones.map(zone => {
const cells = zone.cells
.map(cellId => {
const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
if (!isInMap(x, y)) return null;
return findAll(x, y, getSearchRadius(cellId));
})
.filter(Boolean)
.flat();
return {...zone, cells: unique(cells)};
});
}
function restoreFeatureDetails(parentMap, inverse) {
pack.features.forEach(feature => {
if (!feature) return;
const [x, y] = pack.cells.p[feature.firstCell];
const [parentX, parentY] = inverse(x, y);
const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
if (parentCell === undefined) return;
const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
if (parentFeature.group) feature.group = parentFeature.group;
if (parentFeature.name) feature.name = parentFeature.name;
if (parentFeature.height) feature.height = parentFeature.height;
});
}
function groupCellsByType(graph) {
return graph.cells.p.reduce(
(acc, [x, y], cellId) => {
const group = isWater(graph, cellId) ? "water" : "land";
acc[group].push([x, y, cellId]);
return acc;
},
{land: [], water: []}
);
}
function isWater(graph, cellId) {
return graph.cells.h[cellId] < 20;
}
function isInMap(x, y) {
return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
}
return {process};
})();

View file

@ -8,6 +8,7 @@ window.Rivers = (function () {
const riversData = {}; // rivers data
const riverParents = {};
const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell);
@ -19,7 +20,7 @@ window.Rivers = (function () {
let riverNext = 1; // first river id is 1
const h = alterHeights();
Lakes.prepareLakeData(h);
Lakes.detectCloseLakes(h);
resolveDepressions(h);
drainWater();
defineRivers();
@ -35,14 +36,12 @@ window.Rivers = (function () {
TIME && console.timeEnd("generateRivers");
function drainWater() {
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30;
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec;
const area = pack.cells.area;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
const lakeOutCells = Lakes.defineClimateData(h);
land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
@ -191,7 +190,15 @@ window.Rivers = (function () {
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
const sourceWidth = getSourceWidth(cells.fl[source]);
const width = getWidth(
getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
pack.rivers.push({
i: riverId,
@ -201,7 +208,7 @@ window.Rivers = (function () {
length,
width,
widthFactor,
sourceWidth: 0,
sourceWidth,
parent,
cells: riverCells
});
@ -307,59 +314,49 @@ window.Rivers = (function () {
// add points at 1/3 and 2/3 of a line between adjacents river cells
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const {fl, conf, h} = pack.cells;
const {fl, h} = pack.cells;
const meandered = [];
const lastStep = riverCells.length - 1;
const points = getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10;
let fluxPrev = 0;
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i];
const isLastCell = i === lastStep;
const [x1, y1] = points[i];
const flux1 = getFlux(i, fl[cell]);
fluxPrev = flux1;
meandered.push([x1, y1, flux1]);
meandered.push([x1, y1, fl[cell]]);
if (isLastCell) break;
const nextCell = riverCells[i + 1];
const [x2, y2] = points[i + 1];
if (nextCell === -1) {
meandered.push([x2, y2, fluxPrev]);
meandered.push([x2, y2, fl[cell]]);
break;
}
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
if (dist2 <= 25 && riverCells.length >= 6) continue;
const flux2 = getFlux(i + 1, fl[nextCell]);
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
const angle = Math.atan2(y2 - y1, x2 - x1);
const sinMeander = Math.sin(angle) * meander;
const cosMeander = Math.cos(angle) * meander;
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
} else if (dist2 > 25 || riverCells.length < 6) {
// if dist is medium or river is small add 1 extra middlepoint
const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (y1 + y2) / 2 + cosMeander;
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
meandered.push([p1x, p1y, p1fl]);
meandered.push([p1x, p1y, 0]);
}
}
@ -386,29 +383,35 @@ window.Rivers = (function () {
};
const FLUX_FACTOR = 500;
const MAX_FLUX_WIDTH = 2;
const MAX_FLUX_WIDTH = 1;
const LENGTH_FACTOR = 200;
const STEP_WIDTH = 1 / LENGTH_FACTOR;
const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
if (pointIndex === 0) return startingWidth;
const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || LENGTH_PROGRESSION.at(-1));
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
};
const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
// build polygon from a list of points and calculated offset (width)
const getRiverPath = function (points, widthFactor, startingWidth = 0) {
const getRiverPath = (points, widthFactor, startingWidth) => {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPointsLeft = [];
const riverPointsRight = [];
let flux = 0;
for (let p = 0; p < points.length; p++) {
const [x0, y0] = points[p - 1] || points[p];
const [x1, y1, flux] = points[p];
const [x2, y2] = points[p + 1] || points[p];
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
const [x1, y1, pointFlux] = points[pointIndex];
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
if (pointFlux > flux) flux = pointFlux;
const offset = getOffset(flux, p, widthFactor, startingWidth);
const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
const angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset;
@ -508,6 +511,7 @@ window.Rivers = (function () {
getBasin,
getWidth,
getOffset,
getSourceWidth,
getApproximateLength,
getRiverPoints,
remove,

View file

@ -1,6 +1,15 @@
const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115;
const MIN_PASSABLE_SEA_TEMP = -4;
const ROUTE_TYPE_MODIFIERS = {
"-1": 1, // coastline
"-2": 1.8, // sea
"-3": 4, // open sea
"-4": 6, // ocean
default: 8 // far ocean
};
window.Routes = (function () {
function generate(lockedRoutes = []) {
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
@ -118,10 +127,9 @@ window.Routes = (function () {
}
function findPathSegments({isWater, connections, start, exit}) {
const from = findPath(isWater, start, exit, connections);
if (!from) return [];
const pathCells = restorePath(start, exit, from);
const getCost = createCostEvaluator({isWater, connections});
const pathCells = findPath(start, current => current === exit, getCost);
if (!pathCells) return [];
const segments = getRouteSegments(pathCells, connections);
return segments;
}
@ -172,29 +180,61 @@ window.Routes = (function () {
return routesMerged > 1 ? mergeRoutes(routes) : routes;
}
}
function buildLinks(routes) {
const links = {};
function createCostEvaluator({isWater, connections}) {
return isWater ? getWaterPathCost : getLandPathCost;
for (const {points, i: routeId} of routes) {
const cells = points.map(p => p[2]);
function getLandPathCost(current, next) {
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
for (let i = 0; i < cells.length - 1; i++) {
const cellId = cells[i];
const nextCellId = cells[i + 1];
const habitability = biomesData.habitability[pack.cells.biome[next]];
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
if (cellId !== nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = routeId;
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
const burgModifier = pack.cells.burg[next] ? 1 : 3;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = routeId;
}
const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
return pathCost;
}
function getWaterPathCost(current, next) {
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
const pathCost = distanceCost * typeModifier * connectionModifier;
return pathCost;
}
}
function buildLinks(routes) {
const links = {};
for (const {points, i: routeId} of routes) {
const cells = points.map(p => p[2]);
for (let i = 0; i < cells.length - 1; i++) {
const cellId = cells[i];
const nextCellId = cells[i + 1];
if (cellId !== nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = routeId;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = routeId;
}
}
return links;
}
return links;
}
function preparePointsArray() {
@ -249,109 +289,6 @@ window.Routes = (function () {
return data; // [[x, y, cell], [x, y, cell]];
}
const MIN_PASSABLE_SEA_TEMP = -4;
const TYPE_MODIFIERS = {
"-1": 1, // coastline
"-2": 1.8, // sea
"-3": 4, // open sea
"-4": 6, // ocean
default: 8 // far ocean
};
function findPath(isWater, start, exit, connections) {
const {temp} = grid.cells;
const {cells} = pack;
const from = [];
const cost = [];
const queue = new FlatQueue();
queue.push(start, 0);
return isWater ? findWaterPath() : findLandPath();
function findLandPath() {
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
for (const neibCellId of cells.c[next]) {
if (neibCellId === exit) {
from[neibCellId] = next;
return from;
}
if (cells.h[neibCellId] < 20) continue; // ignore water cells
const habitability = biomesData.habitability[cells.biome[neibCellId]];
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
const burgModifier = cells.burg[neibCellId] ? 1 : 3;
const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
const totalCost = priority + cellsCost;
if (totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null; // path is not found
}
function findWaterPath() {
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
for (const neibCellId of cells.c[next]) {
if (neibCellId === exit) {
from[neibCellId] = next;
return from;
}
if (cells.h[neibCellId] >= 20) continue; // ignore land cells
if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default;
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
const cellsCost = distanceCost * typeModifier * connectionModifier;
const totalCost = priority + cellsCost;
if (totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null; // path is not found
}
}
function restorePath(start, end, from) {
const cells = [];
let current = end;
let prev = end;
while (current !== start) {
cells.push(current);
prev = from[current];
current = prev;
}
cells.push(current);
return cells;
}
function getRouteSegments(pathCells, connections) {
const segments = [];
let segment = [];
@ -422,21 +359,16 @@ window.Routes = (function () {
// connect cell with routes system by land
function connect(cellId) {
if (isConnected(cellId)) return;
const getCost = createCostEvaluator({isWater: false, connections: new Map()});
const pathCells = findPath(cellId, isConnected, getCost);
if (!pathCells) return;
const {cells, routes} = pack;
const path = findConnectionPath(cellId);
if (!path) return;
const pathCells = restorePath(...path);
const pointsArray = preparePointsArray();
const points = getPoints("trails", pathCells, pointsArray);
const feature = cells.f[cellId];
const routeId = Math.max(...routes.map(route => route.i)) + 1;
const feature = pack.cells.f[cellId];
const routeId = getNextId();
const newRoute = {i: routeId, group: "trails", feature, points};
routes.push(newRoute);
pack.routes.push(newRoute);
for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i];
@ -446,43 +378,6 @@ window.Routes = (function () {
return newRoute;
function findConnectionPath(start) {
const from = [];
const cost = [];
const queue = new FlatQueue();
queue.push(start, 0);
while (queue.length) {
const priority = queue.peekValue();
const next = queue.pop();
for (const neibCellId of cells.c[next]) {
if (isConnected(neibCellId)) {
from[neibCellId] = next;
return [start, neibCellId, from];
}
if (cells.h[neibCellId] < 20) continue; // ignore water cells
const habitability = biomesData.habitability[cells.biome[neibCellId]];
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
const cellsCost = distanceCost * habitabilityModifier * heightModifier;
const totalCost = priority + cellsCost;
if (totalCost >= cost[neibCellId]) continue;
from[neibCellId] = next;
cost[neibCellId] = totalCost;
queue.push(neibCellId, totalCost);
}
}
return null; // path is not found
}
function addConnection(from, to, routeId) {
const routes = pack.cells.routes;
@ -496,7 +391,7 @@ window.Routes = (function () {
// utility functions
function isConnected(cellId) {
const {routes} = pack.cells;
const routes = pack.cells.routes;
return routes[cellId] && Object.keys(routes[cellId]).length > 0;
}
@ -507,22 +402,34 @@ window.Routes = (function () {
function getRoute(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId === undefined ? null : pack.routes[routeId];
if (routeId === undefined) return null;
const route = pack.routes.find(route => route.i === routeId);
if (!route) return null;
return route;
}
function hasRoad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return Object.values(connections).some(routeId => pack.routes[routeId].group === "roads");
return Object.values(connections).some(routeId => {
const route = pack.routes.find(route => route.i === routeId);
if (!route) return false;
return route.group === "roads";
});
}
function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return (
Object.keys(connections).length > 3 ||
Object.values(connections).filter(routeId => pack.routes[routeId].group === "roads").length > 2
);
if (Object.keys(connections).length > 3) return true;
const roadConnections = Object.values(connections).filter(routeId => {
const route = pack.routes.find(route => route.i === routeId);
return route?.group === "roads";
});
return roadConnections.length > 2;
}
// name generator data
@ -706,11 +613,16 @@ window.Routes = (function () {
return path.getTotalLength();
}
function getNextId() {
return pack.routes.length ? Math.max(...pack.routes.map(r => r.i)) + 1 : 0;
}
function remove(route) {
const routes = pack.cells.routes;
for (const point of route.points) {
const from = point[2];
if (!routes[from]) continue;
for (const [to, routeId] of Object.entries(routes[from])) {
if (routeId === route.i) {
@ -721,14 +633,12 @@ window.Routes = (function () {
}
pack.routes = pack.routes.filter(r => r.i !== route.i);
viewbox
.select("#routes")
.select("#route" + route.i)
.remove();
viewbox.select("#route" + route.i).remove();
}
return {
generate,
buildLinks,
connect,
isConnected,
areConnected,
@ -738,6 +648,7 @@ window.Routes = (function () {
generateName,
getPath,
getLength,
getNextId,
remove
};
})();

View file

@ -1,31 +1,26 @@
"use strict";
/*
Cell resampler module used by submapper and resampler (transform)
main function: resample(options);
*/
window.Submap = (function () {
const isWater = (pack, id) => pack.cells.h[id] < 20;
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
function resample(parentMap, options) {
/*
/*
generate new map based on an existing one (resampling parentMap)
parentMap: {seed, grid, pack} from original map
options = {
projection: f(Number,Number)->[Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
projection: f(Number,Number)->[Number, Number]
function to calculate new coordinates
inverse: g(Number,Number)->[Number, Number]
inverse of f
depressRivers: Bool carve out riverbeds?
smoothHeightMap: Bool run smooth filter on heights
addLakesInDepressions: call FMG original funtion on heightmap
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
lockMarkers: Bool Auto lock all copied markers
lockBurgs: Bool Auto lock all copied burgs
}
*/
function resample(parentMap, options) {
const projection = options.projection;
const inverse = options.inverse;
const stage = s => INFO && console.info("SUBMAP:", s);
@ -36,9 +31,7 @@ window.Submap = (function () {
seed = parentMap.seed;
Math.random = aleaPRNG(seed);
INFO && console.group("SubMap with seed: " + seed);
DEBUG && console.info("Using Options:", options);
// create new grid
applyGraphSize();
grid = generateGrid();
@ -53,7 +46,7 @@ window.Submap = (function () {
}
};
stage("Resampling heightmap, temperature and precipitation.");
stage("Resampling heightmap, temperature and precipitation");
// resample heightmap from old WorldState
const n = grid.points.length;
grid.cells.h = new Uint8Array(n); // heightmap
@ -87,7 +80,7 @@ window.Submap = (function () {
}
if (options.depressRivers) {
stage("Generating riverbeds.");
stage("Generating riverbeds");
const rbeds = new Uint16Array(grid.cells.i.length);
// and erode riverbeds
@ -96,7 +89,7 @@ window.Submap = (function () {
if (oldpc < 0) return; // ignore out-of-map marker (-1)
const oldc = parentMap.pack.cells.g[oldpc];
const targetCells = forwardGridMap[oldc];
if (!targetCells) throw "TargetCell shouldn't be empty.";
if (!targetCells) throw "TargetCell shouldn't be empty";
targetCells.forEach(c => {
if (grid.cells.h[c] < 20) return;
rbeds[c] = 1;
@ -110,33 +103,27 @@ window.Submap = (function () {
});
}
stage("Detect features, ocean and generating lakes.");
markFeatures();
markupGridOcean();
stage("Detect features, ocean and generating lakes");
Features.markupGrid();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
addLakesInDeepDepressions();
openNearSeaLakes();
}
addLakesInDeepDepressions();
openNearSeaLakes();
OceanLayers();
calculateMapCoordinates();
// calculateTemperatures();
// generatePrecipitation();
stage("Cell cleanup.");
calculateTemperatures();
generatePrecipitation();
stage("Cell cleanup");
reGraph();
// remove misclassified cells
stage("Define coastline.");
drawCoastline();
stage("Define coastline");
Features.markupPack();
createDefaultRuler();
/****************************************************/
/* Packed Graph */
/****************************************************/
// Packed Graph
const oldCells = parentMap.pack.cells;
// const reverseMap = new Map(); // cellmap from new -> oldcell
const forwardMap = parentMap.pack.cells.p.map(_ => []); // old -> [newcelllist]
const pn = pack.cells.i.length;
@ -147,7 +134,7 @@ window.Submap = (function () {
cells.religion = new Uint16Array(pn);
cells.province = new Uint16Array(pn);
stage("Resampling culture, state and religion map.");
stage("Resampling culture, state and religion map");
for (const [id, gridCellId] of cells.g.entries()) {
const oldGridId = reverseGridMap[gridCellId];
if (oldGridId === undefined) {
@ -206,14 +193,12 @@ window.Submap = (function () {
forwardMap[oldid].push(id);
}
stage("Regenerating river network.");
stage("Regenerating river network");
Rivers.generate();
drawRivers();
Lakes.defineGroup();
// biome calculation based on (resampled) grid.cells.temp and prec
// it's safe to recalculate.
stage("Regenerating Biome.");
stage("Regenerating Biome");
Biomes.define();
// recalculate suitability and population
// TODO: normalize according to the base-map
@ -234,11 +219,11 @@ window.Submap = (function () {
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
});
stage("Porting and locking burgs.");
stage("Porting and locking burgs");
copyBurgs(parentMap, projection, options);
// transfer states, mark states without land as removed.
stage("Porting states.");
stage("Porting states");
const validStates = new Set(pack.cells.state);
pack.states = parentMap.pack.states;
// keep valid states and neighbors only
@ -252,9 +237,10 @@ window.Submap = (function () {
? pack.burgs[s.capital].cell // capital is the best bet
: pack.cells.state.findIndex(x => x === i); // otherwise use the first valid cell
});
BurgsAndStates.getPoles();
// transfer provinces, mark provinces without land as removed.
stage("Porting provinces.");
stage("Porting provinces");
const validProvinces = new Set(pack.cells.province);
pack.provinces = parentMap.pack.provinces;
// mark uneccesary provinces
@ -267,20 +253,15 @@ window.Submap = (function () {
const newCenters = forwardMap[p.center];
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
});
Provinces.getPoles();
BurgsAndStates.drawBurgs();
stage("Regenerating routes network.");
stage("Regenerating routes network");
regenerateRoutes();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
stage("Porting military.");
stage("Porting military");
for (const s of pack.states) {
if (!s.military) continue;
for (const m of s.military) {
@ -291,9 +272,8 @@ window.Submap = (function () {
}
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
}
Military.redraw();
stage("Copying markers.");
stage("Copying markers");
for (const m of pack.markers) {
const [x, y] = projection(m.x, m.y);
if (!inMap(x, y)) {
@ -307,14 +287,12 @@ window.Submap = (function () {
}
if (layerIsOn("toggleMarkers")) drawMarkers();
stage("Redraw emblems.");
drawEmblems();
stage("Regenerating Zones.");
addZones();
stage("Regenerating Zones");
Zones.generate();
Names.getMapName();
stage("Restoring Notes.");
stage("Restoring Notes");
notes = parentMap.notes;
stage("Submap done.");
stage("Submap done");
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
showStatistics();
@ -394,7 +372,7 @@ window.Submap = (function () {
b.removed = true;
return;
}
DEBUG && console.info(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`);
[b.x, b.y] = b.port ? getCloseToEdgePoint(newCell, neighbor) : cells.p[newCell];
if (b.port) b.port = cells.f[neighbor]; // copy feature number
b.cell = newCell;

View file

@ -373,7 +373,7 @@ window.ThreeD = (function () {
}
// icons
if (layerIsOn("toggleIcons")) {
if (layerIsOn("toggleBurgIcons")) {
const geometry = isCity ? city_icon_geometry : town_icon_geometry;
const material = isCity ? city_icon_material : town_icon_material;
const iconMesh = new THREE.Mesh(geometry, material);
@ -444,6 +444,7 @@ window.ThreeD = (function () {
const url = await getMapURL("mesh", {
noLabels: options.labels3d,
noWater: options.extendedWater,
noViewbox: true,
fullMap: true
});
const canvas = document.createElement("canvas");
@ -623,7 +624,7 @@ window.ThreeD = (function () {
material.map = texture;
if (addMesh) addGlobe3dMesh();
};
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true});
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true, noVignette: true});
}
function addGlobe3dMesh() {

228
modules/ui/ai-generator.js Normal file
View file

@ -0,0 +1,228 @@
"use strict";
const PROVIDERS = {
openai: {
keyLink: "https://platform.openai.com/account/api-keys",
generate: generateWithOpenAI
},
anthropic: {
keyLink: "https://console.anthropic.com/account/keys",
generate: generateWithAnthropic
},
ollama: {
keyLink: "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Ollama-text-generation",
generate: generateWithOllama
}
};
const DEFAULT_MODEL = "gpt-4o-mini";
const MODELS = {
"gpt-4o-mini": "openai",
"chatgpt-4o-latest": "openai",
"gpt-4o": "openai",
"gpt-4-turbo": "openai",
o3: "openai",
"o3-mini": "openai",
"o3-pro": "openai",
"o4-mini": "openai",
"claude-opus-4-20250514": "anthropic",
"claude-sonnet-4-20250514": "anthropic",
"claude-3-5-haiku-latest": "anthropic",
"claude-3-5-sonnet-latest": "anthropic",
"claude-3-opus-latest": "anthropic",
"ollama (local models)": "ollama"
};
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
async function generateWithOpenAI({key, model, prompt, temperature, onContent}) {
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${key}`
};
const messages = [
{role: "system", content: SYSTEM_MESSAGE},
{role: "user", content: prompt}
];
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers,
body: JSON.stringify({model, messages, temperature, stream: true})
});
const getContent = json => {
const content = json.choices?.[0]?.delta?.content;
if (content) onContent(content);
};
await handleStream(response, getContent);
}
async function generateWithAnthropic({key, model, prompt, temperature, onContent}) {
const headers = {
"Content-Type": "application/json",
"x-api-key": key,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true"
};
const messages = [{role: "user", content: prompt}];
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers,
body: JSON.stringify({model, system: SYSTEM_MESSAGE, messages, temperature, max_tokens: 4096, stream: true})
});
const getContent = json => {
const content = json.delta?.text;
if (content) onContent(content);
};
await handleStream(response, getContent);
}
async function generateWithOllama({key, model, prompt, temperature, onContent}) {
const ollamaModelName = key; // for Ollama, 'key' is the actual model name entered by the user
const response = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
model: ollamaModelName,
prompt,
system: SYSTEM_MESSAGE,
options: {temperature},
stream: true
})
});
const getContent = json => {
if (json.response) onContent(json.response);
};
await handleStream(response, getContent);
}
async function handleStream(response, getContent) {
if (!response.ok) {
let errorMessage = `Failed to generate (${response.status} ${response.statusText})`;
try {
const json = await response.json();
errorMessage = json.error?.message || json.error || errorMessage;
} catch {}
throw new Error(errorMessage);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (!line) continue;
if (line === "data: [DONE]") break;
try {
const parsed = line.startsWith("data: ") ? JSON.parse(line.slice(6)) : JSON.parse(line);
getContent(parsed);
} catch (error) {
ERROR && console.error("Failed to parse line:", line, error);
}
}
buffer = lines.at(-1);
}
}
function generateWithAi(defaultPrompt, onApply) {
updateValues();
$("#aiGenerator").dialog({
title: "AI Text Generator",
position: {my: "center", at: "center", of: "svg"},
resizable: false,
buttons: {
Generate: function (e) {
generate(e.target);
},
Apply: function () {
const result = byId("aiGeneratorResult").value;
if (!result) return tip("No result to apply", true, "error", 4000);
onApply(result);
$(this).dialog("close");
},
Close: function () {
$(this).dialog("close");
}
}
});
if (modules.generateWithAi) return;
modules.generateWithAi = true;
byId("aiGeneratorKeyHelp").on("click", function (e) {
const model = byId("aiGeneratorModel").value;
const provider = MODELS[model];
openURL(PROVIDERS[provider].keyLink);
});
function updateValues() {
byId("aiGeneratorResult").value = "";
byId("aiGeneratorPrompt").value = defaultPrompt;
byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1";
const select = byId("aiGeneratorModel");
select.options.length = 0;
Object.keys(MODELS).forEach(model => select.options.add(new Option(model, model)));
select.value = localStorage.getItem("fmg-ai-model");
if (!select.value || !MODELS[select.value]) select.value = DEFAULT_MODEL;
const provider = MODELS[select.value];
byId("aiGeneratorKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
}
async function generate(button) {
const key = byId("aiGeneratorKey").value;
if (!key) return tip("Please enter an API key", true, "error", 4000);
const model = byId("aiGeneratorModel").value;
if (!model) return tip("Please select a model", true, "error", 4000);
localStorage.setItem("fmg-ai-model", model);
const provider = MODELS[model];
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
const prompt = byId("aiGeneratorPrompt").value;
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
const temperature = byId("aiGeneratorTemperature").valueAsNumber;
if (isNaN(temperature)) return tip("Temperature must be a number", true, "error", 4000);
localStorage.setItem("fmg-ai-temperature", temperature);
try {
button.disabled = true;
const resultArea = byId("aiGeneratorResult");
resultArea.disabled = true;
resultArea.value = "";
const onContent = content => (resultArea.value += content);
await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
} catch (error) {
return tip(error.message, true, "error", 4000);
} finally {
button.disabled = false;
byId("aiGeneratorResult").disabled = false;
}
}
}

View file

@ -36,45 +36,31 @@ class Battle {
modules.Battle = true;
// add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document
.getElementById("battleType")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document
.getElementById("battleNameShow")
.addEventListener("click", () => Battle.prototype.context.showNameSection());
document
.getElementById("battleNamePlace")
.addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document
.getElementById("battleNameCulture")
.addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document
.getElementById("battleNameRandom")
.addEventListener("click", () => Battle.prototype.context.generateName("random"));
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
document.getElementById("battleRun").addEventListener("click", () => Battle.prototype.context.run());
document.getElementById("battleApply").addEventListener("click", () => Battle.prototype.context.applyResults());
document.getElementById("battleCancel").addEventListener("click", () => Battle.prototype.context.cancelResults());
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
byId("battleType").on("click", ev => this.toggleChange(ev));
byId("battleType").nextElementSibling.on("click", ev => Battle.prototype.context.changeType(ev));
byId("battleNameShow").on("click", () => Battle.prototype.context.showNameSection());
byId("battleNamePlace").on("change", ev => (Battle.prototype.context.place = ev.target.value));
byId("battleNameFull").on("change", ev => Battle.prototype.context.changeName(ev));
byId("battleNameCulture").on("click", () => Battle.prototype.context.generateName("culture"));
byId("battleNameRandom").on("click", () => Battle.prototype.context.generateName("random"));
byId("battleNameHide").on("click", this.hideNameSection);
byId("battleAddRegiment").on("click", this.addSide);
byId("battleRoll").on("click", () => Battle.prototype.context.randomize());
byId("battleRun").on("click", () => Battle.prototype.context.run());
byId("battleApply").on("click", () => Battle.prototype.context.applyResults());
byId("battleCancel").on("click", () => Battle.prototype.context.cancelResults());
byId("battleWiki").on("click", () => wiki("Battle-Simulator"));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
document
.getElementById("battlePhase_attackers")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
document
.getElementById("battlePhase_defenders")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document
.getElementById("battleDie_attackers")
.addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document
.getElementById("battleDie_defenders")
.addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
byId("battlePhase_attackers").on("click", ev => this.toggleChange(ev));
byId("battlePhase_attackers").nextElementSibling.on("click", ev =>
Battle.prototype.context.changePhase(ev, "attackers")
);
byId("battlePhase_defenders").on("click", ev => this.toggleChange(ev));
byId("battlePhase_defenders").nextElementSibling.on("click", ev =>
Battle.prototype.context.changePhase(ev, "defenders")
);
byId("battleDie_attackers").on("click", () => Battle.prototype.context.rollDie("attackers"));
byId("battleDie_defenders").on("click", () => Battle.prototype.context.rollDie("defenders"));
}
defineType() {
@ -97,20 +83,16 @@ class Battle {
}
setType() {
document.getElementById("battleType").className = "icon-button-" + this.type;
byId("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific
? sideSpecific.content
: document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific
? document.getElementById("battlePhases_" + this.type + "_defenders").content
: attackers;
const sideSpecific = byId("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : byId("battlePhases_" + this.type).content;
const defenders = sideSpecific ? byId("battlePhases_" + this.type + "_defenders").content : attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
document.getElementById("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
byId("battlePhase_attackers").nextElementSibling.innerHTML = "";
byId("battlePhase_defenders").nextElementSibling.innerHTML = "";
byId("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
byId("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
}
definePlace() {
@ -149,7 +131,9 @@ class Battle {
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " "));
headers += `<th data-tip="${label}">${u.icon}</th>`;
const isExternal = u.icon.startsWith("http") || u.icon.startsWith("data:image");
const iconHTML = isExternal ? `<img src="${u.icon}" width="15" height="15">` : u.icon;
headers += `<th data-tip="${label}">${iconHTML}</th>`;
}
headers += "<th data-tip='Total military''>Total</th></tr></thead>";
@ -163,9 +147,13 @@ class Battle {
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScale) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const isExternal = regiment.icon.startsWith("http") || regiment.icon.startsWith("data:image");
const iconHtml = isExternal
? `<image href="${regiment.icon}" x="0.1em" y="0.1em" width="1.2em" height="1.2em"></image>`
: `<text x="50%" y="1em" style="text-anchor: middle">${regiment.icon}</text>`;
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>${iconHtml}</svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
@ -200,7 +188,7 @@ class Battle {
}
addSide() {
const body = document.getElementById("regimentSelectorBody");
const body = byId("regimentSelectorBody");
const context = Battle.prototype.context;
const regiments = pack.states
.filter(s => s.military && !s.removed)
@ -246,7 +234,7 @@ class Battle {
});
applySorting(regimentSelectorHeader);
body.addEventListener("click", selectLine);
body.on("click", selectLine);
function selectLine(ev) {
if (ev.target.className === "inactive") {
@ -277,7 +265,7 @@ class Battle {
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x;
regiment.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
});
}
@ -289,15 +277,15 @@ class Battle {
showNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("battleNameSection").style.display = "inline-block";
byId("battleNameSection").style.display = "inline-block";
document.getElementById("battleNamePlace").value = this.place;
document.getElementById("battleNameFull").value = this.name;
byId("battleNamePlace").value = this.place;
byId("battleNameFull").value = this.name;
}
hideNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("battleNameSection").style.display = "none";
byId("battleNameSection").style.display = "none";
}
changeName(ev) {
@ -310,8 +298,8 @@ class Battle {
type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName();
byId("battleNamePlace").value = this.place = place;
byId("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
}
@ -495,7 +483,7 @@ class Battle {
this[side].power =
d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
byId("battlePower_" + side).innerHTML = UIvalue;
}
getInitialMorale() {
@ -509,7 +497,7 @@ class Battle {
}
updateMorale(side) {
const morale = document.getElementById("battleMorale_" + side);
const morale = byId("battleMorale_" + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value;
@ -524,7 +512,7 @@ class Battle {
}
rollDie(side) {
const el = document.getElementById("battleDie_" + side);
const el = byId("battleDie_" + side);
const prev = +el.innerHTML;
do {
el.innerHTML = rand(1, 6);
@ -672,11 +660,11 @@ class Battle {
this.attackers.phase = phase[0];
this.defenders.phase = phase[1];
const buttonA = document.getElementById("battlePhase_attackers");
const buttonA = byId("battlePhase_attackers");
buttonA.className = "icon-button-" + this.attackers.phase;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip;
const buttonD = document.getElementById("battlePhase_defenders");
const buttonD = byId("battlePhase_defenders");
buttonD.className = "icon-button-" + this.defenders.phase;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip;
}
@ -760,7 +748,7 @@ class Battle {
updateTable(side) {
for (const r of this[side].regiments) {
const tbody = document.getElementById("battle" + r.state + "-" + r.i);
const tbody = byId("battle" + r.state + "-" + r.i);
const battleCasualties = tbody.querySelector(".battleCasualties");
const battleSurvivors = tbody.querySelector(".battleSurvivors");
@ -794,7 +782,7 @@ class Battle {
button.style.opacity = 0.5;
div.style.display = "block";
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
document.getElementsByTagName("body")[0].on("click", hideSection, {once: true});
}
changeType(ev) {
@ -811,7 +799,7 @@ class Battle {
changePhase(ev, side) {
if (ev.target.tagName !== "BUTTON") return;
const phase = (this[side].phase = ev.target.dataset.phase);
const button = document.getElementById("battlePhase_" + side);
const button = byId("battlePhase_" + side);
button.className = "icon-button-" + phase;
button.dataset.tip = ev.target.dataset.tip;
this.calculateStrength(side);
@ -873,6 +861,8 @@ class Battle {
r.u = Object.assign({}, r.survivors);
r.a = d3.sum(Object.values(r.u)); // reg total
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box
moveRegiment(r, r.px, r.py); // move regiment back to initial position
}
const i = last(pack.markers)?.i + 1 || 0;
@ -881,7 +871,7 @@ class Battle {
const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: "⚔️", type: "battlefields", dy: 52};
pack.markers.push(marker);
const markerHTML = drawMarker(marker);
document.getElementById("markers").insertAdjacentHTML("beforeend", markerHTML);
byId("markers").insertAdjacentHTML("beforeend", markerHTML);
}
const getSide = (regs, n) =>
@ -909,7 +899,9 @@ class Battle {
cancelResults() {
// move regiments back to initial positions
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
this.attackers.regiments.forEach(r => moveRegiment(r, r.px, r.py));
this.defenders.regiments.forEach(r => moveRegiment(r, r.px, r.py));
$("#battleScreen").dialog("close");
this.cleanData();
}

View file

@ -317,7 +317,7 @@ function editBiomes() {
}
function regenerateIcons() {
ReliefIcons();
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
}
@ -383,7 +383,7 @@ function editBiomes() {
}
function dragBiomeBrush() {
const r = +biomesManuallyBrush.value;
const r = +biomesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
@ -425,7 +425,7 @@ function editBiomes() {
function moveBiomeBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +biomesManuallyBrush.value;
const radius = +biomesBrush.value;
moveCircle(point[0], point[1], radius);
}

View file

@ -2,7 +2,7 @@
function editBurg(id) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = id || d3.event.target.dataset.id;
@ -47,6 +47,7 @@ function editBurg(id) {
byId("burgEmblem").addEventListener("click", openEmblemEdit);
byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
byId("burgLocate").addEventListener("click", zoomIntoBurg);
byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
byId("burglLegend").addEventListener("click", editBurgLegend);
byId("burgLock").addEventListener("click", toggleBurgLockButton);
@ -74,7 +75,8 @@ function editBurg(id) {
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
byId("burgTemperature").innerHTML = convertTemperature(temperature);
byId("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
byId("burgTemperatureLikeIn").dataset.tip =
"Average yearly temperature is like in " + getTemperatureLikeness(temperature);
byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features
@ -228,34 +230,26 @@ function editBurg(id) {
const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}?
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}`;
$("#alert").dialog({
resizable: false,
confirmationDialog({
title: "Remove burg group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#burgEditor").dialog("close");
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
message: `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}?<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}. This action cannot be reverted`,
confirm: "Remove",
onConfirm: () => {
$("#burgEditor").dialog("close");
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector("#burgLabels > #" + group.id);
const iconG = document.querySelector("#burgIcons > #" + group.id);
const anchorG = document.querySelector("#anchors > #" + group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
},
Cancel: function () {
$(this).dialog("close");
if (!basic && !capital) {
const labelG = document.querySelector("#burgLabels > #" + group.id);
const iconG = document.querySelector("#burgIcons > #" + group.id);
const anchorG = document.querySelector("#anchors > #" + group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
}
});
@ -405,6 +399,14 @@ function editBurg(id) {
byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o";
}
function zoomIntoBurg() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const x = burg.x;
const y = burg.y;
zoomTo(x, y, 8, 2000);
}
function toggleRelocateBurg() {
const toggler = byId("toggleCells");
byId("burgRelocate").classList.toggle("pressed");
@ -509,19 +511,13 @@ function editBurg(id) {
}
});
} else {
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
$("#alert").dialog({
resizable: false,
confirmationDialog({
title: "Remove burg",
buttons: {
Remove: function () {
$(this).dialog("close");
removeBurg(id); // see Editors module
$("#burgEditor").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
removeBurg(id); // see Editors module
$("#burgEditor").dialog("close");
}
});
}
@ -535,46 +531,47 @@ function editBurg(id) {
}
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
const meanTempCityMap = {
"-5": "Snag (Yukon)",
"-4": "Yellowknife (Canada)",
"-3": "Okhotsk (Russia)",
"-2": "Fairbanks (Alaska)",
"-1": "Nuuk (Greenland)",
0: "Murmansk (Russia)",
1: "Arkhangelsk (Russia)",
2: "Anchorage (Alaska)",
3: "Tromsø (Norway)",
4: "Reykjavik (Iceland)",
5: "Harbin (China)",
6: "Stockholm (Sweden)",
7: "Montreal (Canada)",
8: "Prague (Czechia)",
9: "Copenhagen (Denmark)",
10: "London (England)",
11: "Antwerp (Belgium)",
12: "Paris (France)",
13: "Milan (Italy)",
14: "Washington (D.C.)",
15: "Rome (Italy)",
16: "Dubrovnik (Croatia)",
17: "Lisbon (Portugal)",
18: "Barcelona (Spain)",
19: "Marrakesh (Morocco)",
20: "Alexandria (Egypt)",
21: "Tegucigalpa (Honduras)",
22: "Guangzhou (China)",
23: "Rio de Janeiro (Brazil)",
24: "Dakar (Senegal)",
25: "Miami (USA)",
26: "Jakarta (Indonesia)",
27: "Mogadishu (Somalia)",
28: "Bangkok (Thailand)",
29: "Niamey (Niger)",
30: "Khartoum (Sudan)"
};
function getTemperatureLikeness(temperature) {
if (temperature < -5) return "Yakutsk";
const cities = [
"Snag (Yukon)",
"Yellowknife (Canada)",
"Okhotsk (Russia)",
"Fairbanks (Alaska)",
"Nuuk (Greenland)",
"Murmansk", // -5 - 0
"Arkhangelsk",
"Anchorage",
"Tromsø",
"Reykjavik",
"Riga",
"Stockholm",
"Halifax",
"Prague",
"Copenhagen",
"London", // 1 - 10
"Antwerp",
"Paris",
"Milan",
"Batumi",
"Rome",
"Dubrovnik",
"Lisbon",
"Barcelona",
"Marrakesh",
"Alexandria", // 11 - 20
"Tegucigalpa",
"Guangzhou",
"Rio de Janeiro",
"Dakar",
"Miami",
"Jakarta",
"Mogadishu",
"Bangkok",
"Aden",
"Khartoum"
]; // 21 - 30
if (temperature > 30) return "Mecca";
return cities[temperature + 5] || null;
if (temperature < -5) return "Yakutsk (Russia)";
if (temperature > 30) return "Mecca (Saudi Arabia)";
return meanTempCityMap[temperature] || null;
}

View file

@ -2,7 +2,7 @@
function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (customization) return;
closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const body = byId("burgsBody");
@ -75,7 +75,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
for (const b of filtered) {
const population = b.population * populationRate * urbanization;
totalPopulation += population;
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
const features = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
const state = pack.states[b.state].name;
const prov = pack.cells.province[b.cell];
const province = prov ? pack.provinces[prov].name : "";
@ -89,7 +89,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
data-province="${province}"
data-culture="${culture}"
data-population=${population}
data-type="${type}"
data-features="${features}"
>
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${
@ -101,15 +101,16 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
${getCultureOptions(b.culture)}
</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)} />
<div class="burgType">
<input data-tip="Burg population. Type to change" value=${si(
population
)} class="burgPopulation" style="width: 5em" />
<div style="width: 3em">
<span
data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}"
class="icon-star-empty${b.capital ? "" : " inactive pointer"}"
></span>
class="icon-star-empty${b.capital ? "" : " inactive pointer"}" style="padding: 0 1px;"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${
b.port ? "" : " inactive"
}" style="font-size:.9em"></span>
}" style="font-size: .9em; padding: 0 1px;"></span>
</div>
<span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${
@ -154,9 +155,9 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
}
function burgHighlightOn(event) {
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
const label = burgLabels.select("[data-id='" + burg + "']");
if (label.size()) label.classed("drag", true);
}
function burgHighlightOff() {
@ -245,7 +246,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
confirmationDialog({
title: "Remove burg",
message: "Are you sure you want to remove the burg? This actiove cannot be reverted",
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
removeBurg(burg);
@ -340,8 +341,8 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
.sum(d => d.population)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value;
const height = 150 + 200 * uiSizeOutput.value;
const width = 150 + 200 * uiSize.value;
const height = 150 + 200 * uiSize.value;
const margin = {top: 0, right: -50, bottom: -10, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
@ -607,7 +608,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
const allLocked = activeBurgs.every(burg => burg.lock);
pack.burgs.forEach(burg => {
activeBurgs.forEach(burg => {
burg.lock = !allLocked;
});

View file

@ -1,5 +1,6 @@
"use strict";
function editCoastline(node = d3.event.target) {
function editCoastline() {
if (customization) return;
closeDialogs(".stable");
if (layerIsOn("toggleCells")) toggleCells();
@ -12,6 +13,7 @@ function editCoastline(node = d3.event.target) {
});
debug.append("g").attr("id", "vertices");
const node = d3.event.target;
elSelected = d3.select(node);
selectCoastlineGroup(node);
drawCoastlineVertices();
@ -21,93 +23,98 @@ function editCoastline(node = d3.event.target) {
modules.editCoastline = true;
// add listeners
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
document.getElementById("coastlineGroup").addEventListener("change", changeCoastlineGroup);
document.getElementById("coastlineGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("coastlineGroupName").addEventListener("change", createNewGroup);
document.getElementById("coastlineGroupRemove").addEventListener("click", removeCoastlineGroup);
document.getElementById("coastlineGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("coastlineEditStyle").addEventListener("click", editGroupStyle);
byId("coastlineGroupsShow").on("click", showGroupSection);
byId("coastlineGroup").on("change", changeCoastlineGroup);
byId("coastlineGroupAdd").on("click", toggleNewGroupInput);
byId("coastlineGroupName").on("change", createNewGroup);
byId("coastlineGroupRemove").on("click", removeCoastlineGroup);
byId("coastlineGroupsHide").on("click", hideGroupSection);
byId("coastlineEditStyle").on("click", editGroupStyle);
function drawCoastlineVertices() {
const f = +elSelected.attr("data-f"); // feature id
const v = pack.features[f].vertices; // coastline outer vertices
const featureId = +elSelected.attr("data-f");
const {vertices, area} = pack.features[featureId];
const l = pack.cells.i.length;
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())].filter(c => c < l);
const cellsNumber = pack.cells.i.length;
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat()).filter(cellId => cellId < cellsNumber);
debug
.select("#vertices")
.selectAll("polygon")
.data(c)
.data(neibCells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("points", getPackPolygon)
.attr("data-c", d => d);
debug
.select("#vertices")
.selectAll("circle")
.data(v)
.data(vertices)
.enter()
.append("circle")
.attr("cx", d => pack.vertices.p[d][0])
.attr("cy", d => pack.vertices.p[d][1])
.attr("r", 0.4)
.attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
.on("mousemove", () =>
tip("Drag to move the vertex. Please use for fine-tuning only. Edit heightmap to change actual cell heights!")
);
const area = pack.features[f].area;
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
}
function dragVertex() {
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
function handleVertexDrag() {
const {vertices, features} = pack;
const x = rn(d3.event.x, 2);
const y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug
.select("#vertices")
.selectAll("polygon")
.attr("points", d => getPackPolygon(d));
redrawCoastline();
const vertexId = d3.select(this).datum();
vertices.p[vertexId] = [x, y];
const featureId = +elSelected.attr("data-f");
const feature = features[featureId];
// change coastline path
defs.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature));
// update area
const points = feature.vertices.map(vertex => vertices.p[vertex]);
feature.area = Math.abs(d3.polygonArea(points));
coastlineArea.innerHTML = si(getArea(feature.area)) + " " + getAreaUnit();
// update cell
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
}
function redrawCoastline() {
lineGen.curve(d3.curveBasisClosed);
const f = +elSelected.attr("data-f");
const vertices = pack.features[f].vertices;
const points = clipPoly(
vertices.map(v => pack.vertices.p[v]),
1
);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_" + f).attr("d", d); // update land mask
defs.select("mask#water > path#water_" + f).attr("d", d); // update water mask
const area = Math.abs(d3.polygonArea(points));
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
function handleVertexDragEnd() {
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleCultures")) drawCultures();
}
function showGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("coastlineGroupsSelection").style.display = "inline-block";
byId("coastlineGroupsSelection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("coastlineGroupsSelection").style.display = "none";
document.getElementById("coastlineGroupName").style.display = "none";
document.getElementById("coastlineGroupName").value = "";
document.getElementById("coastlineGroup").style.display = "inline-block";
byId("coastlineGroupsSelection").style.display = "none";
byId("coastlineGroupName").style.display = "none";
byId("coastlineGroupName").value = "";
byId("coastlineGroup").style.display = "inline-block";
}
function selectCoastlineGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("coastlineGroup");
const select = byId("coastlineGroup");
select.options.length = 0; // remove all options
coastline.selectAll("g").each(function () {
@ -116,7 +123,7 @@ function editCoastline(node = d3.event.target) {
}
function changeCoastlineGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
@ -131,54 +138,44 @@ function editCoastline(node = d3.event.target) {
}
function createNewGroup() {
if (!this.value) {
tip("Please provide a valid group name");
return;
}
if (!this.value) return tip("Please provide a valid group name");
const group = this.value
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
if (byId(group)) return tip("Element with this id already exists. Please provide a unique name", false, "error");
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
return;
}
if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error");
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["sea_island", "lake_island"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
byId("coastlineGroup").selectedOptions[0].remove();
byId("coastlineGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
byId("coastlineGroupName").value = "";
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("coastline").appendChild(newGroup);
byId("coastline").appendChild(newGroup);
newGroup.id = group;
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("coastlineGroup").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
byId("coastlineGroupName").value = "";
}
function removeCoastlineGroup() {
const group = elSelected.node().parentNode.id;
if (["sea_island", "lake_island"].includes(group)) {
tip("This is one of the default groups, it cannot be removed", false, "error");
return;
}
if (["sea_island", "lake_island"].includes(group))
return tip("This is one of the default groups, it cannot be removed", false, "error");
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove the group? All coastline elements of the group (${count}) will be moved under
@ -190,14 +187,14 @@ function editCoastline(node = d3.event.target) {
buttons: {
Remove: function () {
$(this).dialog("close");
const sea = document.getElementById("sea_island");
const groupEl = document.getElementById(group);
const sea = byId("sea_island");
const groupEl = byId(group);
while (groupEl.childNodes.length) {
sea.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").value = "sea_island";
byId("coastlineGroup").selectedOptions[0].remove();
byId("coastlineGroup").value = "sea_island";
},
Cancel: function () {
$(this).dialog("close");

View file

@ -8,33 +8,30 @@ function restoreDefaultEvents() {
svg.call(zoom);
viewbox.style("cursor", "default").on(".drag", null).on("click", clicked).on("touchmove mousemove", onMouseMove);
legend.call(d3.drag().on("start", dragLegendBox));
svg.call(zoom);
}
// on viewbox click event - run function based on target
// handle viewbox click
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement;
const grand = parent.parentElement;
const great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
const parent = el?.parentElement;
const grand = parent?.parentElement;
const great = grand?.parentElement;
const ancestor = great?.parentElement;
if (!ancestor) return;
if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel();
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon();
else if (grand.id === "markers" || great.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline();
else if (grand.id === "lakes") editLake();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
const node = byId("island_" + pack.cells.f[i]);
editCoastline(node);
} else if (grand.id === "lakes") editLake();
}
// clear elSelected variable
@ -182,6 +179,7 @@ function addBurg(point) {
burgLabels
.select("#towns")
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "burgLabel" + i)
.attr("data-id", i)
.attr("x", x)
@ -397,12 +395,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const connections = pack.cells.routes[cell] || {};
const roads = Object.values(connections).filter(routeId => {
const route = pack.routes[routeId];
const roadsNumber = Object.values(pack.cells.routes[cell] || {}).filter(routeId => {
const route = pack.routes.find(route => route.i === routeId);
if (!route) return false;
return route.group === "roads" || route.group === "trails";
}).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated");
tags.push(roadsNumber > 1 ? "highway" : roadsNumber === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
@ -467,6 +465,7 @@ function drawLegend(name, data) {
labels
.append("text")
.attr("text-rendering", "optimizeSpeed")
.text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6)
.attr("y", fontSize / 1.6 + lineHeight + l * lineHeight + vOffset);
@ -477,6 +476,7 @@ function drawLegend(name, data) {
const offset = colOffset + legend.node().getBBox().width / 2;
labels
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "1.2em")
@ -516,13 +516,14 @@ function fitLegendBox() {
// draw legend with the same data, but using different settings
function redrawLegend() {
if (!legend.select("rect").size()) return;
const name = legend.select("#legendLabel").text();
const data = legend
.attr("data")
.split("|")
.map(l => l.split(","));
drawLegend(name, data);
if (legend.select("rect").size()) {
const name = legend.select("#legendLabel").text();
const data = legend
.attr("data")
.split("|")
.map(l => l.split(","));
drawLegend(name, data);
}
}
function dragLegendBox() {
@ -1167,25 +1168,66 @@ function selectIcon(initial, callback) {
const cell = row.insertCell(i % 17);
cell.innerHTML = icons[i];
}
// find external images used as icons and show them
const externalResources = new Set();
const isExternal = url => url.startsWith("http") || url.startsWith("data:image");
options.military.forEach(unit => {
if (isExternal(unit.icon)) externalResources.add(unit.icon);
});
pack.states.forEach(state => {
state?.military?.forEach(regiment => {
if (isExternal(regiment.icon)) externalResources.add(regiment.icon);
});
});
externalResources.forEach(addExternalImage);
}
input.oninput = e => callback(input.value);
input.oninput = () => callback(input.value);
table.onclick = e => {
if (e.target.tagName === "TD") {
input.value = e.target.textContent;
callback(input.value);
}
};
table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.textContent} icon`);
};
function addExternalImage(url) {
const addedIcons = byId("addedIcons");
const image = document.createElement("div");
image.style.cssText = `width: 2.2em; height: 2.2em; background-size: cover; background-image: url(${url})`;
addedIcons.appendChild(image);
image.onclick = () => callback(url);
}
byId("addImage").onclick = function () {
const input = this.previousElementSibling;
const ulr = input.value;
if (!ulr) return tip("Enter image URL to add", false, "error", 4000);
if (!ulr.match(/^((http|https):\/\/)|data\:image\//)) return tip("Enter valid URL", false, "error", 4000);
addExternalImage(ulr);
callback(ulr);
input.value = "";
};
byId("addedIcons")
.querySelectorAll("div")
.forEach(div => {
div.onclick = () => callback(div.style.backgroundImage.slice(5, -2));
});
$("#iconSelector").dialog({
width: fitContent(),
title: "Select Icon",
buttons: {
Apply: function () {
callback(input.value || "");
$(this).dialog("close");
},
Close: function () {
@ -1251,18 +1293,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.00");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.108.1");
Editor.open();
}
async function editCultures() {
if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.96.01");
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.105.23");
Editor.open();
}
async function editReligions() {
if (customization) return;
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.96.00");
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.104.0");
Editor.open();
}

View file

@ -153,21 +153,26 @@ function showMapTooltip(point, e, i, g) {
if (group === "routes") {
const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name;
if (name) return tip(`${name}. Click to edit the Route`);
return tip("Click to edit the Route");
const route = pack.routes.find(route => route.i === routeId);
if (route) {
if (route.name) return tip(`${route.name}. Click to edit the Route`);
return tip("Click to edit the Route");
}
}
if (group === "terrain") return tip("Click to edit the Relief Icon");
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate * urbanization);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
const burgId = +path[path.length - 10].dataset.id;
if (burgId) {
const burg = pack.burgs[burgId];
const population = si(burg.population * populationRate * urbanization);
tip(`${burg.name}. Population: ${population}. Click to edit`);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burgId, 5000);
return;
}
}
if (group === "labels") return tip("Click to edit the Label");
if (group === "markers") return tip("Click to edit the Marker. Hold Shift to not close the assosiated note");
@ -199,18 +204,20 @@ function showMapTooltip(point, e, i, g) {
if (group === "coastline") return tip("Click to edit the coastline");
if (group === "zones") {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
const element = path[path.length - 8];
const zoneId = +element.dataset.id;
const zone = pack.zones.find(zone => zone.i === zoneId);
tip(zone.name);
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zoneId, 5000);
return;
}
if (group === "ice") return tip("Click to edit the Ice");
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
if (layerIsOn("togglePrecipitation") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i));
else if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn("toggleTemperature")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i];
tip("Biome: " + biomesData.name[biome]);
@ -256,10 +263,11 @@ function updateCellInfo(point, i, g) {
const f = cells.f[i];
infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat");
infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon");
infoGeozone.innerHTML = getGeozone(getLatitude(y, 4));
infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoElevation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
@ -283,6 +291,18 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
function getGeozone(latitude) {
if (latitude > 66.5) return "Arctic";
if (latitude > 35) return "Temperate North";
if (latitude > 23.5) return "Subtropical North";
if (latitude > 1) return "Tropical North";
if (latitude > -1) return "Equatorial";
if (latitude > -23.5) return "Tropical South";
if (latitude > -35) return "Subtropical South";
if (latitude > -66.5) return "Temperate South";
return "Antarctic";
}
// convert coordinate to DMS format
function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord));
@ -426,17 +446,17 @@ function highlightEmblemElement(type, el) {
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function (e) {
e.addEventListener("mouseover", function (event) {
e.addEventListener("mouseover", function (e) {
e.stopPropagation();
if (this.className === "icon-lock")
tip("Click to unlock the option and allow it to be randomized on new map generation");
else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
});
e.addEventListener("click", function () {
const id = this.id.slice(5);
if (this.className === "icon-lock") unlock(id);
else lock(id);
const ids = this.dataset.ids ? this.dataset.ids.split(",") : [this.id.slice(5)];
const fn = this.className === "icon-lock" ? unlock : lock;
ids.forEach(fn);
});
});

View file

@ -75,7 +75,8 @@ function editHeightmap(options) {
changeOnlyLand.checked = true;
} else if (mode === "risk") {
defs.selectAll("#land, #water").selectAll("path").remove();
viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove();
defs.select("#featurePaths").selectAll("path").remove();
viewbox.selectAll("#coastline use, #lakes path, #oceanLayers path").remove();
changeOnlyLand.checked = false;
}
@ -90,7 +91,7 @@ function editHeightmap(options) {
if (!sessionStorage.getItem("noExitButtonAnimation")) {
sessionStorage.setItem("noExitButtonAnimation", true);
exitCustomization.style.opacity = 0;
const width = 12 * uiSizeOutput.value * 11;
const width = 12 * uiSize.value * 11;
exitCustomization.style.right = (svgWidth - width) / 2 + "px";
exitCustomization.style.bottom = svgHeight / 2 + "px";
exitCustomization.style.transform = "scale(2)";
@ -111,7 +112,9 @@ function editHeightmap(options) {
layersPreset.value = "heightmap";
layersPreset.disabled = true;
mockHeightmap();
viewbox.on("touchmove mousemove", moveCursor);
svg.on("dblclick.zoom", null);
if (tool === "templateEditor") openTemplateEditor();
else if (tool === "imageConverter") openImageConverter();
@ -136,7 +139,7 @@ function editHeightmap(options) {
return;
}
moveCircle(x, y, brushRadius.valueAsNumber, "#333");
moveCircle(x, y, heightmapBrushRadius.valueAsNumber, "#333");
}
// get user-friendly (real-world) height value from map data
@ -157,11 +160,7 @@ function editHeightmap(options) {
// Exit customization mode
function finalizeHeightmap() {
if (viewbox.select("#heights").selectAll("*").size() < 200)
return tip(
"Insufficient land area! There should be at least 200 land cells to finalize the heightmap",
null,
"error"
);
return tip("Insufficient land area. There should be at least 200 land cells!", null, "error");
if (byId("imageConverter").offsetParent) return tip("Please exit the Image Conversion mode first", null, "error");
delete window.edits; // remove global variable
@ -173,6 +172,7 @@ function editHeightmap(options) {
if (byId("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
layersPreset.disabled = false;
exitCustomization.style.display = "none"; // hide finalize button
restoreDefaultEvents();
clearMainTip();
closeDialogs();
@ -187,6 +187,7 @@ function editHeightmap(options) {
else if (mode === "risk") restoreRiskedData();
// restore initial layers
drawFeatures();
byId("heights").remove();
turnButtonOff("toggleHeight");
document
@ -215,8 +216,7 @@ function editHeightmap(options) {
pack.religions = [];
const erosionAllowed = allowErosion.checked;
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) {
addLakesInDeepDepressions();
openNearSeaLakes();
@ -225,7 +225,7 @@ function editHeightmap(options) {
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
Features.markupPack();
Rivers.generate(erosionAllowed);
@ -237,8 +237,6 @@ function editHeightmap(options) {
}
}
drawRivers();
Lakes.defineGroup();
Biomes.define();
rankCells();
@ -249,19 +247,16 @@ function editHeightmap(options) {
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
Provinces.generate();
Provinces.getPoles();
BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
Military.generate();
Markers.generate();
addZones();
Zones.generate();
TIME && console.timeEnd("regenerateErasedData");
INFO && console.groupEnd("Edit Heightmap");
}
@ -338,14 +333,13 @@ function editHeightmap(options) {
zone.selectAll("*").remove();
});
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
Features.markupPack();
if (erosionAllowed) Rivers.generate(true);
@ -439,13 +433,9 @@ function editHeightmap(options) {
c.center = findCell(c.x, c.y);
}
drawStateLabels();
drawStates();
drawBorders();
if (erosionAllowed) {
Rivers.specify();
Lakes.generateName();
Features.specify();
}
// restore zones from grid
@ -489,10 +479,14 @@ function editHeightmap(options) {
updateHistory();
}
function getColor(value, scheme = getColorScheme()) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// draw or update heightmap
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
viewbox
.select("#heights")
.selectAll("polygon")
@ -500,13 +494,12 @@ function editHeightmap(options) {
.join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + d)
.attr("fill", d => getColor(grid.cells.h[d], scheme));
.attr("fill", d => getColor(grid.cells.h[d]));
}
// draw or update heightmap for a selection of cells
function mockHeightmapSelection(selection) {
const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function (i) {
let cell = viewbox.select("#heights").select("#cell" + i);
@ -518,7 +511,7 @@ function editHeightmap(options) {
.append("polygon")
.attr("points", getGridPolygon(i))
.attr("id", "cell" + i);
cell.attr("fill", getColor(grid.cells.h[i], scheme));
cell.attr("fill", getColor(grid.cells.h[i]));
});
}
@ -664,7 +657,7 @@ function editHeightmap(options) {
const fromCell = +lineCircle.attr("data-cell");
debug.selectAll("*").remove();
const power = byId("linePower").valueAsNumber;
const power = byId("heightmapLinePower").valueAsNumber;
if (power === 0) return tip("Power should not be zero", false, "error");
const heights = grid.cells.h;
@ -686,7 +679,7 @@ function editHeightmap(options) {
}
function dragBrush() {
const r = brushRadius.valueAsNumber;
const r = heightmapBrushRadius.valueAsNumber;
const [x, y] = d3.mouse(this);
const start = findGridCell(x, y, grid);
@ -704,7 +697,7 @@ function editHeightmap(options) {
}
function changeHeightForSelection(selection, start) {
const power = brushPower.valueAsNumber;
const power = heightmapBrushPower.valueAsNumber;
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
@ -1349,7 +1342,7 @@ function editHeightmap(options) {
return lum | 0; // land
};
const scheme = d3.range(101).map(i => getColor(i, color()));
const scheme = d3.range(101).map(i => getColor(i));
const hues = scheme.map(rgb => d3.hsl(rgb).h | 0);
const getHeightByScheme = function (color) {
let height = scheme.indexOf(color);

View file

@ -18,10 +18,9 @@ function handleKeyup(event) {
event.stopPropagation();
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
const {code, key, ctrlKey, metaKey, shiftKey} = event;
const ctrl = ctrlKey || metaKey || key === "Control";
const shift = shiftKey || key === "Shift";
const alt = altKey || key === "Alt";
if (code === "F1") showInfo();
else if (code === "F2") regeneratePrompt();
@ -30,7 +29,7 @@ function handleKeyup(event) {
else if (code === "Tab") toggleOptions(event);
else if (code === "Escape") closeAllDialogs();
else if (code === "Delete") removeElementOnKey();
else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
else if (code === "KeyO" && byId("canvas3d")) toggle3dOptions();
else if (ctrl && code === "KeyQ") toggleSaveReminder();
else if (ctrl && code === "KeyS") saveMap("machine");
else if (ctrl && code === "KeyC") saveMap("dropbox");
@ -60,11 +59,6 @@ function handleKeyup(event) {
else if (key === "#") toggleAddRiver();
else if (key === "$") createRoute();
else if (key === "%") toggleAddMarker();
else if (alt && code === "KeyB") console.table(pack.burgs);
else if (alt && code === "KeyS") console.table(pack.states);
else if (alt && code === "KeyC") console.table(pack.cultures);
else if (alt && code === "KeyR") console.table(pack.religions);
else if (alt && code === "KeyF") console.table(pack.features);
else if (code === "KeyX") toggleTexture();
else if (code === "KeyH") toggleHeight();
else if (code === "KeyB") toggleBiomes();
@ -81,13 +75,13 @@ function handleKeyup(event) {
else if (code === "KeyD") toggleBorders();
else if (code === "KeyR") toggleReligions();
else if (code === "KeyU") toggleRoutes();
else if (code === "KeyT") toggleTemp();
else if (code === "KeyT") toggleTemperature();
else if (code === "KeyN") togglePopulation();
else if (code === "KeyJ") toggleIce();
else if (code === "KeyA") togglePrec();
else if (code === "KeyA") togglePrecipitation();
else if (code === "KeyY") toggleEmblems();
else if (code === "KeyL") toggleLabels();
else if (code === "KeyI") toggleIcons();
else if (code === "KeyI") toggleBurgIcons();
else if (code === "KeyM") toggleMilitary();
else if (code === "KeyK") toggleMarkers();
else if (code === "Equal" && !customization) toggleRulers();
@ -123,24 +117,21 @@ function allowHotkeys() {
function handleSizeChange(key) {
let brush = null;
if (document.getElementById("brushRadius")?.offsetParent) brush = document.getElementById("brushRadius");
else if (document.getElementById("linePower")?.offsetParent) brush = document.getElementById("linePower");
else if (document.getElementById("biomesManuallyBrush")?.offsetParent)
brush = document.getElementById("biomesManuallyBrush");
else if (document.getElementById("statesManuallyBrush")?.offsetParent)
brush = document.getElementById("statesManuallyBrush");
else if (document.getElementById("provincesManuallyBrush")?.offsetParent)
brush = document.getElementById("provincesManuallyBrush");
else if (document.getElementById("culturesManuallyBrush")?.offsetParent)
brush = document.getElementById("culturesManuallyBrush");
else if (document.getElementById("zonesBrush")?.offsetParent) brush = document.getElementById("zonesBrush");
else if (document.getElementById("religionsManuallyBrush")?.offsetParent)
brush = document.getElementById("religionsManuallyBrush");
if (byId("heightmapBrushRadius")?.offsetParent) brush = byId("heightmapBrushRadius");
else if (byId("heightmapLinePower")?.offsetParent) brush = byId("heightmapLinePower");
else if (byId("biomesBrush")?.offsetParent) brush = byId("biomesBrush");
else if (byId("culturesBrush")?.offsetParent) brush = byId("culturesBrush");
else if (byId("statesBrush")?.offsetParent) brush = byId("statesBrush");
else if (byId("provincesBrush")?.offsetParent) brush = byId("provincesBrush");
else if (byId("religionsBrush")?.offsetParent) brush = byId("religionsBrush");
else if (byId("zonesBrush")?.offsetParent) brush = byId("zonesBrush");
if (brush) {
const change = key === "-" ? -5 : 5;
const value = minmax(+brush.value + change, +brush.min, +brush.max);
brush.value = document.getElementById(brush.id + "Number").value = value;
const min = +brush.getAttribute("min") || 5;
const max = +brush.getAttribute("max") || 100;
const value = +brush.value + change;
brush.value = minmax(value, min, max);
return;
}

View file

@ -26,28 +26,32 @@ function editLabel() {
modules.editLabel = true;
// add listeners
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("labelGroupSelect").addEventListener("click", changeGroup);
document.getElementById("labelGroupInput").addEventListener("change", createNewGroup);
document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput);
document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup);
byId("labelGroupShow").on("click", showGroupSection);
byId("labelGroupHide").on("click", hideGroupSection);
byId("labelGroupSelect").on("click", changeGroup);
byId("labelGroupInput").on("change", createNewGroup);
byId("labelGroupNew").on("click", toggleNewGroupInput);
byId("labelGroupRemove").on("click", removeLabelsGroup);
document.getElementById("labelTextShow").addEventListener("click", showTextSection);
document.getElementById("labelTextHide").addEventListener("click", hideTextSection);
document.getElementById("labelText").addEventListener("input", changeText);
document.getElementById("labelTextRandom").addEventListener("click", generateRandomName);
byId("labelTextShow").on("click", showTextSection);
byId("labelTextHide").on("click", hideTextSection);
byId("labelText").on("input", changeText);
byId("labelTextRandom").on("click", generateRandomName);
document.getElementById("labelEditStyle").addEventListener("click", editGroupStyle);
byId("labelEditStyle").on("click", editGroupStyle);
document.getElementById("labelSizeShow").addEventListener("click", showSizeSection);
document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection);
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
byId("labelSizeShow").on("click", showSizeSection);
byId("labelSizeHide").on("click", hideSizeSection);
byId("labelStartOffset").on("input", changeStartOffset);
byId("labelRelativeSize").on("input", changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
byId("labelLetterSpacingShow").on("click", showLetterSpacingSection);
byId("labelLetterSpacingHide").on("click", hideLetterSpacingSection);
byId("labelLetterSpacingSize").on("input", changeLetterSpacingSize);
byId("labelAlign").on("click", editLabelAlign);
byId("labelLegend").on("click", editLabelLegend);
byId("labelRemoveSingle").on("click", removeLabel);
function showEditorTips() {
showMainTip();
@ -62,12 +66,12 @@ function editLabel() {
const group = text.parentNode.id;
if (group === "states" || group === "burgLabels") {
document.getElementById("labelGroupShow").style.display = "none";
byId("labelGroupShow").style.display = "none";
return;
}
hideGroupSection();
const select = document.getElementById("labelGroupSelect");
const select = byId("labelGroupSelect");
select.options.length = 0; // remove all options
labels.selectAll(":scope > g").each(function () {
@ -78,17 +82,17 @@ function editLabel() {
}
function updateValues(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")]
.map(tspan => tspan.textContent)
.join("|");
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
byId("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0;
byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize);
}
function drawControlPointsAndLine() {
debug.select("#controlPoints").remove();
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
const path = document.getElementById("textPath_" + elSelected.attr("id"));
const path = byId("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength();
if (!l) return;
@ -117,8 +121,8 @@ function editLabel() {
}
function redrawLabelPath() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1));
const path = byId("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveNatural);
const points = [];
debug
.select("#controlPoints")
@ -188,19 +192,19 @@ function editLabel() {
function showGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelGroupSection").style.display = "inline-block";
byId("labelGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = "";
document.getElementById("labelGroupSelect").style.display = "inline-block";
byId("labelGroupSection").style.display = "none";
byId("labelGroupInput").style.display = "none";
byId("labelGroupInput").value = "";
byId("labelGroupSelect").style.display = "inline-block";
}
function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
@ -224,7 +228,7 @@ function editLabel() {
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
@ -237,22 +241,22 @@ function editLabel() {
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) {
document.getElementById("labelGroupSelect").selectedOptions[0].remove();
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
byId("labelGroupSelect").selectedOptions[0].remove();
byId("labelGroupSelect").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
byId("labelGroupInput").value = "";
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("labels").appendChild(newGroup);
byId("labels").appendChild(newGroup);
newGroup.id = group;
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("labelGroupSelect").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
byId("labelGroupInput").value = "";
}
function removeLabelsGroup() {
@ -275,7 +279,7 @@ function editLabel() {
.select("#" + group)
.selectAll("text")
.each(function () {
document.getElementById("textPath_" + this.id).remove();
byId("textPath_" + this.id).remove();
this.remove();
});
if (!basic) labels.select("#" + group).remove();
@ -289,16 +293,16 @@ function editLabel() {
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelTextSection").style.display = "inline-block";
byId("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none";
byId("labelTextSection").style.display = "none";
}
function changeText() {
const input = document.getElementById("labelText").value;
const input = byId("labelText").value;
const el = elSelected.select("textPath").node();
const lines = input.split("|");
@ -323,7 +327,7 @@ function editLabel() {
const culture = pack.cells.culture[cell];
name = Names.getCulture(culture);
}
document.getElementById("labelText").value = name;
byId("labelText").value = name;
changeText();
}
@ -334,12 +338,22 @@ function editLabel() {
function showSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelSizeSection").style.display = "inline-block";
byId("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelSizeSection").style.display = "none";
byId("labelSizeSection").style.display = "none";
}
function showLetterSpacingSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
byId("labelLetterSpacingSection").style.display = "inline-block";
}
function hideLetterSpacingSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
byId("labelLetterSpacingSection").style.display = "none";
}
function changeStartOffset() {
@ -353,6 +367,12 @@ function editLabel() {
changeText();
}
function changeLetterSpacingSize() {
elSelected.select("textPath").attr("letter-spacing", this.value + "px");
tip("Label letter-spacing size: " + this.value + "px");
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];

View file

@ -15,7 +15,7 @@ function editLake() {
debug.append("g").attr("id", "vertices");
elSelected = d3.select(node);
updateLakeValues();
selectLakeGroup(node);
selectLakeGroup();
drawLakeVertices();
viewbox.on("touchmove mousemove", null);
@ -23,17 +23,15 @@ function editLake() {
modules.editLake = true;
// add listeners
document.getElementById("lakeName").addEventListener("input", changeName);
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup);
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup);
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup);
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
byId("lakeName").on("input", changeName);
byId("lakeNameCulture").on("click", generateNameCulture);
byId("lakeNameRandom").on("click", generateNameRandom);
byId("lakeGroup").on("change", changeLakeGroup);
byId("lakeGroupAdd").on("click", toggleNewGroupInput);
byId("lakeGroupName").on("change", createNewGroup);
byId("lakeGroupRemove").on("click", removeLakeGroup);
byId("lakeEditStyle").on("click", editGroupStyle);
byId("lakeLegend").on("click", editLakeLegend);
function getLake() {
const lakeId = +elSelected.attr("data-f");
@ -41,85 +39,91 @@ function editLake() {
}
function updateLakeValues() {
const cells = pack.cells;
const {cells, vertices, rivers} = pack;
const l = getLake();
document.getElementById("lakeName").value = l.name;
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
byId("lakeName").value = l.name;
byId("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const length = d3.polygonLength(l.vertices.map(v => vertices.p[v]));
byId("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]);
document.getElementById("lakeElevation").value = getHeight(l.height);
document.getElementById("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
byId("lakeElevation").value = getHeight(l.height);
byId("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
byId("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
document.getElementById("lakeFlux").value = l.flux;
document.getElementById("lakeEvaporation").value = l.evaporation;
byId("lakeFlux").value = l.flux;
byId("lakeEvaporation").value = l.evaporation;
const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no";
document.getElementById("lakeInlets").value = inlets ? inlets.length : "no";
document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : "";
document.getElementById("lakeOutlet").value = outlet;
const inlets = l.inlets && l.inlets.map(inlet => rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? rivers.find(river => river.i === l.outlet)?.name : "no";
byId("lakeInlets").value = inlets ? inlets.length : "no";
byId("lakeInlets").title = inlets ? inlets.join(", ") : "";
byId("lakeOutlet").value = outlet;
}
function drawLakeVertices() {
const v = getLake().vertices; // lake outer vertices
const vertices = getLake().vertices;
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())];
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat());
debug
.select("#vertices")
.selectAll("polygon")
.data(c)
.data(neibCells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("points", getPackPolygon)
.attr("data-c", d => d);
debug
.select("#vertices")
.selectAll("circle")
.data(v)
.data(vertices)
.enter()
.append("circle")
.attr("cx", d => pack.vertices.p[d][0])
.attr("cy", d => pack.vertices.p[d][1])
.attr("r", 0.4)
.attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex))
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
.on("mousemove", () =>
tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")
tip("Drag to move the vertex. Please use for fine-tuning only! Edit heightmap to change actual cell heights")
);
}
function dragVertex() {
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
function handleVertexDrag() {
const x = rn(d3.event.x, 2);
const y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug
.select("#vertices")
.selectAll("polygon")
.attr("points", d => getPackPolygon(d));
redrawLake();
const vertexId = d3.select(this).datum();
pack.vertices.p[vertexId] = [x, y];
const feature = getLake();
// update lake path
defs.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature));
// update area
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
feature.area = Math.abs(d3.polygonArea(points));
byId("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
// update cell
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
}
function redrawLake() {
lineGen.curve(d3.curveBasisClosed);
const feature = getLake();
const points = feature.vertices.map(v => pack.vertices.p[v]);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_" + feature.i).attr("d", d); // update land mask
feature.area = Math.abs(d3.polygonArea(points));
document.getElementById("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
function handleVertexDragEnd() {
if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleCultures")) drawCultures();
}
function changeName() {
@ -136,18 +140,18 @@ function editLake() {
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
}
function selectLakeGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("lakeGroup");
select.options.length = 0; // remove all options
function selectLakeGroup() {
const lake = getLake();
const select = byId("lakeGroup");
select.options.length = 0; // remove all options
lakes.selectAll("g").each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
select.options.add(new Option(this.id, this.id, false, this.id === lake.group));
});
}
function changeLakeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
getLake().group = this.value;
}
@ -172,7 +176,7 @@ function editLake() {
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
@ -186,23 +190,23 @@ function editLake() {
const oldGroup = elSelected.node().parentNode;
const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
byId("lakeGroup").selectedOptions[0].remove();
byId("lakeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
byId("lakeGroupName").value = "";
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("lakes").appendChild(newGroup);
byId("lakes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("lakeGroup").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
byId("lakeGroupName").value = "";
}
function removeLakeGroup() {
@ -221,14 +225,14 @@ function editLake() {
buttons: {
Remove: function () {
$(this).dialog("close");
const freshwater = document.getElementById("freshwater");
const groupEl = document.getElementById(group);
const freshwater = byId("freshwater");
const groupEl = byId(group);
while (groupEl.childNodes.length) {
freshwater.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").value = "freshwater";
byId("lakeGroup").selectedOptions[0].remove();
byId("lakeGroup").value = "freshwater";
},
Cancel: function () {
$(this).dialog("close");

File diff suppressed because it is too large Load diff

View file

@ -8,25 +8,24 @@ function editMarker(markerI) {
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
if (byId("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerType = byId("markerType");
const markerIconSelect = byId("markerIconSelect");
const markerIconSize = byId("markerIconSize");
const markerIconShiftX = byId("markerIconShiftX");
const markerIconShiftY = byId("markerIconShiftY");
const markerSize = byId("markerSize");
const markerPin = byId("markerPin");
const markerFill = byId("markerFill");
const markerStroke = byId("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
const markerNotes = byId("markerNotes");
const markerLock = byId("markerLock");
const addMarker = byId("addMarker");
const markerAdd = byId("markerAdd");
const markerRemove = byId("markerRemove");
updateInputs();
@ -39,8 +38,7 @@ function editMarker(markerI) {
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSelect, "click", changeMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
@ -61,7 +59,7 @@ function editMarker(markerI) {
return [element, marker];
}
const element = document.getElementById(`marker${markerI}`);
const element = byId(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
@ -97,19 +95,20 @@ function editMarker(markerI) {
}
function updateInputs() {
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
byId("markerIcon").innerHTML = marker.icon.startsWith("http") || marker.icon.startsWith("data:image")
? `<img src="${marker.icon}" style="width: 1em; height: 1em;">`
: marker.icon;
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerType.value = marker.type || "";
markerIconSize.value = marker.px || 12;
markerIconShiftX.value = marker.dx || 50;
markerIconShiftY.value = marker.dy || 50;
markerSize.value = marker.size || 30;
markerPin.value = marker.pin || "bubble";
markerFill.value = marker.fill || "#ffffff";
markerStroke.value = marker.stroke || "#000000";
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
markerLock.className = marker.lock ? "icon-lock" : "icon-lock-open";
}
function changeMarkerType() {
@ -117,18 +116,12 @@ function editMarker(markerI) {
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
}
selectIcon(marker.icon, value => {
const isExternal = value.startsWith("http") || value.startsWith("data:image");
byId("markerIcon").innerHTML = isExternal ? `<img src="${value}" style="width: 1em; height: 1em;">` : value;
function selectMarkerIcon() {
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
marker.icon = value;
redrawIcon(marker);
});
});
@ -165,7 +158,7 @@ function editMarker(markerI) {
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
const el = !hidden && byId(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
@ -201,12 +194,23 @@ function editMarker(markerI) {
}
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
const iconText = !hidden && document.querySelector(`#marker${i} > text`);
if (iconText) {
iconText.innerHTML = isExternal ? "" : icon;
iconText.setAttribute("x", dx + "%");
iconText.setAttribute("y", dy + "%");
iconText.setAttribute("font-size", px + "px");
}
const iconImage = !hidden && document.querySelector(`#marker${i} > image`);
if (iconImage) {
iconImage.setAttribute("x", dx / 2 + "%");
iconImage.setAttribute("y", dy / 2 + "%");
iconImage.setAttribute("width", px + "px");
iconImage.setAttribute("height", px + "px");
iconImage.setAttribute("href", isExternal ? icon : "");
}
}
@ -241,10 +245,10 @@ function editMarker(markerI) {
}
function deleteMarker() {
Markers.deleteMarker(marker.i)
Markers.deleteMarker(marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {

View file

@ -69,18 +69,24 @@ function overviewMarkers() {
function addLines() {
const lines = pack.markers
.map(({i, type, icon, pinned, lock}) => {
return `<div class="states" data-i=${i} data-type="${type}">
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${
pinned ? "" : "inactive"
}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${
lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove marker" class="icon-trash-empty"></span>
</div>`;
return /* html */ `
<div class="states" data-i=${i} data-type="${type}">
${
icon.startsWith("http") || icon.startsWith("data:image")
? `<img src="${icon}" data-tip="Marker icon" style="width:1.2em; height:1.2em; vertical-align: middle;">`
: `<span data-tip="Marker icon" style="width:1.2em">${icon}</span>`
}
<div data-tip="Marker type" style="width:10em">${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${
pinned ? "" : "inactive"
}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${
lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove marker" class="icon-trash-empty"></span>
</div>`;
})
.join("");
@ -208,15 +214,15 @@ function overviewMarkers() {
const body = pack.markers.map(marker => {
const {i, type, icon, x, y} = marker;
const id = `marker${i}`;
const note = notes.find(note => note.id === id);
const note = notes.find(note => note.id === "marker" + i);
const name = note ? quote(note.name) : "Unknown";
const legend = note ? quote(note.legend) : "";
const lat = getLatitude(y, 2);
const lon = getLongitude(x, 2);
return [id, type, icon, name, legend, x, y, lat, lon].join(",");
return [i, type, icon, name, legend, x, y, lat, lon].join(",");
});
const data = headers + body.join("\n");

View file

@ -530,3 +530,32 @@ class Planimeter extends Measurer {
this.el.select("text").attr("x", c[0]).attr("y", c[1]).text(area);
}
}
function createDefaultRuler() {
TIME && console.time("createDefaultRuler");
const {features, vertices} = pack;
const areas = features.map(f => (f.land ? f.area || 0 : -Infinity));
const largestLand = areas.indexOf(Math.max(...areas));
const featureVertices = features[largestLand].vertices;
const MIN_X = 100;
const MAX_X = graphWidth - 100;
const MIN_Y = 100;
const MAX_Y = graphHeight - 100;
let leftmostVertex = [graphWidth - MIN_X, graphHeight / 2];
let rightmostVertex = [MIN_X, graphHeight / 2];
for (const vertex of featureVertices) {
const [x, y] = vertices.p[vertex];
if (y < MIN_Y || y > MAX_Y) continue;
if (x < leftmostVertex[0] && x >= MIN_X) leftmostVertex = [x, y];
if (x > rightmostVertex[0] && x <= MAX_X) rightmostVertex = [x, y];
}
rulers = new Rulers();
rulers.create(Ruler, [leftmostVertex, rightmostVertex]);
TIME && console.timeEnd("createDefaultRuler");
}

View file

@ -284,7 +284,14 @@ function overviewMilitary() {
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.textContent, v => (el.textContent = v));
if (type === "icon") {
return selectIcon(el.textContent, function (value) {
el.innerHTML = value.startsWith("http") || value.startsWith("data:image")
? `<img src="${value}" style="width:1.2em;height:1.2em;pointer-events:none;">`
: value;
});
}
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
@ -329,9 +336,15 @@ function overviewMilitary() {
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${
icon || " "
}</button></td>
row.innerHTML = /* html */ `<td>
<button data-type="icon" data-tip="Click to select unit icon">
${
icon.startsWith("http") || icon.startsWith("data:image")
? `<img src="${icon}" style="width:1.2em;height:1.2em;pointer-events:none;">`
: icon || ""
}
</button>
</td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
@ -424,7 +437,11 @@ function overviewMilitary() {
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.textContent || "";
if (type === "icon") {
const value = el.innerHTML.trim();
const isImage = value.startsWith("<img");
return isImage ? value.match(/src="([^"]*)"/)[1] : value || "";
}
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;

View file

@ -41,7 +41,7 @@ function editNamesbase() {
$("#namesbaseEditor").dialog({
title: "Namesbase Editor",
width: "auto",
width: "60vw",
position: {my: "center", at: "center", of: "svg"}
});
@ -66,7 +66,7 @@ function editNamesbase() {
function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value;
let examples = "";
for (let i = 0; i < 10; i++) {
for (let i = 0; i < 7; i++) {
const example = Names.getBase(base);
if (example === undefined) {
examples = "Cannot generate examples. Please verify the data";
@ -250,7 +250,7 @@ function editNamesbase() {
const [rawName, min, max, d, m, rawNames] = base.split("|");
const name = rawName.replace(unsafe, "");
const names = rawNames.replace(unsafe, "");
nameBases.push({name, min, max, d, m, b: names});
nameBases.push({name, min: +min, max: +max, d, m: +m, b: names});
});
createBasesList();

View file

@ -40,8 +40,8 @@ function editNotes(id, name) {
$("#notesEditor").dialog({
title: "Notes Editor",
width: window.innerWidth * 0.8,
height: window.innerHeight * 0.75,
width: svgWidth * 0.8,
height: svgHeight * 0.75,
position: {my: "center", at: "center", of: "svg"},
close: removeEditor
});
@ -55,6 +55,7 @@ function editNotes(id, name) {
byId("notesLegend").addEventListener("blur", updateLegend);
byId("notesPin").addEventListener("click", toggleNotesPin);
byId("notesFocus").addEventListener("click", validateHighlightElement);
byId("notesGenerateWithAi").addEventListener("click", openAiGenerator);
byId("notesDownload").addEventListener("click", downloadLegends);
byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
byId("legendsToLoad").addEventListener("change", function () {
@ -143,6 +144,25 @@ function editNotes(id, name) {
});
}
function openAiGenerator() {
const note = notes.find(note => note.id === notesSelect.value);
let prompt = `Respond with description. Use simple dry language. Invent facts, names and details. Split to paragraphs and format to HTML. Remove h tags, remove markdown.`;
if (note?.name) prompt += ` Name: ${note.name}.`;
if (note?.legend) prompt += ` Data: ${note.legend}`;
const onApply = result => {
notesLegend.innerHTML = result;
if (note) {
note.legend = result;
updateNotesBox(note);
if (window.tinymce) tinymce.activeEditor.setContent(note.legend);
}
};
generateWithAi(prompt, onApply);
}
function downloadLegends() {
const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";

View file

@ -66,12 +66,18 @@ document
.querySelectorAll(".tabcontent")
.forEach(e => (e.style.display = "none"));
if (id === "layersTab") layersContent.style.display = "block";
else if (id === "styleTab") styleContent.style.display = "block";
else if (id === "optionsTab") optionsContent.style.display = "block";
else if (id === "toolsTab")
if (id === "layersTab") {
layersContent.style.display = "block";
} else if (id === "styleTab") {
styleContent.style.display = "block";
selectStyleElement();
} else if (id === "optionsTab") {
optionsContent.style.display = "block";
} else if (id === "toolsTab") {
customization === 1 ? (customizationMenu.style.display = "block") : (toolsContent.style.display = "block");
else if (id === "aboutTab") aboutContent.style.display = "block";
} else if (id === "aboutTab") {
aboutContent.style.display = "block";
}
});
// show popup with a list of Patreon supportes (updated manually)
@ -125,9 +131,9 @@ optionsContent.addEventListener("input", event => {
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
else if (id === "pointsInput") changeCellsDensity(+value);
else if (id === "culturesSet") changeCultureSet();
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "statesNumber") changeStatesNumber(value);
else if (id === "emblemShape") changeEmblemShape(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "tooltipSize") changeTooltipSize(value);
else if (id === "themeHueInput") changeThemeHue(value);
else if (id === "themeColorInput") changeDialogsTheme(themeColorInput.value, transparencyInput.value);
else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
@ -137,11 +143,12 @@ optionsContent.addEventListener("change", event => {
const {id, value} = event.target;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUiSize(value);
else if (id === "uiSize") changeUiSize(+value);
else if (id === "shapeRendering") setRendering(value);
else if (id === "yearInput") changeYear();
else if (id === "eraInput") changeEra();
else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
else if (id === "azgaarAssistant") toggleAssistant();
});
optionsContent.addEventListener("click", event => {
@ -203,16 +210,16 @@ function fitMapToScreen() {
svgHeight = Math.min(+mapHeightInput.value, window.innerHeight);
svg.attr("width", svgWidth).attr("height", svgHeight);
const zoomExtent = [
[0, 0],
[graphWidth, graphHeight]
];
const zoomMin = rn(Math.max(svgWidth / graphWidth, svgHeight / graphHeight), 3);
zoomExtentMin.value = zoomMin;
const zoomMax = +zoomExtentMax.value;
zoom.translateExtent(zoomExtent).scaleExtent([zoomMin, zoomMax]).scaleTo(svg, zoomMin);
zoom
.translateExtent([
[0, 0],
[graphWidth, graphHeight]
])
.scaleExtent([zoomMin, zoomMax]);
fitScaleBar(scaleBar, svgWidth, svgHeight);
if (window.fitLegendBox) fitLegendBox();
@ -244,8 +251,7 @@ const voiceInterval = setInterval(function () {
select.options.add(new Option(voice.name, i, false));
});
if (stored("speakerVoice")) select.value = stored("speakerVoice");
// se voice to store
else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US
else select.value = voices.findIndex(voice => voice.lang === "en-US");
}, 1000);
function testSpeaker() {
@ -326,16 +332,12 @@ const cellsDensityMap = {
function changeCellsDensity(value) {
pointsInput.value = value;
const cells = cellsDensityMap[value] || 1000;
const cells = cellsDensityMap[value] || pointsInput.dataset.cells;
pointsInput.dataset.cells = cells;
pointsOutputFormatted.value = getCellsDensityValue(cells);
pointsOutputFormatted.value = cells / 1000 + "K";
pointsOutputFormatted.style.color = getCellsDensityColor(cells);
}
function getCellsDensityValue(cells) {
return cells / 1000 + "K";
}
function getCellsDensityColor(cells) {
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
}
@ -389,18 +391,18 @@ function changeEmblemShape(emblemShape) {
}
function changeStatesNumber(value) {
regionsOutput.style.color = +value ? null : "#b12117";
byId("statesNumber").style.color = +value ? null : "#b12117";
burgLabels.select("#capitals").attr("data-size", Math.max(rn(6 - value / 20), 3));
labels.select("#countries").attr("data-size", Math.max(rn(18 - value / 6), 4));
}
function changeUiSize(value) {
if (isNaN(+value) || +value < 0.5) return;
if (isNaN(value) || value < 0.5) return;
const max = getUImaxSize();
if (+value > max) value = max;
if (value > max) value = max;
uiSizeInput.value = uiSizeOutput.value = value;
uiSize.value = value;
document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px";
byId("options").style.width = value * 300 + "px";
}
@ -427,7 +429,7 @@ function changeThemeHue(hue) {
// change color and transparency for modal windows
function changeDialogsTheme(themeColor, transparency) {
transparencyInput.value = transparencyOutput.value = transparency;
transparencyInput.value = transparency;
const alpha = (100 - +transparency) / 100;
const alphaReduced = Math.min(alpha + 0.3, 1);
@ -441,6 +443,7 @@ function changeDialogsTheme(themeColor, transparency) {
};
const theme = [
{name: "--bg-opacity", value: alpha},
{name: "--bg-main", h, s, l, alpha},
{name: "--bg-lighter", h, s, l: l + 0.02, alpha},
{name: "--bg-light", h, s: s - 0.02, l: l + 0.06, alpha},
@ -453,8 +456,9 @@ function changeDialogsTheme(themeColor, transparency) {
];
const sx = document.documentElement.style;
theme.forEach(({name, h, s, l, alpha}) => {
sx.setProperty(name, getRGBA(h, s, l, alpha));
theme.forEach(({name, value, h, s, l, alpha}) => {
if (value !== undefined) sx.setProperty(name, value);
else sx.setProperty(name, getRGBA(h, s, l, alpha));
});
}
@ -489,11 +493,11 @@ function resetLanguage() {
if (!languageSelect.value) return;
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
languageSelect.handleChange(new Event("change"));
// do once again to actually reset the language
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
languageSelect.handleChange(new Event("change"));
}
function changeZoomExtent(value) {
@ -533,7 +537,6 @@ function applyStoredOptions() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key === "speakerVoice") continue;
const input = byId(key + "Input") || byId(key);
@ -551,17 +554,17 @@ function applyStoredOptions() {
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
}
if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number);
if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator");
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole");
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole");
if (stored("winds")) options.winds = stored("winds").split(",").map(Number);
if (stored("temperatureEquator")) options.temperatureEquator = +stored("temperatureEquator");
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +stored("temperatureNorthPole");
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +stored("temperatureSouthPole");
if (stored("military")) options.military = JSON.parse(stored("military"));
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
if (stored("regions")) changeStatesNumber(stored("regions"));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (stored("uiSize")) changeUiSize(stored("uiSize"));
uiSize.max = uiSize.max = getUImaxSize();
if (stored("uiSize")) changeUiSize(+stored("uiSize"));
else changeUiSize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
@ -586,15 +589,15 @@ function randomizeOptions() {
// 'Options' settings
if (randomize || !locked("points")) changeCellsDensity(4); // reset to default, no need to randomize
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
if (randomize || !locked("statesNumber")) statesNumber.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provincesRatio")) provincesRatio.value = gauss(20, 10, 20, 100);
if (randomize || !locked("manors")) {
manorsInput.value = 1000;
manorsOutput.value = "auto";
}
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("religionsNumber")) religionsNumber.value = gauss(6, 3, 2, 10);
if (randomize || !locked("sizeVariety")) sizeVariety.value = gauss(4, 2, 0, 10, 1);
if (randomize || !locked("growthRate")) growthRate.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
if (randomize || !locked("culturesSet")) randomizeCultureSet();
@ -606,8 +609,7 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === "en-US";
if (randomize || !locked("distanceScale"))
distanceScale = distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (randomize || !locked("distanceScale")) distanceScale = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
@ -699,12 +701,6 @@ async function openTemplateSelectionDialog() {
HeightmapSelectionDialog.open();
}
// remove all saved data from LocalStorage and reload the page
function restoreDefaultOptions() {
localStorage.clear();
location.reload();
}
// Sticked menu Options listeners
byId("sticked").addEventListener("click", function (event) {
const id = event.target.id;
@ -778,7 +774,7 @@ function showExportPane() {
}
async function exportToJson(type) {
const {exportToJson} = await import("../dynamic/export-json.js?v=1.97.08");
const {exportToJson} = await import("../dynamic/export-json.js?v=1.100.00");
exportToJson(type);
}

View file

@ -8,7 +8,7 @@ function editProvinces() {
if (layerIsOn("toggleCultures")) toggleCultures();
provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true);
const body = document.getElementById("provincesBodySection");
const body = byId("provincesBodySection");
refreshProvincesEditor();
if (modules.editProvinces) return;
@ -23,22 +23,22 @@ function editProvinces() {
});
// add listeners
document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
document.getElementById("provincesEditStyle").addEventListener("click", () => editStyle("provs"));
document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("provincesChart").addEventListener("click", showChart);
document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels);
document.getElementById("provincesExport").addEventListener("click", downloadProvincesData);
document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces);
document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesRelease").addEventListener("click", triggerProvincesRelease);
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
byId("provincesEditorRefresh").on("click", refreshProvincesEditor);
byId("provincesEditStyle").on("click", () => editStyle("provs"));
byId("provincesFilterState").on("change", provincesEditorAddLines);
byId("provincesPercentage").on("click", togglePercentageMode);
byId("provincesChart").on("click", showChart);
byId("provincesToggleLabels").on("click", toggleLabels);
byId("provincesExport").on("click", downloadProvincesData);
byId("provincesRemoveAll").on("click", removeAllProvinces);
byId("provincesManually").on("click", enterProvincesManualAssignent);
byId("provincesManuallyApply").on("click", applyProvincesManualAssignent);
byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment());
byId("provincesRelease").on("click", triggerProvincesRelease);
byId("provincesAdd").on("click", enterAddProvinceMode);
byId("provincesRecolor").on("click", recolorProvinces);
body.addEventListener("click", function (ev) {
body.on("click", function (ev) {
if (customization) return;
const el = ev.target,
cl = el.classList,
@ -58,7 +58,7 @@ function editProvinces() {
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
});
body.addEventListener("change", function (ev) {
body.on("change", function (ev) {
const el = ev.target,
cl = el.classList,
line = el.parentNode,
@ -100,7 +100,7 @@ function editProvinces() {
}
function updateFilter() {
const stateFilter = document.getElementById("provincesFilterState");
const stateFilter = byId("provincesFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
@ -111,7 +111,7 @@ function editProvinces() {
// add line for each province
function provincesEditorAddLines() {
const unit = " " + getAreaUnit();
const selectedState = +document.getElementById("provincesFilterState").value;
const selectedState = +byId("provincesFilterState").value;
let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state
body.innerHTML = "";
@ -194,9 +194,9 @@ function editProvinces() {
byId("provincesFooterPopulation").dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectProvinceOnLineClick);
el.addEventListener("mouseenter", ev => provinceHighlightOn(ev));
el.addEventListener("mouseleave", ev => provinceHighlightOff(ev));
el.on("click", selectProvinceOnLineClick);
el.on("mouseenter", ev => provinceHighlightOn(ev));
el.on("mouseleave", ev => provinceHighlightOff(ev));
});
if (body.dataset.type === "percentage") {
@ -306,7 +306,7 @@ function editProvinces() {
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = province.coa;
const coaEl = document.getElementById("provinceCOA" + provinceId);
const coaEl = byId("provinceCOA" + provinceId);
if (coaEl) coaEl.id = "stateCOA" + newStateId;
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
@ -367,10 +367,7 @@ function editProvinces() {
function updateStatesPostRelease(oldStates, newStates) {
const allStates = unique([...oldStates, ...newStates]);
layerIsOn("toggleProvinces") && toggleProvinces();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms(newStates);
drawStateLabels(allStates);
@ -382,6 +379,10 @@ function editProvinces() {
COArenderer.add("state", stateId, coa, ...pole);
});
layerIsOn("toggleProvinces") && toggleProvinces();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
unfog();
closeDialogs();
editStates();
@ -454,6 +455,7 @@ function editProvinces() {
p.burgs.forEach(b => (pack.burgs[b].population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
refreshProvincesEditor();
}
}
@ -482,7 +484,7 @@ function editProvinces() {
unfog("focusProvince" + p);
const coaId = "provinceCOA" + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
if (byId(coaId)) byId(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
pack.provinces[p] = {i: p, removed: true};
@ -490,8 +492,7 @@ function editProvinces() {
const g = provs.select("#provincesBody");
g.select("#province" + p).remove();
g.select("#province-gap" + p).remove();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleBorders")) drawBorders();
refreshProvincesEditor();
$(this).dialog("close");
},
@ -504,13 +505,13 @@ function editProvinces() {
function editProvinceName(province) {
const p = pack.provinces[province];
document.getElementById("provinceNameEditor").dataset.province = province;
document.getElementById("provinceNameEditorShort").value = p.name;
byId("provinceNameEditor").dataset.province = province;
byId("provinceNameEditorShort").value = p.name;
applyOption(provinceNameEditorSelectForm, p.formName);
document.getElementById("provinceNameEditorFull").value = p.fullName;
byId("provinceNameEditorFull").value = p.fullName;
const cultureId = pack.cells.culture[p.center];
document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
$("#provinceNameEditor").dialog({
resizable: false,
@ -531,22 +532,22 @@ function editProvinces() {
modules.editProvinceName = true;
// add listeners
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture);
document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm);
document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture);
byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom);
byId("provinceNameEditorAddForm").on("click", addCustomForm);
byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName);
function regenerateShortNameCulture() {
const province = +provinceNameEditor.dataset.province;
const culture = pack.cells.culture[pack.provinces[province].center];
const name = Names.getState(Names.getCultureShort(culture), culture);
document.getElementById("provinceNameEditorShort").value = name;
byId("provinceNameEditorShort").value = name;
}
function regenerateShortNameRandom() {
const base = rand(nameBases.length - 1);
const name = Names.getState(Names.getBase(base), undefined, base);
document.getElementById("provinceNameEditorShort").value = name;
byId("provinceNameEditorShort").value = name;
}
function addCustomForm() {
@ -558,9 +559,9 @@ function editProvinces() {
}
function regenerateFullName() {
const short = document.getElementById("provinceNameEditorShort").value;
const form = document.getElementById("provinceNameEditorSelectForm").value;
document.getElementById("provinceNameEditorFull").value = getFullName();
const short = byId("provinceNameEditorShort").value;
const form = byId("provinceNameEditorSelectForm").value;
byId("provinceNameEditorFull").value = getFullName();
function getFullName() {
if (!form) return short;
@ -570,9 +571,9 @@ function editProvinces() {
}
function applyNameChange(p) {
p.name = document.getElementById("provinceNameEditorShort").value;
p.formName = document.getElementById("provinceNameEditorSelectForm").value;
p.fullName = document.getElementById("provinceNameEditorFull").value;
p.name = byId("provinceNameEditorShort").value;
p.formName = byId("provinceNameEditorSelectForm").value;
p.fullName = byId("provinceNameEditorFull").value;
provs.select("#provinceLabel" + p.i).text(p.name);
refreshProvincesEditor();
}
@ -628,8 +629,8 @@ function editProvinces() {
.parentId(d => d.state)(data)
.sum(d => d.area);
const width = 300 + 300 * uiSizeOutput.value,
height = 90 + 90 * uiSizeOutput.value;
const width = 300 + 300 * uiSize.value,
height = 90 + 90 * uiSize.value;
const margin = {top: 10, right: 10, bottom: 0, left: 10};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
@ -651,7 +652,7 @@ function editProvinces() {
.attr("height", height)
.attr("font-size", "10px");
const graph = svg.append("g").attr("transform", `translate(10, 0)`);
document.getElementById("provincesTreeType").addEventListener("change", updateChart);
byId("provincesTreeType").on("change", updateChart);
treeLayout(root);
@ -688,7 +689,7 @@ function editProvinces() {
function hideInfo(ev) {
provinceHighlightOff(ev);
if (!document.getElementById("provinceInfo")) return;
if (!byId("provinceInfo")) return;
provinceInfo.innerHTML = "&#8205;";
d3.select(ev.target).select("rect").classed("selected", 0);
}
@ -705,6 +706,7 @@ function editProvinces() {
node
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("dx", ".2em")
.attr("dy", "1em")
.attr("x", d => d.x0)
@ -816,7 +818,7 @@ function editProvinces() {
stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2);
customization = 11;
provs.select("g#provincesBody").append("g").attr("id", "temp");
provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3);
provs
.select("g#provincesBody")
.append("g")
@ -826,7 +828,7 @@ function editProvinces() {
.attr("stroke-width", 1);
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
byId("provincesManuallyButtons").style.display = "inline-block";
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em";
@ -879,7 +881,7 @@ function editProvinces() {
}
function dragBrush() {
const r = +provincesManuallyBrush.value;
const r = +provincesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
@ -937,7 +939,7 @@ function editProvinces() {
function moveBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +provincesManuallyBrush.value;
const radius = +provincesBrush.value;
moveCircle(point[0], point[1], radius);
}
@ -950,10 +952,10 @@ function editProvinces() {
pack.cells.province[i] = +this.dataset.province;
});
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
Provinces.getPoles();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
exitProvincesManualAssignment();
refreshProvincesEditor();
}
@ -970,7 +972,7 @@ function editProvinces() {
debug.selectAll("path.selected").remove();
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block"));
document.getElementById("provincesManuallyButtons").style.display = "none";
byId("provincesManuallyButtons").style.display = "none";
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
@ -1044,12 +1046,11 @@ function editProvinces() {
cells.province[c] = province;
});
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces();
else drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
collectStatistics();
document.getElementById("provincesFilterState").value = state;
byId("provincesFilterState").value = state;
provincesEditorAddLines();
}
@ -1062,7 +1063,7 @@ function editProvinces() {
}
function recolorProvinces() {
const state = +document.getElementById("provincesFilterState").value;
const state = +byId("provincesFilterState").value;
pack.provinces.forEach(p => {
if (!p || p.removed) return;
@ -1120,8 +1121,7 @@ function editProvinces() {
pack.states.forEach(s => (s.provinces = []));
unfog();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleBorders")) drawBorders();
provs.select("#provincesBody").remove();
turnButtonOff("toggleProvinces");

View file

@ -24,18 +24,17 @@ function editRegiment(selector) {
modules.editRegiment = true;
// add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
document.getElementById("regimentType").addEventListener("click", changeType);
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentEmblem").addEventListener("input", changeEmblem);
document.getElementById("regimentEmblemSelect").addEventListener("click", selectEmblem);
document.getElementById("regimentAttack").addEventListener("click", toggleAttack);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
document.getElementById("regimentAdd").addEventListener("click", toggleAdd);
document.getElementById("regimentAttach").addEventListener("click", toggleAttach);
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
byId("regimentNameRestore").addEventListener("click", restoreName);
byId("regimentType").addEventListener("click", changeType);
byId("regimentName").addEventListener("change", changeName);
byId("regimentEmblemChange").addEventListener("click", changeEmblem);
byId("regimentAttack").addEventListener("click", toggleAttack);
byId("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
byId("regimentLegend").addEventListener("click", editLegend);
byId("regimentSplit").addEventListener("click", splitRegiment);
byId("regimentAdd").addEventListener("click", toggleAdd);
byId("regimentAttach").addEventListener("click", toggleAttach);
byId("regimentRemove").addEventListener("click", removeRegiment);
// get regiment data element
function getRegiment() {
@ -43,11 +42,13 @@ function editRegiment(selector) {
}
function updateRegimentData(regiment) {
document.getElementById("regimentType").className = regiment.n ? "icon-anchor" : "icon-users";
document.getElementById("regimentName").value = regiment.name;
document.getElementById("regimentEmblem").value = regiment.icon;
const composition = document.getElementById("regimentComposition");
byId("regimentType").className = regiment.n ? "icon-anchor" : "icon-users";
byId("regimentName").value = regiment.name;
byId("regimentEmblem").innerHTML = regiment.icon.startsWith("http") || regiment.icon.startsWith("data:image")
? `<img src="${regiment.icon}" style="width: 1em; height: 1em;">`
: regiment.icon;
const composition = byId("regimentComposition");
composition.innerHTML = options.military
.map(u => {
return `<div data-tip="${capitalize(u.name)} number. Input to change">
@ -126,12 +127,13 @@ function editRegiment(selector) {
function changeType() {
const reg = getRegiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
byId("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
const size = +armies.attr("box-size");
const baseRect = elSelected.querySelectorAll("rect")[0];
const iconRect = elSelected.querySelectorAll("rect")[1];
const icon = elSelected.querySelector(".regimentIcon");
const image = elSelected.querySelector(".regimentIcon");
const x = reg.n ? reg.x - size * 2 : reg.x - size * 3;
baseRect.setAttribute("x", x);
baseRect.setAttribute("width", reg.n ? size * 4 : size * 6);
@ -148,19 +150,19 @@ function editRegiment(selector) {
const reg = getRegiment(),
regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
}
function selectEmblem() {
selectIcon(regimentEmblem.value, v => {
regimentEmblem.value = v;
changeEmblem();
});
elSelected.dataset.name = reg.name = byId("regimentName").value = name;
}
function changeEmblem() {
const emblem = document.getElementById("regimentEmblem").value;
getRegiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
const regiment = getRegiment();
selectIcon(regiment.icon, value => {
regiment.icon = value;
const isExternal = value.startsWith("http") || value.startsWith("data:image");
byId("regimentEmblem").innerHTML = isExternal ? `<img src="${value}" style="width: 1em; height: 1em;">` : value;
elSelected.querySelector(".regimentIcon").innerHTML = isExternal ? "" : value;
elSelected.querySelector(".regimentImage").setAttribute("href", isExternal ? value : "");
});
}
function changeUnit() {
@ -218,14 +220,14 @@ function editRegiment(selector) {
newReg.name = Military.getName(newReg, military);
military.push(newReg);
Military.generateNote(newReg, pack.states[state]); // add legend
Military.drawRegiment(newReg, state); // draw new reg below
drawRegiment(newReg, state); // draw new reg below
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
}
function toggleAdd() {
document.getElementById("regimentAdd").classList.toggle("pressed");
if (document.getElementById("regimentAdd").classList.contains("pressed")) {
byId("regimentAdd").classList.toggle("pressed");
if (byId("regimentAdd").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
} else {
@ -246,14 +248,14 @@ function editRegiment(selector) {
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
Military.drawRegiment(reg, state);
drawRegiment(reg, state);
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
toggleAdd();
}
function toggleAttack() {
document.getElementById("regimentAttack").classList.toggle("pressed");
if (document.getElementById("regimentAttack").classList.contains("pressed")) {
byId("regimentAttack").classList.toggle("pressed");
if (byId("regimentAttack").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attackRegimentOnClick);
tip("Click on another regiment to initiate battle", true);
armies.selectAll(":scope > g").classed("draggable", false);
@ -296,7 +298,7 @@ function editRegiment(selector) {
(defender.px = defender.x), (defender.py = defender.y);
// move attacker to defender
Military.moveRegiment(attacker, defender.x, defender.y - 8);
moveRegiment(attacker, defender.x, defender.y - 8);
// draw battle icon
const attack = d3
@ -307,6 +309,7 @@ function editRegiment(selector) {
.on("end", () => new Battle(attacker, defender));
svg
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("x", window.innerWidth / 2)
.attr("y", window.innerHeight / 2)
.text("⚔️")
@ -324,8 +327,8 @@ function editRegiment(selector) {
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
byId("regimentAttach").classList.toggle("pressed");
if (byId("regimentAttach").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attachRegimentOnClick);
tip("Click on another regiment to unite both regiments. The current regiment will be removed", true);
armies.selectAll(":scope > g").classed("draggable", false);
@ -427,6 +430,7 @@ function editRegiment(selector) {
const text = this.querySelector("text");
const iconRect = this.querySelectorAll("rect")[1];
const icon = this.querySelector(".regimentIcon");
const image = this.querySelector(".regimentImage");
const self = elSelected === this;
const baseLine = viewbox.select("g#regimentBase > line");
@ -448,6 +452,8 @@ function editRegiment(selector) {
iconRect.setAttribute("y", y1);
icon.setAttribute("x", x1 - size);
icon.setAttribute("y", y);
image.setAttribute("x", x1 - h);
image.setAttribute("y", y1);
if (self) {
baseLine.attr("x2", x).attr("y2", y);
rotationControl
@ -479,9 +485,9 @@ function editRegiment(selector) {
viewbox.selectAll("g#regimentBase").remove();
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttack").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
byId("regimentAdd").classList.remove("pressed");
byId("regimentAttack").classList.remove("pressed");
byId("regimentAttach").classList.remove("pressed");
restoreDefaultEvents();
elSelected = null;
}

View file

@ -67,14 +67,24 @@ function overviewRegiments(state) {
)
.join(" ");
lines += /* html */ `<div class="states" data-id="${r.i}" data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
lines += /* html */ `<div class="states" data-id="${r.i}" data-s="${s.i}" data-state="${s.name}" data-name="${
r.name
}" ${sortData} data-total="${r.a}">
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
${
r.icon.startsWith("http") || r.icon.startsWith("data:image")
? `<img src="${r.icon}" data-tip="Regiment's emblem" style="width:1.2em; height:1.2em; vertical-align: middle;">`
: `<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>`
}
<input data-tip="Regiment's name" style="width:13em" value="${r.name}" readonly />
${lineData}
<div data-type="total" data-tip="Total military personnel (not considering crew)" style="font-weight: bold">${r.a}</div>
<span data-tip="Edit regiment" onclick="editRegiment('#regiment${s.i}-${r.i}')" class="icon-pencil pointer"></span>
<div data-type="total" data-tip="Total military personnel (not considering crew)" style="font-weight: bold">${
r.a
}</div>
<span data-tip="Edit regiment" onclick="editRegiment('#regiment${s.i}-${
r.i
}')" class="icon-pencil pointer"></span>
</div>`;
regiments.push(r);
@ -179,7 +189,7 @@ function overviewRegiments(state) {
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
Military.drawRegiment(reg, state);
drawRegiment(reg, state);
toggleAdd();
}

View file

@ -74,13 +74,10 @@ function createRiver() {
function addRiver() {
const {rivers, cells} = pack;
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 = getNextId(rivers);
const riverId = Rivers.getNextId(rivers);
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
@ -89,17 +86,24 @@ function createRiver() {
const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
const sourceWidth = 0.05;
const sourceWidth = Rivers.getSourceWidth(cells.fl[source]);
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = 1.2 * defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells);
const meanderedPoints = Rivers.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const name = getName(mouth);
const basin = getBasin(parent);
const length = Rivers.getApproximateLength(meanderedPoints);
const width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
const name = Rivers.getName(mouth);
const basin = Rivers.getBasin(parent);
rivers.push({
i: riverId,
@ -118,13 +122,11 @@ function createRiver() {
});
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));
.attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id);
}

View file

@ -86,10 +86,16 @@ function editRiver(id) {
}
function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river;
const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const meanderedPoints = Rivers.addMeandering(cells);
river.width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
byId("riverWidth").value = width;
@ -158,11 +164,9 @@ function editRiver(id) {
river.points = debug.selectAll("#controlPoints > *").data();
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
const path = Rivers.getRiverPath(meanderedPoints, river.widthFactor, river.sourceWidth);
elSelected.attr("d", path);
updateRiverLength(river);

View file

@ -18,7 +18,7 @@ function editRouteGroups() {
// add listeners
byId("routeGroupsEditorAdd").addEventListener("click", addGroup);
byId("routeGroupsEditorBody").on("click", ev => {
const group = ev.target.parentNode.dataset.id;
const group = ev.target.closest(".states")?.dataset.id;
if (ev.target.classList.contains("editStyle")) editStyle("routes", group);
else if (ev.target.classList.contains("removeGroup")) removeGroup(group);
});
@ -72,12 +72,11 @@ function editRouteGroups() {
confirmationDialog({
title: "Remove route group",
message:
"Are you sure you want to remove the entire route group? All routes in this group will be removed. This action can't be reverted.",
"Are you sure you want to remove the entire route group? All routes in this group will be removed.<br>This action can't be reverted",
confirm: "Remove",
onConfirm: () => {
const routes = pack.routes.filter(r => r.group === group);
routes.forEach(r => Routes.remove(r));
if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
pack.routes.filter(r => r.group === group).forEach(Routes.remove);
if (!DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
addLines();
}
});

View file

@ -97,7 +97,7 @@ function createRoute(defaultGroup) {
const points = createRoute.points;
if (points.length < 2) return tip("Add at least 2 points", false, "error");
const routeId = Math.max(...pack.routes.map(route => route.i)) + 1;
const routeId = Routes.getNextId();
const group = byId("routeCreatorGroupSelect").value;
const feature = pack.cells.f[points[0][2]];
const route = {points, group, feature, i: routeId};

View file

@ -174,9 +174,10 @@ function editRoute(id) {
function handleControlPointClick() {
const controlPoint = d3.select(this);
const point = controlPoint.datum();
const route = getRoute();
if (route.points.length < 3) return; // can't remove or split point if only 2 points in route
const index = route.points.indexOf(point);
const isSplitMode = byId("routeSplit").classList.contains("pressed");
@ -194,7 +195,7 @@ function editRoute(id) {
// create new route
const newRoute = {
i: Math.max(...pack.routes.map(route => route.i)) + 1,
i: Routes.getNextId(),
group: route.group,
feature: route.feature,
name: route.name,
@ -389,20 +390,13 @@ function editRoute(id) {
}
function removeRoute() {
alertMessage.innerHTML = "Are you sure you want to remove the route";
$("#alert").dialog({
resizable: false,
width: "22em",
confirmationDialog({
title: "Remove route",
buttons: {
Remove: function () {
Routes.remove(getRoute());
$(this).dialog("close");
$("#routeEditor").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
message: "Are you sure you want to remove the route? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
Routes.remove(getRoute());
$("#routeEditor").dialog("close");
}
});
}

View file

@ -32,6 +32,7 @@ function overviewRoutes() {
let lines = "";
for (const route of pack.routes) {
if (!route.points || route.points.length < 2) continue;
route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i);
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
@ -58,7 +59,7 @@ function overviewRoutes() {
// update footer
routesFooterNumber.innerHTML = pack.routes.length;
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)));
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)) || 0);
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
// add listeners
@ -92,8 +93,8 @@ function overviewRoutes() {
}
function zoomToRoute() {
const r = +this.parentNode.dataset.id;
const route = routes.select("#route" + r).node();
const routeId = +this.parentNode.dataset.id;
const route = routes.select("#route" + routeId).node();
highlightElement(route, 3);
}
@ -111,15 +112,16 @@ function overviewRoutes() {
}
function openRouteEditor() {
const id = "route" + this.parentNode.dataset.id;
editRoute(id);
const routeId = "route" + this.parentNode.dataset.id;
editRoute(routeId);
}
function toggleLockStatus() {
const routeId = +this.parentNode.dataset.id;
const route = pack.routes[routeId];
route.lock = !route.lock;
const route = pack.routes.find(route => route.i === routeId);
if (!route) return;
route.lock = !route.lock;
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
@ -144,22 +146,14 @@ function overviewRoutes() {
function triggerRouteRemove() {
const routeId = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
$("#alert").dialog({
resizable: false,
width: "22em",
confirmationDialog({
title: "Remove route",
buttons: {
Remove: function () {
const route = pack.routes.find(r => r.i === routeId);
Routes.remove(route);
routesOverviewAddLines();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
message: "Are you sure you want to remove the route? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
const route = pack.routes.find(r => r.i === routeId);
Routes.remove(route);
routesOverviewAddLines();
}
});
}
@ -175,8 +169,8 @@ function overviewRoutes() {
pack.routes = [];
routes.selectAll("path").remove();
routesOverviewAddLines();
$(this).dialog("close");
$("#routesOverview").dialog("close");
},
Cancel: function () {
$(this).dialog("close");

View file

@ -10,6 +10,7 @@ const systemPresets = [
"watercolor",
"clean",
"atlas",
"darkSeas",
"cyberpunk",
"night",
"monochrome"
@ -63,7 +64,7 @@ async function getStylePreset(desiredPreset) {
async function fetchSystemPreset(preset) {
try {
const res = await fetch(`./styles/${preset}.json?v=${version}`);
const res = await fetch(`./styles/${preset}.json?v=${VERSION}`);
return await res.json();
} catch (err) {
throw new Error("Cannot fetch style preset", preset);
@ -237,6 +238,9 @@ function addStylePreset() {
],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#emblems > #stateEmblems": ["data-size"],
"#emblems > #provinceEmblems": ["data-size"],
"#emblems > #burgEmblems": ["data-size"],
"#texture": ["opacity", "filter", "mask", "data-x", "data-y", "data-href"],
"#zones": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#oceanLayers": ["filter", "layers"],
@ -267,7 +271,15 @@ function addStylePreset() {
"data-columns"
],
"#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgLabels > #cities": [
"opacity",
"fill",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family"
],
"#burgIcons > #cities": [
"opacity",
"fill",
@ -279,7 +291,15 @@ function addStylePreset() {
"stroke-linecap"
],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgLabels > #towns": [
"opacity",
"fill",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family"
],
"#burgIcons > #towns": [
"opacity",
"fill",
@ -297,6 +317,7 @@ function addStylePreset() {
"stroke",
"stroke-width",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family",
@ -308,6 +329,7 @@ function addStylePreset() {
"stroke",
"stroke-width",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family",

View file

@ -18,7 +18,7 @@
}
// store some style inputs as options
styleElements.addEventListener("change", function (ev) {
styleElements.on("change", function (ev) {
if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
});
@ -70,8 +70,13 @@ function getColorScheme(scheme = "bright") {
return heightmapColorSchemes[scheme];
}
function getColor(value, scheme = getColorScheme("bright")) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// Toggle style sections on element select
styleElementSelect.addEventListener("change", selectStyleElement);
styleElementSelect.on("change", selectStyleElement);
function selectStyleElement() {
const styleElement = styleElementSelect.value;
let el = d3.select("#" + styleElement);
@ -92,7 +97,7 @@ function selectStyleElement() {
// opacity
if (!["landmass", "ocean", "regions", "legend"].includes(styleElement)) {
styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
styleOpacityInput.value = el.attr("opacity") || 1;
}
// filter
@ -111,32 +116,41 @@ function selectStyleElement() {
if (
[
"armies",
"routes",
"lakes",
"biomes",
"borders",
"cults",
"relig",
"cells",
"coastline",
"prec",
"coordinates",
"cults",
"gridOverlay",
"ice",
"icons",
"coordinates",
"zones",
"gridOverlay"
"lakes",
"prec",
"relig",
"routes",
"zones"
].includes(styleElement)
) {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
}
// stroke dash
if (
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(
styleElement
)
[
"borders",
"cells",
"coordinates",
"gridOverlay",
"legend",
"population",
"routes",
"temperature",
"zones"
].includes(styleElement)
) {
styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
@ -146,15 +160,17 @@ function selectStyleElement() {
// clipping
if (
[
"cells",
"gridOverlay",
"coordinates",
"compass",
"terrain",
"temperature",
"routes",
"texture",
"biomes",
"cells",
"compass",
"coordinates",
"gridOverlay",
"population",
"prec",
"routes",
"temperature",
"terrain",
"texture",
"zones"
].includes(styleElement)
) {
@ -176,9 +192,9 @@ function selectStyleElement() {
styleHeightmapRenderOcean.checked = +el.attr("data-render");
styleHeightmapScheme.value = el.attr("scheme");
styleHeightmapTerracingInput.value = styleHeightmapTerracingOutput.value = el.attr("terracing");
styleHeightmapSkipInput.value = styleHeightmapSkipOutput.value = el.attr("skip");
styleHeightmapSimplificationInput.value = styleHeightmapSimplificationOutput.value = el.attr("relax");
styleHeightmapTerracing.value = el.attr("terracing");
styleHeightmapSkip.value = el.attr("skip");
styleHeightmapSimplification.value = el.attr("relax");
styleHeightmapCurve.value = el.attr("curve");
}
@ -201,13 +217,13 @@ function selectStyleElement() {
const tr = parseTransform(compass.select("use").attr("transform"));
styleCompassShiftX.value = tr[0];
styleCompassShiftY.value = tr[1];
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2];
styleCompassSizeInput.value = tr[2];
}
if (styleElement === "terrain") {
styleRelief.style.display = "block";
styleReliefSizeOutput.innerHTML = styleReliefSizeInput.value = terrain.attr("size");
styleReliefDensityOutput.innerHTML = styleReliefDensityInput.value = terrain.attr("density");
styleReliefSize.value = terrain.attr("size") || 1;
styleReliefDensity.value = terrain.attr("density") || 0.4;
styleReliefSet.value = terrain.attr("set");
}
@ -220,30 +236,31 @@ function selectStyleElement() {
.select("#urban")
.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
}
if (styleElement === "regions") {
styleStates.style.display = "block";
styleStatesBodyOpacity.value = styleStatesBodyOpacityOutput.value = statesBody.attr("opacity") || 1;
styleStatesBodyOpacity.value = statesBody.attr("opacity") || 1;
styleStatesBodyFilter.value = statesBody.attr("filter") || "";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("data-width") || 10;
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity") || 1;
const blur = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0;
styleStatesHaloBlur.value = styleStatesHaloBlurOutput.value = blur;
styleStatesHaloWidth.value = statesHalo.attr("data-width") || 10;
styleStatesHaloOpacity.value = statesHalo.attr("opacity") || 1;
styleStatesHaloBlur.value = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0;
}
if (styleElement === "labels") {
styleFill.style.display = "block";
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
styleLetterSpacing.style.display = "block";
styleShadow.style.display = "block";
styleSize.style.display = "block";
styleVisibility.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
styleLetterSpacingInput.value = el.attr("letter-spacing") || 0;
styleShadowInput.value = el.style("text-shadow") || "white 0 0 4px";
styleFont.style.display = "block";
@ -258,7 +275,7 @@ function selectStyleElement() {
styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family");
styleFontSize.value = el.attr("data-size");
styleFontSize.value = el.attr("font-size");
}
if (styleElement == "burgIcons") {
@ -269,7 +286,7 @@ function selectStyleElement() {
styleRadius.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.24;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.24;
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
styleRadiusInput.value = el.attr("size") || 1;
@ -282,7 +299,7 @@ function selectStyleElement() {
styleIconSize.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.24;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.24;
styleIconSizeInput.value = el.attr("size") || 2;
}
@ -292,12 +309,13 @@ function selectStyleElement() {
styleSize.style.display = "block";
styleLegend.style.display = "block";
styleLegendColItemsOutput.value = styleLegendColItems.value = el.attr("data-columns");
styleLegendBackOutput.value = styleLegendBack.value = el.select("#legendBox").attr("fill");
styleLegendOpacityOutput.value = styleLegendOpacity.value = el.select("#legendBox").attr("fill-opacity");
styleLegendColItems.value = el.attr("data-columns");
const legendBox = el.select("#legendBox");
styleLegendBack.value = styleLegendBackOutput.value = legendBox.size() ? legendBox.attr("fill") : "#ffffff";
styleLegendOpacity.value = legendBox.size() ? legendBox.attr("fill-opacity") : 1;
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#111111";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.5;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.5;
styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family");
@ -308,18 +326,17 @@ function selectStyleElement() {
styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = byId("oceanicPattern")?.getAttribute("href");
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value =
byId("oceanicPattern").getAttribute("opacity") || 1;
styleOceanPatternOpacity.value = byId("oceanicPattern").getAttribute("opacity") || 1;
outlineLayers.value = oceanLayers.attr("layers");
}
if (styleElement === "temperature") {
styleStrokeWidth.style.display = "block";
styleTemperature.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || 0.1;
styleStrokeWidthInput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = el.attr("fill-opacity") || 0.1;
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000";
styleTemperatureFontSizeInput.value = styleTemperatureFontSizeOutput.value = el.attr("font-size") || "8px";
styleTemperatureFontSizeInput.value = el.attr("font-size") || "8px";
}
if (styleElement === "coordinates") {
@ -329,14 +346,17 @@ function selectStyleElement() {
if (styleElement === "armies") {
styleArmies.style.display = "block";
styleArmiesFillOpacity.value = styleArmiesFillOpacityOutput.value = el.attr("fill-opacity");
styleArmiesSize.value = styleArmiesSizeOutput.value = el.attr("box-size");
styleArmiesFillOpacity.value = el.attr("fill-opacity");
styleArmiesSize.value = el.attr("box-size");
}
if (styleElement === "emblems") {
styleEmblems.style.display = "block";
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 1;
styleStrokeWidthInput.value = el.attr("stroke-width") || 1;
emblemsStateSizeInput.value = emblems.select("#stateEmblems").attr("data-size") || 1;
emblemsProvinceSizeInput.value = emblems.select("#provinceEmblems").attr("data-size") || 1;
emblemsBurgSizeInput.value = emblems.select("#burgEmblems").attr("data-size") || 1;
}
// update group options
@ -372,11 +392,9 @@ function selectStyleElement() {
const scaleBarBack = el.select("#scaleBarBack");
if (scaleBarBack.size()) {
styleScaleBarBackgroundOpacityInput.value = styleScaleBarBackgroundOpacityOutput.value =
scaleBarBack.attr("opacity");
styleScaleBarBackgroundFillInput.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundStrokeInput.value = styleScaleBarBackgroundStrokeOutput.value =
scaleBarBack.attr("stroke");
styleScaleBarBackgroundOpacity.value = scaleBarBack.attr("opacity");
styleScaleBarBackgroundFill.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundStroke.value = styleScaleBarBackgroundStrokeOutput.value = scaleBarBack.attr("stroke");
styleScaleBarBackgroundStrokeWidth.value = scaleBarBack.attr("stroke-width");
styleScaleBarBackgroundFilter.value = scaleBarBack.attr("filter");
styleScaleBarBackgroundPaddingTop.value = scaleBarBack.attr("data-top");
@ -398,13 +416,13 @@ function selectStyleElement() {
styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter"));
styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
}
}
}
// Handle style inputs change
styleGroupSelect.addEventListener("change", selectStyleElement);
styleGroupSelect.on("change", selectStyleElement);
function getEl() {
const el = styleElementSelect.value;
@ -413,44 +431,46 @@ function getEl() {
else return svg.select("#" + el).select("#" + g);
}
styleFillInput.addEventListener("input", function () {
styleFillInput.on("input", function () {
styleFillOutput.value = this.value;
getEl().attr("fill", this.value);
});
styleStrokeInput.addEventListener("input", function () {
styleStrokeInput.on("input", function () {
styleStrokeOutput.value = this.value;
getEl().attr("stroke", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeWidthInput.addEventListener("input", function () {
styleStrokeWidthOutput.value = this.value;
getEl().attr("stroke-width", +this.value);
styleStrokeWidthInput.on("input", e => {
getEl().attr("stroke-width", e.target.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeDasharrayInput.addEventListener("input", function () {
styleLetterSpacingInput.on("input", e => {
getEl().attr("letter-spacing", e.target.value);
});
styleStrokeDasharrayInput.on("input", function () {
getEl().attr("stroke-dasharray", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeLinecapInput.addEventListener("change", function () {
styleStrokeLinecapInput.on("change", function () {
getEl().attr("stroke-linecap", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleOpacityInput.addEventListener("input", function () {
styleOpacityOutput.value = this.value;
getEl().attr("opacity", this.value);
styleOpacityInput.on("input", e => {
getEl().attr("opacity", e.target.value);
});
styleFilterInput.addEventListener("change", function () {
styleFilterInput.on("change", function () {
if (styleGroupSelect.value === "ocean") return oceanLayers.attr("filter", this.value);
getEl().attr("filter", this.value);
});
styleTextureInput.addEventListener("change", function () {
styleTextureInput.on("change", function () {
changeTexture(this.value);
});
@ -469,7 +489,7 @@ function updateTextureSelectValue(href) {
}
}
styleTextureShiftX.addEventListener("input", function () {
styleTextureShiftX.on("input", function () {
texture.attr("data-x", this.value);
texture
.select("image")
@ -477,7 +497,7 @@ styleTextureShiftX.addEventListener("input", function () {
.attr("width", graphWidth - this.valueAsNumber);
});
styleTextureShiftY.addEventListener("input", function () {
styleTextureShiftY.on("input", function () {
texture.attr("data-y", this.value);
texture
.select("image")
@ -485,17 +505,17 @@ styleTextureShiftY.addEventListener("input", function () {
.attr("height", graphHeight - this.valueAsNumber);
});
styleClippingInput.addEventListener("change", function () {
styleClippingInput.on("change", function () {
getEl().attr("mask", this.value);
});
styleGridType.addEventListener("change", function () {
styleGridType.on("change", function () {
getEl().attr("type", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize();
});
styleGridScale.addEventListener("input", function () {
styleGridScale.on("input", function () {
getEl().attr("scale", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize();
@ -507,53 +527,52 @@ function calculateFriendlyGridSize() {
styleGridSizeFriendly.value = friendly;
}
styleGridShiftX.addEventListener("input", function () {
styleGridShiftX.on("input", function () {
getEl().attr("dx", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
});
styleGridShiftY.addEventListener("input", function () {
styleGridShiftY.on("input", function () {
getEl().attr("dy", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
});
styleRescaleMarkers.addEventListener("change", function () {
styleRescaleMarkers.on("change", function () {
markers.attr("rescale", +this.checked);
invokeActiveZooming();
});
styleCoastlineAuto.addEventListener("change", function () {
styleCoastlineAuto.on("change", function () {
coastline.select("#sea_island").attr("auto-filter", +this.checked);
styleFilter.style.display = this.checked ? "none" : "block";
invokeActiveZooming();
});
styleOceanFill.addEventListener("input", function () {
styleOceanFill.on("input", function () {
oceanLayers.select("rect").attr("fill", this.value);
styleOceanFillOutput.value = this.value;
});
styleOceanPattern.addEventListener("change", function () {
styleOceanPattern.on("change", function () {
byId("oceanicPattern")?.setAttribute("href", this.value);
});
styleOceanPatternOpacity.addEventListener("input", function () {
byId("oceanicPattern").setAttribute("opacity", this.value);
styleOceanPatternOpacityOutput.value = this.value;
styleOceanPatternOpacity.on("input", e => {
byId("oceanicPattern").setAttribute("opacity", e.target.value);
});
outlineLayers.addEventListener("change", function () {
outlineLayers.on("change", function () {
oceanLayers.selectAll("path").remove();
oceanLayers.attr("layers", this.value);
OceanLayers();
});
styleHeightmapScheme.addEventListener("change", function () {
styleHeightmapScheme.on("change", function () {
getEl().attr("scheme", this.value);
drawHeightmap();
});
openCreateHeightmapSchemeButton.addEventListener("click", function () {
openCreateHeightmapSchemeButton.on("click", function () {
// start with current scheme
const scheme = getEl().attr("scheme");
this.dataset.stops = scheme.startsWith("#")
@ -672,106 +691,97 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () {
});
});
styleHeightmapRenderOcean.addEventListener("change", function () {
getEl().attr("data-render", +this.checked);
styleHeightmapRenderOcean.on("change", e => {
const checked = +e.target.checked;
getEl().attr("data-render", checked);
drawHeightmap();
});
styleHeightmapTerracingInput.addEventListener("input", function () {
getEl().attr("terracing", this.value);
styleHeightmapTerracing.on("input", e => {
getEl().attr("terracing", e.target.value);
drawHeightmap();
});
styleHeightmapSkipInput.addEventListener("input", function () {
getEl().attr("skip", this.value);
styleHeightmapSkip.on("input", e => {
getEl().attr("skip", e.target.value);
drawHeightmap();
});
styleHeightmapSimplificationInput.addEventListener("input", function () {
getEl().attr("relax", this.value);
styleHeightmapSimplification.on("input", e => {
getEl().attr("relax", e.target.value);
drawHeightmap();
});
styleHeightmapCurve.addEventListener("change", function () {
getEl().attr("curve", this.value);
styleHeightmapCurve.on("change", e => {
getEl().attr("curve", e.target.value);
drawHeightmap();
});
styleReliefSet.addEventListener("change", function () {
terrain.attr("set", this.value);
ReliefIcons();
styleReliefSet.on("change", e => {
terrain.attr("set", e.target.value);
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSizeInput.addEventListener("change", function () {
terrain.attr("size", this.value);
styleReliefSizeOutput.value = this.value;
ReliefIcons();
styleReliefSize.on("change", e => {
terrain.attr("size", e.target.value);
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefDensityInput.addEventListener("change", function () {
terrain.attr("density", this.value);
styleReliefDensityOutput.value = this.value;
ReliefIcons();
styleReliefDensity.on("change", e => {
terrain.attr("density", e.target.value);
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleTemperatureFillOpacityInput.addEventListener("input", function () {
temperature.attr("fill-opacity", this.value);
styleTemperatureFillOpacityOutput.value = this.value;
styleTemperatureFillOpacityInput.on("input", e => {
temperature.attr("fill-opacity", e.target.value);
});
styleTemperatureFontSizeInput.addEventListener("input", function () {
temperature.attr("font-size", this.value + "px");
styleTemperatureFontSizeOutput.value = this.value + "px";
styleTemperatureFontSizeInput.on("input", e => {
temperature.attr("font-size", e.target.value + "px");
});
styleTemperatureFillInput.addEventListener("input", function () {
temperature.attr("fill", this.value);
styleTemperatureFillOutput.value = this.value;
styleTemperatureFillInput.on("input", e => {
temperature.attr("fill", e.target.value);
styleTemperatureFillOutput.value = e.target.value;
});
stylePopulationRuralStrokeInput.addEventListener("input", function () {
population.select("#rural").attr("stroke", this.value);
stylePopulationRuralStrokeOutput.value = this.value;
stylePopulationRuralStrokeInput.on("input", e => {
population.select("#rural").attr("stroke", e.target.value);
stylePopulationRuralStrokeOutput.value = e.target.value;
});
stylePopulationUrbanStrokeInput.addEventListener("input", function () {
population.select("#urban").attr("stroke", this.value);
stylePopulationUrbanStrokeOutput.value = this.value;
stylePopulationUrbanStrokeInput.on("input", e => {
population.select("#urban").attr("stroke", e.target.value);
stylePopulationUrbanStrokeOutput.value = e.target.value;
});
styleCompassSizeInput.addEventListener("input", function () {
styleCompassSizeOutput.value = this.value;
shiftCompass();
});
styleCompassShiftX.addEventListener("input", shiftCompass);
styleCompassShiftY.addEventListener("input", shiftCompass);
styleCompassSizeInput.on("input", shiftCompass);
styleCompassShiftX.on("input", shiftCompass);
styleCompassShiftY.on("input", shiftCompass);
function shiftCompass() {
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
compass.select("use").attr("transform", tr);
}
styleLegendColItems.addEventListener("input", function () {
styleLegendColItemsOutput.value = this.value;
legend.select("#legendBox").attr("data-columns", this.value);
styleLegendColItems.on("input", e => {
legend.select("#legendBox").attr("data-columns", e.target.value);
redrawLegend();
});
styleLegendBack.addEventListener("input", function () {
styleLegendBackOutput.value = this.value;
legend.select("#legendBox").attr("fill", this.value);
styleLegendBack.on("input", e => {
styleLegendBackOutput.value = e.target.value;
legend.select("#legendBox").attr("fill", e.target.value);
});
styleLegendOpacity.addEventListener("input", function () {
styleLegendOpacityOutput.value = this.value;
legend.select("#legendBox").attr("fill-opacity", this.value);
styleLegendOpacity.on("input", e => {
legend.select("#legendBox").attr("fill-opacity", e.target.value);
});
styleSelectFont.addEventListener("change", changeFont);
styleSelectFont.on("change", changeFont);
function changeFont() {
const family = styleSelectFont.value;
getEl().attr("font-family", family);
@ -779,11 +789,11 @@ function changeFont() {
if (styleElementSelect.value === "legend") redrawLegend();
}
styleShadowInput.addEventListener("input", function () {
styleShadowInput.on("input", function () {
getEl().style("text-shadow", this.value);
});
styleFontAdd.addEventListener("click", function () {
styleFontAdd.on("click", function () {
addFontNameInput.value = "";
addFontURLInput.value = "";
@ -820,22 +830,22 @@ styleFontAdd.addEventListener("click", function () {
});
});
addFontMethod.addEventListener("change", function () {
addFontMethod.on("change", function () {
addFontURLInput.style.display = this.value === "fontURL" ? "inline" : "none";
});
styleFontSize.addEventListener("change", function () {
styleFontSize.on("change", function () {
changeFontSize(getEl(), +this.value);
});
styleFontPlus.addEventListener("click", function () {
const size = +getEl().attr("data-size") + 1;
changeFontSize(getEl(), Math.min(size, 999));
styleFontPlus.on("click", function () {
const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.min(current + 1, 999));
});
styleFontMinus.addEventListener("click", function () {
const size = +getEl().attr("data-size") - 1;
changeFontSize(getEl(), Math.max(size, 1));
styleFontMinus.on("click", function () {
const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.max(current - 1, 1));
});
function changeFontSize(el, size) {
@ -856,16 +866,16 @@ function changeFontSize(el, size) {
if (styleElementSelect.value === "legend") redrawLegend();
}
styleRadiusInput.addEventListener("change", function () {
styleRadiusInput.on("change", function () {
changeRadius(+this.value);
});
styleRadiusPlus.addEventListener("click", function () {
styleRadiusPlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeRadius(size);
});
styleRadiusMinus.addEventListener("click", function () {
styleRadiusMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeRadius(size);
});
@ -887,16 +897,16 @@ function changeRadius(size, group) {
changeIconSize(size * 2, g); // change also anchor icons
}
styleIconSizeInput.addEventListener("change", function () {
styleIconSizeInput.on("change", function () {
changeIconSize(+this.value);
});
styleIconSizePlus.addEventListener("click", function () {
styleIconSizePlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeIconSize(size);
});
styleIconSizeMinus.addEventListener("click", function () {
styleIconSizeMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeIconSize(size);
});
@ -921,49 +931,58 @@ function changeIconSize(size, group) {
styleIconSizeInput.value = size;
}
styleStatesBodyOpacity.addEventListener("input", function () {
styleStatesBodyOpacityOutput.value = this.value;
statesBody.attr("opacity", this.value);
styleStatesBodyOpacity.on("input", e => {
statesBody.attr("opacity", e.target.value);
});
styleStatesBodyFilter.addEventListener("change", function () {
styleStatesBodyFilter.on("change", function () {
statesBody.attr("filter", this.value);
});
styleStatesHaloWidth.addEventListener("input", function () {
styleStatesHaloWidthOutput.value = this.value;
statesHalo.attr("data-width", this.value).attr("stroke-width", this.value);
styleStatesHaloWidth.on("input", e => {
const value = e.target.value;
statesHalo.attr("data-width", value).attr("stroke-width", value);
});
styleStatesHaloOpacity.addEventListener("input", function () {
styleStatesHaloOpacityOutput.value = this.value;
statesHalo.attr("opacity", this.value);
styleStatesHaloOpacity.on("input", e => {
statesHalo.attr("opacity", e.target.value);
});
styleStatesHaloBlur.addEventListener("input", function () {
styleStatesHaloBlurOutput.value = this.value;
const blur = +this.value > 0 ? `blur(${this.value}px)` : null;
styleStatesHaloBlur.on("input", e => {
const value = Number(e.target.value);
const blur = value > 0 ? `blur(${value}px)` : null;
statesHalo.attr("filter", blur);
});
styleArmiesFillOpacity.addEventListener("input", function () {
armies.attr("fill-opacity", this.value);
styleArmiesFillOpacityOutput.value = this.value;
styleArmiesFillOpacity.on("input", e => {
armies.attr("fill-opacity", e.target.value);
});
styleArmiesSize.addEventListener("input", function () {
armies.attr("box-size", this.value).attr("font-size", this.value * 2);
styleArmiesSizeOutput.value = this.value;
styleArmiesSize.on("input", e => {
const value = Number(e.target.value);
armies.attr("box-size", value).attr("font-size", value * 2);
armies.selectAll("g").remove(); // clear armies layer
pack.states.forEach(s => {
if (!s.i || s.removed || !s.military.length) return;
Military.drawRegiments(s.military, s.i);
drawRegiments(s.military, s.i);
});
});
emblemsStateSizeInput.addEventListener("change", drawEmblems);
emblemsProvinceSizeInput.addEventListener("change", drawEmblems);
emblemsBurgSizeInput.addEventListener("change", drawEmblems);
emblemsStateSizeInput.on("change", e => {
emblems.select("#stateEmblems").attr("data-size", e.target.value);
drawEmblems();
});
emblemsProvinceSizeInput.on("change", e => {
emblems.select("#provinceEmblems").attr("data-size", e.target.value);
drawEmblems();
});
emblemsBurgSizeInput.on("change", e => {
emblems.select("#burgEmblems").attr("data-size", e.target.value);
drawEmblems();
});
// request a URL to image to be used as a texture
function textureProvideURL() {
@ -1015,7 +1034,7 @@ Object.keys(vignettePresets).forEach(preset => {
styleVignettePreset.options.add(new Option(preset, preset, false, false));
});
styleVignettePreset.addEventListener("change", function () {
styleVignettePreset.on("change", function () {
const attributes = JSON.parse(vignettePresets[this.value]);
for (const selector in attributes) {
@ -1029,7 +1048,7 @@ styleVignettePreset.addEventListener("change", function () {
const vignette = byId("vignette");
if (vignette) {
styleOpacityInput.value = styleOpacityOutput.value = vignette.getAttribute("opacity");
styleOpacityInput.value = vignette.getAttribute("opacity");
styleFillInput.value = styleFillOutput.value = vignette.getAttribute("fill");
styleFilterInput.value = vignette.getAttribute("filter");
}
@ -1043,40 +1062,39 @@ styleVignettePreset.addEventListener("change", function () {
styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter"));
styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
}
});
styleVignetteX.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("x", `${this.value}%`);
styleVignetteX.on("input", e => {
byId("vignette-rect")?.setAttribute("x", `${e.target.value}%`);
});
styleVignetteWidth.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("width", `${this.value}%`);
styleVignetteWidth.on("input", e => {
byId("vignette-rect")?.setAttribute("width", `${e.target.value}%`);
});
styleVignetteY.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("y", `${this.value}%`);
styleVignetteY.on("input", e => {
byId("vignette-rect")?.setAttribute("y", `${e.target.value}%`);
});
styleVignetteHeight.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("height", `${this.value}%`);
styleVignetteHeight.on("input", e => {
byId("vignette-rect")?.setAttribute("height", `${e.target.value}%`);
});
styleVignetteRx.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("rx", `${this.value}%`);
styleVignetteRx.on("input", e => {
byId("vignette-rect")?.setAttribute("rx", `${e.target.value}%`);
});
styleVignetteRy.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("ry", `${this.value}%`);
styleVignetteRy.on("input", e => {
byId("vignette-rect")?.setAttribute("ry", `${e.target.value}%`);
});
styleVignetteBlur.addEventListener("input", function () {
styleVignetteBlurOutput.value = this.value;
byId("vignette-rect")?.setAttribute("filter", `blur(${this.value}px)`);
styleVignetteBlur.on("input", e => {
byId("vignette-rect")?.setAttribute("filter", `blur(${e.target.value}px)`);
});
styleScaleBar.addEventListener("input", function (event) {
styleScaleBar.on("input", function (event) {
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (!scaleBarBack.size()) return;
@ -1087,9 +1105,9 @@ styleScaleBar.addEventListener("input", function (event) {
else if (id === "styleScaleBarPositionX") scaleBar.attr("data-x", value);
else if (id === "styleScaleBarPositionY") scaleBar.attr("data-y", value);
else if (id === "styleScaleBarLabel") scaleBar.attr("data-label", value);
else if (id === "styleScaleBarBackgroundOpacityInput") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFillInput") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStrokeInput") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundOpacity") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFill") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStroke") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundStrokeWidth") scaleBarBack.attr("stroke-width", value);
else if (id === "styleScaleBarBackgroundFilter") scaleBarBack.attr("filter", value);
else if (id === "styleScaleBarBackgroundPaddingTop") scaleBarBack.attr("data-top", value);
@ -1156,7 +1174,7 @@ function updateElements() {
}
// GLOBAL FILTERS
mapFilters.addEventListener("click", applyMapFilter);
mapFilters.on("click", applyMapFilter);
function applyMapFilter(event) {
if (event.target.tagName !== "BUTTON") return;
const button = event.target;

97
modules/ui/submap-tool.js Normal file
View file

@ -0,0 +1,97 @@
"use strict";
function openSubmapTool() {
resetInputs();
$("#submapTool").dialog({
title: "Create a submap",
resizable: false,
width: "32em",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Submap: function () {
closeDialogs();
generateSubmap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
if (modules.openSubmapTool) return;
modules.openSubmapTool = true;
function resetInputs() {
updateCellsNumber(byId("pointsInput").value);
byId("submapPointsInput").oninput = e => updateCellsNumber(e.target.value);
function updateCellsNumber(value) {
byId("submapPointsInput").value = value;
const cells = cellsDensityMap[value];
byId("submapPointsInput").dataset.cells = cells;
const output = byId("submapPointsFormatted");
output.value = cells / 1000 + "K";
output.style.color = getCellsDensityColor(cells);
}
}
function generateSubmap() {
INFO && console.group("generateSubmap");
const [x0, y0] = [Math.abs(viewX / scale), Math.abs(viewY / scale)]; // top-left corner
recalculateMapSize(x0, y0);
const submapPointsValue = byId("submapPointsInput").value;
const globalPointsValue = byId("pointsInput").value;
if (submapPointsValue !== globalPointsValue) changeCellsDensity(submapPointsValue);
const projection = (x, y) => [(x - x0) * scale, (y - y0) * scale];
const inverse = (x, y) => [x / scale + x0, y / scale + y0];
applyGraphSize();
fitMapToScreen();
resetZoom(0);
undraw();
Resample.process({projection, inverse, scale});
if (byId("submapRescaleBurgStyles").checked) rescaleBurgStyles(scale);
drawLayers();
INFO && console.groupEnd("generateSubmap");
}
function recalculateMapSize(x0, y0) {
const mapSize = +byId("mapSizeOutput").value;
byId("mapSizeOutput").value = byId("mapSizeInput").value = rn(mapSize / scale, 2);
const latT = mapCoordinates.latT / scale;
const latN = getLatitude(y0);
const latShift = (90 - latN) / (180 - latT);
byId("latitudeOutput").value = byId("latitudeInput").value = rn(latShift * 100, 2);
const lotT = mapCoordinates.lonT / scale;
const lonE = getLongitude(x0 + graphWidth / scale);
const lonShift = (180 - lonE) / (360 - lotT);
byId("longitudeOutput").value = byId("longitudeInput").value = rn(lonShift * 100, 2);
distanceScale = distanceScaleInput.value = rn(distanceScale / scale, 2);
populationRate = populationRateInput.value = rn(populationRate / scale, 2);
}
function rescaleBurgStyles(scale) {
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
for (const group of burgIcons) {
const newRadius = rn(minmax(group.getAttribute("size") * scale, 0.2, 10), 2);
changeRadius(newRadius, group.id);
const strokeWidth = group.attributes["stroke-width"];
strokeWidth.value = strokeWidth.value * scale;
}
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
for (const group of burgLabels) {
const size = +group.dataset.size;
group.dataset.size = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
}
}
}

View file

@ -1,354 +0,0 @@
"use strict";
// UI elements for submap generation
window.UISubmap = (function () {
byId("submapPointsInput").addEventListener("input", function () {
const output = byId("submapPointsOutputFormatted");
const cells = cellsDensityMap[+this.value] || 1000;
this.dataset.cells = cells;
output.value = getCellsDensityValue(cells);
output.style.color = getCellsDensityColor(cells);
});
byId("submapScaleInput").addEventListener("input", function (event) {
const exp = Math.pow(1.1, +event.target.value);
byId("submapScaleOutput").value = rn(exp, 2);
});
byId("submapAngleInput").addEventListener("input", function (event) {
byId("submapAngleOutput").value = event.target.value;
});
const $previewBox = byId("submapPreview");
const $scaleInput = byId("submapScaleInput");
const $shiftX = byId("submapShiftX");
const $shiftY = byId("submapShiftY");
function openSubmapMenu() {
$("#submapOptionsDialog").dialog({
title: "Create a submap",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Submap: function () {
$(this).dialog("close");
generateSubmap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
const getTransformInput = _ => ({
angle: (+byId("submapAngleInput").value / 180) * Math.PI,
shiftX: +byId("submapShiftX").value,
shiftY: +byId("submapShiftY").value,
ratio: +byId("submapScaleInput").value,
mirrorH: byId("submapMirrorH").checked,
mirrorV: byId("submapMirrorV").checked
});
async function openResampleMenu() {
resetZoom(0);
byId("submapAngleInput").value = 0;
byId("submapAngleOutput").value = "0";
byId("submapScaleOutput").value = 1;
byId("submapMirrorH").checked = false;
byId("submapMirrorV").checked = false;
$scaleInput.value = 0;
$shiftX.value = 0;
$shiftY.value = 0;
const w = Math.min(400, window.innerWidth * 0.5);
const previewScale = w / graphWidth;
const h = graphHeight * previewScale;
$previewBox.style.width = w + "px";
$previewBox.style.height = h + "px";
// handle mouse input
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
// mouse wheel
$previewBox.onwheel = e => {
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
dispatchInput($scaleInput);
};
// mouse drag
let mouseIsDown = false,
mouseX = 0,
mouseY = 0;
$previewBox.onmousedown = e => {
mouseIsDown = true;
mouseX = $shiftX.value - e.clientX / previewScale;
mouseY = $shiftY.value - e.clientY / previewScale;
};
$previewBox.onmouseup = _ => (mouseIsDown = false);
$previewBox.onmouseleave = _ => (mouseIsDown = false);
$previewBox.onmousemove = e => {
if (!mouseIsDown) return;
e.preventDefault();
$shiftX.value = Math.round(mouseX + e.clientX / previewScale);
$shiftY.value = Math.round(mouseY + e.clientY / previewScale);
dispatchInput($shiftX);
// dispatchInput($shiftY); // not needed X bubbles anyway
};
$("#resampleDialog").dialog({
title: "Transform map",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Transform: function () {
$(this).dialog("close");
resampleCurrentMap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
// use double resolution for PNG to get sharper image
const $preview = await loadPreview($previewBox, w * 2, h * 2);
// could be done with SVG. Faster to load, slower to use.
// const $preview = await loadPreviewSVG($previewBox, w, h);
$preview.style.position = "absolute";
$preview.style.width = w + "px";
$preview.style.height = h + "px";
byId("resampleDialog").oninput = event => {
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const scale = Math.pow(1.1, ratio);
const transformStyle = `
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
rotate(${angle}rad)
`;
$preview.style.transform = transformStyle;
$preview.style["transform-origin"] = "center";
event.stopPropagation();
};
}
async function loadPreview($container, w, h) {
const url = await getMapURL("png", {
globe: false,
noWater: true,
fullMap: true,
noLabels: true,
noScaleBar: true,
noIce: true
});
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
};
$container.textContent = "";
$container.appendChild(canvas);
return canvas;
}
// currently unused alternative to PNG version
async function loadPreviewSVG($container, w, h) {
$container.innerHTML = /*html*/ `
<svg id="submapPreviewSVG" viewBox="0 0 ${graphWidth} ${graphHeight}">
<rect width="100%" height="100%" fill="${byId("styleOceanFill").value}" />
<rect fill="url(#oceanic)" width="100%" height="100%" />
<use href="#map"></use>
</svg>
`;
return byId("submapPreviewSVG");
}
// Resample the whole map to different cell resolution or shape
const resampleCurrentMap = debounce(function () {
WARN && console.warn("Resampling current map");
const cellNumId = +byId("submapPointsInput").value;
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
const rot = alfa => (x, y) =>
[
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
];
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
const flipH = (x, y) => [-x + 2 * cx, y];
const flipV = (x, y) => [x, -y + 2 * cy];
const app = (f, g) => (x, y) => f(...g(x, y));
const id = (x, y) => [x, y];
let projection = id;
let inverse = id;
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
if (ratio)
[projection, inverse] = [
app(scale(Math.pow(1.1, ratio)), projection),
app(inverse, scale(Math.pow(1.1, -ratio)))
];
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
if (shiftX || shiftY) {
projection = app(shift(shiftX, shiftY), projection);
inverse = app(inverse, shift(-shiftX, -shiftY));
}
changeCellsDensity(cellNumId);
startResample({
lockMarkers: false,
lockBurgs: false,
depressRivers: false,
addLakesInDepressions: false,
promoteTowns: false,
smoothHeightMap: false,
rescaleStyles: false,
scale: 1,
projection,
inverse
});
}, 1000);
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
const generateSubmap = debounce(function () {
WARN && console.warn("Resampling current map");
closeDialogs("#worldConfigurator, #options3d");
const checked = id => Boolean(byId(id).checked);
// Create projection func from current zoom extents
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
const origScale = scale;
const options = {
lockMarkers: checked("submapLockMarkers"),
lockBurgs: checked("submapLockBurgs"),
depressRivers: checked("submapDepressRivers"),
addLakesInDepressions: checked("submapAddLakeInDepression"),
promoteTowns: checked("submapPromoteTowns"),
rescaleStyles: checked("submapRescaleStyles"),
smoothHeightMap: scale > 2,
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
scale: origScale
};
// converting map position on the planet
const mapSizeOutput = byId("mapSizeOutput");
const latitudeOutput = byId("latitudeOutput");
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
mapSizeOutput.value /= scale;
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
byId("mapSizeInput").value = mapSizeOutput.value;
byId("latitudeInput").value = latitudeOutput.value;
// fix scale
distanceScale =
distanceScaleInput.value =
distanceScaleOutput.value =
rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn(
(populationRate = populationRateOutput.value / scale),
2
);
customization = 0;
startResample(options);
}, 1000);
async function startResample(options) {
// Do model changes with Submap.resample then do view changes if needed
resetZoom(0);
let oldstate = {
grid: deepCopy(grid),
pack: deepCopy(pack),
notes: deepCopy(notes),
seed,
graphWidth,
graphHeight
};
undraw();
try {
const oldScale = scale;
await Submap.resample(oldstate, options);
if (options.promoteTowns) {
const groupName = "largetowns";
moveAllBurgsToGroup("towns", groupName);
changeRadius(rn(oldScale * 0.8, 2), groupName);
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
invokeActiveZooming();
}
if (options.rescaleStyles) changeStyles(oldScale);
} catch (error) {
showSubmapErrorHandler(error);
}
oldstate = null; // destroy old state to free memory
restoreLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
}
function changeStyles(scale) {
// resize burgIcons
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
for (const bi of burgIcons) {
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
changeRadius(newRadius, bi.id);
const swAttr = bi.attributes["stroke-width"];
swAttr.value = +swAttr.value * scale;
}
// burglabels
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
for (const bl of burgLabels) {
const size = +bl.dataset["size"];
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
}
// emblems
const emblemMod = minmax((scale - 1) * 0.3 + 1, 0.5, 5);
emblemsStateSizeInput.value = minmax(+emblemsStateSizeInput.value * emblemMod, 0.5, 5);
emblemsProvinceSizeInput.value = minmax(+emblemsProvinceSizeInput.value * emblemMod, 0.5, 5);
emblemsBurgSizeInput.value = minmax(+emblemsBurgSizeInput.value * emblemMod, 0.5, 5);
drawEmblems();
}
function showSubmapErrorHandler(error) {
ERROR && console.error(error);
clearMainTip();
alertMessage.innerHTML = /* html */ `Map resampling failed: <br />You may retry after clearing stored data or contact us at discord.
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Resampling error",
width: "32em",
buttons: {
Ok: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
}
return {openSubmapMenu, openResampleMenu};
})();

View file

@ -47,11 +47,13 @@ function showBurgTemperatureGraph(id) {
// Standard deviation for average temperature for the year from [0, 1] to [min, max]
const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165;
// Standard deviation for the difference between the minimum and maximum temperatures for the year
const yearDelTmpSig =
lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig
? yearSig
: lstOut[1] * 13.541688670361175 + 0.1414213562373084;
// Expected value for the difference between the minimum and maximum temperatures for the year
const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663;
@ -60,7 +62,7 @@ function showBurgTemperatureGraph(id) {
const minT = burgTemp - Math.max(yearSig + delT, 15);
const maxT = burgTemp + (burgTemp - minT);
const chartWidth = Math.max(window.innerWidth / 2, 580);
const chartWidth = Math.max(window.innerWidth / 2, 520);
const chartHeight = 300;
// drawing starting point from top-left (y = 0) of SVG
@ -107,9 +109,9 @@ function showBurgTemperatureGraph(id) {
});
drawGraph();
$("#alert").dialog({
title: "Annual temperature in " + b.name,
width: "auto",
title: "Average temperature in " + b.name,
position: {my: "center", at: "center", of: "svg"}
});

View file

@ -3,7 +3,7 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener("click", function (event) {
if (customization) return tip("Please exit the customization mode first", false, "warning");
if (customization) return tip("Please exit the customization mode first", false, "error");
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
const button = event.target.id;
@ -70,14 +70,16 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
else if (button === "openResampleMenu") UISubmap.openResampleMenu();
else if (button === "openSubmapTool") openSubmapTool();
else if (button === "openTransformTool") openTransformTool();
});
function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") drawStateLabels();
else if (button === "regenerateReliefIcons") {
ReliefIcons();
if (button === "regenerateStateLabels") {
$("#labels").fadeIn();
drawStateLabels();
} else if (button === "regenerateReliefIcons") {
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
regenerateRoutes();
@ -126,14 +128,14 @@ function regenerateRoutes() {
function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
else drawRivers();
Features.specify();
if (layerIsOn("toggleRivers")) drawRivers();
}
function recalculatePopulation() {
rankCells();
pack.burgs.forEach(b => {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
@ -143,6 +145,8 @@ function recalculatePopulation() {
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
});
layerIsOn("togglePopulation") ? drawPopulation() : togglePopulation();
}
function regenerateStates() {
@ -152,12 +156,14 @@ function regenerateStates() {
pack.states = newStates;
BurgsAndStates.expandStates();
BurgsAndStates.normalizeStates();
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics();
BurgsAndStates.assignColors();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
Provinces.generate(true);
Provinces.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
@ -167,16 +173,16 @@ function regenerateStates() {
Military.generate();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
}
function recreateStates() {
const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value;
const statesCount = +byId("statesNumber").value;
if (!statesCount) {
tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error");
return null;
@ -198,7 +204,7 @@ function recreateStates() {
const lockedStatesIds = lockedStates.map(s => s.i);
const lockedStatesCapitals = lockedStates.map(s => s.capital);
if (lockedStates.length === validStates.length) {
if (validStates.length && lockedStates.length === validStates.length) {
tip("Unable to regenerate as all states are locked", false, "error");
return null;
}
@ -317,7 +323,7 @@ function recreateStates() {
: pack.cultures[culture].type === "Nomadic"
? "Generic"
: pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
@ -332,9 +338,11 @@ function recreateStates() {
function regenerateProvinces() {
unfog();
BurgsAndStates.generateProvinces(true, true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
Provinces.generate(true, true);
Provinces.getPoles();
if (layerIsOn("toggleBorders")) drawBorders();
layerIsOn("toggleProvinces") ? drawProvinces() : toggleProvinces();
// remove emblems
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
@ -437,16 +445,18 @@ function regenerateBurgs() {
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
regenerateRoutes();
drawBurgIcons();
drawBurgLabels();
// remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
}
function regenerateEmblems() {
@ -498,13 +508,13 @@ function regenerateEmblems() {
province.coa.shield = COA.getShield(culture, province.state);
});
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
layerIsOn("toggleEmblems") ? drawEmblems() : toggleEmblems();
}
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions();
else drawReligions();
layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
refreshAllEditors();
}
@ -513,15 +523,17 @@ function regenerateCultures() {
Cultures.expand();
BurgsAndStates.updateCultures();
Religions.updateCultures();
if (!layerIsOn("toggleCultures")) toggleCultures();
else drawCultures();
layerIsOn("toggleCultures") ? drawCultures() : toggleCultures();
refreshAllEditors();
}
function regenerateMilitary() {
Military.generate();
if (!layerIsOn("toggleMilitary")) toggleMilitary();
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
if (layerIsOn("toggleMilitary")) drawMilitary();
else toggleMilitary();
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
}
function regenerateIce() {
@ -534,7 +546,7 @@ function regenerateMarkers() {
Markers.regenerate();
turnButtonOn("toggleMarkers");
drawMarkers();
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function regenerateZones(event) {
@ -545,10 +557,9 @@ function regenerateZones(event) {
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
function addNumberOfZones(number) {
zones.selectAll("g").remove(); // remove existing zones
addZones(number);
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (!layerIsOn("toggleZones")) toggleZones();
Zones.generate(number);
if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (layerIsOn("toggleZones")) drawZones();
}
}
@ -559,7 +570,7 @@ function unpressClickToAddButton() {
}
function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed");
const pressed = byId("addLabel").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
@ -607,8 +618,10 @@ function addLabelOnClick() {
group.classed("hidden", false);
group
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", id)
.append("textPath")
.attr("text-rendering", "optimizeSpeed")
.attr("xlink:href", "#textPath_" + id)
.attr("startOffset", "50%")
.attr("font-size", "100%")
@ -627,22 +640,22 @@ function addLabelOnClick() {
function toggleAddBurg() {
unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed");
byId("addBurgTool").classList.add("pressed");
overviewBurgs();
document.getElementById("addNewBurg").click();
byId("addNewBurg").click();
}
function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed");
const pressed = byId("addRiver").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
byId("addNewRiver").classList.remove("pressed");
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add("pressed");
document.getElementById("addNewRiver").classList.add("pressed");
byId("addNewRiver").classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
@ -657,28 +670,15 @@ function addRiverOnClick() {
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return;
const {
alterHeights,
resolveDepressions,
addMeandering,
getRiverPath,
getBasin,
getName,
getType,
getWidth,
getOffset,
getApproximateLength,
getNextId
} = Rivers;
const riverCells = [];
let riverId = getNextId(rivers);
let riverId = Rivers.getNextId(rivers);
let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux;
const h = alterHeights();
resolveDepressions(h);
const h = Rivers.alterHeights();
Rivers.resolveDepressions(h);
while (i) {
cells.r[i] = riverId;
@ -728,7 +728,7 @@ function addRiverOnClick() {
}
// continue old river
document.getElementById("river" + oldRiverId)?.remove();
byId("river" + oldRiverId)?.remove();
riverCells.forEach(i => (cells.r[i] = oldRiverId));
oldRiverCells.forEach(cell => {
if (h[cell] > h[min]) {
@ -752,11 +752,19 @@ function addRiverOnClick() {
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor =
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
const meanderedPoints = addMeandering(riverCells);
const sourceWidth = river?.sourceWidth || Rivers.getSourceWidth(cells.fl[source]);
const meanderedPoints = Rivers.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
const length = Rivers.getApproximateLength(meanderedPoints);
const width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
if (river) {
river.source = source;
@ -765,9 +773,9 @@ function addRiverOnClick() {
river.width = width;
river.cells = riverCells;
} else {
const basin = getBasin(parent);
const name = getName(mouth);
const type = getType({i: riverId, length, parent});
const basin = Rivers.getBasin(parent);
const name = Rivers.getName(mouth);
const type = Rivers.getType({i: riverId, length, parent});
rivers.push({
i: riverId,
@ -777,7 +785,7 @@ function addRiverOnClick() {
length,
width,
widthFactor,
sourceWidth: 0,
sourceWidth,
parent,
cells: riverCells,
basin,
@ -787,8 +795,7 @@ function addRiverOnClick() {
}
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = getRiverPath(meanderedPoints, widthFactor);
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
const id = "river" + riverId;
const riversG = viewbox.select("#rivers");
riversG.append("path").attr("id", id).attr("d", path);
@ -796,13 +803,13 @@ function addRiverOnClick() {
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
byId("addNewRiver").classList.remove("pressed");
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
}
}
function toggleAddMarker() {
const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
const pressed = byId("addMarker")?.classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
@ -830,7 +837,7 @@ function addMarkerOnClick() {
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
const selectedType = document.getElementById("addedMarkerType").value;
const selectedType = byId("addedMarkerType").value;
const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
@ -840,13 +847,13 @@ function addMarkerOnClick() {
selectedConfig.add("marker" + marker.i, cell);
}
const markersElement = document.getElementById("markers");
const markersElement = byId("markers");
const rescale = +markersElement.getAttribute("rescale");
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
if (d3.event.shiftKey === false) {
document.getElementById("markerAdd").classList.remove("pressed");
document.getElementById("markersAddFromOverview").classList.remove("pressed");
byId("markerAdd").classList.remove("pressed");
byId("markersAddFromOverview").classList.remove("pressed");
unpressClickToAddButton();
}
}
@ -855,33 +862,47 @@ function configMarkersGeneration() {
drawConfigTable();
function drawConfigTable() {
const {markers} = pack;
const config = Markers.getConfig();
const headers = `<thead style='font-weight:bold'><tr>
const headers = /* html */ `<thead style='font-weight:bold'><tr>
<td data-tip="Marker type name">Type</td>
<td data-tip="Marker icon">Icon</td>
<td data-tip="Marker number multiplier">Multiplier</td>
<td data-tip="Number of markers of that type on the current map">Number</td>
</tr></thead>`;
const lines = config.map(({type, icon, multiplier}, index) => {
const inputId = `markerIconInput${index}`;
return `<tr>
<td><input value="${type}" /></td>
const lines = config.map(({type, icon, multiplier}) => {
const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
return /* html */ `<tr>
<td><input class="type" value="${type}" /></td>
<td style="position: relative">
<input id="${inputId}" style="width: 5em" value="${icon}" />
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i>
<img class="image" src="${isExternal ? icon : ""}" ${
isExternal ? "" : "hidden"
} style="width:1.2em; height:1.2em; vertical-align: middle;">
<span class="emoji" style="font-size:1.2em">${isExternal ? "" : icon}</span>
<button class="changeIcon icon-pencil"></button>
</td>
<td><input type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${markers.filter(marker => marker.type === type).length}</td>
<td><input class="multiplier" type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${pack.markers.filter(marker => marker.type === type).length}</td>
</tr>`;
});
const table = `<table class="table">${headers}<tbody>${lines.join("")}</tbody></table>`;
alertMessage.innerHTML = table;
alertMessage.querySelectorAll("i").forEach(selectIconButton => {
alertMessage.querySelectorAll("button.changeIcon").forEach(selectIconButton => {
selectIconButton.addEventListener("click", function () {
const input = this.previousElementSibling;
selectIcon(input.value, icon => (input.value = icon));
const image = this.parentElement.querySelector(".image");
const emoji = this.parentElement.querySelector(".emoji");
const icon = image.getAttribute("src") || emoji.textContent;
selectIcon(icon, value => {
const isExternal = value.startsWith("http") || value.startsWith("data:image");
image.setAttribute("src", isExternal ? value : "");
image.hidden = !isExternal;
emoji.textContent = isExternal ? "" : value;
});
});
});
}
@ -889,12 +910,14 @@ function configMarkersGeneration() {
const applyChanges = () => {
const rows = alertMessage.querySelectorAll("tbody > tr");
const rowsData = Array.from(rows).map(row => {
const inputs = row.querySelectorAll("input");
return {
type: inputs[0].value,
icon: inputs[1].value,
multiplier: parseFloat(inputs[2].value)
};
const type = row.querySelector(".type").value;
const image = row.querySelector(".image");
const emoji = row.querySelector(".emoji");
const icon = image.getAttribute("src") || emoji.textContent;
const multiplier = parseFloat(row.querySelector(".multiplier").value);
return {type, icon, multiplier};
});
const config = Markers.getConfig();

View file

@ -0,0 +1,204 @@
"use strict";
async function openTransformTool() {
const width = Math.min(400, window.innerWidth * 0.5);
const previewScale = width / graphWidth;
const height = graphHeight * previewScale;
let mouseIsDown = false;
let mouseX = 0;
let mouseY = 0;
resetInputs();
loadPreview();
$("#transformTool").dialog({
title: "Transform map",
resizable: false,
position: {my: "center", at: "center", of: "svg"},
buttons: {
Transform: function () {
closeDialogs();
transformMap();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
if (modules.openTransformTool) return;
modules.openTransformTool = true;
// add listeners
byId("transformToolBody").on("input", handleInput);
byId("transformPreview")
.on("mousedown", handleMousedown)
.on("mouseup", _ => (mouseIsDown = false))
.on("mousemove", handleMousemove)
.on("wheel", handleWheel);
async function loadPreview() {
byId("transformPreview").style.width = width + "px";
byId("transformPreview").style.height = height + "px";
const options = {noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noVignette: true, noIce: true};
const url = await getMapURL("png", options);
const SCALE = 4;
const img = new Image();
img.src = url;
img.onload = function () {
const $canvas = byId("transformPreviewCanvas");
$canvas.style.width = width + "px";
$canvas.style.height = height + "px";
$canvas.width = width * SCALE;
$canvas.height = height * SCALE;
$canvas.getContext("2d").drawImage(img, 0, 0, width * SCALE, height * SCALE);
};
}
function resetInputs() {
byId("transformAngleInput").value = 0;
byId("transformAngleOutput").value = "0";
byId("transformMirrorH").checked = false;
byId("transformMirrorV").checked = false;
byId("transformScaleInput").value = 0;
byId("transformScaleResult").value = 1;
byId("transformShiftX").value = 0;
byId("transformShiftY").value = 0;
handleInput();
updateCellsNumber(byId("pointsInput").value);
byId("transformPointsInput").oninput = e => updateCellsNumber(e.target.value);
function updateCellsNumber(value) {
byId("transformPointsInput").value = value;
const cells = cellsDensityMap[value];
byId("transformPointsInput").dataset.cells = cells;
const output = byId("transformPointsFormatted");
output.value = cells / 1000 + "K";
output.style.color = getCellsDensityColor(cells);
}
}
function handleInput() {
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
const mirrorH = byId("transformMirrorH").checked;
const mirrorV = byId("transformMirrorV").checked;
const EXP = 1.0965;
const scale = rn(EXP ** +byId("transformScaleInput").value, 2); // [0.1, 10]x
byId("transformScaleResult").value = scale;
byId("transformPreviewCanvas").style.transform = `
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
rotate(${angle}rad)
`;
}
function handleMousedown(e) {
mouseIsDown = true;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
mouseX = shiftX - e.clientX / previewScale;
mouseY = shiftY - e.clientY / previewScale;
}
function handleMousemove(e) {
if (!mouseIsDown) return;
e.preventDefault();
byId("transformShiftX").value = Math.round(mouseX + e.clientX / previewScale);
byId("transformShiftY").value = Math.round(mouseY + e.clientY / previewScale);
handleInput();
}
function handleWheel(e) {
const $scaleInput = byId("transformScaleInput");
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
handleInput();
}
function transformMap() {
INFO && console.group("transformMap");
const transformPointsValue = byId("transformPointsInput").value;
const globalPointsValue = byId("pointsInput").value;
if (transformPointsValue !== globalPointsValue) changeCellsDensity(transformPointsValue);
const [projection, inverse] = getProjection();
applyGraphSize();
fitMapToScreen();
resetZoom(0);
undraw();
Resample.process({projection, inverse, scale: 1});
drawLayers();
INFO && console.groupEnd("transformMap");
}
function getProjection() {
const centerX = graphWidth / 2;
const centerY = graphHeight / 2;
const shiftX = +byId("transformShiftX").value;
const shiftY = +byId("transformShiftY").value;
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const scale = +byId("transformScaleResult").value;
const mirrorH = byId("transformMirrorH").checked;
const mirrorV = byId("transformMirrorV").checked;
function project(x, y) {
// center the point
x -= centerX;
y -= centerY;
// apply scale
if (scale !== 1) {
x *= scale;
y *= scale;
}
// apply rotation
if (angle) [x, y] = [x * cos - y * sin, x * sin + y * cos];
// apply mirroring
if (mirrorH) x = -x;
if (mirrorV) y = -y;
// uncenter the point and apply shift
return [x + centerX + shiftX, y + centerY + shiftY];
}
function inverse(x, y) {
// undo shift and center the point
x -= centerX + shiftX;
y -= centerY + shiftY;
// undo mirroring
if (mirrorV) y = -y;
if (mirrorH) x = -x;
// undo rotation
if (angle !== 0) [x, y] = [x * cos + y * sin, -x * sin + y * cos];
// undo scale
if (scale !== 1) {
x /= scale;
y /= scale;
}
// uncenter the point
return [x + centerX, y + centerY];
}
return [project, inverse];
}
}

View file

@ -17,27 +17,22 @@ function editUnits() {
};
// add listeners
byId("distanceUnitInput").addEventListener("change", changeDistanceUnit);
byId("distanceScaleOutput").addEventListener("input", changeDistanceScale);
byId("distanceScaleInput").addEventListener("change", changeDistanceScale);
byId("heightUnit").addEventListener("change", changeHeightUnit);
byId("heightExponentInput").addEventListener("input", changeHeightExponent);
byId("heightExponentOutput").addEventListener("input", changeHeightExponent);
byId("temperatureScale").addEventListener("change", changeTemperatureScale);
byId("distanceUnitInput").on("change", changeDistanceUnit);
byId("distanceScaleInput").on("change", changeDistanceScale);
byId("heightUnit").on("change", changeHeightUnit);
byId("heightExponentInput").on("input", changeHeightExponent);
byId("temperatureScale").on("change", changeTemperatureScale);
byId("populationRateOutput").addEventListener("input", changePopulationRate);
byId("populationRateInput").addEventListener("change", changePopulationRate);
byId("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
byId("urbanizationInput").addEventListener("change", changeUrbanizationRate);
byId("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
byId("urbanDensityInput").addEventListener("change", changeUrbanDensity);
byId("populationRateInput").on("change", changePopulationRate);
byId("urbanizationInput").on("change", changeUrbanizationRate);
byId("urbanDensityInput").on("change", changeUrbanDensity);
byId("addLinearRuler").addEventListener("click", addRuler);
byId("addOpisometer").addEventListener("click", toggleOpisometerMode);
byId("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
byId("addPlanimeter").addEventListener("click", togglePlanimeterMode);
byId("removeRulers").addEventListener("click", removeAllRulers);
byId("unitsRestore").addEventListener("click", restoreDefaultUnits);
byId("addLinearRuler").on("click", addRuler);
byId("addOpisometer").on("click", toggleOpisometerMode);
byId("addRouteOpisometer").on("click", toggleRouteOpisometerMode);
byId("addPlanimeter").on("click", togglePlanimeterMode);
byId("removeRulers").on("click", removeAllRulers);
byId("unitsRestore").on("click", restoreDefaultUnits);
function changeDistanceUnit() {
if (this.value === "custom_name") {
@ -71,11 +66,11 @@ function editUnits() {
function changeHeightExponent() {
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("toggleTemperature")) drawTemperature();
}
function changeTemperatureScale() {
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("toggleTemperature")) drawTemperature();
}
function changePopulationRate() {
@ -92,7 +87,6 @@ function editUnits() {
function restoreDefaultUnits() {
distanceScale = 3;
byId("distanceScaleOutput").value = distanceScale;
byId("distanceScaleInput").value = distanceScale;
unlock("distanceScale");
@ -110,16 +104,16 @@ function editUnits() {
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
heightExponentInput.value = 1.8;
localStorage.removeItem("heightExponent");
calculateTemperatures();
renderScaleBar();
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
populationRate = populationRateInput.value = 1000;
urbanization = urbanizationInput.value = 1;
urbanDensity = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
localStorage.removeItem("urbanDensity");
@ -127,11 +121,16 @@ function editUnits() {
function addRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
const width = Math.min(graphWidth, svgWidth);
const height = Math.min(graphHeight, svgHeight);
const pt = byId("map").createSVGPoint();
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
pt.x = width / 2;
pt.y = height / 4;
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = graphWidth / 4 / scale;
const dy = (rulers.data.length * 40) % (graphHeight / 2);
const dx = width / 4 / scale;
const dy = (rulers.data.length * 40) % (height / 2);
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw();

View file

@ -15,7 +15,7 @@ function editWorld() {
pane.insertAdjacentHTML("afterbegin", checkbox);
const button = this.parentElement.querySelector(".ui-dialog-buttonset > button");
button.on("mousemove", () => tip("Apply curreny settings to the map"));
button.on("mousemove", () => tip("Apply current settings to the map"));
},
close: function () {
$(this).dialog("destroy");
@ -86,13 +86,13 @@ function editWorld() {
generatePrecipitation();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
pack.cells.h = new Float32Array(heights);
Biomes.define();
Features.specify();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleTemperature")) drawTemperature();
if (layerIsOn("togglePrecipitation")) drawPrecipitation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleRivers")) drawRivers();

View file

@ -1,7 +1,7 @@
"use strict";
function editZones() {
closeDialogs();
closeDialogs("#zonesEditor, .stable");
if (!layerIsOn("toggleZones")) toggleZones();
const body = byId("zonesBodySection");
@ -14,7 +14,6 @@ function editZones() {
$("#zonesEditor").dialog({
title: "Zones Editor",
resizable: false,
width: fitContent(),
close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@ -31,34 +30,40 @@ function editZones() {
byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent);
byId("zonesAdd").on("click", addZonesLayer);
byId("zonesExport").on("click", downloadZonesData);
byId("zonesRemove").on("click", toggleEraseMode);
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
body.on("click", function (ev) {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
if (el.tagName === "FILL-BOX") changeFill(el);
else if (cl.contains("culturePopulation")) changePopulation(zone);
else if (cl.contains("icon-trash-empty")) zoneRemove(zone);
else if (cl.contains("icon-eye")) toggleVisibility(el);
else if (cl.contains("icon-pin")) toggleFog(zone, cl);
if (customization) selectZone(el);
const line = ev.target.closest("div.states");
const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
if (customization) {
if (zone.hidden) return;
body.querySelector("div.selected").classList.remove("selected");
line.classList.add("selected");
return;
}
if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
else if (ev.target.classList.contains("zoneRemove")) zoneRemove(zone);
else if (ev.target.classList.contains("zoneHide")) toggleVisibility(zone);
else if (ev.target.classList.contains("zoneFog")) toggleFog(zone, ev.target.classList);
});
body.on("input", function (ev) {
const el = ev.target;
const zone = zones.select("#" + el.parentNode.dataset.id);
const line = ev.target.closest("div.states");
const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
if (el.classList.contains("zoneName")) zone.attr("data-description", el.value);
else if (el.classList.contains("zoneType")) zone.attr("data-type", el.value);
if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value);
else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value);
});
// update type filter with a list of used types
function updateFilters() {
const zones = Array.from(document.querySelectorAll("#zones > g"));
const types = unique(zones.map(zone => zone.dataset.type));
const filterSelect = byId("zonesFilterType");
const types = unique(pack.zones.map(zone => zone.type));
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
filterSelect.innerHTML =
@ -68,47 +73,42 @@ function editZones() {
// add line for each zone
function zonesEditorAddLines() {
const unit = " " + getAreaUnit();
const typeToFilterBy = byId("zonesFilterType").value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy);
const filteredZones =
typeToFilterBy === "all" ? pack.zones : pack.zones.filter(zone => zone.type === typeToFilterBy);
const lines = filteredZones.map(zoneEl => {
const c = zoneEl.dataset.cells ? zoneEl.dataset.cells.split(",").map(c => +c) : [];
const description = zoneEl.dataset.description;
const type = zoneEl.dataset.type;
const fill = zoneEl.getAttribute("fill");
const area = getArea(d3.sum(c.map(i => pack.cells.area[i])));
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const lines = filteredZones.map(({i, name, type, cells, color, hidden}) => {
const area = getArea(d3.sum(cells.map(i => pack.cells.area[i])));
const rural = d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate;
const urban =
d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
rural
)}; Urban population: ${si(urban)}. Click to change`;
const inactive = zoneEl.style.display === "none";
const focused = defs.select("#fog #focus" + zoneEl.id).size();
const focused = defs.select("#fog #focusZone" + i).size();
return `<div class="states" data-id="${zoneEl.id}" data-fill="${fill}" data-description="${description}"
data-type="${type}" data-cells=${c.length} data-area=${area} data-population=${population}>
<fill-box fill="${fill}"></fill-box>
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${description}" autocorrect="off" spellcheck="false">
return /* html */ `<div class="states" data-id="${i}" data-color="${color}" data-description="${name}"
data-type="${type}" data-cells=${cells.length} data-area=${area} data-population=${population} style="${
hidden && "opacity: 0.5"
}">
<fill-box fill="${color}"></fill-box>
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${name}" autocorrect="off" spellcheck="false">
<input data-tip="Zone type. Click and type to change" class="zoneType" value="${type}">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<div data-tip="Cells count" class="stateCells hide">${cells.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + " " + getAreaUnit()}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<div data-tip="${populationTip}" class="zonePopulation hide pointer">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${
c.length ? "" : " placeholder"
<span data-tip="Toggle zone focus" class="zoneFog icon-pin ${focused ? "" : "inactive"} hide ${
cells.length ? "" : "placeholder"
}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${
c.length ? "" : " placeholder"
}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
<span data-tip="Toggle zone visibility" class="zoneHide icon-eye hide ${
cells.length ? "" : " placeholder"
}"></span>
<span data-tip="Remove zone" class="zoneRemove icon-trash-empty hide"></span>
</div>`;
});
@ -121,14 +121,13 @@ function editZones() {
(d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) *
populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`;
zonesFooterNumber.innerHTML = `${filteredZones.length} of ${pack.zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterArea.innerHTML = si(totalArea) + " " + getAreaUnit();
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => zoneHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", zoneHighlightOn));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", zoneHighlightOff));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
@ -138,25 +137,17 @@ function editZones() {
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", "1px solid red");
const zoneId = event.target.dataset.id;
zones.select("#zone" + zoneId).style("outline", "1px solid red");
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", null);
const zoneId = event.target.dataset.id;
zones.select("#zone" + zoneId).style("outline", null);
}
function filterZonesByType() {
const typeToFilterBy = this.value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
for (const zone of zones) {
const type = zone.dataset.type;
const visible = typeToFilterBy === "all" || type === typeToFilterBy;
zone.style.display = visible ? "block" : "none";
}
drawZones();
zonesEditorAddLines();
}
@ -167,23 +158,24 @@ function editZones() {
axis: "y",
update: movezone
});
function movezone(ev, ui) {
const zone = $("#" + ui.item.attr("data-id"));
const prev = $("#" + ui.item.prev().attr("data-id"));
if (prev) {
zone.insertAfter(prev);
return;
}
const next = $("#" + ui.item.next().attr("data-id"));
if (next) zone.insertBefore(next);
function movezone(_ev, ui) {
const zone = pack.zones.find(z => z.i === +ui.item[0].dataset.id);
const oldIndex = pack.zones.indexOf(zone);
const newIndex = ui.item.index();
if (oldIndex === newIndex) return;
pack.zones.splice(oldIndex, 1);
pack.zones.splice(newIndex, 0, zone);
drawZones();
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
byId("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
@ -197,21 +189,32 @@ function editZones() {
.on("touchmove mousemove", moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function () {
this.setAttribute("data-init", this.getAttribute("data-cells"));
});
}
function selectZone(el) {
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
// draw zones as individual cells
zones.selectAll("*").remove();
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
const data = visibleZones.map(({i, cells, color}) => cells.map(cell => ({cell, zoneId: i, fill: color}))).flat();
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d.cell))
.attr("fill", d => d.fill)
.attr("data-zone", d => d.zoneId)
.attr("data-cell", d => d.cell);
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
if (d3.event.target.parentElement.id !== "zones") return;
const zoneId = d3.event.target.dataset.zone;
const el = body.querySelector("div[data-id='" + zoneId + "']");
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
}
function dragZoneBrush() {
@ -219,43 +222,41 @@ function editZones() {
const eraseMode = byId("zonesRemove").classList.contains("pressed");
const landOnly = byId("zonesBrushLandOnly").checked;
const selected = body.querySelector("div.selected");
const zone = zones.select("#" + selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const [x, y] = d3.mouse(this);
moveCircle(x, y, radius);
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y, radius)];
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y)];
if (landOnly) selection = selection.filter(i => pack.cells.h[i] >= 20);
if (!selection) return;
if (!selection.length) return;
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const zoneId = +body.querySelector("div.selected")?.dataset.id;
const zone = pack.zones.find(z => z.i === zoneId);
if (!zone) return;
if (eraseMode) {
// remove
selection.forEach(i => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
cells.splice(index, 1);
});
const data = zones
.selectAll("polygon")
.data()
.filter(d => !(d.zoneId === zoneId && selection.includes(d.cell)));
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.exit()
.remove();
} else {
// add
selection.forEach(i => {
if (cells.includes(i)) return;
cells.push(i);
zone
.append("polygon")
.attr("points", getPackPolygon(i))
.attr("id", base + i);
});
const data = selection.map(cell => ({cell, zoneId, fill: zone.color}));
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d.cell))
.attr("fill", d => d.fill)
.attr("data-zone", d => d.zoneId)
.attr("data-cell", d => d.cell);
}
zone.attr("data-cells", cells);
});
}
@ -263,39 +264,29 @@ function editZones() {
showMainTip();
const point = d3.mouse(this);
const radius = +zonesBrush.value;
moveCircle(point[0], point[1], radius);
moveCircle(...point, radius);
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function () {
if (this.dataset.cells) return;
// all zone cells are removed
unfog("focusZone" + this.id);
this.style.display = "block";
});
const data = zones.selectAll("polygon").data();
const zoneCells = data.reduce((acc, d) => {
if (!acc[d.zoneId]) acc[d.zoneId] = [];
acc[d.zoneId].push(d.cell);
return acc;
}, {});
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
visibleZones.forEach(zone => (zone.cells = zoneCells[zone.i] || []));
drawZones();
zonesEditorAddLines();
exitZonesManualAssignment();
}
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
drawZones();
exitZonesManualAssignment();
}
@ -313,60 +304,49 @@ function editZones() {
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function () {
this.removeAttribute("data-init");
});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function changeFill(el) {
const fill = el.getAttribute("fill");
function changeFill(fill, zone) {
const callback = newFill => {
el.fill = newFill;
byId(el.parentNode.dataset.id).setAttribute("fill", newFill);
zone.color = newFill;
drawZones();
zonesEditorAddLines();
};
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#" + el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
function toggleVisibility(zone) {
const isHidden = Boolean(zone.hidden);
if (isHidden) delete zone.hidden;
else zone.hidden = true;
drawZones();
zonesEditorAddLines();
}
function toggleFog(z, cl) {
const dataCells = zones.select("#" + z).attr("data-cells");
if (!dataCells) return;
const path =
"M" +
dataCells
.split(",")
.map(c => getPackPolygon(+c))
.join("M") +
"Z",
id = "focusZone" + z;
cl.contains("inactive") ? fog(id, path) : unfog(id);
function toggleFog(zone, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
const path = zones.select("#zone" + zone.i).attr("d");
fog("focusZone" + zone.i, path);
} else {
unfog("focusZone" + zone.i);
}
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
clearLegend();
return;
} // hide legend
const data = [];
zones.selectAll("g").each(function () {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
data.push([id, fill, description]);
});
if (legend.selectAll("*").size()) return clearLegend(); // hide legend
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
const data = visibleZones.map(({i, name, color}) => ["zone" + i, color, name]);
drawLegend("Zones", data);
}
@ -380,8 +360,7 @@ function editZones() {
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML =
rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
el.querySelector(".zonePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
@ -390,28 +369,23 @@ function editZones() {
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const zoneId = pack.zones.length ? Math.max(...pack.zones.map(z => z.i)) + 1 : 0;
const name = "Unknown zone";
const type = "Unknown";
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
zones
.append("g")
.attr("id", id)
.attr("data-description", description)
.attr("data-type", type)
.attr("data-cells", "")
.attr("fill", fill);
const color = "url(#hatch" + (zoneId % 42) + ")";
pack.zones.push({i: zoneId, name, type, color, cells: []});
zonesEditorAddLines();
drawZones();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
let data = "Id,Color,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.color + ",";
data += el.dataset.description + ",";
data += el.dataset.type + ",";
data += el.dataset.cells + ",";
@ -423,27 +397,24 @@ function editZones() {
downloadFile(data, name);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
function changeDescription(zone, value) {
zone.name = value;
zones.select("#zone" + zone.i).attr("data-description", value);
}
function changeType(zone, value) {
zone.type = value;
zones.select("#zone" + zone.i).attr("data-type", value);
}
function changePopulation(zone) {
const dataCells = zones.select("#" + zone).attr("data-cells");
const cells = dataCells
? dataCells
.split(",")
.map(i => +i)
.filter(i => pack.cells.h[i] >= 20)
: [];
if (!cells.length) {
tip("Zone does not have any land cells, cannot change population", false, "error");
return;
}
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const landCells = zone.cells.filter(i => pack.cells.h[i] >= 20);
if (!landCells.length) return tip("Zone does not have any land cells, cannot change population", false, "error");
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
const burgs = pack.burgs.filter(b => !b.removed && landCells.includes(b.cell));
const rural = rn(d3.sum(landCells.map(i => pack.cells.pop[i])) * populationRate);
const urban = rn(
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
d3.sum(landCells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
@ -485,12 +456,12 @@ function editZones() {
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
landCells.forEach(i => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const pop = rn(points / cells.length);
cells.forEach(i => (pack.cells.pop[i] = pop));
const pop = rn(points / landCells.length);
landCells.forEach(i => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
@ -503,13 +474,22 @@ function editZones() {
burgs.forEach(b => (b.population = population));
}
if (layerIsOn("togglePopulation")) drawPopulation();
zonesEditorAddLines();
}
}
function zoneRemove(zone) {
zones.select("#" + zone).remove();
unfog("focusZone" + zone);
zonesEditorAddLines();
confirmationDialog({
title: "Remove zone",
message: "Are you sure you want to remove the zone? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
pack.zones = pack.zones.filter(z => z.i !== zone.i);
zones.select("#zone" + zone.i).remove();
unfog("focusZone" + zone.i);
zonesEditorAddLines();
}
});
}
}

454
modules/zones-generator.js Normal file
View file

@ -0,0 +1,454 @@
"use strict";
window.Zones = (function () {
const config = {
invasion: {quantity: 2, generate: addInvasion}, // invasion of enemy lands
rebels: {quantity: 1.5, generate: addRebels}, // rebels along a state border
proselytism: {quantity: 1.6, generate: addProselytism}, // proselitism of organized religion
crusade: {quantity: 1.6, generate: addCrusade}, // crusade on heresy lands
disease: {quantity: 1.4, generate: addDisease}, // disease starting in a random city
disaster: {quantity: 1, generate: addDisaster}, // disaster starting in a random city
eruption: {quantity: 1, generate: addEruption}, // eruption aroung volcano
avalanche: {quantity: 0.8, generate: addAvalanche}, // avalanche impacting highland road
fault: {quantity: 1, generate: addFault}, // fault line in elevated areas
flood: {quantity: 1, generate: addFlood}, // flood on river banks
tsunami: {quantity: 1, generate: addTsunami} // tsunami starting near coast
};
const generate = function (globalModifier = 1) {
TIME && console.time("generateZones");
const usedCells = new Uint8Array(pack.cells.i.length);
pack.zones = [];
Object.values(config).forEach(type => {
const expectedNumber = type.quantity * globalModifier;
let number = gauss(expectedNumber, expectedNumber / 2, 0, 100);
while (number--) type.generate(usedCells);
});
TIME && console.timeEnd("generateZones");
};
function addInvasion(usedCells) {
const {cells, states} = pack;
const ongoingConflicts = states
.filter(s => s.i && !s.removed && s.campaigns)
.map(s => s.campaigns)
.flat()
.filter(c => !c.end);
if (!ongoingConflicts.length) return;
const {defender, attacker} = ra(ongoingConflicts);
const borderCells = cells.i.filter(cellId => {
if (usedCells[cellId]) return false;
if (cells.state[cellId] !== defender) return false;
return cells.c[cellId].some(c => cells.state[c] === attacker);
});
const startCell = ra(borderCells);
if (startCell === undefined) return;
const invasionCells = [];
const queue = [startCell];
const maxCells = rand(5, 30);
while (queue.length) {
const cellId = P(0.4) ? queue.shift() : queue.pop();
invasionCells.push(cellId);
if (invasionCells.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId]) return;
if (cells.state[neibCellId] !== defender) return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const subtype = rw({
Invasion: 5,
Occupation: 4,
Conquest: 3,
Incursion: 2,
Intervention: 2,
Assault: 1,
Foray: 1,
Intrusion: 1,
Irruption: 1,
Offensive: 1,
Pillaging: 1,
Plunder: 1,
Raid: 1,
Skirmishes: 1
});
const name = getAdjective(states[attacker].name) + " " + subtype;
pack.zones.push({i: pack.zones.length, name, type: "Invasion", cells: invasionCells, color: "url(#hatch1)"});
}
function addRebels(usedCells) {
const {cells, states} = pack;
const state = ra(states.filter(s => s.i && !s.removed && s.neighbors.some(Boolean)));
if (!state) return;
const neibStateId = ra(state.neighbors.filter(n => n && !states[n].removed));
if (!neibStateId) return;
const cellsArray = [];
const queue = [];
const borderCellId = cells.i.find(
i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neibStateId)
);
if (borderCellId) queue.push(borderCellId);
const maxCells = rand(10, 30);
while (queue.length) {
const cellId = queue.shift();
cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId]) return;
if (cells.state[neibCellId] !== state.i) return;
usedCells[neibCellId] = 1;
if (neibCellId % 4 !== 0 && !cells.c[neibCellId].some(c => cells.state[c] === neibStateId)) return;
queue.push(neibCellId);
});
}
const rebels = rw({
Rebels: 5,
Insurrection: 2,
Mutineers: 1,
Insurgents: 1,
Rebellion: 1,
Renegades: 1,
Revolters: 1,
Revolutionaries: 1,
Rioters: 1,
Separatists: 1,
Secessionists: 1,
Conspiracy: 1
});
const name = getAdjective(states[neibStateId].name) + " " + rebels;
pack.zones.push({i: pack.zones.length, name, type: "Rebels", cells: cellsArray, color: "url(#hatch3)"});
}
function addProselytism(usedCells) {
const {cells, religions} = pack;
const organizedReligions = religions.filter(r => r.i && !r.removed && r.type === "Organized");
const religion = ra(organizedReligions);
if (!religion) return;
const targetBorderCells = cells.i.filter(
i =>
cells.h[i] < 20 &&
cells.pop[i] &&
cells.religion[i] !== religion.i &&
cells.c[i].some(c => cells.religion[c] === religion.i)
);
const startCell = ra(targetBorderCells);
if (!startCell) return;
const targetReligionId = cells.religion[startCell];
const proselytismCells = [];
const queue = [startCell];
const maxCells = rand(10, 30);
while (queue.length) {
const cellId = queue.shift();
proselytismCells.push(cellId);
if (proselytismCells.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId]) return;
if (cells.religion[neibCellId] !== targetReligionId) return;
if (cells.h[neibCellId] < 20 || !cells.pop[i]) return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const name = `${getAdjective(religion.name.split(" ")[0])} Proselytism`;
pack.zones.push({i: pack.zones.length, name, type: "Proselytism", cells: proselytismCells, color: "url(#hatch6)"});
}
function addCrusade(usedCells) {
const {cells, religions} = pack;
const heresies = religions.filter(r => !r.removed && r.type === "Heresy");
if (!heresies.length) return;
const heresy = ra(heresies);
const crusadeCells = cells.i.filter(i => !usedCells[i] && cells.religion[i] === heresy.i);
if (!crusadeCells.length) return;
crusadeCells.forEach(i => (usedCells[i] = 1));
const name = getAdjective(heresy.name.split(" ")[0]) + " Crusade";
pack.zones.push({
i: pack.zones.length,
name,
type: "Crusade",
cells: Array.from(crusadeCells),
color: "url(#hatch6)"
});
}
function addDisease(usedCells) {
const {cells, burgs} = pack;
const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed)); // random burg
if (!burg) return;
const cellsArray = [];
const cost = [];
const maxCells = rand(20, 40);
const queue = new FlatQueue();
queue.push({e: burg.cell, p: 0}, 0);
while (queue.length) {
const next = queue.pop();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
usedCells[next.e] = 1;
cells.c[next.e].forEach(nextCellId => {
const c = Routes.getRoute(next.e, nextCellId) ? 5 : 100;
const p = next.p + c;
if (p > maxCells) return;
if (!cost[nextCellId] || p < cost[nextCellId]) {
cost[nextCellId] = p;
queue.push({e: nextCellId, p}, p);
}
});
}
// prettier-ignore
const name = `${(() => {
const model = rw({color: 2, animal: 1, adjective: 1});
if (model === "color") return ra(["Amber", "Azure", "Black", "Blue", "Brown", "Crimson", "Emerald", "Golden", "Green", "Grey", "Orange", "Pink", "Purple", "Red", "Ruby", "Scarlet", "Silver", "Violet", "White", "Yellow"]);
if (model === "animal") return ra(["Ape", "Bear", "Bird", "Boar", "Cat", "Cow", "Deer", "Dog", "Fox", "Goat", "Horse", "Lion", "Pig", "Rat", "Raven", "Sheep", "Spider", "Tiger", "Viper", "Wolf", "Worm", "Wyrm"]);
if (model === "adjective") return ra(["Blind", "Bloody", "Brutal", "Burning", "Deadly", "Fatal", "Furious", "Great", "Grim", "Horrible", "Invisible", "Lethal", "Loud", "Mortal", "Savage", "Severe", "Silent", "Unknown", "Venomous", "Vicious"]);
})()} ${rw({Fever: 5, Plague: 3, Cough: 3, Flu: 2, Pox: 2, Cholera: 2, Typhoid: 2, Leprosy: 1, Smallpox: 1, Pestilence: 1, Consumption: 1, Malaria: 1, Dropsy: 1})}`;
pack.zones.push({i: pack.zones.length, name, type: "Disease", cells: cellsArray, color: "url(#hatch12)"});
}
function addDisaster(usedCells) {
const {cells, burgs} = pack;
const burg = ra(burgs.filter(b => !usedCells[b.cell] && b.i && !b.removed));
if (!burg) return;
usedCells[burg.cell] = 1;
const cellsArray = [];
const cost = [];
const maxCells = rand(5, 25);
const queue = new FlatQueue();
queue.push({e: burg.cell, p: 0}, 0);
while (queue.length) {
const next = queue.pop();
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
usedCells[next.e] = 1;
cells.c[next.e].forEach(function (e) {
const c = rand(1, 10);
const p = next.p + c;
if (p > maxCells) return;
if (!cost[e] || p < cost[e]) {
cost[e] = p;
queue.push({e, p}, p);
}
});
}
const type = rw({
Famine: 5,
Drought: 3,
Earthquake: 3,
Dearth: 1,
Tornadoes: 1,
Wildfires: 1,
Storms: 1,
Blight: 1
});
const name = getAdjective(burg.name) + " " + type;
pack.zones.push({i: pack.zones.length, name, type: "Disaster", cells: cellsArray, color: "url(#hatch5)"});
}
function addEruption(usedCells) {
const {cells, markers} = pack;
const volcanoe = markers.find(m => m.type === "volcanoes" && !usedCells[m.cell]);
if (!volcanoe) return;
usedCells[volcanoe.cell] = 1;
const note = notes.find(n => n.id === "marker" + volcanoe.i);
if (note) note.legend = note.legend.replace("Active volcano", "Erupting volcano");
const name = note ? note.name.replace(" Volcano", "") + " Eruption" : "Volcano Eruption";
const cellsArray = [];
const queue = [volcanoe.cell];
const maxCells = rand(10, 30);
while (queue.length) {
const cellId = P(0.5) ? queue.shift() : queue.pop();
cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId] || cells.h[neibCellId] < 20) return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
pack.zones.push({i: pack.zones.length, name, type: "Eruption", cells: cellsArray, color: "url(#hatch7)"});
}
function addAvalanche(usedCells) {
const {cells} = pack;
const routeCells = cells.i.filter(i => !usedCells[i] && Routes.isConnected(i) && cells.h[i] >= 70);
if (!routeCells.length) return;
const startCell = ra(routeCells);
usedCells[startCell] = 1;
const cellsArray = [];
const queue = [startCell];
const maxCells = rand(3, 15);
while (queue.length) {
const cellId = P(0.3) ? queue.shift() : queue.pop();
cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId] || cells.h[neibCellId] < 65) return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Avalanche";
pack.zones.push({i: pack.zones.length, name, type: "Avalanche", cells: cellsArray, color: "url(#hatch5)"});
}
function addFault(usedCells) {
const cells = pack.cells;
const elevatedCells = cells.i.filter(i => !usedCells[i] && cells.h[i] > 50 && cells.h[i] < 70);
if (!elevatedCells.length) return;
const startCell = ra(elevatedCells);
usedCells[startCell] = 1;
const cellsArray = [];
const queue = [startCell];
const maxCells = rand(3, 15);
while (queue.length) {
const cellId = queue.pop();
if (cells.h[cellId] >= 20) cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId] || cells.r[neibCellId]) return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Fault";
pack.zones.push({i: pack.zones.length, name, type: "Fault", cells: cellsArray, color: "url(#hatch2)"});
}
function addFlood(usedCells) {
const cells = pack.cells;
const fl = cells.fl.filter(Boolean);
const meanFlux = d3.mean(fl);
const maxFlux = d3.max(fl);
const fluxThreshold = (maxFlux - meanFlux) / 2 + meanFlux;
const bigRiverCells = cells.i.filter(
i => !usedCells[i] && cells.h[i] < 50 && cells.r[i] && cells.fl[i] > fluxThreshold && cells.burg[i]
);
if (!bigRiverCells.length) return;
const startCell = ra(bigRiverCells);
usedCells[startCell] = 1;
const riverId = cells.r[startCell];
const cellsArray = [];
const queue = [startCell];
const maxCells = rand(5, 30);
while (queue.length) {
const cellId = queue.pop();
cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (
usedCells[neibCellId] ||
cells.h[neibCellId] < 20 ||
cells.r[neibCellId] !== riverId ||
cells.h[neibCellId] > 50 ||
cells.fl[neibCellId] < meanFlux
)
return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const name = getAdjective(pack.burgs[cells.burg[startCell]].name) + " Flood";
pack.zones.push({i: pack.zones.length, name, type: "Flood", cells: cellsArray, color: "url(#hatch13)"});
}
function addTsunami(usedCells) {
const {cells, features} = pack;
const coastalCells = cells.i.filter(
i => !usedCells[i] && cells.t[i] === -1 && features[cells.f[i]].type !== "lake"
);
if (!coastalCells.length) return;
const startCell = ra(coastalCells);
usedCells[startCell] = 1;
const cellsArray = [];
const queue = [startCell];
const maxCells = rand(10, 30);
while (queue.length) {
const cellId = queue.shift();
if (cells.t[cellId] === 1) cellsArray.push(cellId);
if (cellsArray.length >= maxCells) break;
cells.c[cellId].forEach(neibCellId => {
if (usedCells[neibCellId]) return;
if (cells.t[neibCellId] > 2) return;
if (pack.features[cells.f[neibCellId]].type === "lake") return;
usedCells[neibCellId] = 1;
queue.push(neibCellId);
});
}
const name = getAdjective(Names.getCultureShort(cells.culture[startCell])) + " Tsunami";
pack.zones.push({i: pack.zones.length, name, type: "Tsunami", cells: cellsArray, color: "url(#hatch13)"});
}
return {generate};
})();

13
run_python_server.sh Normal file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env sh
if command -v python3 >/dev/null 2>&1; then
PYTHON=python3
elif command -v python >/dev/null 2>&1; then
PYTHON=python
else
echo "Neither 'python' nor 'python3' was found. Please install Python 3 package." >&2
exit 1
fi
chromium http://localhost:8000
$PYTHON -m http.server 8000

View file

@ -258,6 +258,15 @@
"stroke-width": 0.8,
"filter": "url(#dropShadow05)"
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.6,
"filter": "",
@ -323,6 +332,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 12,
"font-size": 12,
"font-family": "Great Vibes"
@ -348,6 +358,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 5,
"font-size": 5,
"font-family": "Great Vibes"
@ -375,6 +386,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 22,
"font-size": 22,
"font-family": "Great Vibes",
@ -386,6 +398,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Times New Roman",

View file

@ -258,6 +258,15 @@
"stroke-width": 1,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.2,
"filter": null,
@ -323,6 +332,7 @@
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 5,
"font-size": 5,
"font-family": "Amarante"
@ -348,6 +358,7 @@
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 4,
"font-size": 4,
"font-family": "Amarante"
@ -375,6 +386,7 @@
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 21,
"font-size": 21,
"font-family": "Amarante",
@ -386,6 +398,7 @@
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Amarante",

View file

@ -260,6 +260,15 @@
"stroke-width": 1,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.2,
"filter": null,
@ -310,22 +319,22 @@
"mask": "url(#land)"
},
"#legend": {
"data-size": 12.74,
"font-size": 12.74,
"data-size": 12,
"font-size": 12,
"font-family": "Arial",
"stroke": "#909090",
"stroke-width": 1.13,
"stroke-width": 1,
"stroke-dasharray": 0,
"stroke-linecap": "round",
"data-x": 98.39,
"data-y": 12.67,
"data-columns": null
"data-x": 99,
"data-y": 93,
"data-columns": 8
},
"#legendBox": {},
"#burgLabels > #cities": {
"opacity": 1,
"fill": "#414141",
"text-shadow": "white 0 0 4px",
"letter-spacing": 0,
"data-size": 7,
"font-size": 7,
"font-family": "Arial"
@ -350,6 +359,8 @@
"#burgLabels > #towns": {
"opacity": 1,
"fill": "#414141",
"text-shadow": "none",
"letter-spacing": 0,
"data-size": 3,
"font-size": 3,
"font-family": "Arial"
@ -377,6 +388,7 @@
"stroke": "#303030",
"stroke-width": 0,
"text-shadow": "white 0 0 2px",
"letter-spacing": 0,
"data-size": 10,
"font-size": 10,
"font-family": "Arial",
@ -388,6 +400,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0 0 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Arial",

View file

@ -256,7 +256,16 @@
"#emblems": {
"opacity": 0.75,
"stroke-width": 0.5,
"filter": ""
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.2,
@ -323,6 +332,7 @@
"opacity": 1,
"fill": "#ffffff",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 8,
"font-size": 8,
"font-family": "Orbitron"
@ -348,6 +358,7 @@
"opacity": 1,
"fill": "#ffffff",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 3,
"font-size": 3,
"font-family": "Orbitron"
@ -375,6 +386,7 @@
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Orbitron",
@ -386,6 +398,7 @@
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Almendra SC",

433
styles/darkSeas.json Normal file
View file

@ -0,0 +1,433 @@
{
"#map": {
"background-color": "#000000",
"filter": null,
"data-filter": null
},
"#armies": {
"font-size": 8,
"box-size": 4,
"stroke": "#000",
"stroke-width": 0.3,
"fill-opacity": 1,
"filter": null
},
"#biomes": {
"opacity": 1,
"filter": null,
"mask": "url(#land)"
},
"#stateBorders": {
"opacity": 1,
"stroke": "#000000",
"stroke-width": 1.25,
"stroke-dasharray": 0,
"stroke-linecap": "butt",
"filter": null
},
"#provinceBorders": {
"opacity": 0.8,
"stroke": "#000000",
"stroke-width": 0.5,
"stroke-dasharray": 0,
"stroke-linecap": "round",
"filter": "url(#blurFilter)"
},
"#cells": {
"opacity": null,
"stroke": "#808080",
"stroke-width": 0.1,
"filter": null,
"mask": null
},
"#gridOverlay": {
"opacity": 1,
"scale": 7.99,
"dx": -2,
"dy": 3,
"type": "square",
"stroke": "#000000",
"stroke-width": 0.05,
"stroke-dasharray": null,
"stroke-linecap": null,
"transform": null,
"filter": null,
"mask": null
},
"#coordinates": {
"opacity": 1,
"data-size": 12,
"font-size": 12,
"stroke": "#d4d4d4",
"stroke-width": 1,
"stroke-dasharray": 5,
"stroke-linecap": null,
"filter": null,
"mask": null
},
"#compass": {
"opacity": 0.8,
"transform": null,
"filter": null,
"mask": "url(#water)",
"shape-rendering": "optimizespeed"
},
"#relig": {
"opacity": 0.7,
"stroke": "#777777",
"stroke-width": 0,
"filter": null
},
"#cults": {
"opacity": 0.6,
"stroke": "#777777",
"stroke-width": 0.5,
"stroke-dasharray": null,
"stroke-linecap": null,
"filter": null
},
"#landmass": {
"opacity": 1,
"fill": "#eef6fb",
"filter": null
},
"#markers": {
"opacity": null,
"rescale": 1,
"filter": null
},
"#prec": {
"opacity": null,
"stroke": "#000000",
"stroke-width": 0.1,
"fill": "#003dff",
"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": 1,
"fill": "#337379",
"stroke": "#236369",
"stroke-width": 1,
"filter": null
},
"#salt": {
"opacity": 1,
"fill": "#409b8a",
"stroke": "#388985",
"stroke-width": 1,
"filter": null
},
"#sinkhole": {
"opacity": 1,
"fill": "#5bc9fd",
"stroke": "#53a3b0",
"stroke-width": 1,
"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": 1,
"filter": null
},
"#sea_island": {
"opacity": 1,
"stroke": "#028ac9",
"stroke-width": 1,
"filter": "",
"auto-filter": 0
},
"#lake_island": {
"opacity": 1,
"stroke": "#7c8eaf",
"stroke-width": 0.35,
"filter": null
},
"#rivers": {
"opacity": null,
"filter": null,
"fill": "#00254c"
},
"#ruler": {
"opacity": null,
"filter": null
},
"#roads": {
"opacity": 1,
"stroke": "#ff6000",
"stroke-width": 1.75,
"stroke-dasharray": 0,
"stroke-linecap": "butt",
"filter": null,
"mask": null
},
"#trails": {
"opacity": 1,
"stroke": "#ff6000",
"stroke-width": 1,
"stroke-dasharray": 0,
"stroke-linecap": "butt",
"filter": null,
"mask": null
},
"#searoutes": {
"opacity": 1,
"stroke": "#00aeff",
"stroke-width": 1,
"stroke-dasharray": "2 2",
"stroke-linecap": "butt",
"filter": null,
"mask": null
},
"#statesBody": {
"opacity": 0.5,
"filter": null
},
"#statesHalo": {
"opacity": 0,
"data-width": 0,
"stroke-width": 0,
"filter": null
},
"#provs": {
"opacity": 1,
"fill": "#000000",
"font-size": 10,
"font-family": "Georgia",
"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.8,
"fill": "#e8f0f6",
"stroke": "#e8f0f6",
"stroke-width": 1,
"filter": "url(#dropShadow05)"
},
"#emblems": {
"opacity": 1,
"stroke-width": 1,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.2,
"filter": null,
"mask": "url(#land)",
"data-x": 0,
"data-y": 0,
"data-href": "./images/textures/plaster.jpg"
},
"#zones": {
"opacity": 0.6,
"stroke": "#333333",
"stroke-width": 0,
"stroke-dasharray": null,
"stroke-linecap": "butt",
"filter": null,
"mask": null
},
"#oceanLayers": {
"filter": "",
"layers": "none"
},
"#oceanBase": {
"fill": "#00254d"
},
"#oceanicPattern": {
"href": "",
"opacity": 1
},
"#terrs #oceanHeights": {
"data-render": 0,
"opacity": 1,
"scheme": "bright",
"terracing": 0,
"skip": 0,
"relax": 1,
"curve": "curveBasisClosed",
"filter": null,
"mask": null
},
"#terrs #landHeights": {
"opacity": 1,
"scheme": "natural",
"terracing": 5,
"skip": 0,
"relax": 1,
"curve": "curveBasisClosed",
"filter": null,
"mask": "url(#land)"
},
"#legend": {
"data-size": 13,
"font-size": 13,
"font-family": "Georgia",
"stroke": "#812929",
"stroke-width": 2.5,
"stroke-dasharray": "0 4 10 4",
"stroke-linecap": "round",
"data-x": 99,
"data-y": 93,
"data-columns": 8
},
"#burgLabels > #cities": {
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 7,
"font-size": 7,
"font-family": "Lugrasimo"
},
"#burgIcons > #cities": {
"opacity": 0.7,
"fill": "#000000",
"size": 1.75,
"stroke": "#000000",
"stroke-width": 0,
"stroke-dasharray": 0,
"stroke-linecap": "butt"
},
"#anchors > #cities": {
"opacity": 1,
"fill": "#ffffff",
"size": 3.5,
"stroke": "#3e3e4b",
"stroke-width": 1.2
},
"#burgLabels > #towns": {
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 5,
"font-size": 5,
"font-family": "Lugrasimo"
},
"#burgIcons > #towns": {
"opacity": 0.7,
"fill": "#000000",
"size": 1.25,
"stroke": "#000000",
"stroke-width": 0,
"stroke-dasharray": 0,
"stroke-linecap": "butt"
},
"#anchors > #towns": {
"opacity": 1,
"fill": "#ffffff",
"size": 2,
"stroke": "#3e3e4b",
"stroke-width": 1.2
},
"#labels > #states": {
"opacity": 1,
"fill": "#000000",
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 21,
"font-size": 21,
"font-family": "Eagle Lake",
"filter": null
},
"#labels > #addedLabels": {
"opacity": 1,
"fill": "#000000",
"stroke": "#000000",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Eagle Lake",
"filter": null
},
"#fogging": {
"opacity": 0.98,
"fill": "#30426f",
"filter": null
},
"#vignette": {
"opacity": 0.2,
"fill": "#000000",
"filter": null
},
"#vignette-rect": {
"x": "0.2%",
"y": "0.3%",
"width": "99.8%",
"height": "99.4%",
"rx": "5%",
"ry": "5%",
"filter": "blur(30px)"
},
"#scaleBar": {
"opacity": 1,
"fill": "#f6f6f6",
"font-size": 10,
"data-bar-size": 2,
"data-x": 99,
"data-y": 99,
"data-label": ""
},
"#scaleBarBack": {
"opacity": 1,
"fill": "#00254d",
"stroke": "#00151d",
"stroke-width": 2,
"filter": null,
"data-top": 20,
"data-right": 15,
"data-bottom": 15,
"data-left": 10
}
}

View file

@ -248,16 +248,25 @@
},
"#ice": {
"opacity": 0.9,
"fill": "#e8f0f6",
"fill": "#f1f8fe",
"stroke": "#e8f0f6",
"stroke-width": 1,
"filter": "url(#dropShadow05)"
"stroke-width": 0.5,
"filter": "url(#dropShadow01)"
},
"#emblems": {
"opacity": 0.9,
"stroke-width": 1,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": null,
"filter": null,
@ -323,6 +332,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 7,
"font-size": 7,
"font-family": "Almendra SC"
@ -348,6 +358,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 4,
"font-size": 4,
"font-family": "Almendra SC"
@ -375,6 +386,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 22,
"font-size": 22,
"font-family": "Almendra SC",
@ -386,6 +398,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Almendra SC",

View file

@ -260,6 +260,15 @@
"stroke-width": 0.5,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.8,
"filter": null,
@ -326,6 +335,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0 0 2px",
"letter-spacing": 0,
"data-size": 8,
"font-size": 8,
"font-family": "Underdog"
@ -350,6 +360,8 @@
"#burgLabels > #towns": {
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "none",
"letter-spacing": 0,
"data-size": 4,
"font-size": 4,
"font-family": "Underdog"
@ -377,6 +389,7 @@
"stroke": "#b5b5b5",
"stroke-width": 0,
"text-shadow": "white 0 0 2px",
"letter-spacing": 0,
"data-size": 20,
"font-size": 20,
"font-family": "Underdog",
@ -388,6 +401,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0 0 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Underdog",

View file

@ -258,6 +258,15 @@
"stroke-width": 1,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#texture": {
"opacity": 0.4,
"filter": null,
@ -323,6 +332,7 @@
"opacity": 1,
"fill": "#3a3a3a",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 8,
"font-size": 8,
"font-family": "IM Fell English"
@ -348,6 +358,7 @@
"opacity": 1,
"fill": "#3e3e4b",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 4,
"font-size": 4,
"font-family": "IM Fell English"
@ -375,6 +386,7 @@
"stroke": "#000000",
"stroke-width": 0.3,
"text-shadow": "white 0px 0px 6px",
"letter-spacing": 0,
"data-size": 14,
"font-size": 14,
"font-family": "IM Fell English",
@ -386,6 +398,7 @@
"stroke": "#701b05",
"stroke-width": 0.1,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 6,
"font-size": 6,
"font-family": "IM Fell English",

View file

@ -261,6 +261,15 @@
"stroke-width": 0.5,
"filter": null
},
"#emblems > #stateEmblems": {
"data-size": 1
},
"#emblems > #provinceEmblems": {
"data-size": 1
},
"#emblems > #burgEmblems": {
"data-size": 1
},
"#zones": {
"opacity": 0.6,
"stroke": "#333333",
@ -319,6 +328,7 @@
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 7,
"font-size": 7,
"font-family": "Courier New"
@ -344,6 +354,7 @@
"opacity": 1,
"fill": "#000000",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 4,
"font-size": 4,
"font-family": "Courier New"
@ -371,6 +382,7 @@
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Courier New",
@ -381,7 +393,8 @@
"fill": "#3e3e4b",
"stroke": "#3a3a3a",
"stroke-width": 0,
"text-shadow": "white 0 0 4px",
"text-shadow": "white 0px 0px 4px",
"letter-spacing": 0,
"data-size": 18,
"font-size": 18,
"font-family": "Courier New",

Some files were not shown because too many files have changed in this diff Show more