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 # These are supported funding model platforms
github: Azgaar
patreon: 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 # Versioning
<!-- Update the version if you want the PR to be merged fast. Currently it's a manual 3-steps process: <!-- 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. Just set the next patch (for fixes) or minor version (for new features) * 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`) * 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` --> * 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-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */
.icon-half:before {font-weight: bold;content:'½';} .icon-half:before {font-weight: bold;content:'½';}
.icon-voice:before {content:'🔊';} .icon-voice:before {content:'🔊';}
.icon-robot:before {content:'🤖';}
.icon-die:before {content:'🎲';} .icon-die:before {content:'🎲';}
.icon-button-die:before {content:'🎲'; padding-right: .4em;} .icon-button-die:before {content:'🎲'; padding-right: .4em;}
.icon-button-power:before {content:'💪'; padding-right: .6em;} .icon-button-power:before {content:'💪'; padding-right: .6em;}

207
index.css
View file

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

1075
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 { .ui-dialog .ui-dialog-titlebar {
display: flex; display: flex;
padding: 0.4em 0.3em; padding: 0.3em 0.8em;
justify-content: space-evenly; justify-content: space-between;
align-items: center;
font-size: 1.2em; font-size: 1.2em;
min-width: 150px; min-width: 150px;
} }
.ui-dialog .ui-dialog-title { .ui-dialog .ui-dialog-title {
float: left;
margin: 0.1em 0;
white-space: nowrap; white-space: nowrap;
width: 90%; width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ui-dialog .ui-dialog-titlebar button { .ui-dialog .ui-dialog-titlebar button {
padding: 0; padding: 3px;
width: 1.8em; margin-left: 5px;
height: 1.8em; width: 19px;
height: 18px;
color: #ffffff; color: #ffffff;
background: none; background: none;
font-size: 0.75em; font-size: 0.8em;
border: 1px solid #c5c5c5; 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 { .ui-dialog .ui-dialog-titlebar button:active {
border: 1px solid #5d4651; border: 1px solid #5d4651;
color: #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(); placeTowns();
expandStates(); expandStates();
normalizeStates(); normalizeStates();
getPoles();
specifyBurgs(); specifyBurgs();
collectStatistics(); collectStatistics();
@ -20,11 +22,10 @@ window.BurgsAndStates = (() => {
generateCampaigns(); generateCampaigns();
generateDiplomacy(); generateDiplomacy();
drawBurgs();
function placeCapitals() { function placeCapitals() {
TIME && console.time("placeCapitals"); TIME && console.time("placeCapitals");
let count = +regionsOutput.value; let count = +byId("statesNumber").value;
let burgs = [0]; let burgs = [0];
const rand = () => 0.5 + Math.random() * 0.5; const rand = () => 0.5 + Math.random() * 0.5;
@ -85,7 +86,7 @@ window.BurgsAndStates = (() => {
b.capital = 1; b.capital = 1;
// states data // 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 basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture); const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type; const type = cultures[b.culture].type;
@ -238,16 +239,23 @@ window.BurgsAndStates = (() => {
return [x, y]; return [x, y];
} }
const getType = (i, port) => { const getType = (cellId, port) => {
const cells = pack.cells; const {cells, features, burgs} = pack;
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";
if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) { if (port) return "Naval";
if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting"; 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"; 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) // expand cultures across the map (Dijkstra-like algorithm)
const expandStates = () => { const expandStates = () => {
TIME && console.time("expandStates"); TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack; const {cells, states, cultures, burgs} = pack;
cells.state = cells.state || new Uint16Array(cells.i.length); 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 cost = [];
const globalNeutralRate = byId("neutralInput")?.valueAsNumber || 1; const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
const statesNeutralRate = byId("statesNeutral")?.valueAsNumber || 1; const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1;
const neutral = (cells.i.length / 2) * globalNeutralRate * statesNeutralRate; // limit cost for state growth const growthRate = (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth
// remove state from all cells except of locked // remove state from all cells except of locked
for (const cellId of cells.i) { for (const cellId of cells.i) {
@ -396,12 +308,13 @@ window.BurgsAndStates = (() => {
cells.state[capitalCell] = state.i; cells.state[capitalCell] = state.i;
const cultureCenter = cultures[state.culture].center; const cultureCenter = cultures[state.culture].center;
const b = cells.biome[cultureCenter]; // state native biome 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; cost[state.center] = 1;
} }
while (queue.length) { while (queue.length) {
const next = queue.dequeue(); const next = queue.pop();
const {e, p, s, b} = next; const {e, p, s, b} = next;
const {type, culture} = states[s]; const {type, culture} = states[s];
@ -419,12 +332,12 @@ window.BurgsAndStates = (() => {
const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0); const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
const totalCost = p + 10 + cellCost / states[s].expansionism; const totalCost = p + 10 + cellCost / states[s].expansionism;
if (totalCost > neutral) return; if (totalCost > growthRate) return;
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost; 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 = () => { const normalizeStates = () => {
TIME && console.time("normalizeStates"); TIME && console.time("normalizeStates");
const cells = pack.cells, const {cells, burgs} = pack;
burgs = pack.burgs;
for (const i of cells.i) { for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
@ -486,26 +398,30 @@ window.BurgsAndStates = (() => {
TIME && console.timeEnd("normalizeStates"); TIME && console.timeEnd("normalizeStates");
}; };
// Resets the cultures of all burgs and states to their // calculate pole of inaccessibility for each state
// cell or center cell's (respectively) culture. 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 = () => { const updateCultures = () => {
TIME && console.time("updateCulturesForBurgsAndStates"); 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) => { 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]}; 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) => { 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]}; return {...state, culture: pack.cells.culture[state.center]};
}); });
@ -611,8 +527,7 @@ window.BurgsAndStates = (() => {
// generate Diplomatic Relationships // generate Diplomatic Relationships
const generateDiplomacy = () => { const generateDiplomacy = () => {
TIME && console.time("generateDiplomacy"); TIME && console.time("generateDiplomacy");
const cells = pack.cells, const {cells, states} = pack;
states = pack.states;
const chronicle = (states[0].diplomacy = []); const chronicle = (states[0].diplomacy = []);
const valid = states.filter(s => s.i && !states.removed); const valid = states.filter(s => s.i && !states.removed);
@ -696,21 +611,23 @@ window.BurgsAndStates = (() => {
const defender = ra( const defender = ra(
ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d) ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d)
); );
let ap = states[attacker].area * states[attacker].expansionism, let ap = states[attacker].area * states[attacker].expansionism;
dp = states[defender].area * states[defender].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 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 an = states[attacker].name;
const attackers = [attacker], const dn = states[defender].name; // names
defenders = [defender]; // attackers and defenders array const attackers = [attacker];
const defenders = [defender]; // attackers and defenders array
const dd = states[defender].diplomacy; // defender relations; const dd = states[defender].diplomacy; // defender relations;
// start a war // start an ongoing war
const war = [`${an}-${trimVowels(dn)}ian War`, `${an} declared a war on its rival ${dn}`]; const name = `${an}-${trimVowels(dn)}ian War`;
const end = options.year; const start = options.year - gauss(2, 3, 0, 10);
const start = end - gauss(2, 2, 0, 5); const war = [name, `${an} declared a war on its rival ${dn}`];
states[attacker].campaigns.push({name: `${trimVowels(dn)}ian War`, start, end}); const campaign = {name, start, attacker, defender};
states[defender].campaigns.push({name: `${trimVowels(an)}ian War`, start, end}); states[attacker].campaigns.push(campaign);
states[defender].campaigns.push(campaign);
// attacker vassals join the war // attacker vassals join the war
ad.forEach((r, d) => { ad.forEach((r, d) => {
@ -790,7 +707,6 @@ window.BurgsAndStates = (() => {
} }
TIME && console.timeEnd("generateDiplomacy"); TIME && console.timeEnd("generateDiplomacy");
//console.table(states.map(s => s.diplomacy));
}; };
// select a forms for listed or all valid states // 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}`; 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 { return {
generate, generate,
expandStates, expandStates,
normalizeStates, normalizeStates,
getPoles,
assignColors, assignColors,
drawBurgs,
specifyBurgs, specifyBurgs,
defineBurgFeatures, defineBurgFeatures,
getType, getType,
@ -1206,7 +880,7 @@ window.BurgsAndStates = (() => {
generateDiplomacy, generateDiplomacy,
defineStateForms, defineStateForms,
getFullName, getFullName,
generateProvinces, updateCultures,
updateCultures getCloseToEdgePoint
}; };
})(); })();

View file

@ -8,7 +8,10 @@ window.Cultures = (function () {
cells = pack.cells; cells = pack.cells;
const cultureIds = new Uint16Array(cells.i.length); // cell cultures 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 const populated = cells.i.filter(i => cells.s[i]); // populated cells
if (populated.length < count * 25) { if (populated.length < count * 25) {
@ -120,26 +123,26 @@ window.Cultures = (function () {
cultures.forEach(c => (c.base = c.base % nameBases.length)); cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(culturesNumber) { function selectCultures(culturesNumber) {
let def = getDefault(culturesNumber); let defaultCultures = getDefault(culturesNumber);
const cultures = []; const cultures = [];
pack.cultures?.forEach(function (culture) { pack.cultures?.forEach(function (culture) {
if (culture.lock) cultures.push(culture); if (culture.lock && !culture.removed) cultures.push(culture);
}); });
if (!cultures.length) { if (!cultures.length) {
if (culturesNumber === def.length) return def; if (culturesNumber === defaultCultures.length) return defaultCultures;
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber); 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 { do {
rnd = rand(def.length - 1); rnd = rand(defaultCultures.length - 1);
culture = def[rnd]; culture = defaultCultures[rnd];
i++; i++;
} while (i < 200 && !P(culture.odd)); } while (i < 200 && !P(culture.odd));
cultures.push(culture); cultures.push(culture);
def.splice(rnd, 1); defaultCultures.splice(rnd, 1);
} }
return cultures; return cultures;
} }
@ -169,7 +172,7 @@ window.Cultures = (function () {
else if (type === "Nomadic") base = 1.5; else if (type === "Nomadic") base = 1.5;
else if (type === "Hunting") base = 0.7; else if (type === "Hunting") base = 0.7;
else if (type === "Highland") base = 1.2; 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"); TIME && console.timeEnd("generateCultures");
@ -515,7 +518,7 @@ window.Cultures = (function () {
TIME && console.time("expandCultures"); TIME && console.time("expandCultures");
const {cells, cultures} = pack; const {cells, cultures} = pack;
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority}); const queue = new FlatQueue();
const cost = []; const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1; const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
@ -535,11 +538,11 @@ window.Cultures = (function () {
for (const culture of cultures) { for (const culture of cultures) {
if (!culture.i || culture.removed || culture.lock) continue; 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) { while (queue.length) {
const {cellId, priority, cultureId} = queue.dequeue(); const {cellId, priority, cultureId} = queue.pop();
const {type, expansionism} = cultures[cultureId]; const {type, expansionism} = cultures[cultureId];
cells.c[cellId].forEach(neibCellId => { cells.c[cellId].forEach(neibCellId => {
@ -563,7 +566,7 @@ window.Cultures = (function () {
if (!cost[neibCellId] || totalCost < cost[neibCellId]) { if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
cost[neibCellId] = totalCost; 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"; "use strict";
// update old map file to the current version // update old map file to the current version
export function resolveVersionConflicts(version) { export function resolveVersionConflicts(mapVersion) {
if (version < 1) { const isOlderThan = tagVersion => compareVersions(mapVersion, tagVersion).isOlder;
if (isOlderThan("1.0.0")) {
// v1.0 added a new religions layer // v1.0 added a new religions layer
relig = viewbox.insert("g", "#terrain").attr("id", "relig"); relig = viewbox.insert("g", "#terrain").attr("id", "relig");
Religions.generate(); Religions.generate();
@ -49,9 +51,8 @@ export function resolveVersionConflicts(version) {
BurgsAndStates.generateCampaigns(); BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy(); BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
drawStates(); Provinces.generate();
BurgsAndStates.generateProvinces(); Provinces.getPoles();
drawBorders();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut(); if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
@ -63,7 +64,7 @@ export function resolveVersionConflicts(version) {
.attr("stroke-width", 0) .attr("stroke-width", 0)
.attr("stroke-dasharray", null) .attr("stroke-dasharray", null)
.attr("stroke-linecap", "butt"); .attr("stroke-linecap", "butt");
addZones(); Zones.generate();
if (!markers.selectAll("*").size()) { if (!markers.selectAll("*").size()) {
Markers.generate(); Markers.generate();
turnButtonOn("toggleMarkers"); turnButtonOn("toggleMarkers");
@ -107,11 +108,11 @@ export function resolveVersionConflicts(version) {
biomesData.habitability.push(12); biomesData.habitability.push(12);
} }
if (version < 1.1) { if (isOlderThan("1.1.0")) {
// v1.0 initial code had a bug with religion layer id // v1.0 code had a bug with religion layer id
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig"); 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) { for (const s of pack.states) {
if (!s.diplomacy) continue; if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r)); s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
@ -200,10 +201,12 @@ export function resolveVersionConflicts(version) {
defs.select("#water").selectAll("path").remove(); defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove(); coastline.selectAll("path").remove();
lakes.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 // v1.11 added new attributes
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0); terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
svg.select("#oceanic > *").attr("id", "oceanicPattern"); svg.select("#oceanic > *").attr("id", "oceanicPattern");
@ -229,7 +232,7 @@ export function resolveVersionConflicts(version) {
if (!terrain.attr("density")) terrain.attr("density", 0.4); 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 // v1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function () { viewbox.selectAll("g").each(function () {
if (this.hasAttribute("display")) { 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 // v1.22 changed state neighbors from Set object to array
BurgsAndStates.collectStatistics(); BurgsAndStates.collectStatistics();
} }
if (version < 1.3) { if (isOlderThan("1.3.0")) {
// v1.3 added global options object // v1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19] const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000); const year = rand(100, 2000);
@ -285,7 +288,7 @@ export function resolveVersionConflicts(version) {
Military.generate(); Military.generate();
} }
if (version < 1.4) { if (isOlderThan("1.4.0")) {
// v1.35 added dry lakes // v1.35 added dry lakes
if (!lakes.select("#dry").size()) { if (!lakes.select("#dry").size()) {
lakes.append("g").attr("id", "dry"); 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))); 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 // not need to store default styles from v 1.5
localStorage.removeItem("styleClean"); localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom"); 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 // v1.6 changed rivers data
for (const river of pack.rivers) { for (const river of pack.rivers) {
const el = document.getElementById("river" + river.i); 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 // v1.61 changed rulers data
ruler.style("display", null); ruler.style("display", null);
rulers = new Rulers(); 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>`; 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 // v1.62 changed grid data
gridOverlay.attr("size", null); gridOverlay.attr("size", null);
} }
if (version < 1.63) { if (isOlderThan("1.63.0")) {
// v1.63 changed ocean pattern opacity element // v1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById("oceanPattern"); const oceanPattern = document.getElementById("oceanPattern");
if (oceanPattern) oceanPattern.removeAttribute("opacity"); if (oceanPattern) oceanPattern.removeAttribute("opacity");
@ -472,7 +475,7 @@ export function resolveVersionConflicts(version) {
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px"); labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
} }
if (version < 1.64) { if (isOlderThan("1.64.0")) {
// v1.64 change states style // v1.64 change states style
const opacity = regions.attr("opacity"); const opacity = regions.attr("opacity");
const filter = regions.attr("filter"); const filter = regions.attr("filter");
@ -481,7 +484,7 @@ export function resolveVersionConflicts(version) {
regions.attr("opacity", null).attr("filter", null); regions.attr("opacity", null).attr("filter", null);
} }
if (version < 1.65) { if (isOlderThan("1.65.0")) {
// v1.65 changed rivers data // v1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack; 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 // remove style to unhide layers
rivers.attr("style", null); rivers.attr("style", null);
borders.attr("style", null); borders.attr("style", null);
} }
if (version < 1.7) { if (isOlderThan("1.7.0")) {
// v1.7 changed markers data // v1.7 changed markers data
const defs = document.getElementById("defs-markers"); const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("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 // v1.72 renamed custom style presets
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style")); const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style"));
storedStyles.forEach(styleName => { 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 // v1.73 moved the hatching patterns out of the user's SVG
document.getElementById("hatching")?.remove(); 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 // v1.84.0 added grid.cellsDesired to stored data
if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3); 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 // v1.84.0 moved intial screen out of maon svg
svg.select("#initial").remove(); svg.select("#initial").remove();
} }
if (version < 1.86) { if (isOlderThan("1.86.0")) {
// v1.86.0 added multi-origin culture and religion hierarchy trees // v1.86.0 added multi-origin culture and religion hierarchy trees
for (const culture of pack.cultures) { for (const culture of pack.cultures) {
culture.origins = [culture.origin]; 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 // v1.87 may have incorrect shield for some reason
pack.states.forEach(({coa}) => { pack.states.forEach(({coa}) => {
if (coa?.shield === "state") delete coa.shield; 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 // from 1.91.00 custom coa is moved to coa object
pack.states.forEach(state => { pack.states.forEach(state => {
if (state.coa === "custom") state.coa = {custom: true}; 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' // v1.92 change labels text-anchor from 'start' to 'middle'
labels.selectAll("tspan").each(function () { labels.selectAll("tspan").each(function () {
this.setAttribute("x", 0); 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 // from v1.94.00 texture image is removed when layer is off
texture.style("display", null); 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 // v1.95.00 added vignette visual layer
const mask = defs.append("mask").attr("id", "vignette-mask"); 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%"); 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%"); 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 // v1.96 added ocean rendering for heightmap
terrs.selectAll("*").remove(); 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 // v1.97.00 changed MFCG link to an arbitrary preview URL
options.villageMaxPopulation = 2000; options.villageMaxPopulation = 2000;
options.showBurgPreview = options.showMFCGMap; 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 // v1.98.00 changed compass layer and rose element id
const rose = compass.select("use"); const rose = compass.select("use");
rose.attr("xlink:href", "#defs-compass-rose"); 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 // v1.99.00 changed routes generation algorithm and data format
routes.attr("display", null).attr("style", null); 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="culturesHeirarchy" data-tip="Show cultures hierarchy tree" class="icon-sitemap"></button>
<button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button> <button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button>
<div id="culturesManuallyButtons" style="display: none"> <div id="culturesManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic">Brush size: <div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
<input <slider-input id="culturesBrush" min="1" max="100" value="15">Brush size:</slider-input>
id="culturesManuallyBrush" </div>
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 />
<button id="culturesManuallyApply" data-tip="Apply assignment" class="icon-check"></button> <button id="culturesManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="culturesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button> <button id="culturesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div> </div>
@ -281,6 +266,7 @@ function getTypeOptions(type) {
function getBaseOptions(base) { function getBaseOptions(base) {
let options = ""; let options = "";
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`)); 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; return options;
} }
@ -359,10 +345,13 @@ function cultureChangeName() {
} }
function cultureRegenerateName() { function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id; const cultureId = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture); 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; this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name; pack.cultures[cultureId].name = name;
} }
function cultureChangeExpansionism() { function cultureChangeExpansionism() {
@ -500,6 +489,7 @@ function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture)
burgs.forEach(b => (b.population = population)); burgs.forEach(b => (b.population = population));
} }
if (layerIsOn("togglePopulation")) drawPopulation();
refreshCulturesEditor(); refreshCulturesEditor();
} }
@ -507,12 +497,15 @@ function cultureRegenerateBurgs() {
if (customization === 4) return; if (customization === 4) return;
const cultureId = +this.parentNode.dataset.id; const cultureId = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock); const base = pack.cultures[cultureId].base;
cBurgs.forEach(b => { 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); b.name = Names.getCulture(cultureId);
labels.select("[data-id='" + b.i + "']").text(b.name); 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) { function removeCulture(cultureId) {
@ -718,7 +711,7 @@ function selectCultureOnMapClick() {
} }
function dragCultureBrush() { function dragCultureBrush() {
const radius = +culturesManuallyBrush.value; const radius = +culturesBrush.value;
d3.event.on("drag", () => { d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return; if (!d3.event.dx && !d3.event.dy) return;
@ -759,7 +752,7 @@ function changeCultureForSelection(selection) {
function moveCultureBrush() { function moveCultureBrush() {
showMainTip(); showMainTip();
const point = d3.mouse(this); const point = d3.mouse(this);
const radius = +culturesManuallyBrush.value; const radius = +culturesBrush.value;
moveCircle(point[0], point[1], radius); moveCircle(point[0], point[1], radius);
} }
@ -862,14 +855,15 @@ async function uploadCulturesData() {
this.value = ""; this.value = "";
const csv = await file.text(); const csv = await file.text();
const data = d3.csvParse(csv, d => ({ const data = d3.csvParse(csv, d => ({
i: +d.Id,
name: d.Name, name: d.Name,
i: +d.Id,
color: d.Color, color: d.Color,
expansionism: +d.Expansionism, expansionism: +d.Expansionism,
type: d.Type, type: d.Type,
population: +d.Population, population: +d.Population,
emblemsShape: d["Emblems Shape"], emblemsShape: d["Emblems Shape"],
origins: d.Origins origins: d.Origins,
namesbase: d.Namesbase
})); }));
const {cultures, cells} = pack; const {cultures, cells} = pack;
@ -896,7 +890,7 @@ async function uploadCulturesData() {
culture.i culture.i
); );
} else { } 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); cultures.push(current);
} }
@ -916,6 +910,10 @@ async function uploadCulturesData() {
else current.type = "Generic"; 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) { function restoreOrigins(originsString) {
const originNames = originsString const originNames = originsString
.replaceAll('"', "") .replaceAll('"', "")
@ -931,12 +929,6 @@ async function uploadCulturesData() {
current.origins = originIds.filter(id => id !== null); current.origins = originIds.filter(id => id !== null);
if (!current.origins.length) current.origins = [0]; 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)); 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 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 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 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 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 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> <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> <button id="religionsManually" data-tip="Manually re-assign religions" class="icon-brush"></button>
<div id="religionsManuallyButtons" style="display: none"> <div id="religionsManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic">Brush size: <div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
<input <slider-input id="religionsBrush" min="1" max="100" value="15">Brush size:</slider-input>
id="religionsManuallyBrush" </div>
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 />
<button id="religionsManuallyApply" data-tip="Apply assignment" class="icon-check"></button> <button id="religionsManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="religionsManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button> <button id="religionsManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div> </div>
@ -183,7 +167,7 @@ function religionsEditorAddLines() {
<select data-tip="Religion type" class="religionType placeholder" style="width: 5em"> <select data-tip="Religion type" class="religionType placeholder" style="width: 5em">
${getTypeOptions(r.type)} ${getTypeOptions(r.type)}
</select> </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> <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" /> <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> <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"> <select data-tip="Religion type" class="religionType" style="width: 5em">
${getTypeOptions(r.type)} ${getTypeOptions(r.type)}
</select> </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" /> value="${r.form}" autocorrect="off" spellcheck="false" />
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw hide"></span> <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" <input data-tip="Religion supreme deity" class="religionDeity hide" style="width: 17em"
@ -478,6 +462,7 @@ function changePopulation() {
burgs.forEach(b => (b.population = population)); burgs.forEach(b => (b.population = population));
} }
if (layerIsOn("togglePopulation")) drawPopulation();
refreshReligionsEditor(); refreshReligionsEditor();
} }
} }
@ -696,7 +681,7 @@ function selectReligionOnMapClick() {
} }
function dragReligionBrush() { function dragReligionBrush() {
const radius = +byId("religionsManuallyBrushNumber").value; const radius = +byId("religionsBrush").value;
d3.event.on("drag", () => { d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return; if (!d3.event.dx && !d3.event.dy) return;
@ -736,7 +721,7 @@ function changeReligionForSelection(selection) {
function moveReligionBrush() { function moveReligionBrush() {
showMainTip(); showMainTip();
const [x, y] = d3.mouse(this); const [x, y] = d3.mouse(this);
const radius = +byId("religionsManuallyBrushNumber").value; const radius = +byId("religionsBrush").value;
moveCircle(x, y, radius); 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 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 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 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 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 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> <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"> <div id="statesRegenerateButtons" style="display: none">
<button id="statesRegenerateBack" data-tip="Hide the regeneration menu" class="icon-cog-alt"></button> <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> <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"> <div data-tip="Additional growth rate. Defines how many land cells remain neutral" style="display: inline-block">
<label class="italic">Growth rate:</label> <slider-input id="statesGrowthRate" min=".1" max="3" step=".05" value="1">Growth rate:</slider-input>
<input </div>
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>
<button id="statesRecalculate" data-tip="Recalculate states based on current values of growth-related attributes" class="icon-retweet"></button> <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" /> <input id="statesAutoChange" class="checkbox" type="checkbox" />
<label for="statesAutoChange" class="checkbox-label"><i>auto-apply changes</i></label> <label for="statesAutoChange" class="checkbox-label"><i>auto-apply changes</i></label>
</span> </div>
<span data-tip="Allow system to change state labels when states data is change"> <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" /> <input id="adjustLabels" class="checkbox" type="checkbox" />
<label for="adjustLabels" class="checkbox-label"><i>auto-change labels</i></label> <label for="adjustLabels" class="checkbox-label"><i>auto-change labels</i></label>
</span> </div>
</div> </div>
<button id="statesManually" data-tip="Manually re-assign states" class="icon-brush"></button> <button id="statesManually" data-tip="Manually re-assign states" class="icon-brush"></button>
<div id="statesManuallyButtons" style="display: none"> <div id="statesManuallyButtons" style="display: none">
<label data-tip="Change brush size" data-shortcut="+ (increase), (decrease)" class="italic" <div data-tip="Change brush size. Shortcut: + to increase; to decrease" style="margin-block: 0.3em;">
>Brush size: <slider-input id="statesBrush" min="1" max="100" value="15">Brush size:</slider-input>
<input </div>
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 />
<button id="statesManuallyApply" data-tip="Apply assignment" class="icon-check"></button> <button id="statesManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
<button id="statesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button> <button id="statesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
</div> </div>
@ -135,8 +100,7 @@ function addListeners() {
byId("statesRegenerateBack").on("click", exitRegenerationMenu); byId("statesRegenerateBack").on("click", exitRegenerationMenu);
byId("statesRecalculate").on("click", () => recalculateStates(true)); byId("statesRecalculate").on("click", () => recalculateStates(true));
byId("statesRandomize").on("click", randomizeStatesExpansion); byId("statesRandomize").on("click", randomizeStatesExpansion);
byId("statesNeutral").on("input", changeStatesGrowthRate); byId("statesGrowthRate").on("input", () => recalculateStates(false));
byId("statesNeutralNumber").on("change", changeStatesGrowthRate);
byId("statesManually").on("click", enterStatesManualAssignent); byId("statesManually").on("click", enterStatesManualAssignent);
byId("statesManuallyApply").on("click", applyStatesManualAssignent); byId("statesManuallyApply").on("click", applyStatesManualAssignent);
byId("statesManuallyCancel").on("click", () => exitStatesManualAssignment(false)); 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="${ <input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${
s.name s.name
}" readonly /> }" readonly />
<svg class="coaIcon placeholder hide"></svg> <svg class="coaIcon placeholder"></svg>
<input class="stateForm placeholder" value="none" /> <input class="stateForm placeholder" value="none" />
<span class="icon-star-empty placeholder hide"></span> <span class="icon-star-empty placeholder"></span>
<input class="stateCapital placeholder hide" /> <input class="stateCapital placeholder" />
<select class="stateCulture placeholder hide">${getCultureOptions(0)}</select> <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> <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> <div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
@ -267,14 +231,14 @@ function statesEditorAddLines() {
> >
<fill-box fill="${s.color}"></fill-box> <fill-box fill="${s.color}"></fill-box>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly /> <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 s.i
}"></use></svg> }"></use></svg>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${ <input data-tip="State form name. Click to change" class="stateForm name pointer" value="${
s.formName s.formName
}" readonly /> }" readonly />
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span> <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 hide" value="${capital}" autocorrect="off" spellcheck="false" /> <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( <select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(
s.culture s.culture
)}</select> )}</select>
@ -579,6 +543,7 @@ function changePopulation(stateId) {
burgs.forEach(b => (b.population = population)); burgs.forEach(b => (b.population = population));
} }
if (layerIsOn("togglePopulation")) drawPopulation();
refreshStatesEditor(); refreshStatesEditor();
} }
} }
@ -678,11 +643,11 @@ function stateRemove(stateId) {
pack.states[stateId] = {i: stateId, removed: true}; pack.states[stateId] = {i: stateId, removed: true};
debug.selectAll(".highlight").remove(); debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates(); if (layerIsOn("toggleStates")) drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); if (layerIsOn("toggleBorders")) drawBorders();
else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces(); if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor(); refreshStatesEditor();
} }
@ -728,7 +693,7 @@ function showStatesChart() {
.sum(d => d.area) .sum(d => d.area)
.sort((a, b) => b.value - a.value); .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 margin = {top: 0, right: -50, bottom: 0, left: -50};
const w = size - margin.left - margin.right; const w = size - margin.left - margin.right;
const h = size - margin.top - margin.bottom; const h = size - margin.top - margin.bottom;
@ -778,6 +743,7 @@ function showStatesChart() {
node node
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.style("font-size", d => rn((d.r ** 0.97 * 4) / lp(d.data.name), 2) + "px") .style("font-size", d => rn((d.r ** 0.97 * 4) / lp(d.data.name), 2) + "px")
.selectAll("tspan") .selectAll("tspan")
.data(d => d.data.name.split(exp)) .data(d => d.data.name.split(exp))
@ -875,22 +841,16 @@ function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return; if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates(); BurgsAndStates.expandStates();
BurgsAndStates.generateProvinces(); Provinces.generate();
if (!layerIsOn("toggleStates")) toggleStates(); Provinces.getPoles();
else drawStates(); BurgsAndStates.getPoles();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders(); if (layerIsOn("toggleStates")) drawStates();
if (layerIsOn("toggleBorders")) drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces(); if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) drawStateLabels(); if (adjustLabels.checked) drawStateLabels();
refreshStatesEditor();
}
function changeStatesGrowthRate() { refreshStatesEditor();
const growthRate = +this.value;
byId("statesNeutral").value = growthRate;
byId("statesNeutralNumber").value = growthRate;
tip("Growth rate: " + growthRate);
recalculateStates(false);
} }
function randomizeStatesExpansion() { function randomizeStatesExpansion() {
@ -959,7 +919,7 @@ function selectStateOnMapClick() {
} }
function dragStateBrush() { function dragStateBrush() {
const r = +statesManuallyBrush.value; const r = +statesBrush.value;
d3.event.on("drag", () => { d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return; if (!d3.event.dx && !d3.event.dy) return;
@ -1002,7 +962,7 @@ function changeStateForSelection(selection) {
function moveStateBrush() { function moveStateBrush() {
showMainTip(); showMainTip();
const point = d3.mouse(this); const point = d3.mouse(this);
const radius = +statesManuallyBrush.value; const radius = +statesBrush.value;
moveCircle(point[0], point[1], radius); moveCircle(point[0], point[1], radius);
} }
@ -1025,6 +985,7 @@ function applyStatesManualAssignent() {
if (affectedStates.length) { if (affectedStates.length) {
refreshStatesEditor(); refreshStatesEditor();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates(); layerIsOn("toggleStates") ? drawStates() : toggleStates();
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]); if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]); adjustProvinces([...new Set(affectedProvinces)]);
@ -1240,7 +1201,6 @@ function addState() {
const basename = center % 5 === 0 ? burgs[burg].name : Names.getCulture(culture); const basename = center % 5 === 0 ? burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture); const name = Names.getState(basename, culture);
const color = getRandomColor(); const color = getRandomColor();
const pole = cells.p[center];
// generate emblem // generate emblem
const cultureType = pack.cultures[culture].type; const cultureType = pack.cultures[culture].type;
@ -1290,38 +1250,21 @@ function addState() {
culture, culture,
military: [], military: [],
alert: 1, alert: 1,
coa, coa
pole
}); });
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics(); BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]); BurgsAndStates.defineStateForms([newState]);
adjustProvinces([cells.province[center]]); adjustProvinces([cells.province[center]]);
if (layerIsOn("toggleProvinces")) toggleProvinces(); drawStateLabels([newState]);
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);
COArenderer.add("state", newState, coa, states[newState].pole[0], states[newState].pole[1]); 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(); statesEditorAddLines();
} }
@ -1459,6 +1402,7 @@ function openStateMergeDialog() {
unfog(); unfog();
debug.selectAll(".highlight").remove(); debug.selectAll(".highlight").remove();
BurgsAndStates.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates(); layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders(); layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
layerIsOn("toggleProvinces") && drawProvinces(); layerIsOn("toggleProvinces") && drawProvinces();

View file

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

View file

@ -580,4 +580,13 @@ MisterPete
Johanna Martin Johanna Martin
Marmalade_MacGuffin Marmalade_MacGuffin
James Benware 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: 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" "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", family: "Faster One",
src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)", src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
@ -129,6 +135,12 @@ const fonts = [
unicodeRange: 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" "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", family: "Kaushan Script",
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)", 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) { async save(fileName, contents) {
const resp = await this.call("filesUpload", {path: "/" + 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; return true;
}, },
@ -104,7 +104,7 @@ window.Cloud = (function () {
// Callback function for auth window // Callback function for auth window
async setDropBoxToken(token) { async setDropBoxToken(token) {
DEBUG && console.info("Access token:", token); DEBUG.cloud && console.info("Access token:", token);
setToken(this.name, token); setToken(this.name, token);
await this.connect(token); await this.connect(token);
this.authWindow.close(); this.authWindow.close();
@ -131,7 +131,7 @@ window.Cloud = (function () {
allow_download: true allow_download: true
}; };
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings}); 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; return resp.result.url;
} }
}; };

View file

@ -175,6 +175,7 @@ async function getMapURL(type, options) {
noWater = false, noWater = false,
noScaleBar = false, noScaleBar = false,
noIce = false, noIce = false,
noVignette = false,
fullMap = false fullMap = false
} = options || {}; } = options || {};
@ -199,6 +200,7 @@ async function getMapURL(type, options) {
clone.select("#oceanPattern").attr("opacity", 0); clone.select("#oceanPattern").attr("opacity", 0);
} }
if (noIce) clone.select("#ice")?.remove(); if (noIce) clone.select("#ice")?.remove();
if (noVignette) clone.select("#vignette")?.remove();
if (fullMap) { if (fullMap) {
// reset transform to show the whole map // reset transform to show the whole map
clone.attr("width", graphWidth).attr("height", graphHeight); clone.attr("width", graphWidth).attr("height", graphHeight);
@ -318,6 +320,40 @@ async function getMapURL(type, options) {
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true)); 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("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
@ -440,14 +476,24 @@ function inlineStyle(clone) {
emptyG.remove(); emptyG.remove();
} }
function saveGeoJSON_Cells() { function saveGeoJsonCells() {
const {cells, vertices} = pack;
const json = {type: "FeatureCollection", features: []}; const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
const getPopulation = i => { const getPopulation = i => {
const [r, u] = getCellPopulation(i); const [r, u] = getCellPopulation(i);
return rn(r + u); 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 => { cells.i.forEach(i => {
const coordinates = getCellCoordinates(cells.v[i]); const coordinates = getCellCoordinates(cells.v[i]);
@ -470,20 +516,13 @@ function saveGeoJSON_Cells() {
downloadFile(JSON.stringify(json), fileName, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Routes() { function saveGeoJsonRoutes() {
const {cells, burgs} = pack; const features = pack.routes.map(({i, points, group, name = null}) => {
let points = cells.p.map(([x, y], cellId) => { const coordinates = points.map(([x, y]) => getCoordinates(x, y, 4));
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);
return { return {
type: "Feature", type: "Feature",
geometry: {type: "LineString", coordinates}, geometry: {type: "LineString", coordinates},
properties: {id: route.id, group: route.group} properties: {id: i, group, name}
}; };
}); });
const json = {type: "FeatureCollection", features}; const json = {type: "FeatureCollection", features};
@ -492,30 +531,31 @@ function saveGeoJSON_Routes() {
downloadFile(JSON.stringify(json), fileName, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Rivers() { function saveGeoJsonRivers() {
const json = {type: "FeatureCollection", features: []}; const features = pack.rivers.map(
({i, cells, points, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}) => {
rivers.selectAll("path").each(function () { if (!cells || cells.length < 2) return;
const river = pack.rivers.find(r => r.i === +this.id.slice(5)); const meanderedPoints = Rivers.addMeandering(cells, points);
if (!river) return; const coordinates = meanderedPoints.map(([x, y]) => getCoordinates(x, y, 4));
return {
const coordinates = getRiverPoints(this); type: "Feature",
const properties = {...river, id: this.id}; geometry: {type: "LineString", coordinates},
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties}; properties: {id: i, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}
json.features.push(feature); };
}); }
);
const json = {type: "FeatureCollection", features};
const fileName = getFileName("Rivers") + ".geojson"; const fileName = getFileName("Rivers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
} }
function saveGeoJSON_Markers() { function saveGeoJsonMarkers() {
const features = pack.markers.map(marker => { const features = pack.markers.map(marker => {
const {i, type, icon, x, y, size, fill, stroke} = marker; const {i, type, icon, x, y, size, fill, stroke} = marker;
const coordinates = getCoordinates(x, y, 4); const coordinates = getCoordinates(x, y, 4);
const id = `marker${i}`; const note = notes.find(note => note.id === "marker" + i);
const note = notes.find(note => note.id === id); const properties = {id: i, type, icon, x, y, ...note, size, fill, stroke};
const properties = {id, type, icon, x, y, ...note, size, fill, stroke};
return {type: "Feature", geometry: {type: "Point", coordinates}, properties}; return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
}); });
@ -524,22 +564,3 @@ function saveGeoJSON_Markers() {
const fileName = getFileName("Markers") + ".geojson"; const fileName = getFileName("Markers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json"); 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"; "use strict";
// Functions to load and parse .map/.gz files // Functions to load and parse .map/.gz files
async function quickLoad() { async function quickLoad() {
const blob = await ldb.get("lastMap"); const blob = await ldb.get("lastMap");
@ -12,7 +13,7 @@ async function quickLoad() {
async function loadFromDropbox() { async function loadFromDropbox() {
const mapPath = byId("loadFromDropboxSelect")?.value; 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); const blob = await Cloud.providers.dropbox.load(mapPath);
uploadMap(blob); uploadMap(blob);
} }
@ -95,6 +96,7 @@ function showUploadErrorMessage(error, URL, random) {
title: "Loading error", title: "Loading error",
width: "32em", width: "32em",
buttons: { buttons: {
"Clear cache": () => cleanupData(),
OK: function () { OK: function () {
$(this).dialog("close"); $(this).dialog("close");
} }
@ -104,26 +106,28 @@ function showUploadErrorMessage(error, URL, random) {
function uploadMap(file, callback) { function uploadMap(file, callback) {
uploadMap.timeStart = performance.now(); uploadMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onloadend = async function (fileLoadedEvent) { fileReader.onloadend = async function (fileLoadedEvent) {
if (callback) callback(); if (callback) callback();
byId("coas").innerHTML = ""; // remove auto-generated emblems byId("coas").innerHTML = ""; // remove auto-generated emblems
const result = fileLoadedEvent.target.result; const result = fileLoadedEvent.target.result;
const [mapData, mapVersion] = await parseLoadedResult(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 isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 10 || !mapData[5];
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion); 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); if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
if (isNewer) return showUploadMessage("newer", mapData, mapVersion); if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion); if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
}; };
@ -149,53 +153,65 @@ async function uncompress(compressedData) {
async function parseLoadedResult(result) { async function parseLoadedResult(result) {
try { try {
const resultAsString = new TextDecoder().decode(result); const resultAsString = new TextDecoder().decode(result);
// data can be in FMG internal format or base64 encoded // data can be in FMG internal format or base64 encoded
const isDelimited = resultAsString.substring(0, 10).includes("|"); 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"); // fix if svg part has CRLF line endings instead of LF
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]); const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
return [mapData, mapVersion]; 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) { } catch (error) {
// map file can be compressed with gzip const uncompressedData = await uncompress(result); // file can be gzip compressed
const uncompressedData = await uncompress(result);
if (uncompressedData) return parseLoadedResult(uncompressedData); if (uncompressedData) return parseLoadedResult(uncompressedData);
ERROR && console.error(error); ERROR && console.error(error);
return [null, null]; return {mapData: null, mapVersion: null};
} }
} }
function showUploadMessage(type, mapData, mapVersion) { function showUploadMessage(type, mapData, mapVersion) {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version"); let message, title;
let message, title, canBeLoaded;
if (type === "invalid") { 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"; title = "Invalid file";
canBeLoaded = false; } else if (type === "updated") {
parseLoadedData(mapData, mapVersion);
return;
} else if (type === "ancient") { } 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}`; 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"; title = "Ancient file";
canBeLoaded = false;
} else if (type === "newer") { } 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`; 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"; title = "Newer file";
canBeLoaded = false;
} else if (type === "outdated") { } 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); parseLoadedData(mapData, mapVersion);
return; return;
} }
alertMessage.innerHTML = message; alertMessage.innerHTML = message;
const buttons = { $("#alert").dialog({
title,
buttons: {
"Clear cache": () => cleanupData(),
OK: function () { OK: function () {
$(this).dialog("close"); $(this).dialog("close");
if (canBeLoaded) parseLoadedData(mapData, mapVersion);
} }
}; }
$("#alert").dialog({title, buttons}); });
} }
async function parseLoadedData(data, mapVersion) { async function parseLoadedData(data, mapVersion) {
@ -205,31 +221,29 @@ async function parseLoadedData(data, mapVersion) {
customization = 0; customization = 0;
if (customizationMenu.offsetParent) styleTab.click(); if (customizationMenu.offsetParent) styleTab.click();
{
const params = data[0].split("|"); const params = data[0].split("|");
void (function parseParameters() {
if (params[3]) { if (params[3]) {
seed = params[3]; seed = params[3];
optionsSeed.value = seed; optionsSeed.value = seed;
} INFO && console.group("Loaded Map " + seed);
} else INFO && console.group("Loaded Map");
if (params[4]) graphWidth = +params[4]; if (params[4]) graphWidth = +params[4];
if (params[5]) graphHeight = +params[5]; if (params[5]) graphHeight = +params[5];
mapId = params[6] ? +params[6] : Date.now(); 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("|"); const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]); 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[2]) areaUnit.value = settings[2];
if (settings[3]) applyOption(heightUnit, settings[3]); 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]; if (settings[5]) temperatureScale.value = settings[5];
// setting 6-11 (scaleBar) are part of style now, kept as "" in newer versions for compatibility // 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[12]) populationRate = populationRateInput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13]; if (settings[13]) urbanization = urbanizationInput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100); 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[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
if (settings[18]) precInput.value = precOutput.value = settings[18]; 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[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22]; if (settings[22]) stylePreset.value = settings[22];
if (settings[23]) rescaleLabels.checked = +settings[23]; 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[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; stateLabelsModeInput.value = options.stateLabelsMode;
yearInput.value = options.year; yearInput.value = options.year;
eraInput.value = options.era; eraInput.value = options.era;
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision"; shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
})(); }
void (function parseConfiguration() { {
if (data[2]) mapCoordinates = JSON.parse(data[2]); if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]); if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]); if (data[33]) rulers.fromString(data[33]);
@ -268,13 +283,14 @@ async function parseLoadedData(data, mapVersion) {
declareFont(usedFont); declareFont(usedFont);
}); });
} }
}
{
const biomes = data[3].split("|"); const biomes = data[3].split("|");
biomesData = Biomes.getDefault(); biomesData = Biomes.getDefault();
biomesData.color = biomes[0].split(","); biomesData.color = biomes[0].split(",");
biomesData.habitability = biomes[1].split(",").map(h => +h); biomesData.habitability = biomes[1].split(",").map(h => +h);
biomesData.name = biomes[2].split(","); biomesData.name = biomes[2].split(",");
// push custom biomes if any // push custom biomes if any
for (let i = biomesData.i.length; i < biomesData.name.length; i++) { for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
biomesData.i.push(biomesData.i.length); biomesData.i.push(biomesData.i.length);
@ -282,14 +298,14 @@ async function parseLoadedData(data, mapVersion) {
biomesData.icons.push([]); biomesData.icons.push([]);
biomesData.cost.push(50); biomesData.cost.push(50);
} }
})(); }
void (function replaceSVG() { {
svg.remove(); svg.remove();
document.body.insertAdjacentHTML("afterbegin", data[5]); document.body.insertAdjacentHTML("afterbegin", data[5]);
})(); }
void (function redefineElements() { {
svg = d3.select("#map"); svg = d3.select("#map");
defs = svg.select("#deftemp"); defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox"); viewbox = svg.select("#viewbox");
@ -339,38 +355,33 @@ async function parseLoadedData(data, mapVersion) {
fogging = viewbox.select("#fogging"); fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug"); debug = viewbox.select("#debug");
burgLabels = labels.select("#burgLabels"); burgLabels = labels.select("#burgLabels");
})();
void (function addMissingElements() {
if (!texture.size()) { if (!texture.size()) {
texture = viewbox texture = viewbox
.insert("g", "#landmass") .insert("g", "#landmass")
.attr("id", "texture") .attr("id", "texture")
.attr("data-href", "./images/textures/plaster.jpg"); .attr("data-href", "./images/textures/plaster.jpg");
} }
if (!emblems.size()) { if (!emblems.size()) {
emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none"); emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none");
} }
})(); }
void (function parseGridData() { {
grid = JSON.parse(data[6]); grid = JSON.parse(data[6]);
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary); const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
grid.cells = cells; grid.cells = cells;
grid.vertices = vertices; grid.vertices = vertices;
grid.cells.h = Uint8Array.from(data[7].split(",")); grid.cells.h = Uint8Array.from(data[7].split(","));
grid.cells.prec = Uint8Array.from(data[8].split(",")); grid.cells.prec = Uint8Array.from(data[8].split(","));
grid.cells.f = Uint16Array.from(data[9].split(",")); grid.cells.f = Uint16Array.from(data[9].split(","));
grid.cells.t = Int8Array.from(data[10].split(",")); grid.cells.t = Int8Array.from(data[10].split(","));
grid.cells.temp = Int8Array.from(data[11].split(",")); grid.cells.temp = Int8Array.from(data[11].split(","));
})(); }
void (function parsePackData() { {
reGraph(); reGraph();
reMarkFeatures(); Features.markupPack();
pack.features = JSON.parse(data[12]); pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]); pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]); pack.states = JSON.parse(data[14]);
@ -380,22 +391,21 @@ async function parseLoadedData(data, mapVersion) {
pack.rivers = data[32] ? JSON.parse(data[32]) : []; pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.markers = data[35] ? JSON.parse(data[35]) : []; pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : []; pack.routes = data[37] ? JSON.parse(data[37]) : [];
pack.zones = data[38] ? JSON.parse(data[38]) : [];
const cells = pack.cells; pack.cells.biome = Uint8Array.from(data[16].split(","));
cells.biome = Uint8Array.from(data[16].split(",")); pack.cells.burg = Uint16Array.from(data[17].split(","));
cells.burg = Uint16Array.from(data[17].split(",")); pack.cells.conf = Uint8Array.from(data[18].split(","));
cells.conf = Uint8Array.from(data[18].split(",")); pack.cells.culture = Uint16Array.from(data[19].split(","));
cells.culture = Uint16Array.from(data[19].split(",")); pack.cells.fl = Uint16Array.from(data[20].split(","));
cells.fl = Uint16Array.from(data[20].split(",")); pack.cells.pop = Float32Array.from(data[21].split(","));
cells.pop = Float32Array.from(data[21].split(",")); pack.cells.r = Uint16Array.from(data[22].split(","));
cells.r = Uint16Array.from(data[22].split(",")); // data[23] had deprecated cells.road
// data[23] for deprecated cells.road pack.cells.s = Uint16Array.from(data[24].split(","));
cells.s = Uint16Array.from(data[24].split(",")); pack.cells.state = Uint16Array.from(data[25].split(","));
cells.state = Uint16Array.from(data[25].split(",")); pack.cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(pack.cells.i.length);
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length); pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length); // data[28] had deprecated cells.crossroad
// data[28] for deprecated cells.crossroad pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
cells.routes = data[36] ? JSON.parse(data[36]) : {};
if (data[31]) { if (data[31]) {
const namesDL = data[31].split("/"); 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}; 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 isVisible = selection => selection.node() && selection.style("display") !== "none";
const isVisibleNode = node => node && node.style.display !== "none"; const isVisibleNode = node => node && node.style.display !== "none";
const hasChildren = selection => selection.node()?.hasChildNodes(); const hasChildren = selection => selection.node()?.hasChildNodes();
@ -422,7 +432,7 @@ async function parseLoadedData(data, mapVersion) {
// turn on active layers // turn on active layers
if (hasChild(texture, "image")) turnOn("toggleTexture"); 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(biomes)) turnOn("toggleBiomes");
if (hasChildren(cells)) turnOn("toggleCells"); if (hasChildren(cells)) turnOn("toggleCells");
if (hasChildren(gridOverlay)) turnOn("toggleGrid"); if (hasChildren(gridOverlay)) turnOn("toggleGrid");
@ -437,13 +447,13 @@ async function parseLoadedData(data, mapVersion) {
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones"); if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders"); if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes"); 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 (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChildren(ice)) turnOn("toggleIce"); 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(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels"); if (isVisible(labels)) turnOn("toggleLabels");
if (isVisible(icons)) turnOn("toggleIcons"); if (isVisible(icons)) turnOn("toggleBurgIcons");
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary"); if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
if (hasChildren(markers)) turnOn("toggleMarkers"); if (hasChildren(markers)) turnOn("toggleMarkers");
if (isVisible(ruler)) turnOn("toggleRulers"); if (isVisible(ruler)) turnOn("toggleRulers");
@ -451,20 +461,19 @@ async function parseLoadedData(data, mapVersion) {
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette"); if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
getCurrentPreset(); getCurrentPreset();
})(); }
void (function restoreEvents() { {
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits()); scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend legend
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")) .on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
.on("click", () => clearLegend()); .on("click", () => clearLegend());
})(); }
{ {
// dynamically import and run auto-update script // dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]); const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.108.0");
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.01"); resolveVersionConflicts(mapVersion);
resolveVersionConflicts(versionNumber);
} }
// add custom heightmap color scheme if any // add custom heightmap color scheme if any
@ -481,19 +490,23 @@ async function parseLoadedData(data, mapVersion) {
if (textureHref) updateTextureSelectValue(textureHref); if (textureHref) updateTextureSelectValue(textureHref);
} }
void (function checkDataIntegrity() { // data integrity checks
const cells = pack.cells; {
const {cells, vertices} = pack;
if (pack.cells.i.length !== pack.cells.state.length) { const cellsMismatch = cells.i.length !== cells.state.length;
const message = "Data integrity check. Striping issue detected. To fix edit the heightmap in ERASE mode"; const featureVerticesMismatch = pack.features.some(f => f?.vertices?.some(vertex => !vertices.p[vertex]));
ERROR && console.error(message);
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); const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
invalidStates.forEach(s => { invalidStates.forEach(s => {
const invalidCells = cells.i.filter(i => cells.state[i] === s); const invalidCells = cells.i.filter(i => cells.state[i] === s);
invalidCells.forEach(i => (cells.state[i] = 0)); 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( const invalidProvinces = [...new Set(cells.province)].filter(
@ -502,14 +515,14 @@ async function parseLoadedData(data, mapVersion) {
invalidProvinces.forEach(p => { invalidProvinces.forEach(p => {
const invalidCells = cells.i.filter(i => cells.province[i] === p); const invalidCells = cells.i.filter(i => cells.province[i] === p);
invalidCells.forEach(i => (cells.province[i] = 0)); 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); const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
invalidCultures.forEach(c => { invalidCultures.forEach(c => {
const invalidCells = cells.i.filter(i => cells.culture[i] === c); const invalidCells = cells.i.filter(i => cells.culture[i] === c);
invalidCells.forEach(i => (cells.province[i] = 0)); 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( const invalidReligions = [...new Set(cells.religion)].filter(
@ -518,14 +531,14 @@ async function parseLoadedData(data, mapVersion) {
invalidReligions.forEach(r => { invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r); const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => (cells.religion[i] = 0)); 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]); const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
invalidFeatures.forEach(f => { invalidFeatures.forEach(f => {
const invalidCells = cells.i.filter(i => cells.f[i] === f); const invalidCells = cells.i.filter(i => cells.f[i] === f);
// No fix as for now // 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( const invalidBurgs = [...new Set(cells.burg)].filter(
@ -534,7 +547,7 @@ async function parseLoadedData(data, mapVersion) {
invalidBurgs.forEach(burgId => { invalidBurgs.forEach(burgId => {
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId); const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
invalidCells.forEach(i => (cells.burg[i] = 0)); 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)); 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); const invalidCells = cells.i.filter(i => cells.r[i] === r);
invalidCells.forEach(i => (cells.r[i] = 0)); invalidCells.forEach(i => (cells.r[i] = 0));
rivers.select("river" + r).remove(); 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 => { pack.burgs.forEach(burg => {
if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital); if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);
if (!burg.i && burg.lock) { 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; delete burg.lock;
return; return;
} }
if (burg.removed && burg.lock) { if (burg.removed && burg.lock) {
ERROR && ERROR && console.error(`[Data integrity] Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
console.error(`Data integrity check. Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
delete burg.lock; delete burg.lock;
return; return;
} }
@ -565,36 +577,34 @@ async function parseLoadedData(data, mapVersion) {
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) { if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
ERROR && ERROR &&
console.error( console.error(`[Data integrity] Burg ${burg.i} is missing cell info or coordinates. Removing the burg`);
`Data integrity check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
);
burg.removed = true; burg.removed = true;
} }
if (burg.port < 0) { 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; burg.port = 0;
} }
if (burg.cell >= cells.i.length) { 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); burg.cell = findCell(burg.x, burg.y);
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0)); cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
cells.burg[burg.cell] = burg.i; cells.burg[burg.cell] = burg.i;
} }
if (burg.state && !pack.states[burg.state]) { 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; burg.state = 0;
} }
if (burg.state && pack.states[burg.state].removed) { 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; burg.state = 0;
} }
if (burg.state === undefined) { 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; burg.state = 0;
} }
}); });
@ -608,7 +618,7 @@ async function parseLoadedData(data, mapVersion) {
if (!state.i && capitalBurgs.length) { if (!state.i && capitalBurgs.length) {
ERROR && ERROR &&
console.error( console.error(
`Data integrity check. Neutral burgs (${capitalBurgs `[Data integrity] Neutral burgs (${capitalBurgs
.map(b => b.i) .map(b => b.i)
.join(", ")}) marked as capitals. Moving them to towns` .join(", ")}) marked as capitals. Moving them to towns`
); );
@ -622,7 +632,7 @@ async function parseLoadedData(data, mapVersion) {
} }
if (capitalBurgs.length > 1) { 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) .map(b => b.i)
.join(", ")}) assigned. Keeping the first as capital and moving others to towns`; .join(", ")}) assigned. Keeping the first as capital and moving others to towns`;
ERROR && console.error(message); ERROR && console.error(message);
@ -638,7 +648,7 @@ async function parseLoadedData(data, mapVersion) {
if (state.i && stateBurgs.length && !capitalBurgs.length) { if (state.i && stateBurgs.length && !capitalBurgs.length) {
ERROR && 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; stateBurgs[0].capital = 1;
moveBurgToGroup(stateBurgs[0].i, "cities"); moveBurgToGroup(stateBurgs[0].i, "cities");
} }
@ -647,17 +657,48 @@ async function parseLoadedData(data, mapVersion) {
pack.provinces.forEach(p => { pack.provinces.forEach(p => {
if (!p.i || p.removed) return; if (!p.i || p.removed) return;
if (pack.states[p.state] && !pack.states[p.state].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); ERROR &&
p.removed = true; // remove incorrect province 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 = []; const markerIds = [];
let nextId = last(pack.markers)?.i + 1 || 0; let nextId = last(pack.markers)?.i + 1 || 0;
pack.markers.forEach(marker => { pack.markers.forEach(marker => {
if (markerIds[marker.i]) { 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); const domElements = document.querySelectorAll("#marker" + marker.i);
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element 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 // sort markers by index
pack.markers.sort((a, b) => a.i - b.i); pack.markers.sort((a, b) => a.i - b.i);
} }
})(); }
fitMapToScreen();
{
// remove href from emblems, to trigger rendering on load // remove href from emblems, to trigger rendering on load
emblems.selectAll("use").attr("href", null); emblems.selectAll("use").attr("href", null);
}
// draw data layers (no kept in svg) {
// draw data layers (not kept in svg)
if (rulers && layerIsOn("toggleRulers")) rulers.draw(); if (rulers && layerIsOn("toggleRulers")) rulers.draw();
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
}
{
if (window.restoreDefaultEvents) restoreDefaultEvents(); if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming(); invokeActiveZooming();
fitMapToScreen();
}
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`); WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
showStatistics(); showStatistics();
@ -698,14 +744,15 @@ async function parseLoadedData(data, mapVersion) {
ERROR && console.error(error); ERROR && console.error(error);
clearMainTip(); 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>`; <p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Loading error", title: "Loading error",
maxWidth: "50em", maxWidth: "40em",
buttons: { buttons: {
"Clear cache": () => cleanupData(),
"Select file": function () { "Select file": function () {
$(this).dialog("close"); $(this).dialog("close");
mapToLoad.click(); mapToLoad.click();

View file

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

View file

@ -1,98 +1,87 @@
"use strict"; "use strict";
window.Lakes = (function () { window.Lakes = (function () {
const setClimateData = function (h) { const LAKE_ELEVATION_DELTA = 0.1;
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
pack.features.forEach(f => { // check if lake can be potentially open (not in deep depression)
if (f.type !== "lake") return; const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
// default flux: sum of precipitation around lake pack.features.forEach(feature => {
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); if (feature.type !== "lake") return;
delete feature.closed;
// temperature and evaporation to detect closed lakes const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
f.temp = if (MAX_ELEVATION > 99) {
f.cells < 6 feature.closed = false;
? 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;
return; return;
} }
let deep = true; let isDeep = true;
const threshold = f.height + ELEVATION_LIMIT; const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [min]; const queue = [lowestShorelineCell];
const checked = []; const checked = [];
checked[min] = true; checked[lowestShorelineCell] = true;
// check if elevated lake can potentially pour to another water body while (queue.length && isDeep) {
while (deep && queue.length) { const cellId = queue.pop();
const q = queue.pop();
for (const n of cells.c[q]) { for (const neibCellId of cells.c[cellId]) {
if (checked[n]) continue; if (checked[neibCellId]) continue;
if (h[n] >= threshold) continue; if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[n] < 20) { if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[n]]; const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || f.height > nFeature.height) { if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
deep = false; }
break;
checked[neibCellId] = true;
queue.push(neibCellId);
} }
} }
checked[n] = true; feature.closed = isDeep;
queue.push(n);
}
}
f.closed = deep;
}); });
}; };
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 () { const cleanupLakeData = function () {
for (const feature of pack.features) { for (const feature of pack.features) {
if (feature.type !== "lake") continue; if (feature.type !== "lake") continue;
@ -111,23 +100,10 @@ window.Lakes = (function () {
} }
}; };
const defineGroup = function () { const getHeight = function (feature) {
for (const feature of pack.features) { const heights = pack.cells.h;
if (feature.type !== "lake") continue; const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node(); return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
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 getName = function (feature) { const getName = function (feature) {
@ -136,19 +112,5 @@ window.Lakes = (function () {
return Names.getCulture(culture); return Names.getCulture(culture);
}; };
function getGroup(feature) { return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
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};
})(); })();

View file

@ -11,7 +11,7 @@ window.Markers = (function () {
/* /*
Default markers config: Default markers config:
type - short description (snake-case) 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 dx: icon offset in x direction, in pixels
dy: icon offset in y direction, in pixels dy: icon offset in y direction, in pixels
min: minimum number of candidates to add at least 1 marker min: minimum number of candidates to add at least 1 marker
@ -117,6 +117,7 @@ window.Markers = (function () {
while (quantity && candidates.length) { while (quantity && candidates.length) {
const [cell] = extractAnyElement(candidates); const [cell] = extractAnyElement(candidates);
const marker = addMarker({icon, type, dx, dy, px}, {cell}); const marker = addMarker({icon, type, dx, dy, px}, {cell});
if (!marker) continue;
add("marker" + marker.i, cell); add("marker" + marker.i, cell);
quantity--; quantity--;
} }
@ -150,6 +151,7 @@ window.Markers = (function () {
} }
function addMarker(base, marker) { function addMarker(base, marker) {
if (marker.cell === undefined) return;
const i = last(pack.markers)?.i + 1 || 0; const i = last(pack.markers)?.i + 1 || 0;
const [x, y] = getMarkerCoordinates(marker.cell); const [x, y] = getMarkerCoordinates(marker.cell);
marker = {...base, x, y, ...marker, i}; marker = {...base, x, y, ...marker, i};

View file

@ -2,7 +2,7 @@
window.Military = (function () { window.Military = (function () {
const generate = function () { const generate = function () {
TIME && console.time("generateMilitaryForces"); TIME && console.time("generateMilitary");
const {cells, states} = pack; const {cells, states} = pack;
const {p} = cells; const {p} = cells;
const valid = states.filter(s => s.i && !s.removed); // valid states 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 delete s.temp; // do not store temp data
}); });
redraw();
function createRegiments(nodes, s) { function createRegiments(nodes, s) {
if (!nodes.length) return []; if (!nodes.length) return [];
@ -312,19 +310,9 @@ window.Military = (function () {
return regiments; 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 () { const getDefaultOptions = function () {
return [ return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0}, {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 // 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); 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 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 conflict = campaign ? ` during the ${campaign.name}` : "";
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`; 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 { return {
generate, generate,
redraw,
getDefaultOptions, getDefaultOptions,
getName, getName,
generateNote, generateNote,
drawRegiments,
drawRegiment,
moveRegiment,
getTotal, getTotal,
getEmblem getEmblem
}; };

View file

@ -48,18 +48,28 @@ window.Names = (function () {
return chain; return chain;
}; };
// update chain for specific base const updateChain = i => {
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null); chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
};
// update chains for all used bases const clearChains = () => {
const clearChains = () => (chains = []); chains = [];
};
// generate name using Markov's chain // generate name using Markov's chain
const getBase = function (base, min, max, dupl) { const getBase = function (base, min, max, dupl) {
if (base === undefined) { if (base === undefined) return ERROR && console.error("Please define a base");
ERROR && console.error("Please define a base");
return; 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); if (!chains[base]) updateChain(base);
const data = chains[base]; const data = chains[base];
@ -141,16 +151,8 @@ window.Names = (function () {
// generate short name for base // generate short name for base
const getBaseShort = function (base) { const getBaseShort = function (base) {
if (nameBases[base] === undefined) { const min = nameBases[base] ? nameBases[base].min - 1 : null;
tip( const max = min ? Math.max(nameBases[base].max - 2, min) : null;
`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);
return getBase(base, min, max, "", 0); 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: "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: "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: "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: "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: "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"}, {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 lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
const folkReligions = generateFolkReligions(); const folkReligions = generateFolkReligions();
const organizedReligions = generateOrganizedReligions(+religionsInput.value, lockedReligions); const organizedReligions = generateOrganizedReligions(+religionsNumber.value, lockedReligions);
const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]); const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
const indexedReligions = combineReligions(namedReligions, lockedReligions); const indexedReligions = combineReligions(namedReligions, lockedReligions);
@ -695,23 +695,24 @@ window.Religions = (function () {
const {cells, routes} = pack; const {cells, routes} = pack;
const religionIds = spreadFolkReligions(religions); const religionIds = spreadFolkReligions(religions);
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new FlatQueue();
const cost = []; 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 religions
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed) .filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
.forEach(r => { .forEach(r => {
religionIds[r.center] = r.i; 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; cost[r.center] = 1;
}); });
const religionsMap = new Map(religions.map(r => [r.i, r])); const religionsMap = new Map(religions.map(r => [r.i, r]));
while (queue.length) { 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); const {culture, expansion, expansionism} = religionsMap.get(r);
cells.c[cellId].forEach(nextCell => { cells.c[cellId].forEach(nextCell => {
@ -731,7 +732,7 @@ window.Religions = (function () {
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost; 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 // list - an optional array of stateIds to regenerate
function drawStateLabels(list) { function drawStateLabels(list) {
console.time("drawStateLabels"); TIME && console.time("drawStateLabels");
// temporary make the labels visible // temporary make the labels visible
const layerDisplay = labels.style("display"); 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 // increase step to 15 or 30 to make it faster and more horyzontal
// decrease step to 5 to improve accuracy // decrease step to 5 to improve accuracy
const ANGLE_STEP = 9; const ANGLE_STEP = 9;
const raycast = precalculateAngles(ANGLE_STEP); const angles = precalculateAngles(ANGLE_STEP);
const INITIAL_DISTANCE = 10; const LENGTH_START = 5;
const DISTANCE_STEP = 15; const LENGTH_STEP = 5;
const MAX_ITERATIONS = 100; const LENGTH_MAX = 300;
const labelPaths = getLabelPaths(); const labelPaths = getLabelPaths();
const letterLength = checkExampleLetterLength(); const letterLength = checkExampleLetterLength();
@ -35,87 +35,27 @@ function drawStateLabels(list) {
if (list && !list.includes(state.i)) continue; if (list && !list.includes(state.i)) continue;
const offset = getOffsetWidth(state.cells); const offset = getOffsetWidth(state.cells);
const maxLakeSize = state.cells / 50; const maxLakeSize = state.cells / 20;
const [x0, y0] = state.pole; const [x0, y0] = state.pole;
const offsetPoints = new Map( const rays = angles.map(({angle, dx, dy}) => {
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => { const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
const [x, y] = [x0 + offset * x1, y0 + offset * y1]; return {angle, length, x, y};
return [angle, {x, y}]; });
}) const [ray1, ray2] = findBestRayPair(rays);
);
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => { const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
let distanceMin; if (ray1.x > ray2.x) pathPoints.reverse();
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
if (offset) { if (DEBUG.stateLabels) {
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90); drawPoint(state.pole, {color: "black", radius: 1});
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize); drawPath(pathPoints, {color: "black", width: 0.2});
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 {
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 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}
);
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
if (x1 > x2) pathPoints.reverse();
labelPaths.push([state.i, pathPoints]); labelPaths.push([state.i, pathPoints]);
} }
return labelPaths; 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() { function checkExampleLetterLength() {
@ -129,7 +69,7 @@ function drawStateLabels(list) {
function drawLabelPath(letterLength) { function drawLabelPath(letterLength) {
const mode = options.stateLabelsMode || "auto"; 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 textGroup = d3.select("g#labels > g#states");
const pathGroup = d3.select("defs > g#deftemp > g#textPaths"); const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
@ -166,6 +106,7 @@ function drawStateLabels(list) {
const textElement = textGroup const textElement = textGroup
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "stateLabel" + stateId) .attr("id", "stateLabel" + stateId)
.append("textPath") .append("textPath")
.attr("startOffset", "50%") .attr("startOffset", "50%")
@ -192,35 +133,15 @@ function drawStateLabels(list) {
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name; const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
textElement.innerHTML = `<tspan x="0">${text}</tspan>`; 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 + "%"); textElement.setAttribute("font-size", correctedRatio + "%");
} }
} }
// point offset to reduce label overlap with state borders
function getOffsetWidth(cellsNumber) { function getOffsetWidth(cellsNumber) {
if (cellsNumber < 80) return 0; if (cellsNumber < 40) return 0;
if (cellsNumber < 140) return 5; if (cellsNumber < 200) return 5;
if (cellsNumber < 200) return 15; return 10;
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
} }
function precalculateAngles(step) { function precalculateAngles(step) {
@ -228,38 +149,136 @@ function drawStateLabels(list) {
const RAD = Math.PI / 180; const RAD = Math.PI / 180;
for (let angle = 0; angle < 360; angle += step) { for (let angle = 0; angle < 360; angle += step) {
const x = Math.cos(angle * RAD); const dx = Math.cos(angle * RAD);
const y = Math.sin(angle * RAD); const dy = Math.sin(angle * RAD);
const angleDif = 90 - Math.abs((angle % 180) - 90); angles.push({angle, dx, dy});
const modifier = 1 - angleDif / 120; // [0.25, 1]
angles.push({angle, modifier, x, y});
} }
return angles; 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) { function getLinesAndRatio(mode, name, fullName, pathLength) {
// short name if (mode === "short") return getShortOneLine();
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) { if (pathLength > fullName.length * 2) return getFullOneLine();
const lines = splitInTwo(name); return getFullTwoLines();
const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength; function getShortOneLine() {
return [lines, minmax(rn(ratio * 60), 50, 150)]; const ratio = pathLength / name.length;
return [[name], minmax(rn(ratio * 60), 50, 150)];
} }
// full name: one line function getFullOneLine() {
if (pathLength > fullName.length * 2) { const ratio = pathLength / fullName.length;
const lines = [fullName]; return [[fullName], minmax(rn(ratio * 70), 70, 170)];
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 70), 70, 170)];
} }
// full name: two lines function getFullTwoLines() {
const lines = splitInTwo(fullName); const lines = splitInTwo(fullName);
const longestLineLength = d3.max(lines.map(({length}) => length)); const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength; const ratio = pathLength / longestLineLength;
return [lines, minmax(rn(ratio * 60), 70, 150)]; 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 // check whether multi-lined label is mostly inside the state. If no, replace it with short name label
function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) { function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) {
@ -289,5 +308,5 @@ function drawStateLabels(list) {
return false; 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 riversData = {}; // rivers data
const riverParents = {}; const riverParents = {};
const addCellToRiver = function (cell, river) { const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell]; if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell); else riversData[river].push(cell);
@ -19,7 +20,7 @@ window.Rivers = (function () {
let riverNext = 1; // first river id is 1 let riverNext = 1; // first river id is 1
const h = alterHeights(); const h = alterHeights();
Lakes.prepareLakeData(h); Lakes.detectCloseLakes(h);
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
@ -35,14 +36,12 @@ window.Rivers = (function () {
TIME && console.timeEnd("generateRivers"); TIME && console.timeEnd("generateRivers");
function drainWater() { function drainWater() {
//const MIN_FLUX_TO_FORM_RIVER = 10 * distanceScale;
const MIN_FLUX_TO_FORM_RIVER = 30; const MIN_FLUX_TO_FORM_RIVER = 30;
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25; const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec; 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 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) { land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
@ -191,7 +190,15 @@ window.Rivers = (function () {
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); 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({ pack.rivers.push({
i: riverId, i: riverId,
@ -201,7 +208,7 @@ window.Rivers = (function () {
length, length,
width, width,
widthFactor, widthFactor,
sourceWidth: 0, sourceWidth,
parent, parent,
cells: riverCells cells: riverCells
}); });
@ -307,59 +314,49 @@ window.Rivers = (function () {
// add points at 1/3 and 2/3 of a line between adjacents river cells // 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 addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const {fl, conf, h} = pack.cells; const {fl, h} = pack.cells;
const meandered = []; const meandered = [];
const lastStep = riverCells.length - 1; const lastStep = riverCells.length - 1;
const points = getRiverPoints(riverCells, riverPoints); const points = getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10; 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++) { for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i]; const cell = riverCells[i];
const isLastCell = i === lastStep; const isLastCell = i === lastStep;
const [x1, y1] = points[i]; 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; if (isLastCell) break;
const nextCell = riverCells[i + 1]; const nextCell = riverCells[i + 1];
const [x2, y2] = points[i + 1]; const [x2, y2] = points[i + 1];
if (nextCell === -1) { if (nextCell === -1) {
meandered.push([x2, y2, fluxPrev]); meandered.push([x2, y2, fl[cell]]);
break; break;
} }
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
if (dist2 <= 25 && riverCells.length >= 6) continue; 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 meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
const angle = Math.atan2(y2 - y1, x2 - x1); const angle = Math.atan2(y2 - y1, x2 - x1);
const sinMeander = Math.sin(angle) * meander; const sinMeander = Math.sin(angle) * meander;
const cosMeander = Math.cos(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 // 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 p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (y1 * 2 + y2) / 3 + cosMeander; const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2; const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 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, 0], [p2x, p2y, 0]);
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
} else if (dist2 > 25 || riverCells.length < 6) { } else if (dist2 > 25 || riverCells.length < 6) {
// if dist is medium or river is small add 1 extra middlepoint // if dist is medium or river is small add 1 extra middlepoint
const p1x = (x1 + x2) / 2 + -sinMeander; const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (y1 + y2) / 2 + cosMeander; const p1y = (y1 + y2) / 2 + cosMeander;
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2; meandered.push([p1x, p1y, 0]);
meandered.push([p1x, p1y, p1fl]);
} }
} }
@ -386,29 +383,35 @@ window.Rivers = (function () {
}; };
const FLUX_FACTOR = 500; const FLUX_FACTOR = 500;
const MAX_FLUX_WIDTH = 2; const MAX_FLUX_WIDTH = 1;
const LENGTH_FACTOR = 200; 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 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 getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH); if (pointIndex === 0) return startingWidth;
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
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; 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) // 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 riverPointsLeft = [];
const riverPointsRight = []; const riverPointsRight = [];
let flux = 0;
for (let p = 0; p < points.length; p++) { for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
const [x0, y0] = points[p - 1] || points[p]; const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
const [x1, y1, flux] = points[p]; const [x1, y1, pointFlux] = points[pointIndex];
const [x2, y2] = points[p + 1] || points[p]; 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 angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset; const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset; const cosOffset = Math.cos(angle) * offset;
@ -508,6 +511,7 @@ window.Rivers = (function () {
getBasin, getBasin,
getWidth, getWidth,
getOffset, getOffset,
getSourceWidth,
getApproximateLength, getApproximateLength,
getRiverPoints, getRiverPoints,
remove, remove,

View file

@ -1,6 +1,15 @@
const ROUTES_SHARP_ANGLE = 135; const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115; 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 () { window.Routes = (function () {
function generate(lockedRoutes = []) { function generate(lockedRoutes = []) {
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs); const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
@ -118,10 +127,9 @@ window.Routes = (function () {
} }
function findPathSegments({isWater, connections, start, exit}) { function findPathSegments({isWater, connections, start, exit}) {
const from = findPath(isWater, start, exit, connections); const getCost = createCostEvaluator({isWater, connections});
if (!from) return []; const pathCells = findPath(start, current => current === exit, getCost);
if (!pathCells) return [];
const pathCells = restorePath(start, exit, from);
const segments = getRouteSegments(pathCells, connections); const segments = getRouteSegments(pathCells, connections);
return segments; return segments;
} }
@ -172,6 +180,39 @@ window.Routes = (function () {
return routesMerged > 1 ? mergeRoutes(routes) : routes; return routesMerged > 1 ? mergeRoutes(routes) : routes;
} }
}
function createCostEvaluator({isWater, connections}) {
return isWater ? getWaterPathCost : getLandPathCost;
function getLandPathCost(current, next) {
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
const habitability = biomesData.habitability[pack.cells.biome[next]];
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
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;
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) { function buildLinks(routes) {
const links = {}; const links = {};
@ -195,7 +236,6 @@ window.Routes = (function () {
return links; return links;
} }
}
function preparePointsArray() { function preparePointsArray() {
const {cells, burgs} = pack; const {cells, burgs} = pack;
@ -249,109 +289,6 @@ window.Routes = (function () {
return data; // [[x, y, cell], [x, y, cell]]; 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) { function getRouteSegments(pathCells, connections) {
const segments = []; const segments = [];
let segment = []; let segment = [];
@ -422,21 +359,16 @@ window.Routes = (function () {
// connect cell with routes system by land // connect cell with routes system by land
function connect(cellId) { 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 pointsArray = preparePointsArray();
const points = getPoints("trails", pathCells, pointsArray); const points = getPoints("trails", pathCells, pointsArray);
const feature = cells.f[cellId]; const feature = pack.cells.f[cellId];
const routeId = getNextId();
const routeId = Math.max(...routes.map(route => route.i)) + 1;
const newRoute = {i: routeId, group: "trails", feature, points}; const newRoute = {i: routeId, group: "trails", feature, points};
routes.push(newRoute); pack.routes.push(newRoute);
for (let i = 0; i < pathCells.length; i++) { for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i]; const cellId = pathCells[i];
@ -446,43 +378,6 @@ window.Routes = (function () {
return newRoute; 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) { function addConnection(from, to, routeId) {
const routes = pack.cells.routes; const routes = pack.cells.routes;
@ -496,7 +391,7 @@ window.Routes = (function () {
// utility functions // utility functions
function isConnected(cellId) { function isConnected(cellId) {
const {routes} = pack.cells; const routes = pack.cells.routes;
return routes[cellId] && Object.keys(routes[cellId]).length > 0; return routes[cellId] && Object.keys(routes[cellId]).length > 0;
} }
@ -507,22 +402,34 @@ window.Routes = (function () {
function getRoute(from, to) { function getRoute(from, to) {
const routeId = pack.cells.routes[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) { function hasRoad(cellId) {
const connections = pack.cells.routes[cellId]; const connections = pack.cells.routes[cellId];
if (!connections) return false; 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) { function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId]; const connections = pack.cells.routes[cellId];
if (!connections) return false; if (!connections) return false;
return ( if (Object.keys(connections).length > 3) return true;
Object.keys(connections).length > 3 || const roadConnections = Object.values(connections).filter(routeId => {
Object.values(connections).filter(routeId => pack.routes[routeId].group === "roads").length > 2 const route = pack.routes.find(route => route.i === routeId);
); return route?.group === "roads";
});
return roadConnections.length > 2;
} }
// name generator data // name generator data
@ -706,11 +613,16 @@ window.Routes = (function () {
return path.getTotalLength(); return path.getTotalLength();
} }
function getNextId() {
return pack.routes.length ? Math.max(...pack.routes.map(r => r.i)) + 1 : 0;
}
function remove(route) { function remove(route) {
const routes = pack.cells.routes; const routes = pack.cells.routes;
for (const point of route.points) { for (const point of route.points) {
const from = point[2]; const from = point[2];
if (!routes[from]) continue;
for (const [to, routeId] of Object.entries(routes[from])) { for (const [to, routeId] of Object.entries(routes[from])) {
if (routeId === route.i) { if (routeId === route.i) {
@ -721,14 +633,12 @@ window.Routes = (function () {
} }
pack.routes = pack.routes.filter(r => r.i !== route.i); pack.routes = pack.routes.filter(r => r.i !== route.i);
viewbox viewbox.select("#route" + route.i).remove();
.select("#routes")
.select("#route" + route.i)
.remove();
} }
return { return {
generate, generate,
buildLinks,
connect, connect,
isConnected, isConnected,
areConnected, areConnected,
@ -738,6 +648,7 @@ window.Routes = (function () {
generateName, generateName,
getPath, getPath,
getLength, getLength,
getNextId,
remove remove
}; };
})(); })();

View file

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

View file

@ -373,7 +373,7 @@ window.ThreeD = (function () {
} }
// icons // icons
if (layerIsOn("toggleIcons")) { if (layerIsOn("toggleBurgIcons")) {
const geometry = isCity ? city_icon_geometry : town_icon_geometry; const geometry = isCity ? city_icon_geometry : town_icon_geometry;
const material = isCity ? city_icon_material : town_icon_material; const material = isCity ? city_icon_material : town_icon_material;
const iconMesh = new THREE.Mesh(geometry, material); const iconMesh = new THREE.Mesh(geometry, material);
@ -444,6 +444,7 @@ window.ThreeD = (function () {
const url = await getMapURL("mesh", { const url = await getMapURL("mesh", {
noLabels: options.labels3d, noLabels: options.labels3d,
noWater: options.extendedWater, noWater: options.extendedWater,
noViewbox: true,
fullMap: true fullMap: true
}); });
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@ -623,7 +624,7 @@ window.ThreeD = (function () {
material.map = texture; material.map = texture;
if (addMesh) addGlobe3dMesh(); 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() { 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; modules.Battle = true;
// add listeners // add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev)); byId("battleType").on("click", ev => this.toggleChange(ev));
document byId("battleType").nextElementSibling.on("click", ev => Battle.prototype.context.changeType(ev));
.getElementById("battleType") byId("battleNameShow").on("click", () => Battle.prototype.context.showNameSection());
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev)); byId("battleNamePlace").on("change", ev => (Battle.prototype.context.place = ev.target.value));
document byId("battleNameFull").on("change", ev => Battle.prototype.context.changeName(ev));
.getElementById("battleNameShow") byId("battleNameCulture").on("click", () => Battle.prototype.context.generateName("culture"));
.addEventListener("click", () => Battle.prototype.context.showNameSection()); byId("battleNameRandom").on("click", () => Battle.prototype.context.generateName("random"));
document byId("battleNameHide").on("click", this.hideNameSection);
.getElementById("battleNamePlace") byId("battleAddRegiment").on("click", this.addSide);
.addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value)); byId("battleRoll").on("click", () => Battle.prototype.context.randomize());
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev)); byId("battleRun").on("click", () => Battle.prototype.context.run());
document byId("battleApply").on("click", () => Battle.prototype.context.applyResults());
.getElementById("battleNameCulture") byId("battleCancel").on("click", () => Battle.prototype.context.cancelResults());
.addEventListener("click", () => Battle.prototype.context.generateName("culture")); byId("battleWiki").on("click", () => wiki("Battle-Simulator"));
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"));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev)); byId("battlePhase_attackers").on("click", ev => this.toggleChange(ev));
document byId("battlePhase_attackers").nextElementSibling.on("click", ev =>
.getElementById("battlePhase_attackers") Battle.prototype.context.changePhase(ev, "attackers")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers")); );
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev)); byId("battlePhase_defenders").on("click", ev => this.toggleChange(ev));
document byId("battlePhase_defenders").nextElementSibling.on("click", ev =>
.getElementById("battlePhase_defenders") Battle.prototype.context.changePhase(ev, "defenders")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders")); );
document byId("battleDie_attackers").on("click", () => Battle.prototype.context.rollDie("attackers"));
.getElementById("battleDie_attackers") byId("battleDie_defenders").on("click", () => Battle.prototype.context.rollDie("defenders"));
.addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document
.getElementById("battleDie_defenders")
.addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
} }
defineType() { defineType() {
@ -97,20 +83,16 @@ class Battle {
} }
setType() { 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 sideSpecific = byId("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific const attackers = sideSpecific ? sideSpecific.content : byId("battlePhases_" + this.type).content;
? sideSpecific.content const defenders = sideSpecific ? byId("battlePhases_" + this.type + "_defenders").content : attackers;
: document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific
? document.getElementById("battlePhases_" + this.type + "_defenders").content
: attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = ""; byId("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = ""; byId("battlePhase_defenders").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true)); byId("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
document.getElementById("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true)); byId("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
} }
definePlace() { definePlace() {
@ -149,7 +131,9 @@ class Battle {
for (const u of options.military) { for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " ")); 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>"; headers += "<th data-tip='Total military''>Total</th></tr></thead>";
@ -163,9 +147,13 @@ class Battle {
const state = pack.states[regiment.state]; 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 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 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"> 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> <rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>${iconHtml}</svg>`;
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`; const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${ let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
@ -200,7 +188,7 @@ class Battle {
} }
addSide() { addSide() {
const body = document.getElementById("regimentSelectorBody"); const body = byId("regimentSelectorBody");
const context = Battle.prototype.context; const context = Battle.prototype.context;
const regiments = pack.states const regiments = pack.states
.filter(s => s.military && !s.removed) .filter(s => s.military && !s.removed)
@ -246,7 +234,7 @@ class Battle {
}); });
applySorting(regimentSelectorHeader); applySorting(regimentSelectorHeader);
body.addEventListener("click", selectLine); body.on("click", selectLine);
function selectLine(ev) { function selectLine(ev) {
if (ev.target.className === "inactive") { if (ev.target.className === "inactive") {
@ -277,7 +265,7 @@ class Battle {
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8; const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x; regiment.px = regiment.x;
regiment.py = regiment.y; 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() { showNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none")); 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; byId("battleNamePlace").value = this.place;
document.getElementById("battleNameFull").value = this.name; byId("battleNameFull").value = this.name;
} }
hideNameSection() { hideNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block")); document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("battleNameSection").style.display = "none"; byId("battleNameSection").style.display = "none";
} }
changeName(ev) { changeName(ev) {
@ -310,8 +298,8 @@ class Battle {
type === "culture" type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "") ? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length - 1)); : Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place; byId("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName(); byId("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name}); $("#battleScreen").dialog({title: this.name});
} }
@ -495,7 +483,7 @@ class Battle {
this[side].power = this[side].power =
d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster; 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; 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() { getInitialMorale() {
@ -509,7 +497,7 @@ class Battle {
} }
updateMorale(side) { updateMorale(side) {
const morale = document.getElementById("battleMorale_" + side); const morale = byId("battleMorale_" + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, ""); morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
morale.value = this[side].morale | 0; morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value; morale.dataset.tip += morale.value;
@ -524,7 +512,7 @@ class Battle {
} }
rollDie(side) { rollDie(side) {
const el = document.getElementById("battleDie_" + side); const el = byId("battleDie_" + side);
const prev = +el.innerHTML; const prev = +el.innerHTML;
do { do {
el.innerHTML = rand(1, 6); el.innerHTML = rand(1, 6);
@ -672,11 +660,11 @@ class Battle {
this.attackers.phase = phase[0]; this.attackers.phase = phase[0];
this.defenders.phase = phase[1]; this.defenders.phase = phase[1];
const buttonA = document.getElementById("battlePhase_attackers"); const buttonA = byId("battlePhase_attackers");
buttonA.className = "icon-button-" + this.attackers.phase; buttonA.className = "icon-button-" + this.attackers.phase;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip; 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.className = "icon-button-" + this.defenders.phase;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip; buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip;
} }
@ -760,7 +748,7 @@ class Battle {
updateTable(side) { updateTable(side) {
for (const r of this[side].regiments) { 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 battleCasualties = tbody.querySelector(".battleCasualties");
const battleSurvivors = tbody.querySelector(".battleSurvivors"); const battleSurvivors = tbody.querySelector(".battleSurvivors");
@ -794,7 +782,7 @@ class Battle {
button.style.opacity = 0.5; button.style.opacity = 0.5;
div.style.display = "block"; div.style.display = "block";
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true}); document.getElementsByTagName("body")[0].on("click", hideSection, {once: true});
} }
changeType(ev) { changeType(ev) {
@ -811,7 +799,7 @@ class Battle {
changePhase(ev, side) { changePhase(ev, side) {
if (ev.target.tagName !== "BUTTON") return; if (ev.target.tagName !== "BUTTON") return;
const phase = (this[side].phase = ev.target.dataset.phase); 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.className = "icon-button-" + phase;
button.dataset.tip = ev.target.dataset.tip; button.dataset.tip = ev.target.dataset.tip;
this.calculateStrength(side); this.calculateStrength(side);
@ -873,6 +861,8 @@ class Battle {
r.u = Object.assign({}, r.survivors); r.u = Object.assign({}, r.survivors);
r.a = d3.sum(Object.values(r.u)); // reg total r.a = d3.sum(Object.values(r.u)); // reg total
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box 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; 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}; const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: "⚔️", type: "battlefields", dy: 52};
pack.markers.push(marker); pack.markers.push(marker);
const markerHTML = drawMarker(marker); const markerHTML = drawMarker(marker);
document.getElementById("markers").insertAdjacentHTML("beforeend", markerHTML); byId("markers").insertAdjacentHTML("beforeend", markerHTML);
} }
const getSide = (regs, n) => const getSide = (regs, n) =>
@ -909,7 +899,9 @@ class Battle {
cancelResults() { cancelResults() {
// move regiments back to initial positions // 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"); $("#battleScreen").dialog("close");
this.cleanData(); this.cleanData();
} }

View file

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

View file

@ -2,7 +2,7 @@
function editBurg(id) { function editBurg(id) {
if (customization) return; if (customization) return;
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons(); if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
if (!layerIsOn("toggleLabels")) toggleLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = id || d3.event.target.dataset.id; const burg = id || d3.event.target.dataset.id;
@ -47,6 +47,7 @@ function editBurg(id) {
byId("burgEmblem").addEventListener("click", openEmblemEdit); byId("burgEmblem").addEventListener("click", openEmblemEdit);
byId("burgTogglePreview").addEventListener("click", toggleBurgPreview); byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
byId("burgEditEmblem").addEventListener("click", openEmblemEdit); byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
byId("burgLocate").addEventListener("click", zoomIntoBurg);
byId("burgRelocate").addEventListener("click", toggleRelocateBurg); byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
byId("burglLegend").addEventListener("click", editBurgLegend); byId("burglLegend").addEventListener("click", editBurgLegend);
byId("burgLock").addEventListener("click", toggleBurgLockButton); byId("burgLock").addEventListener("click", toggleBurgLockButton);
@ -74,7 +75,8 @@ function editBurg(id) {
const temperature = grid.cells.temp[pack.cells.g[b.cell]]; const temperature = grid.cells.temp[pack.cells.g[b.cell]];
byId("burgTemperature").innerHTML = convertTemperature(temperature); 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]); byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features // toggle features
@ -228,24 +230,20 @@ function editBurg(id) {
const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock)); const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length; const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${ confirmationDialog({
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,
title: "Remove burg group", title: "Remove burg group",
buttons: { message: `Are you sure you want to remove ${
Remove: function () { basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
$(this).dialog("close"); }?<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"); $("#burgEditor").dialog("close");
hideGroupSection(); hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b)); burgsToRemove.forEach(b => removeBurg(b));
if (!basic && !capital) { if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector("#burgLabels > #" + group.id); const labelG = document.querySelector("#burgLabels > #" + group.id);
const iconG = document.querySelector("#burgIcons > #" + group.id); const iconG = document.querySelector("#burgIcons > #" + group.id);
const anchorG = document.querySelector("#anchors > #" + group.id); const anchorG = document.querySelector("#anchors > #" + group.id);
@ -253,10 +251,6 @@ function editBurg(id) {
if (iconG) iconG.remove(); if (iconG) iconG.remove();
if (anchorG) anchorG.remove(); if (anchorG) anchorG.remove();
} }
},
Cancel: function () {
$(this).dialog("close");
}
} }
}); });
} }
@ -405,6 +399,14 @@ function editBurg(id) {
byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o"; 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() { function toggleRelocateBurg() {
const toggler = byId("toggleCells"); const toggler = byId("toggleCells");
byId("burgRelocate").classList.toggle("pressed"); byId("burgRelocate").classList.toggle("pressed");
@ -509,19 +511,13 @@ function editBurg(id) {
} }
}); });
} else { } else {
alertMessage.innerHTML = "Are you sure you want to remove the burg?"; confirmationDialog({
$("#alert").dialog({
resizable: false,
title: "Remove burg", title: "Remove burg",
buttons: { message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
Remove: function () { confirm: "Remove",
$(this).dialog("close"); onConfirm: () => {
removeBurg(id); // see Editors module removeBurg(id); // see Editors module
$("#burgEditor").dialog("close"); $("#burgEditor").dialog("close");
},
Cancel: function () {
$(this).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 // 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) { function getTemperatureLikeness(temperature) {
if (temperature < -5) return "Yakutsk"; if (temperature < -5) return "Yakutsk (Russia)";
const cities = [ if (temperature > 30) return "Mecca (Saudi Arabia)";
"Snag (Yukon)", return meanTempCityMap[temperature] || null;
"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;
} }

View file

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

View file

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

View file

@ -8,33 +8,30 @@ function restoreDefaultEvents() {
svg.call(zoom); svg.call(zoom);
viewbox.style("cursor", "default").on(".drag", null).on("click", clicked).on("touchmove mousemove", onMouseMove); viewbox.style("cursor", "default").on(".drag", null).on("click", clicked).on("touchmove mousemove", onMouseMove);
legend.call(d3.drag().on("start", dragLegendBox)); legend.call(d3.drag().on("start", dragLegendBox));
svg.call(zoom);
} }
// on viewbox click event - run function based on target // handle viewbox click
function clicked() { function clicked() {
const el = d3.event.target; const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return; const parent = el?.parentElement;
const parent = el.parentElement; const grand = parent?.parentElement;
const grand = parent.parentElement; const great = grand?.parentElement;
const great = grand.parentElement; const ancestor = great?.parentElement;
const p = d3.mouse(this); if (!ancestor) return;
const i = findCell(p[0], p[1]);
if (grand.id === "emblems") editEmblem(); if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id); else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(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 === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg(); else if (grand.id === "burgIcons") editBurg();
else if (parent.id === "ice") editIce(); else if (parent.id === "ice") editIce();
else if (parent.id === "terrain") editReliefIcon(); else if (parent.id === "terrain") editReliefIcon();
else if (grand.id === "markers" || great.id === "markers") editMarker(); else if (grand.id === "markers" || great.id === "markers") editMarker();
else if (grand.id === "coastline") editCoastline(); else if (grand.id === "coastline") editCoastline();
else if (grand.id === "lakes") editLake();
else if (great.id === "armies") editRegiment(); 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 // clear elSelected variable
@ -182,6 +179,7 @@ function addBurg(point) {
burgLabels burgLabels
.select("#towns") .select("#towns")
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "burgLabel" + i) .attr("id", "burgLabel" + i)
.attr("data-id", i) .attr("data-id", i)
.attr("x", x) .attr("x", x)
@ -397,12 +395,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river"); else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond"); else if (pop < 200 && each(4)(cell)) tags.push("pond");
const connections = pack.cells.routes[cell] || {}; const roadsNumber = Object.values(pack.cells.routes[cell] || {}).filter(routeId => {
const roads = Object.values(connections).filter(routeId => { const route = pack.routes.find(route => route.i === routeId);
const route = pack.routes[routeId]; if (!route) return false;
return route.group === "roads" || route.group === "trails"; return route.group === "roads" || route.group === "trails";
}).length; }).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 biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8]; 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 labels
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.text(data[i][2]) .text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6) .attr("x", offset + colorBoxSize * 1.6)
.attr("y", fontSize / 1.6 + lineHeight + l * lineHeight + vOffset); .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; const offset = colOffset + legend.node().getBBox().width / 2;
labels labels
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("text-anchor", "middle") .attr("text-anchor", "middle")
.attr("font-weight", "bold") .attr("font-weight", "bold")
.attr("font-size", "1.2em") .attr("font-size", "1.2em")
@ -516,7 +516,7 @@ function fitLegendBox() {
// draw legend with the same data, but using different settings // draw legend with the same data, but using different settings
function redrawLegend() { function redrawLegend() {
if (!legend.select("rect").size()) return; if (legend.select("rect").size()) {
const name = legend.select("#legendLabel").text(); const name = legend.select("#legendLabel").text();
const data = legend const data = legend
.attr("data") .attr("data")
@ -524,6 +524,7 @@ function redrawLegend() {
.map(l => l.split(",")); .map(l => l.split(","));
drawLegend(name, data); drawLegend(name, data);
} }
}
function dragLegendBox() { function dragLegendBox() {
const tr = parseTransform(this.getAttribute("transform")); const tr = parseTransform(this.getAttribute("transform"));
@ -1167,25 +1168,66 @@ function selectIcon(initial, callback) {
const cell = row.insertCell(i % 17); const cell = row.insertCell(i % 17);
cell.innerHTML = icons[i]; 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 => { table.onclick = e => {
if (e.target.tagName === "TD") { if (e.target.tagName === "TD") {
input.value = e.target.textContent; input.value = e.target.textContent;
callback(input.value); callback(input.value);
} }
}; };
table.onmouseover = e => { table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.textContent} icon`); 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({ $("#iconSelector").dialog({
width: fitContent(), width: fitContent(),
title: "Select Icon", title: "Select Icon",
buttons: { buttons: {
Apply: function () { Apply: function () {
callback(input.value || "");
$(this).dialog("close"); $(this).dialog("close");
}, },
Close: function () { Close: function () {
@ -1251,18 +1293,18 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; 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(); Editor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; 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(); Editor.open();
} }
async function editReligions() { async function editReligions() {
if (customization) return; 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(); Editor.open();
} }

View file

@ -153,21 +153,26 @@ function showMapTooltip(point, e, i, g) {
if (group === "routes") { if (group === "routes") {
const routeId = +e.target.id.slice(5); const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name; const route = pack.routes.find(route => route.i === routeId);
if (name) return tip(`${name}. Click to edit the Route`); if (route) {
if (route.name) return tip(`${route.name}. Click to edit the Route`);
return tip("Click to edit the Route"); return tip("Click to edit the Route");
} }
}
if (group === "terrain") return tip("Click to edit the Relief Icon"); if (group === "terrain") return tip("Click to edit the Relief Icon");
if (subgroup === "burgLabels" || subgroup === "burgIcons") { if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id; const burgId = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg]; if (burgId) {
const population = si(b.population * populationRate * urbanization); const burg = pack.burgs[burgId];
tip(`${b.name}. Population: ${population}. Click to edit`); const population = si(burg.population * populationRate * urbanization);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); tip(`${burg.name}. Population: ${population}. Click to edit`);
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burgId, 5000);
return; return;
} }
}
if (group === "labels") return tip("Click to edit the Label"); 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"); 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 === "coastline") return tip("Click to edit the coastline");
if (group === "zones") { if (group === "zones") {
const zone = path[path.length - 8]; const element = path[path.length - 8];
tip(zone.dataset.description); const zoneId = +element.dataset.id;
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000); const zone = pack.zones.find(zone => zone.i === zoneId);
tip(zone.name);
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zoneId, 5000);
return; return;
} }
if (group === "ice") return tip("Click to edit the Ice"); if (group === "ice") return tip("Click to edit the Ice");
// covering elements // 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("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]) { else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i]; const biome = pack.cells.biome[i];
tip("Biome: " + biomesData.name[biome]); tip("Biome: " + biomesData.name[biome]);
@ -256,10 +263,11 @@ function updateCellInfo(point, i, g) {
const f = cells.f[i]; const f = cells.f[i];
infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat"); infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat");
infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon"); infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon");
infoGeozone.innerHTML = getGeozone(getLatitude(y, 4));
infoCell.innerHTML = i; infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a"; 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); infoDepth.innerHTML = getDepth(pack.features[f], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; 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]]; 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 // convert coordinate to DMS format
function toDMS(coord, c) { function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord)); const degrees = Math.floor(Math.abs(coord));
@ -426,17 +446,17 @@ function highlightEmblemElement(type, el) {
// assign lock behavior // assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function (e) { document.querySelectorAll("[data-locked]").forEach(function (e) {
e.addEventListener("mouseover", function (event) { e.addEventListener("mouseover", function (e) {
e.stopPropagation();
if (this.className === "icon-lock") if (this.className === "icon-lock")
tip("Click to unlock the option and allow it to be randomized on new map generation"); 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"); else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
}); });
e.addEventListener("click", function () { e.addEventListener("click", function () {
const id = this.id.slice(5); const ids = this.dataset.ids ? this.dataset.ids.split(",") : [this.id.slice(5)];
if (this.className === "icon-lock") unlock(id); const fn = this.className === "icon-lock" ? unlock : lock;
else lock(id); ids.forEach(fn);
}); });
}); });

View file

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

View file

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

View file

@ -26,28 +26,32 @@ function editLabel() {
modules.editLabel = true; modules.editLabel = true;
// add listeners // add listeners
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection); byId("labelGroupShow").on("click", showGroupSection);
document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection); byId("labelGroupHide").on("click", hideGroupSection);
document.getElementById("labelGroupSelect").addEventListener("click", changeGroup); byId("labelGroupSelect").on("click", changeGroup);
document.getElementById("labelGroupInput").addEventListener("change", createNewGroup); byId("labelGroupInput").on("change", createNewGroup);
document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput); byId("labelGroupNew").on("click", toggleNewGroupInput);
document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup); byId("labelGroupRemove").on("click", removeLabelsGroup);
document.getElementById("labelTextShow").addEventListener("click", showTextSection); byId("labelTextShow").on("click", showTextSection);
document.getElementById("labelTextHide").addEventListener("click", hideTextSection); byId("labelTextHide").on("click", hideTextSection);
document.getElementById("labelText").addEventListener("input", changeText); byId("labelText").on("input", changeText);
document.getElementById("labelTextRandom").addEventListener("click", generateRandomName); byId("labelTextRandom").on("click", generateRandomName);
document.getElementById("labelEditStyle").addEventListener("click", editGroupStyle); byId("labelEditStyle").on("click", editGroupStyle);
document.getElementById("labelSizeShow").addEventListener("click", showSizeSection); byId("labelSizeShow").on("click", showSizeSection);
document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection); byId("labelSizeHide").on("click", hideSizeSection);
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset); byId("labelStartOffset").on("input", changeStartOffset);
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize); byId("labelRelativeSize").on("input", changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign); byId("labelLetterSpacingShow").on("click", showLetterSpacingSection);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend); byId("labelLetterSpacingHide").on("click", hideLetterSpacingSection);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel); byId("labelLetterSpacingSize").on("input", changeLetterSpacingSize);
byId("labelAlign").on("click", editLabelAlign);
byId("labelLegend").on("click", editLabelLegend);
byId("labelRemoveSingle").on("click", removeLabel);
function showEditorTips() { function showEditorTips() {
showMainTip(); showMainTip();
@ -62,12 +66,12 @@ function editLabel() {
const group = text.parentNode.id; const group = text.parentNode.id;
if (group === "states" || group === "burgLabels") { if (group === "states" || group === "burgLabels") {
document.getElementById("labelGroupShow").style.display = "none"; byId("labelGroupShow").style.display = "none";
return; return;
} }
hideGroupSection(); hideGroupSection();
const select = document.getElementById("labelGroupSelect"); const select = byId("labelGroupSelect");
select.options.length = 0; // remove all options select.options.length = 0; // remove all options
labels.selectAll(":scope > g").each(function () { labels.selectAll(":scope > g").each(function () {
@ -78,17 +82,17 @@ function editLabel() {
} }
function updateValues(textPath) { function updateValues(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")] byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
.map(tspan => tspan.textContent) byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
.join("|"); byId("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")); let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0;
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize);
} }
function drawControlPointsAndLine() { function drawControlPointsAndLine() {
debug.select("#controlPoints").remove(); debug.select("#controlPoints").remove();
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform")); 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); debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength(); const l = path.getTotalLength();
if (!l) return; if (!l) return;
@ -117,8 +121,8 @@ function editLabel() {
} }
function redrawLabelPath() { function redrawLabelPath() {
const path = document.getElementById("textPath_" + elSelected.attr("id")); const path = byId("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1)); lineGen.curve(d3.curveNatural);
const points = []; const points = [];
debug debug
.select("#controlPoints") .select("#controlPoints")
@ -188,19 +192,19 @@ function editLabel() {
function showGroupSection() { function showGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); 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() { function hideGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelGroupSection").style.display = "none"; byId("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none"; byId("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = ""; byId("labelGroupInput").value = "";
document.getElementById("labelGroupSelect").style.display = "inline-block"; byId("labelGroupSelect").style.display = "inline-block";
} }
function changeGroup() { function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node()); byId(this.value).appendChild(elSelected.node());
} }
function toggleNewGroupInput() { function toggleNewGroupInput() {
@ -224,7 +228,7 @@ function editLabel() {
.replace(/ /g, "_") .replace(/ /g, "_")
.replace(/[^\w\s]/gi, ""); .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"); tip("Element with this id already exists. Please provide a unique name", false, "error");
return; return;
} }
@ -237,22 +241,22 @@ function editLabel() {
// just rename if only 1 element left // just rename if only 1 element left
const oldGroup = elSelected.node().parentNode; const oldGroup = elSelected.node().parentNode;
if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) { if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) {
document.getElementById("labelGroupSelect").selectedOptions[0].remove(); byId("labelGroupSelect").selectedOptions[0].remove();
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true)); byId("labelGroupSelect").options.add(new Option(group, group, false, true));
oldGroup.id = group; oldGroup.id = group;
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("labelGroupInput").value = ""; byId("labelGroupInput").value = "";
return; return;
} }
const newGroup = elSelected.node().parentNode.cloneNode(false); const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("labels").appendChild(newGroup); byId("labels").appendChild(newGroup);
newGroup.id = group; newGroup.id = group;
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true)); byId("labelGroupSelect").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node()); byId(group).appendChild(elSelected.node());
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("labelGroupInput").value = ""; byId("labelGroupInput").value = "";
} }
function removeLabelsGroup() { function removeLabelsGroup() {
@ -275,7 +279,7 @@ function editLabel() {
.select("#" + group) .select("#" + group)
.selectAll("text") .selectAll("text")
.each(function () { .each(function () {
document.getElementById("textPath_" + this.id).remove(); byId("textPath_" + this.id).remove();
this.remove(); this.remove();
}); });
if (!basic) labels.select("#" + group).remove(); if (!basic) labels.select("#" + group).remove();
@ -289,16 +293,16 @@ function editLabel() {
function showTextSection() { function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); 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() { function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none"; byId("labelTextSection").style.display = "none";
} }
function changeText() { function changeText() {
const input = document.getElementById("labelText").value; const input = byId("labelText").value;
const el = elSelected.select("textPath").node(); const el = elSelected.select("textPath").node();
const lines = input.split("|"); const lines = input.split("|");
@ -323,7 +327,7 @@ function editLabel() {
const culture = pack.cells.culture[cell]; const culture = pack.cells.culture[cell];
name = Names.getCulture(culture); name = Names.getCulture(culture);
} }
document.getElementById("labelText").value = name; byId("labelText").value = name;
changeText(); changeText();
} }
@ -334,12 +338,22 @@ function editLabel() {
function showSizeSection() { function showSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none")); 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() { function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block")); 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() { function changeStartOffset() {
@ -353,6 +367,12 @@ function editLabel() {
changeText(); changeText();
} }
function changeLetterSpacingSize() {
elSelected.select("textPath").attr("letter-spacing", this.value + "px");
tip("Label letter-spacing size: " + this.value + "px");
changeText();
}
function editLabelAlign() { function editLabelAlign() {
const bbox = elSelected.node().getBBox(); const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]; 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"); debug.append("g").attr("id", "vertices");
elSelected = d3.select(node); elSelected = d3.select(node);
updateLakeValues(); updateLakeValues();
selectLakeGroup(node); selectLakeGroup();
drawLakeVertices(); drawLakeVertices();
viewbox.on("touchmove mousemove", null); viewbox.on("touchmove mousemove", null);
@ -23,17 +23,15 @@ function editLake() {
modules.editLake = true; modules.editLake = true;
// add listeners // add listeners
document.getElementById("lakeName").addEventListener("input", changeName); byId("lakeName").on("input", changeName);
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture); byId("lakeNameCulture").on("click", generateNameCulture);
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom); byId("lakeNameRandom").on("click", generateNameRandom);
byId("lakeGroup").on("change", changeLakeGroup);
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup); byId("lakeGroupAdd").on("click", toggleNewGroupInput);
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput); byId("lakeGroupName").on("change", createNewGroup);
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup); byId("lakeGroupRemove").on("click", removeLakeGroup);
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup); byId("lakeEditStyle").on("click", editGroupStyle);
byId("lakeLegend").on("click", editLakeLegend);
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
function getLake() { function getLake() {
const lakeId = +elSelected.attr("data-f"); const lakeId = +elSelected.attr("data-f");
@ -41,85 +39,91 @@ function editLake() {
} }
function updateLakeValues() { function updateLakeValues() {
const cells = pack.cells; const {cells, vertices, rivers} = pack;
const l = getLake(); const l = getLake();
document.getElementById("lakeName").value = l.name; byId("lakeName").value = l.name;
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit(); byId("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v])); const length = d3.polygonLength(l.vertices.map(v => vertices.p[v]));
document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value; byId("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i)); const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]); const heights = lakeCells.map(i => cells.h[i]);
document.getElementById("lakeElevation").value = getHeight(l.height); byId("lakeElevation").value = getHeight(l.height);
document.getElementById("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs"); byId("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs"); byId("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
document.getElementById("lakeFlux").value = l.flux; byId("lakeFlux").value = l.flux;
document.getElementById("lakeEvaporation").value = l.evaporation; byId("lakeEvaporation").value = l.evaporation;
const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name); const inlets = l.inlets && l.inlets.map(inlet => rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no"; const outlet = l.outlet ? rivers.find(river => river.i === l.outlet)?.name : "no";
document.getElementById("lakeInlets").value = inlets ? inlets.length : "no"; byId("lakeInlets").value = inlets ? inlets.length : "no";
document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : ""; byId("lakeInlets").title = inlets ? inlets.join(", ") : "";
document.getElementById("lakeOutlet").value = outlet; byId("lakeOutlet").value = outlet;
} }
function drawLakeVertices() { 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 debug
.select("#vertices") .select("#vertices")
.selectAll("polygon") .selectAll("polygon")
.data(c) .data(neibCells)
.enter() .enter()
.append("polygon") .append("polygon")
.attr("points", d => getPackPolygon(d)) .attr("points", getPackPolygon)
.attr("data-c", d => d); .attr("data-c", d => d);
debug debug
.select("#vertices") .select("#vertices")
.selectAll("circle") .selectAll("circle")
.data(v) .data(vertices)
.enter() .enter()
.append("circle") .append("circle")
.attr("cx", d => pack.vertices.p[d][0]) .attr("cx", d => pack.vertices.p[d][0])
.attr("cy", d => pack.vertices.p[d][1]) .attr("cy", d => pack.vertices.p[d][1])
.attr("r", 0.4) .attr("r", 0.4)
.attr("data-v", d => d) .attr("data-v", d => d)
.call(d3.drag().on("drag", dragVertex)) .call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
.on("mousemove", () => .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() { function handleVertexDrag() {
const x = rn(d3.event.x, 2), const x = rn(d3.event.x, 2);
y = rn(d3.event.y, 2); const y = rn(d3.event.y, 2);
this.setAttribute("cx", x); this.setAttribute("cx", x);
this.setAttribute("cy", y); this.setAttribute("cy", y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y]; const vertexId = d3.select(this).datum();
debug pack.vertices.p[vertexId] = [x, y];
.select("#vertices")
.selectAll("polygon") const feature = getLake();
.attr("points", d => getPackPolygon(d));
redrawLake(); // 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() { function handleVertexDragEnd() {
lineGen.curve(d3.curveBasisClosed); if (layerIsOn("toggleStates")) drawStates();
const feature = getLake(); if (layerIsOn("toggleProvinces")) drawProvinces();
const points = feature.vertices.map(v => pack.vertices.p[v]); if (layerIsOn("toggleBorders")) drawBorders();
const d = round(lineGen(points)); if (layerIsOn("toggleBiomes")) drawBiomes();
elSelected.attr("d", d); if (layerIsOn("toggleReligions")) drawReligions();
defs.select("mask#land > path#land_" + feature.i).attr("d", d); // update land mask if (layerIsOn("toggleCultures")) drawCultures();
feature.area = Math.abs(d3.polygonArea(points));
document.getElementById("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
} }
function changeName() { function changeName() {
@ -136,18 +140,18 @@ function editLake() {
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1)); lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
} }
function selectLakeGroup(node) { function selectLakeGroup() {
const group = node.parentNode.id; const lake = getLake();
const select = document.getElementById("lakeGroup");
select.options.length = 0; // remove all options
const select = byId("lakeGroup");
select.options.length = 0; // remove all options
lakes.selectAll("g").each(function () { 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() { function changeLakeGroup() {
document.getElementById(this.value).appendChild(elSelected.node()); byId(this.value).appendChild(elSelected.node());
getLake().group = this.value; getLake().group = this.value;
} }
@ -172,7 +176,7 @@ function editLake() {
.replace(/ /g, "_") .replace(/ /g, "_")
.replace(/[^\w\s]/gi, ""); .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"); tip("Element with this id already exists. Please provide a unique name", false, "error");
return; return;
} }
@ -186,23 +190,23 @@ function editLake() {
const oldGroup = elSelected.node().parentNode; const oldGroup = elSelected.node().parentNode;
const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id); const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) { if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("lakeGroup").selectedOptions[0].remove(); byId("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true)); byId("lakeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group; oldGroup.id = group;
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("lakeGroupName").value = ""; byId("lakeGroupName").value = "";
return; return;
} }
// create a new group // create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false); const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("lakes").appendChild(newGroup); byId("lakes").appendChild(newGroup);
newGroup.id = group; newGroup.id = group;
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true)); byId("lakeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node()); byId(group).appendChild(elSelected.node());
toggleNewGroupInput(); toggleNewGroupInput();
document.getElementById("lakeGroupName").value = ""; byId("lakeGroupName").value = "";
} }
function removeLakeGroup() { function removeLakeGroup() {
@ -221,14 +225,14 @@ function editLake() {
buttons: { buttons: {
Remove: function () { Remove: function () {
$(this).dialog("close"); $(this).dialog("close");
const freshwater = document.getElementById("freshwater"); const freshwater = byId("freshwater");
const groupEl = document.getElementById(group); const groupEl = byId(group);
while (groupEl.childNodes.length) { while (groupEl.childNodes.length) {
freshwater.appendChild(groupEl.childNodes[0]); freshwater.appendChild(groupEl.childNodes[0]);
} }
groupEl.remove(); groupEl.remove();
document.getElementById("lakeGroup").selectedOptions[0].remove(); byId("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").value = "freshwater"; byId("lakeGroup").value = "freshwater";
}, },
Cancel: function () { Cancel: function () {
$(this).dialog("close"); $(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); 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 // dom elements
const markerType = document.getElementById("markerType"); const markerType = byId("markerType");
const markerIcon = document.getElementById("markerIcon"); const markerIconSelect = byId("markerIconSelect");
const markerIconSelect = document.getElementById("markerIconSelect"); const markerIconSize = byId("markerIconSize");
const markerIconSize = document.getElementById("markerIconSize"); const markerIconShiftX = byId("markerIconShiftX");
const markerIconShiftX = document.getElementById("markerIconShiftX"); const markerIconShiftY = byId("markerIconShiftY");
const markerIconShiftY = document.getElementById("markerIconShiftY"); const markerSize = byId("markerSize");
const markerSize = document.getElementById("markerSize"); const markerPin = byId("markerPin");
const markerPin = document.getElementById("markerPin"); const markerFill = byId("markerFill");
const markerFill = document.getElementById("markerFill"); const markerStroke = byId("markerStroke");
const markerStroke = document.getElementById("markerStroke");
const markerNotes = document.getElementById("markerNotes"); const markerNotes = byId("markerNotes");
const markerLock = document.getElementById("markerLock"); const markerLock = byId("markerLock");
const addMarker = document.getElementById("addMarker"); const addMarker = byId("addMarker");
const markerAdd = document.getElementById("markerAdd"); const markerAdd = byId("markerAdd");
const markerRemove = document.getElementById("markerRemove"); const markerRemove = byId("markerRemove");
updateInputs(); updateInputs();
@ -39,8 +38,7 @@ function editMarker(markerI) {
const listeners = [ const listeners = [
listen(markerType, "change", changeMarkerType), listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon), listen(markerIconSelect, "click", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSize, "input", changeIconSize), listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX), listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY), listen(markerIconShiftY, "input", changeIconShiftY),
@ -61,7 +59,7 @@ function editMarker(markerI) {
return [element, marker]; return [element, marker];
} }
const element = document.getElementById(`marker${markerI}`); const element = byId(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI); const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker]; return [element, marker];
} }
@ -97,19 +95,20 @@ function editMarker(markerI) {
} }
function updateInputs() { 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; markerType.value = marker.type || "";
markerIcon.value = icon; markerIconSize.value = marker.px || 12;
markerIconSize.value = px; markerIconShiftX.value = marker.dx || 50;
markerIconShiftX.value = dx; markerIconShiftY.value = marker.dy || 50;
markerIconShiftY.value = dy; markerSize.value = marker.size || 30;
markerSize.value = size; markerPin.value = marker.pin || "bubble";
markerPin.value = pin; markerFill.value = marker.fill || "#ffffff";
markerFill.value = fill; markerStroke.value = marker.stroke || "#000000";
markerStroke.value = stroke;
markerLock.className = lock ? "icon-lock" : "icon-lock-open"; markerLock.className = marker.lock ? "icon-lock" : "icon-lock-open";
} }
function changeMarkerType() { function changeMarkerType() {
@ -117,18 +116,12 @@ function editMarker(markerI) {
} }
function changeMarkerIcon() { function changeMarkerIcon() {
const icon = this.value; selectIcon(marker.icon, value => {
getSameTypeMarkers().forEach(marker => { const isExternal = value.startsWith("http") || value.startsWith("data:image");
marker.icon = icon; byId("markerIcon").innerHTML = isExternal ? `<img src="${value}" style="width: 1em; height: 1em;">` : value;
redrawIcon(marker);
});
}
function selectMarkerIcon() {
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => { getSameTypeMarkers().forEach(marker => {
marker.icon = icon; marker.icon = value;
redrawIcon(marker); redrawIcon(marker);
}); });
}); });
@ -165,7 +158,7 @@ function editMarker(markerI) {
getSameTypeMarkers().forEach(marker => { getSameTypeMarkers().forEach(marker => {
marker.size = size; marker.size = size;
const {i, x, y, hidden} = marker; const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`); const el = !hidden && byId(`marker${i}`);
if (!el) return; if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size; 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}) { function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`); const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
if (iconElement) {
iconElement.innerHTML = icon; const iconText = !hidden && document.querySelector(`#marker${i} > text`);
iconElement.setAttribute("x", dx + "%"); if (iconText) {
iconElement.setAttribute("y", dy + "%"); iconText.innerHTML = isExternal ? "" : icon;
iconElement.setAttribute("font-size", px + "px"); 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() { function deleteMarker() {
Markers.deleteMarker(marker.i) Markers.deleteMarker(marker.i);
element.remove(); element.remove();
$("#markerEditor").dialog("close"); $("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click(); if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
} }
function closeMarkerEditor() { function closeMarkerEditor() {

View file

@ -69,8 +69,14 @@ function overviewMarkers() {
function addLines() { function addLines() {
const lines = pack.markers const lines = pack.markers
.map(({i, type, icon, pinned, lock}) => { .map(({i, type, icon, pinned, lock}) => {
return `<div class="states" data-i=${i} data-type="${type}"> return /* html */ `
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div> <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="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="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 ${ <span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${
@ -208,15 +214,15 @@ function overviewMarkers() {
const body = pack.markers.map(marker => { const body = pack.markers.map(marker => {
const {i, type, icon, x, y} = 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 name = note ? quote(note.name) : "Unknown";
const legend = note ? quote(note.legend) : ""; const legend = note ? quote(note.legend) : "";
const lat = getLatitude(y, 2); const lat = getLatitude(y, 2);
const lon = getLongitude(x, 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"); 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); 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; if (el.tagName !== "BUTTON") return;
const type = el.dataset.type; 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") { if (type === "biomes") {
const {i, name, color} = biomesData; const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null); const biomesArray = Array(i.length).fill(null);
@ -329,9 +336,15 @@ function overviewMilitary() {
${getLimitText(unit[attr])} ${getLimitText(unit[attr])}
</button>`; </button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${ row.innerHTML = /* html */ `<td>
icon || " " <button data-type="icon" data-tip="Click to select unit icon">
}</button></td> ${
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><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("biomes")}</td>
<td>${getLimitButton("states")}</td> <td>${getLimitButton("states")}</td>
@ -424,7 +437,11 @@ function overviewMilitary() {
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => { elements.map(el => {
const {type, value} = el.dataset || {}; 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 (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0; if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0; if (el.type === "checkbox") return +el.checked || 0;

View file

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

View file

@ -40,8 +40,8 @@ function editNotes(id, name) {
$("#notesEditor").dialog({ $("#notesEditor").dialog({
title: "Notes Editor", title: "Notes Editor",
width: window.innerWidth * 0.8, width: svgWidth * 0.8,
height: window.innerHeight * 0.75, height: svgHeight * 0.75,
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
close: removeEditor close: removeEditor
}); });
@ -55,6 +55,7 @@ function editNotes(id, name) {
byId("notesLegend").addEventListener("blur", updateLegend); byId("notesLegend").addEventListener("blur", updateLegend);
byId("notesPin").addEventListener("click", toggleNotesPin); byId("notesPin").addEventListener("click", toggleNotesPin);
byId("notesFocus").addEventListener("click", validateHighlightElement); byId("notesFocus").addEventListener("click", validateHighlightElement);
byId("notesGenerateWithAi").addEventListener("click", openAiGenerator);
byId("notesDownload").addEventListener("click", downloadLegends); byId("notesDownload").addEventListener("click", downloadLegends);
byId("notesUpload").addEventListener("click", () => legendsToLoad.click()); byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
byId("legendsToLoad").addEventListener("change", function () { 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() { function downloadLegends() {
const notesData = JSON.stringify(notes); const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt"; const name = getFileName("Notes") + ".txt";

View file

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

View file

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

View file

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

View file

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

View file

@ -74,13 +74,10 @@ function createRiver() {
function addRiver() { function addRiver() {
const {rivers, cells} = pack; const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
Rivers;
const riverCells = createRiver.cells; const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error"); 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; const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => { riverCells.forEach(cell => {
@ -89,17 +86,24 @@ function createRiver() {
const source = riverCells[0]; const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2]; 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 defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = 1.2 * defaultWidthFactor; const widthFactor = 1.2 * defaultWidthFactor;
const meanderedPoints = addMeandering(riverCells); const meanderedPoints = Rivers.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); const length = Rivers.getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth)); const width = Rivers.getWidth(
const name = getName(mouth); Rivers.getOffset({
const basin = getBasin(parent); flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
const name = Rivers.getName(mouth);
const basin = Rivers.getBasin(parent);
rivers.push({ rivers.push({
i: riverId, i: riverId,
@ -118,13 +122,11 @@ function createRiver() {
}); });
const id = "river" + riverId; const id = "river" + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox viewbox
.select("#rivers") .select("#rivers")
.append("path") .append("path")
.attr("id", id) .attr("id", id)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth)); .attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(id); editRiver(id);
} }

View file

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

View file

@ -18,7 +18,7 @@ function editRouteGroups() {
// add listeners // add listeners
byId("routeGroupsEditorAdd").addEventListener("click", addGroup); byId("routeGroupsEditorAdd").addEventListener("click", addGroup);
byId("routeGroupsEditorBody").on("click", ev => { 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); if (ev.target.classList.contains("editStyle")) editStyle("routes", group);
else if (ev.target.classList.contains("removeGroup")) removeGroup(group); else if (ev.target.classList.contains("removeGroup")) removeGroup(group);
}); });
@ -72,12 +72,11 @@ function editRouteGroups() {
confirmationDialog({ confirmationDialog({
title: "Remove route group", title: "Remove route group",
message: 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", confirm: "Remove",
onConfirm: () => { onConfirm: () => {
const routes = pack.routes.filter(r => r.group === group); pack.routes.filter(r => r.group === group).forEach(Routes.remove);
routes.forEach(r => Routes.remove(r)); if (!DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
addLines(); addLines();
} }
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@
} }
// store some style inputs as options // 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); if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
}); });
@ -70,8 +70,13 @@ function getColorScheme(scheme = "bright") {
return heightmapColorSchemes[scheme]; return heightmapColorSchemes[scheme];
} }
function getColor(value, scheme = getColorScheme("bright")) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
// Toggle style sections on element select // Toggle style sections on element select
styleElementSelect.addEventListener("change", selectStyleElement); styleElementSelect.on("change", selectStyleElement);
function selectStyleElement() { function selectStyleElement() {
const styleElement = styleElementSelect.value; const styleElement = styleElementSelect.value;
let el = d3.select("#" + styleElement); let el = d3.select("#" + styleElement);
@ -92,7 +97,7 @@ function selectStyleElement() {
// opacity // opacity
if (!["landmass", "ocean", "regions", "legend"].includes(styleElement)) { if (!["landmass", "ocean", "regions", "legend"].includes(styleElement)) {
styleOpacity.style.display = "block"; styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1; styleOpacityInput.value = el.attr("opacity") || 1;
} }
// filter // filter
@ -111,32 +116,41 @@ function selectStyleElement() {
if ( if (
[ [
"armies", "armies",
"routes", "biomes",
"lakes",
"borders", "borders",
"cults",
"relig",
"cells", "cells",
"coastline", "coastline",
"prec", "coordinates",
"cults",
"gridOverlay",
"ice", "ice",
"icons", "icons",
"coordinates", "lakes",
"zones", "prec",
"gridOverlay" "relig",
"routes",
"zones"
].includes(styleElement) ].includes(styleElement)
) { ) {
styleStroke.style.display = "block"; styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke"); styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
} }
// stroke dash // stroke dash
if ( 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"; styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || ""; styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
@ -146,15 +160,17 @@ function selectStyleElement() {
// clipping // clipping
if ( if (
[ [
"cells",
"gridOverlay",
"coordinates",
"compass",
"terrain",
"temperature",
"routes",
"texture",
"biomes", "biomes",
"cells",
"compass",
"coordinates",
"gridOverlay",
"population",
"prec",
"routes",
"temperature",
"terrain",
"texture",
"zones" "zones"
].includes(styleElement) ].includes(styleElement)
) { ) {
@ -176,9 +192,9 @@ function selectStyleElement() {
styleHeightmapRenderOcean.checked = +el.attr("data-render"); styleHeightmapRenderOcean.checked = +el.attr("data-render");
styleHeightmapScheme.value = el.attr("scheme"); styleHeightmapScheme.value = el.attr("scheme");
styleHeightmapTerracingInput.value = styleHeightmapTerracingOutput.value = el.attr("terracing"); styleHeightmapTerracing.value = el.attr("terracing");
styleHeightmapSkipInput.value = styleHeightmapSkipOutput.value = el.attr("skip"); styleHeightmapSkip.value = el.attr("skip");
styleHeightmapSimplificationInput.value = styleHeightmapSimplificationOutput.value = el.attr("relax"); styleHeightmapSimplification.value = el.attr("relax");
styleHeightmapCurve.value = el.attr("curve"); styleHeightmapCurve.value = el.attr("curve");
} }
@ -201,13 +217,13 @@ function selectStyleElement() {
const tr = parseTransform(compass.select("use").attr("transform")); const tr = parseTransform(compass.select("use").attr("transform"));
styleCompassShiftX.value = tr[0]; styleCompassShiftX.value = tr[0];
styleCompassShiftY.value = tr[1]; styleCompassShiftY.value = tr[1];
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2]; styleCompassSizeInput.value = tr[2];
} }
if (styleElement === "terrain") { if (styleElement === "terrain") {
styleRelief.style.display = "block"; styleRelief.style.display = "block";
styleReliefSizeOutput.innerHTML = styleReliefSizeInput.value = terrain.attr("size"); styleReliefSize.value = terrain.attr("size") || 1;
styleReliefDensityOutput.innerHTML = styleReliefDensityInput.value = terrain.attr("density"); styleReliefDensity.value = terrain.attr("density") || 0.4;
styleReliefSet.value = terrain.attr("set"); styleReliefSet.value = terrain.attr("set");
} }
@ -220,30 +236,31 @@ function selectStyleElement() {
.select("#urban") .select("#urban")
.attr("stroke"); .attr("stroke");
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
} }
if (styleElement === "regions") { if (styleElement === "regions") {
styleStates.style.display = "block"; styleStates.style.display = "block";
styleStatesBodyOpacity.value = styleStatesBodyOpacityOutput.value = statesBody.attr("opacity") || 1; styleStatesBodyOpacity.value = statesBody.attr("opacity") || 1;
styleStatesBodyFilter.value = statesBody.attr("filter") || ""; styleStatesBodyFilter.value = statesBody.attr("filter") || "";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("data-width") || 10; styleStatesHaloWidth.value = statesHalo.attr("data-width") || 10;
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity") || 1; styleStatesHaloOpacity.value = statesHalo.attr("opacity") || 1;
const blur = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0; styleStatesHaloBlur.value = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0;
styleStatesHaloBlur.value = styleStatesHaloBlurOutput.value = blur;
} }
if (styleElement === "labels") { if (styleElement === "labels") {
styleFill.style.display = "block"; styleFill.style.display = "block";
styleStroke.style.display = "block"; styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleLetterSpacing.style.display = "block";
styleShadow.style.display = "block"; styleShadow.style.display = "block";
styleSize.style.display = "block"; styleSize.style.display = "block";
styleVisibility.style.display = "block"; styleVisibility.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b"; styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a"; 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"; styleShadowInput.value = el.style("text-shadow") || "white 0 0 4px";
styleFont.style.display = "block"; styleFont.style.display = "block";
@ -258,7 +275,7 @@ function selectStyleElement() {
styleFont.style.display = "block"; styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family"); styleSelectFont.value = el.attr("font-family");
styleFontSize.value = el.attr("data-size"); styleFontSize.value = el.attr("font-size");
} }
if (styleElement == "burgIcons") { if (styleElement == "burgIcons") {
@ -269,7 +286,7 @@ function selectStyleElement() {
styleRadius.style.display = "block"; styleRadius.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff"; styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b"; 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") || ""; styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit"; styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
styleRadiusInput.value = el.attr("size") || 1; styleRadiusInput.value = el.attr("size") || 1;
@ -282,7 +299,7 @@ function selectStyleElement() {
styleIconSize.style.display = "block"; styleIconSize.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff"; styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b"; 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; styleIconSizeInput.value = el.attr("size") || 2;
} }
@ -292,12 +309,13 @@ function selectStyleElement() {
styleSize.style.display = "block"; styleSize.style.display = "block";
styleLegend.style.display = "block"; styleLegend.style.display = "block";
styleLegendColItemsOutput.value = styleLegendColItems.value = el.attr("data-columns"); styleLegendColItems.value = el.attr("data-columns");
styleLegendBackOutput.value = styleLegendBack.value = el.select("#legendBox").attr("fill"); const legendBox = el.select("#legendBox");
styleLegendOpacityOutput.value = styleLegendOpacity.value = el.select("#legendBox").attr("fill-opacity"); 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"; 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"; styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family"); styleSelectFont.value = el.attr("font-family");
@ -308,18 +326,17 @@ function selectStyleElement() {
styleOcean.style.display = "block"; styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill"); styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = byId("oceanicPattern")?.getAttribute("href"); styleOceanPattern.value = byId("oceanicPattern")?.getAttribute("href");
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value = styleOceanPatternOpacity.value = byId("oceanicPattern").getAttribute("opacity") || 1;
byId("oceanicPattern").getAttribute("opacity") || 1;
outlineLayers.value = oceanLayers.attr("layers"); outlineLayers.value = oceanLayers.attr("layers");
} }
if (styleElement === "temperature") { if (styleElement === "temperature") {
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleTemperature.style.display = "block"; styleTemperature.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; styleStrokeWidthInput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || 0.1; styleTemperatureFillOpacityInput.value = el.attr("fill-opacity") || 0.1;
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000"; 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") { if (styleElement === "coordinates") {
@ -329,14 +346,17 @@ function selectStyleElement() {
if (styleElement === "armies") { if (styleElement === "armies") {
styleArmies.style.display = "block"; styleArmies.style.display = "block";
styleArmiesFillOpacity.value = styleArmiesFillOpacityOutput.value = el.attr("fill-opacity"); styleArmiesFillOpacity.value = el.attr("fill-opacity");
styleArmiesSize.value = styleArmiesSizeOutput.value = el.attr("box-size"); styleArmiesSize.value = el.attr("box-size");
} }
if (styleElement === "emblems") { if (styleElement === "emblems") {
styleEmblems.style.display = "block"; styleEmblems.style.display = "block";
styleStrokeWidth.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 // update group options
@ -372,11 +392,9 @@ function selectStyleElement() {
const scaleBarBack = el.select("#scaleBarBack"); const scaleBarBack = el.select("#scaleBarBack");
if (scaleBarBack.size()) { if (scaleBarBack.size()) {
styleScaleBarBackgroundOpacityInput.value = styleScaleBarBackgroundOpacityOutput.value = styleScaleBarBackgroundOpacity.value = scaleBarBack.attr("opacity");
scaleBarBack.attr("opacity"); styleScaleBarBackgroundFill.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundFillInput.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill"); styleScaleBarBackgroundStroke.value = styleScaleBarBackgroundStrokeOutput.value = scaleBarBack.attr("stroke");
styleScaleBarBackgroundStrokeInput.value = styleScaleBarBackgroundStrokeOutput.value =
scaleBarBack.attr("stroke");
styleScaleBarBackgroundStrokeWidth.value = scaleBarBack.attr("stroke-width"); styleScaleBarBackgroundStrokeWidth.value = scaleBarBack.attr("stroke-width");
styleScaleBarBackgroundFilter.value = scaleBarBack.attr("filter"); styleScaleBarBackgroundFilter.value = scaleBarBack.attr("filter");
styleScaleBarBackgroundPaddingTop.value = scaleBarBack.attr("data-top"); styleScaleBarBackgroundPaddingTop.value = scaleBarBack.attr("data-top");
@ -398,13 +416,13 @@ function selectStyleElement() {
styleVignetteHeight.value = digit(maskRect.getAttribute("height")); styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx")); styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry")); styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter")); styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
} }
} }
} }
// Handle style inputs change // Handle style inputs change
styleGroupSelect.addEventListener("change", selectStyleElement); styleGroupSelect.on("change", selectStyleElement);
function getEl() { function getEl() {
const el = styleElementSelect.value; const el = styleElementSelect.value;
@ -413,44 +431,46 @@ function getEl() {
else return svg.select("#" + el).select("#" + g); else return svg.select("#" + el).select("#" + g);
} }
styleFillInput.addEventListener("input", function () { styleFillInput.on("input", function () {
styleFillOutput.value = this.value; styleFillOutput.value = this.value;
getEl().attr("fill", this.value); getEl().attr("fill", this.value);
}); });
styleStrokeInput.addEventListener("input", function () { styleStrokeInput.on("input", function () {
styleStrokeOutput.value = this.value; styleStrokeOutput.value = this.value;
getEl().attr("stroke", this.value); getEl().attr("stroke", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
}); });
styleStrokeWidthInput.addEventListener("input", function () { styleStrokeWidthInput.on("input", e => {
styleStrokeWidthOutput.value = this.value; getEl().attr("stroke-width", e.target.value);
getEl().attr("stroke-width", +this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); 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); getEl().attr("stroke-dasharray", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
}); });
styleStrokeLinecapInput.addEventListener("change", function () { styleStrokeLinecapInput.on("change", function () {
getEl().attr("stroke-linecap", this.value); getEl().attr("stroke-linecap", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid(); if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
}); });
styleOpacityInput.addEventListener("input", function () { styleOpacityInput.on("input", e => {
styleOpacityOutput.value = this.value; getEl().attr("opacity", e.target.value);
getEl().attr("opacity", this.value);
}); });
styleFilterInput.addEventListener("change", function () { styleFilterInput.on("change", function () {
if (styleGroupSelect.value === "ocean") return oceanLayers.attr("filter", this.value); if (styleGroupSelect.value === "ocean") return oceanLayers.attr("filter", this.value);
getEl().attr("filter", this.value); getEl().attr("filter", this.value);
}); });
styleTextureInput.addEventListener("change", function () { styleTextureInput.on("change", function () {
changeTexture(this.value); 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.attr("data-x", this.value);
texture texture
.select("image") .select("image")
@ -477,7 +497,7 @@ styleTextureShiftX.addEventListener("input", function () {
.attr("width", graphWidth - this.valueAsNumber); .attr("width", graphWidth - this.valueAsNumber);
}); });
styleTextureShiftY.addEventListener("input", function () { styleTextureShiftY.on("input", function () {
texture.attr("data-y", this.value); texture.attr("data-y", this.value);
texture texture
.select("image") .select("image")
@ -485,17 +505,17 @@ styleTextureShiftY.addEventListener("input", function () {
.attr("height", graphHeight - this.valueAsNumber); .attr("height", graphHeight - this.valueAsNumber);
}); });
styleClippingInput.addEventListener("change", function () { styleClippingInput.on("change", function () {
getEl().attr("mask", this.value); getEl().attr("mask", this.value);
}); });
styleGridType.addEventListener("change", function () { styleGridType.on("change", function () {
getEl().attr("type", this.value); getEl().attr("type", this.value);
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
}); });
styleGridScale.addEventListener("input", function () { styleGridScale.on("input", function () {
getEl().attr("scale", this.value); getEl().attr("scale", this.value);
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
@ -507,53 +527,52 @@ function calculateFriendlyGridSize() {
styleGridSizeFriendly.value = friendly; styleGridSizeFriendly.value = friendly;
} }
styleGridShiftX.addEventListener("input", function () { styleGridShiftX.on("input", function () {
getEl().attr("dx", this.value); getEl().attr("dx", this.value);
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
}); });
styleGridShiftY.addEventListener("input", function () { styleGridShiftY.on("input", function () {
getEl().attr("dy", this.value); getEl().attr("dy", this.value);
if (layerIsOn("toggleGrid")) drawGrid(); if (layerIsOn("toggleGrid")) drawGrid();
}); });
styleRescaleMarkers.addEventListener("change", function () { styleRescaleMarkers.on("change", function () {
markers.attr("rescale", +this.checked); markers.attr("rescale", +this.checked);
invokeActiveZooming(); invokeActiveZooming();
}); });
styleCoastlineAuto.addEventListener("change", function () { styleCoastlineAuto.on("change", function () {
coastline.select("#sea_island").attr("auto-filter", +this.checked); coastline.select("#sea_island").attr("auto-filter", +this.checked);
styleFilter.style.display = this.checked ? "none" : "block"; styleFilter.style.display = this.checked ? "none" : "block";
invokeActiveZooming(); invokeActiveZooming();
}); });
styleOceanFill.addEventListener("input", function () { styleOceanFill.on("input", function () {
oceanLayers.select("rect").attr("fill", this.value); oceanLayers.select("rect").attr("fill", this.value);
styleOceanFillOutput.value = this.value; styleOceanFillOutput.value = this.value;
}); });
styleOceanPattern.addEventListener("change", function () { styleOceanPattern.on("change", function () {
byId("oceanicPattern")?.setAttribute("href", this.value); byId("oceanicPattern")?.setAttribute("href", this.value);
}); });
styleOceanPatternOpacity.addEventListener("input", function () { styleOceanPatternOpacity.on("input", e => {
byId("oceanicPattern").setAttribute("opacity", this.value); byId("oceanicPattern").setAttribute("opacity", e.target.value);
styleOceanPatternOpacityOutput.value = this.value;
}); });
outlineLayers.addEventListener("change", function () { outlineLayers.on("change", function () {
oceanLayers.selectAll("path").remove(); oceanLayers.selectAll("path").remove();
oceanLayers.attr("layers", this.value); oceanLayers.attr("layers", this.value);
OceanLayers(); OceanLayers();
}); });
styleHeightmapScheme.addEventListener("change", function () { styleHeightmapScheme.on("change", function () {
getEl().attr("scheme", this.value); getEl().attr("scheme", this.value);
drawHeightmap(); drawHeightmap();
}); });
openCreateHeightmapSchemeButton.addEventListener("click", function () { openCreateHeightmapSchemeButton.on("click", function () {
// start with current scheme // start with current scheme
const scheme = getEl().attr("scheme"); const scheme = getEl().attr("scheme");
this.dataset.stops = scheme.startsWith("#") this.dataset.stops = scheme.startsWith("#")
@ -672,106 +691,97 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () {
}); });
}); });
styleHeightmapRenderOcean.addEventListener("change", function () { styleHeightmapRenderOcean.on("change", e => {
getEl().attr("data-render", +this.checked); const checked = +e.target.checked;
getEl().attr("data-render", checked);
drawHeightmap(); drawHeightmap();
}); });
styleHeightmapTerracingInput.addEventListener("input", function () { styleHeightmapTerracing.on("input", e => {
getEl().attr("terracing", this.value); getEl().attr("terracing", e.target.value);
drawHeightmap(); drawHeightmap();
}); });
styleHeightmapSkipInput.addEventListener("input", function () { styleHeightmapSkip.on("input", e => {
getEl().attr("skip", this.value); getEl().attr("skip", e.target.value);
drawHeightmap(); drawHeightmap();
}); });
styleHeightmapSimplificationInput.addEventListener("input", function () { styleHeightmapSimplification.on("input", e => {
getEl().attr("relax", this.value); getEl().attr("relax", e.target.value);
drawHeightmap(); drawHeightmap();
}); });
styleHeightmapCurve.addEventListener("change", function () { styleHeightmapCurve.on("change", e => {
getEl().attr("curve", this.value); getEl().attr("curve", e.target.value);
drawHeightmap(); drawHeightmap();
}); });
styleReliefSet.addEventListener("change", function () { styleReliefSet.on("change", e => {
terrain.attr("set", this.value); terrain.attr("set", e.target.value);
ReliefIcons(); drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
}); });
styleReliefSizeInput.addEventListener("change", function () { styleReliefSize.on("change", e => {
terrain.attr("size", this.value); terrain.attr("size", e.target.value);
styleReliefSizeOutput.value = this.value; drawReliefIcons();
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
}); });
styleReliefDensityInput.addEventListener("change", function () { styleReliefDensity.on("change", e => {
terrain.attr("density", this.value); terrain.attr("density", e.target.value);
styleReliefDensityOutput.value = this.value; drawReliefIcons();
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
}); });
styleTemperatureFillOpacityInput.addEventListener("input", function () { styleTemperatureFillOpacityInput.on("input", e => {
temperature.attr("fill-opacity", this.value); temperature.attr("fill-opacity", e.target.value);
styleTemperatureFillOpacityOutput.value = this.value;
}); });
styleTemperatureFontSizeInput.addEventListener("input", function () { styleTemperatureFontSizeInput.on("input", e => {
temperature.attr("font-size", this.value + "px"); temperature.attr("font-size", e.target.value + "px");
styleTemperatureFontSizeOutput.value = this.value + "px";
}); });
styleTemperatureFillInput.addEventListener("input", function () { styleTemperatureFillInput.on("input", e => {
temperature.attr("fill", this.value); temperature.attr("fill", e.target.value);
styleTemperatureFillOutput.value = this.value; styleTemperatureFillOutput.value = e.target.value;
}); });
stylePopulationRuralStrokeInput.addEventListener("input", function () { stylePopulationRuralStrokeInput.on("input", e => {
population.select("#rural").attr("stroke", this.value); population.select("#rural").attr("stroke", e.target.value);
stylePopulationRuralStrokeOutput.value = this.value; stylePopulationRuralStrokeOutput.value = e.target.value;
}); });
stylePopulationUrbanStrokeInput.addEventListener("input", function () { stylePopulationUrbanStrokeInput.on("input", e => {
population.select("#urban").attr("stroke", this.value); population.select("#urban").attr("stroke", e.target.value);
stylePopulationUrbanStrokeOutput.value = this.value; stylePopulationUrbanStrokeOutput.value = e.target.value;
}); });
styleCompassSizeInput.addEventListener("input", function () { styleCompassSizeInput.on("input", shiftCompass);
styleCompassSizeOutput.value = this.value; styleCompassShiftX.on("input", shiftCompass);
shiftCompass(); styleCompassShiftY.on("input", shiftCompass);
});
styleCompassShiftX.addEventListener("input", shiftCompass);
styleCompassShiftY.addEventListener("input", shiftCompass);
function shiftCompass() { function shiftCompass() {
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`; const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
compass.select("use").attr("transform", tr); compass.select("use").attr("transform", tr);
} }
styleLegendColItems.addEventListener("input", function () { styleLegendColItems.on("input", e => {
styleLegendColItemsOutput.value = this.value; legend.select("#legendBox").attr("data-columns", e.target.value);
legend.select("#legendBox").attr("data-columns", this.value);
redrawLegend(); redrawLegend();
}); });
styleLegendBack.addEventListener("input", function () { styleLegendBack.on("input", e => {
styleLegendBackOutput.value = this.value; styleLegendBackOutput.value = e.target.value;
legend.select("#legendBox").attr("fill", this.value); legend.select("#legendBox").attr("fill", e.target.value);
}); });
styleLegendOpacity.addEventListener("input", function () { styleLegendOpacity.on("input", e => {
styleLegendOpacityOutput.value = this.value; legend.select("#legendBox").attr("fill-opacity", e.target.value);
legend.select("#legendBox").attr("fill-opacity", this.value);
}); });
styleSelectFont.addEventListener("change", changeFont); styleSelectFont.on("change", changeFont);
function changeFont() { function changeFont() {
const family = styleSelectFont.value; const family = styleSelectFont.value;
getEl().attr("font-family", family); getEl().attr("font-family", family);
@ -779,11 +789,11 @@ function changeFont() {
if (styleElementSelect.value === "legend") redrawLegend(); if (styleElementSelect.value === "legend") redrawLegend();
} }
styleShadowInput.addEventListener("input", function () { styleShadowInput.on("input", function () {
getEl().style("text-shadow", this.value); getEl().style("text-shadow", this.value);
}); });
styleFontAdd.addEventListener("click", function () { styleFontAdd.on("click", function () {
addFontNameInput.value = ""; addFontNameInput.value = "";
addFontURLInput.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"; addFontURLInput.style.display = this.value === "fontURL" ? "inline" : "none";
}); });
styleFontSize.addEventListener("change", function () { styleFontSize.on("change", function () {
changeFontSize(getEl(), +this.value); changeFontSize(getEl(), +this.value);
}); });
styleFontPlus.addEventListener("click", function () { styleFontPlus.on("click", function () {
const size = +getEl().attr("data-size") + 1; const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.min(size, 999)); changeFontSize(getEl(), Math.min(current + 1, 999));
}); });
styleFontMinus.addEventListener("click", function () { styleFontMinus.on("click", function () {
const size = +getEl().attr("data-size") - 1; const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.max(size, 1)); changeFontSize(getEl(), Math.max(current - 1, 1));
}); });
function changeFontSize(el, size) { function changeFontSize(el, size) {
@ -856,16 +866,16 @@ function changeFontSize(el, size) {
if (styleElementSelect.value === "legend") redrawLegend(); if (styleElementSelect.value === "legend") redrawLegend();
} }
styleRadiusInput.addEventListener("change", function () { styleRadiusInput.on("change", function () {
changeRadius(+this.value); changeRadius(+this.value);
}); });
styleRadiusPlus.addEventListener("click", function () { styleRadiusPlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2); const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeRadius(size); changeRadius(size);
}); });
styleRadiusMinus.addEventListener("click", function () { styleRadiusMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2); const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeRadius(size); changeRadius(size);
}); });
@ -887,16 +897,16 @@ function changeRadius(size, group) {
changeIconSize(size * 2, g); // change also anchor icons changeIconSize(size * 2, g); // change also anchor icons
} }
styleIconSizeInput.addEventListener("change", function () { styleIconSizeInput.on("change", function () {
changeIconSize(+this.value); changeIconSize(+this.value);
}); });
styleIconSizePlus.addEventListener("click", function () { styleIconSizePlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2); const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeIconSize(size); changeIconSize(size);
}); });
styleIconSizeMinus.addEventListener("click", function () { styleIconSizeMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2); const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeIconSize(size); changeIconSize(size);
}); });
@ -921,49 +931,58 @@ function changeIconSize(size, group) {
styleIconSizeInput.value = size; styleIconSizeInput.value = size;
} }
styleStatesBodyOpacity.addEventListener("input", function () { styleStatesBodyOpacity.on("input", e => {
styleStatesBodyOpacityOutput.value = this.value; statesBody.attr("opacity", e.target.value);
statesBody.attr("opacity", this.value);
}); });
styleStatesBodyFilter.addEventListener("change", function () { styleStatesBodyFilter.on("change", function () {
statesBody.attr("filter", this.value); statesBody.attr("filter", this.value);
}); });
styleStatesHaloWidth.addEventListener("input", function () { styleStatesHaloWidth.on("input", e => {
styleStatesHaloWidthOutput.value = this.value; const value = e.target.value;
statesHalo.attr("data-width", this.value).attr("stroke-width", this.value); statesHalo.attr("data-width", value).attr("stroke-width", value);
}); });
styleStatesHaloOpacity.addEventListener("input", function () { styleStatesHaloOpacity.on("input", e => {
styleStatesHaloOpacityOutput.value = this.value; statesHalo.attr("opacity", e.target.value);
statesHalo.attr("opacity", this.value);
}); });
styleStatesHaloBlur.addEventListener("input", function () { styleStatesHaloBlur.on("input", e => {
styleStatesHaloBlurOutput.value = this.value; const value = Number(e.target.value);
const blur = +this.value > 0 ? `blur(${this.value}px)` : null; const blur = value > 0 ? `blur(${value}px)` : null;
statesHalo.attr("filter", blur); statesHalo.attr("filter", blur);
}); });
styleArmiesFillOpacity.addEventListener("input", function () { styleArmiesFillOpacity.on("input", e => {
armies.attr("fill-opacity", this.value); armies.attr("fill-opacity", e.target.value);
styleArmiesFillOpacityOutput.value = this.value;
}); });
styleArmiesSize.addEventListener("input", function () { styleArmiesSize.on("input", e => {
armies.attr("box-size", this.value).attr("font-size", this.value * 2); const value = Number(e.target.value);
styleArmiesSizeOutput.value = this.value; armies.attr("box-size", value).attr("font-size", value * 2);
armies.selectAll("g").remove(); // clear armies layer armies.selectAll("g").remove(); // clear armies layer
pack.states.forEach(s => { pack.states.forEach(s => {
if (!s.i || s.removed || !s.military.length) return; if (!s.i || s.removed || !s.military.length) return;
Military.drawRegiments(s.military, s.i); drawRegiments(s.military, s.i);
}); });
}); });
emblemsStateSizeInput.addEventListener("change", drawEmblems); emblemsStateSizeInput.on("change", e => {
emblemsProvinceSizeInput.addEventListener("change", drawEmblems); emblems.select("#stateEmblems").attr("data-size", e.target.value);
emblemsBurgSizeInput.addEventListener("change", drawEmblems); 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 // request a URL to image to be used as a texture
function textureProvideURL() { function textureProvideURL() {
@ -1015,7 +1034,7 @@ Object.keys(vignettePresets).forEach(preset => {
styleVignettePreset.options.add(new Option(preset, preset, false, false)); styleVignettePreset.options.add(new Option(preset, preset, false, false));
}); });
styleVignettePreset.addEventListener("change", function () { styleVignettePreset.on("change", function () {
const attributes = JSON.parse(vignettePresets[this.value]); const attributes = JSON.parse(vignettePresets[this.value]);
for (const selector in attributes) { for (const selector in attributes) {
@ -1029,7 +1048,7 @@ styleVignettePreset.addEventListener("change", function () {
const vignette = byId("vignette"); const vignette = byId("vignette");
if (vignette) { if (vignette) {
styleOpacityInput.value = styleOpacityOutput.value = vignette.getAttribute("opacity"); styleOpacityInput.value = vignette.getAttribute("opacity");
styleFillInput.value = styleFillOutput.value = vignette.getAttribute("fill"); styleFillInput.value = styleFillOutput.value = vignette.getAttribute("fill");
styleFilterInput.value = vignette.getAttribute("filter"); styleFilterInput.value = vignette.getAttribute("filter");
} }
@ -1043,40 +1062,39 @@ styleVignettePreset.addEventListener("change", function () {
styleVignetteHeight.value = digit(maskRect.getAttribute("height")); styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx")); styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry")); styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter")); styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
} }
}); });
styleVignetteX.addEventListener("input", function () { styleVignetteX.on("input", e => {
byId("vignette-rect")?.setAttribute("x", `${this.value}%`); byId("vignette-rect")?.setAttribute("x", `${e.target.value}%`);
}); });
styleVignetteWidth.addEventListener("input", function () { styleVignetteWidth.on("input", e => {
byId("vignette-rect")?.setAttribute("width", `${this.value}%`); byId("vignette-rect")?.setAttribute("width", `${e.target.value}%`);
}); });
styleVignetteY.addEventListener("input", function () { styleVignetteY.on("input", e => {
byId("vignette-rect")?.setAttribute("y", `${this.value}%`); byId("vignette-rect")?.setAttribute("y", `${e.target.value}%`);
}); });
styleVignetteHeight.addEventListener("input", function () { styleVignetteHeight.on("input", e => {
byId("vignette-rect")?.setAttribute("height", `${this.value}%`); byId("vignette-rect")?.setAttribute("height", `${e.target.value}%`);
}); });
styleVignetteRx.addEventListener("input", function () { styleVignetteRx.on("input", e => {
byId("vignette-rect")?.setAttribute("rx", `${this.value}%`); byId("vignette-rect")?.setAttribute("rx", `${e.target.value}%`);
}); });
styleVignetteRy.addEventListener("input", function () { styleVignetteRy.on("input", e => {
byId("vignette-rect")?.setAttribute("ry", `${this.value}%`); byId("vignette-rect")?.setAttribute("ry", `${e.target.value}%`);
}); });
styleVignetteBlur.addEventListener("input", function () { styleVignetteBlur.on("input", e => {
styleVignetteBlurOutput.value = this.value; byId("vignette-rect")?.setAttribute("filter", `blur(${e.target.value}px)`);
byId("vignette-rect")?.setAttribute("filter", `blur(${this.value}px)`);
}); });
styleScaleBar.addEventListener("input", function (event) { styleScaleBar.on("input", function (event) {
const scaleBarBack = scaleBar.select("#scaleBarBack"); const scaleBarBack = scaleBar.select("#scaleBarBack");
if (!scaleBarBack.size()) return; 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 === "styleScaleBarPositionX") scaleBar.attr("data-x", value);
else if (id === "styleScaleBarPositionY") scaleBar.attr("data-y", value); else if (id === "styleScaleBarPositionY") scaleBar.attr("data-y", value);
else if (id === "styleScaleBarLabel") scaleBar.attr("data-label", value); else if (id === "styleScaleBarLabel") scaleBar.attr("data-label", value);
else if (id === "styleScaleBarBackgroundOpacityInput") scaleBarBack.attr("opacity", value); else if (id === "styleScaleBarBackgroundOpacity") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFillInput") scaleBarBack.attr("fill", value); else if (id === "styleScaleBarBackgroundFill") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStrokeInput") scaleBarBack.attr("stroke", value); else if (id === "styleScaleBarBackgroundStroke") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundStrokeWidth") scaleBarBack.attr("stroke-width", value); else if (id === "styleScaleBarBackgroundStrokeWidth") scaleBarBack.attr("stroke-width", value);
else if (id === "styleScaleBarBackgroundFilter") scaleBarBack.attr("filter", value); else if (id === "styleScaleBarBackgroundFilter") scaleBarBack.attr("filter", value);
else if (id === "styleScaleBarBackgroundPaddingTop") scaleBarBack.attr("data-top", value); else if (id === "styleScaleBarBackgroundPaddingTop") scaleBarBack.attr("data-top", value);
@ -1156,7 +1174,7 @@ function updateElements() {
} }
// GLOBAL FILTERS // GLOBAL FILTERS
mapFilters.addEventListener("click", applyMapFilter); mapFilters.on("click", applyMapFilter);
function applyMapFilter(event) { function applyMapFilter(event) {
if (event.target.tagName !== "BUTTON") return; if (event.target.tagName !== "BUTTON") return;
const button = event.target; 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] // Standard deviation for average temperature for the year from [0, 1] to [min, max]
const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165; const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165;
// Standard deviation for the difference between the minimum and maximum temperatures for the year // Standard deviation for the difference between the minimum and maximum temperatures for the year
const yearDelTmpSig = const yearDelTmpSig =
lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig
? yearSig ? yearSig
: lstOut[1] * 13.541688670361175 + 0.1414213562373084; : lstOut[1] * 13.541688670361175 + 0.1414213562373084;
// Expected value for the difference between the minimum and maximum temperatures for the year // Expected value for the difference between the minimum and maximum temperatures for the year
const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663; const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663;
@ -60,7 +62,7 @@ function showBurgTemperatureGraph(id) {
const minT = burgTemp - Math.max(yearSig + delT, 15); const minT = burgTemp - Math.max(yearSig + delT, 15);
const maxT = burgTemp + (burgTemp - minT); const maxT = burgTemp + (burgTemp - minT);
const chartWidth = Math.max(window.innerWidth / 2, 580); const chartWidth = Math.max(window.innerWidth / 2, 520);
const chartHeight = 300; const chartHeight = 300;
// drawing starting point from top-left (y = 0) of SVG // drawing starting point from top-left (y = 0) of SVG
@ -107,9 +109,9 @@ function showBurgTemperatureGraph(id) {
}); });
drawGraph(); drawGraph();
$("#alert").dialog({ $("#alert").dialog({
title: "Annual temperature in " + b.name, title: "Average temperature in " + b.name,
width: "auto",
position: {my: "center", at: "center", of: "svg"} 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) // module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener("click", function (event) { 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; if (!["BUTTON", "I"].includes(event.target.tagName)) return;
const button = event.target.id; const button = event.target.id;
@ -70,14 +70,16 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "addRoute") createRoute(); else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker(); else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons // click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu(); else if (button === "openSubmapTool") openSubmapTool();
else if (button === "openResampleMenu") UISubmap.openResampleMenu(); else if (button === "openTransformTool") openTransformTool();
}); });
function processFeatureRegeneration(event, button) { function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") drawStateLabels(); if (button === "regenerateStateLabels") {
else if (button === "regenerateReliefIcons") { $("#labels").fadeIn();
ReliefIcons(); drawStateLabels();
} else if (button === "regenerateReliefIcons") {
drawReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") { } else if (button === "regenerateRoutes") {
regenerateRoutes(); regenerateRoutes();
@ -126,14 +128,14 @@ function regenerateRoutes() {
function regenerateRivers() { function regenerateRivers() {
Rivers.generate(); Rivers.generate();
Lakes.defineGroup();
Rivers.specify(); Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers(); Features.specify();
else drawRivers(); if (layerIsOn("toggleRivers")) drawRivers();
} }
function recalculatePopulation() { function recalculatePopulation() {
rankCells(); rankCells();
pack.burgs.forEach(b => { pack.burgs.forEach(b => {
if (!b.i || b.removed || b.lock) return; if (!b.i || b.removed || b.lock) return;
const i = b.cell; const i = b.cell;
@ -143,6 +145,8 @@ function recalculatePopulation() {
if (b.port) b.population = b.population * 1.3; // increase port population 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); b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
}); });
layerIsOn("togglePopulation") ? drawPopulation() : togglePopulation();
} }
function regenerateStates() { function regenerateStates() {
@ -152,12 +156,14 @@ function regenerateStates() {
pack.states = newStates; pack.states = newStates;
BurgsAndStates.expandStates(); BurgsAndStates.expandStates();
BurgsAndStates.normalizeStates(); BurgsAndStates.normalizeStates();
BurgsAndStates.getPoles();
BurgsAndStates.collectStatistics(); BurgsAndStates.collectStatistics();
BurgsAndStates.assignColors(); BurgsAndStates.assignColors();
BurgsAndStates.generateCampaigns(); BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy(); BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true); Provinces.generate(true);
Provinces.getPoles();
layerIsOn("toggleStates") ? drawStates() : toggleStates(); layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders(); layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
@ -167,16 +173,16 @@ function regenerateStates() {
Military.generate(); Military.generate();
if (layerIsOn("toggleEmblems")) drawEmblems(); if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click(); if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
} }
function recreateStates() { function recreateStates() {
const localSeed = generateSeed(); const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value; const statesCount = +byId("statesNumber").value;
if (!statesCount) { if (!statesCount) {
tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error"); tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error");
return null; return null;
@ -198,7 +204,7 @@ function recreateStates() {
const lockedStatesIds = lockedStates.map(s => s.i); const lockedStatesIds = lockedStates.map(s => s.i);
const lockedStatesCapitals = lockedStates.map(s => s.capital); 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"); tip("Unable to regenerate as all states are locked", false, "error");
return null; return null;
} }
@ -317,7 +323,7 @@ function recreateStates() {
: pack.cultures[culture].type === "Nomadic" : pack.cultures[culture].type === "Nomadic"
? "Generic" ? "Generic"
: pack.cultures[culture].type; : 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 cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, 0.3, null, cultureType); const coa = COA.generate(capital.coa, 0.3, null, cultureType);
@ -332,9 +338,11 @@ function recreateStates() {
function regenerateProvinces() { function regenerateProvinces() {
unfog(); unfog();
BurgsAndStates.generateProvinces(true, true); Provinces.generate(true, true);
drawBorders(); Provinces.getPoles();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleBorders")) drawBorders();
layerIsOn("toggleProvinces") ? drawProvinces() : toggleProvinces();
// remove emblems // remove emblems
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove()); document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
@ -437,16 +445,18 @@ function regenerateBurgs() {
BurgsAndStates.specifyBurgs(); BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
regenerateRoutes(); regenerateRoutes();
drawBurgIcons();
drawBurgLabels();
// remove emblems // remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove()); document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove(); emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems(); if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click(); if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click(); if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
} }
function regenerateEmblems() { function regenerateEmblems() {
@ -498,13 +508,13 @@ function regenerateEmblems() {
province.coa.shield = COA.getShield(culture, province.state); province.coa.shield = COA.getShield(culture, province.state);
}); });
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems layerIsOn("toggleEmblems") ? drawEmblems() : toggleEmblems();
} }
function regenerateReligions() { function regenerateReligions() {
Religions.generate(); Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions();
else drawReligions(); layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
refreshAllEditors(); refreshAllEditors();
} }
@ -513,15 +523,17 @@ function regenerateCultures() {
Cultures.expand(); Cultures.expand();
BurgsAndStates.updateCultures(); BurgsAndStates.updateCultures();
Religions.updateCultures(); Religions.updateCultures();
if (!layerIsOn("toggleCultures")) toggleCultures();
else drawCultures(); layerIsOn("toggleCultures") ? drawCultures() : toggleCultures();
refreshAllEditors(); refreshAllEditors();
} }
function regenerateMilitary() { function regenerateMilitary() {
Military.generate(); Military.generate();
if (!layerIsOn("toggleMilitary")) toggleMilitary(); if (layerIsOn("toggleMilitary")) drawMilitary();
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click(); else toggleMilitary();
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
} }
function regenerateIce() { function regenerateIce() {
@ -534,7 +546,7 @@ function regenerateMarkers() {
Markers.regenerate(); Markers.regenerate();
turnButtonOn("toggleMarkers"); turnButtonOn("toggleMarkers");
drawMarkers(); drawMarkers();
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click(); if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
} }
function regenerateZones(event) { function regenerateZones(event) {
@ -545,10 +557,9 @@ function regenerateZones(event) {
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2)); else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
function addNumberOfZones(number) { function addNumberOfZones(number) {
zones.selectAll("g").remove(); // remove existing zones Zones.generate(number);
addZones(number); if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click(); if (layerIsOn("toggleZones")) drawZones();
if (!layerIsOn("toggleZones")) toggleZones();
} }
} }
@ -559,7 +570,7 @@ function unpressClickToAddButton() {
} }
function toggleAddLabel() { function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed"); const pressed = byId("addLabel").classList.contains("pressed");
if (pressed) { if (pressed) {
unpressClickToAddButton(); unpressClickToAddButton();
return; return;
@ -607,8 +618,10 @@ function addLabelOnClick() {
group.classed("hidden", false); group.classed("hidden", false);
group group
.append("text") .append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", id) .attr("id", id)
.append("textPath") .append("textPath")
.attr("text-rendering", "optimizeSpeed")
.attr("xlink:href", "#textPath_" + id) .attr("xlink:href", "#textPath_" + id)
.attr("startOffset", "50%") .attr("startOffset", "50%")
.attr("font-size", "100%") .attr("font-size", "100%")
@ -627,22 +640,22 @@ function addLabelOnClick() {
function toggleAddBurg() { function toggleAddBurg() {
unpressClickToAddButton(); unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed"); byId("addBurgTool").classList.add("pressed");
overviewBurgs(); overviewBurgs();
document.getElementById("addNewBurg").click(); byId("addNewBurg").click();
} }
function toggleAddRiver() { function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed"); const pressed = byId("addRiver").classList.contains("pressed");
if (pressed) { if (pressed) {
unpressClickToAddButton(); unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed"); byId("addNewRiver").classList.remove("pressed");
return; return;
} }
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add("pressed"); addRiver.classList.add("pressed");
document.getElementById("addNewRiver").classList.add("pressed"); byId("addNewRiver").classList.add("pressed");
closeDialogs(".stable"); closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick); 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"); 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.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return; if (cells.b[i]) return;
const {
alterHeights,
resolveDepressions,
addMeandering,
getRiverPath,
getBasin,
getName,
getType,
getWidth,
getOffset,
getApproximateLength,
getNextId
} = Rivers;
const riverCells = []; const riverCells = [];
let riverId = getNextId(rivers); let riverId = Rivers.getNextId(rivers);
let parent = riverId; let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]]; const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux; cells.fl[i] = initialFlux;
const h = alterHeights(); const h = Rivers.alterHeights();
resolveDepressions(h); Rivers.resolveDepressions(h);
while (i) { while (i) {
cells.r[i] = riverId; cells.r[i] = riverId;
@ -728,7 +728,7 @@ function addRiverOnClick() {
} }
// continue old river // continue old river
document.getElementById("river" + oldRiverId)?.remove(); byId("river" + oldRiverId)?.remove();
riverCells.forEach(i => (cells.r[i] = oldRiverId)); riverCells.forEach(i => (cells.r[i] = oldRiverId));
oldRiverCells.forEach(cell => { oldRiverCells.forEach(cell => {
if (h[cell] > h[min]) { if (h[cell] > h[min]) {
@ -752,11 +752,19 @@ function addRiverOnClick() {
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
const widthFactor = const widthFactor =
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor); 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 discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints); const length = Rivers.getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor)); const width = Rivers.getWidth(
Rivers.getOffset({
flux: discharge,
pointIndex: meanderedPoints.length,
widthFactor,
startingWidth: sourceWidth
})
);
if (river) { if (river) {
river.source = source; river.source = source;
@ -765,9 +773,9 @@ function addRiverOnClick() {
river.width = width; river.width = width;
river.cells = riverCells; river.cells = riverCells;
} else { } else {
const basin = getBasin(parent); const basin = Rivers.getBasin(parent);
const name = getName(mouth); const name = Rivers.getName(mouth);
const type = getType({i: riverId, length, parent}); const type = Rivers.getType({i: riverId, length, parent});
rivers.push({ rivers.push({
i: riverId, i: riverId,
@ -777,7 +785,7 @@ function addRiverOnClick() {
length, length,
width, width,
widthFactor, widthFactor,
sourceWidth: 0, sourceWidth,
parent, parent,
cells: riverCells, cells: riverCells,
basin, basin,
@ -787,8 +795,7 @@ function addRiverOnClick() {
} }
// render river // render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
const path = getRiverPath(meanderedPoints, widthFactor);
const id = "river" + riverId; const id = "river" + riverId;
const riversG = viewbox.select("#rivers"); const riversG = viewbox.select("#rivers");
riversG.append("path").attr("id", id).attr("d", path); riversG.append("path").attr("id", id).attr("d", path);
@ -796,13 +803,13 @@ function addRiverOnClick() {
if (d3.event.shiftKey === false) { if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();
unpressClickToAddButton(); unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed"); byId("addNewRiver").classList.remove("pressed");
if (addNewRiver.offsetParent) riversOverviewRefresh.click(); if (addNewRiver.offsetParent) riversOverviewRefresh.click();
} }
} }
function toggleAddMarker() { function toggleAddMarker() {
const pressed = document.getElementById("addMarker")?.classList.contains("pressed"); const pressed = byId("addMarker")?.classList.contains("pressed");
if (pressed) { if (pressed) {
unpressClickToAddButton(); unpressClickToAddButton();
return; return;
@ -830,7 +837,7 @@ function addMarkerOnClick() {
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers"; const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null; 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 selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"}; const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
@ -840,13 +847,13 @@ function addMarkerOnClick() {
selectedConfig.add("marker" + marker.i, cell); selectedConfig.add("marker" + marker.i, cell);
} }
const markersElement = document.getElementById("markers"); const markersElement = byId("markers");
const rescale = +markersElement.getAttribute("rescale"); const rescale = +markersElement.getAttribute("rescale");
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale)); markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
if (d3.event.shiftKey === false) { if (d3.event.shiftKey === false) {
document.getElementById("markerAdd").classList.remove("pressed"); byId("markerAdd").classList.remove("pressed");
document.getElementById("markersAddFromOverview").classList.remove("pressed"); byId("markersAddFromOverview").classList.remove("pressed");
unpressClickToAddButton(); unpressClickToAddButton();
} }
} }
@ -855,33 +862,47 @@ function configMarkersGeneration() {
drawConfigTable(); drawConfigTable();
function drawConfigTable() { function drawConfigTable() {
const {markers} = pack;
const config = Markers.getConfig(); 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 type name">Type</td>
<td data-tip="Marker icon">Icon</td> <td data-tip="Marker icon">Icon</td>
<td data-tip="Marker number multiplier">Multiplier</td> <td data-tip="Marker number multiplier">Multiplier</td>
<td data-tip="Number of markers of that type on the current map">Number</td> <td data-tip="Number of markers of that type on the current map">Number</td>
</tr></thead>`; </tr></thead>`;
const lines = config.map(({type, icon, multiplier}, index) => {
const inputId = `markerIconInput${index}`; const lines = config.map(({type, icon, multiplier}) => {
return `<tr> const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
<td><input value="${type}" /></td>
return /* html */ `<tr>
<td><input class="type" value="${type}" /></td>
<td style="position: relative"> <td style="position: relative">
<input id="${inputId}" style="width: 5em" value="${icon}" /> <img class="image" src="${isExternal ? icon : ""}" ${
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i> 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>
<td><input type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td> <td><input class="multiplier" 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 style="text-align:center">${pack.markers.filter(marker => marker.type === type).length}</td>
</tr>`; </tr>`;
}); });
const table = `<table class="table">${headers}<tbody>${lines.join("")}</tbody></table>`; const table = `<table class="table">${headers}<tbody>${lines.join("")}</tbody></table>`;
alertMessage.innerHTML = table; alertMessage.innerHTML = table;
alertMessage.querySelectorAll("i").forEach(selectIconButton => { alertMessage.querySelectorAll("button.changeIcon").forEach(selectIconButton => {
selectIconButton.addEventListener("click", function () { selectIconButton.addEventListener("click", function () {
const input = this.previousElementSibling; const image = this.parentElement.querySelector(".image");
selectIcon(input.value, icon => (input.value = icon)); 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 applyChanges = () => {
const rows = alertMessage.querySelectorAll("tbody > tr"); const rows = alertMessage.querySelectorAll("tbody > tr");
const rowsData = Array.from(rows).map(row => { const rowsData = Array.from(rows).map(row => {
const inputs = row.querySelectorAll("input"); const type = row.querySelector(".type").value;
return {
type: inputs[0].value, const image = row.querySelector(".image");
icon: inputs[1].value, const emoji = row.querySelector(".emoji");
multiplier: parseFloat(inputs[2].value) const icon = image.getAttribute("src") || emoji.textContent;
};
const multiplier = parseFloat(row.querySelector(".multiplier").value);
return {type, icon, multiplier};
}); });
const config = Markers.getConfig(); 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 // add listeners
byId("distanceUnitInput").addEventListener("change", changeDistanceUnit); byId("distanceUnitInput").on("change", changeDistanceUnit);
byId("distanceScaleOutput").addEventListener("input", changeDistanceScale); byId("distanceScaleInput").on("change", changeDistanceScale);
byId("distanceScaleInput").addEventListener("change", changeDistanceScale); byId("heightUnit").on("change", changeHeightUnit);
byId("heightUnit").addEventListener("change", changeHeightUnit); byId("heightExponentInput").on("input", changeHeightExponent);
byId("heightExponentInput").addEventListener("input", changeHeightExponent); byId("temperatureScale").on("change", changeTemperatureScale);
byId("heightExponentOutput").addEventListener("input", changeHeightExponent);
byId("temperatureScale").addEventListener("change", changeTemperatureScale);
byId("populationRateOutput").addEventListener("input", changePopulationRate); byId("populationRateInput").on("change", changePopulationRate);
byId("populationRateInput").addEventListener("change", changePopulationRate); byId("urbanizationInput").on("change", changeUrbanizationRate);
byId("urbanizationOutput").addEventListener("input", changeUrbanizationRate); byId("urbanDensityInput").on("change", changeUrbanDensity);
byId("urbanizationInput").addEventListener("change", changeUrbanizationRate);
byId("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
byId("urbanDensityInput").addEventListener("change", changeUrbanDensity);
byId("addLinearRuler").addEventListener("click", addRuler); byId("addLinearRuler").on("click", addRuler);
byId("addOpisometer").addEventListener("click", toggleOpisometerMode); byId("addOpisometer").on("click", toggleOpisometerMode);
byId("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode); byId("addRouteOpisometer").on("click", toggleRouteOpisometerMode);
byId("addPlanimeter").addEventListener("click", togglePlanimeterMode); byId("addPlanimeter").on("click", togglePlanimeterMode);
byId("removeRulers").addEventListener("click", removeAllRulers); byId("removeRulers").on("click", removeAllRulers);
byId("unitsRestore").addEventListener("click", restoreDefaultUnits); byId("unitsRestore").on("click", restoreDefaultUnits);
function changeDistanceUnit() { function changeDistanceUnit() {
if (this.value === "custom_name") { if (this.value === "custom_name") {
@ -71,11 +66,11 @@ function editUnits() {
function changeHeightExponent() { function changeHeightExponent() {
calculateTemperatures(); calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp(); if (layerIsOn("toggleTemperature")) drawTemperature();
} }
function changeTemperatureScale() { function changeTemperatureScale() {
if (layerIsOn("toggleTemp")) drawTemp(); if (layerIsOn("toggleTemperature")) drawTemperature();
} }
function changePopulationRate() { function changePopulationRate() {
@ -92,7 +87,6 @@ function editUnits() {
function restoreDefaultUnits() { function restoreDefaultUnits() {
distanceScale = 3; distanceScale = 3;
byId("distanceScaleOutput").value = distanceScale;
byId("distanceScaleInput").value = distanceScale; byId("distanceScaleInput").value = distanceScale;
unlock("distanceScale"); unlock("distanceScale");
@ -110,16 +104,16 @@ function editUnits() {
calculateFriendlyGridSize(); calculateFriendlyGridSize();
// height exponent // height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8; heightExponentInput.value = 1.8;
localStorage.removeItem("heightExponent"); localStorage.removeItem("heightExponent");
calculateTemperatures(); calculateTemperatures();
renderScaleBar(); renderScaleBar();
// population // population
populationRate = populationRateOutput.value = populationRateInput.value = 1000; populationRate = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1; urbanization = urbanizationInput.value = 1;
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10; urbanDensity = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate"); localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization"); localStorage.removeItem("urbanization");
localStorage.removeItem("urbanDensity"); localStorage.removeItem("urbanDensity");
@ -127,11 +121,16 @@ function editUnits() {
function addRuler() { function addRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers(); if (!layerIsOn("toggleRulers")) toggleRulers();
const width = Math.min(graphWidth, svgWidth);
const height = Math.min(graphHeight, svgHeight);
const pt = byId("map").createSVGPoint(); 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 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 from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0]; const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw(); rulers.create(Ruler, [from, to]).draw();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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