merge completed... now to fix all the bugs...

This commit is contained in:
howlingsails 2021-12-12 23:02:38 -08:00
commit 87c4d80fbc
3472 changed files with 466748 additions and 6517 deletions

15
.eslintrc.js Normal file
View file

@ -0,0 +1,15 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'airbnb-base',
],
parserOptions: {
ecmaVersion: 13,
sourceType: 'module',
},
rules: {
},
};

9
.idea/Fantasy-Map-Generator.iml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Fantasy-Map-Generator.iml" filepath="$PROJECT_DIR$/.idea/Fantasy-Map-Generator.iml" />
</modules>
</component>
</project>

4
.idea/shelf/For_Other_branch.xml generated Normal file
View file

@ -0,0 +1,4 @@
<changelist name="For_Other_branch" date="1639061594618" recycled="false">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/For_Other_branch/shelved.patch" />
<option name="DESCRIPTION" value="For Other branch" />
</changelist>

View file

@ -0,0 +1,70 @@
Index: _developmentREADME.md
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- _developmentREADME.md (date 1639061423175)
+++ _developmentREADME.md (date 1639061423175)
@@ -0,0 +1,62 @@
+# How to setup development for project
+
+## Useful Commands
+
+```
+# Run using python on port 8000
+python -m http.server 8000
+
+#Check Network Ports if you having issues
+netstat -ano | grep LISTEN
+
+```
+
+## Useful starting points
+
+- Tool uses the d3.js library to visualize, navigate and map.
+- cells[i] is your friend it has all the data.
+
+
+
+
+## Open Tasks
+
+
+[ - ] Merge main back to dev-economics branch
+
+[ ] make cells a typescript class so we get code completion.
+
+[ ] Finish economics
+
+## Docker Host
+
+
+ [ ] Add from command history
+
+
+
+### Networking
+
+
+
+ [ ]
+
+
+##### TLS
+
+ [ x ] Use Kubernetes
+
+ [ ]
+
+
+### Securing
+
+ [ x ] Use Kubernetes
+
+ [ ]
+
+
+## Tools
+
+Intellij
+

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

350
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,350 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BranchesTreeState">
<expand>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="GROUP_NODE:origin" type="e8cecc67:BranchNodeDescriptor" />
</path>
</expand>
<select>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="BRANCH:dev-economics-merge" type="e8cecc67:BranchNodeDescriptor" />
</path>
</select>
</component>
<component name="ChangeListManager">
<list default="true" id="53f140fa-3e36-41e1-9b6f-3a4b888fc394" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/dropbox.html" afterDir="false" />
<change afterPath="$PROJECT_DIR$/libs/umami.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/modules/cloud.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/modules/export.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/modules/markers-generator.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/modules/ui/hotkeys.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/modules/ui/markers-overview.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/arrayUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/colorUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/commonUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/graphUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/nodeUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/numberUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/polyfills.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/probabilityUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/stringUtils.js" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/unitUtils.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Readme.txt" beforeDir="false" afterPath="$PROJECT_DIR$/Readme.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/fonts.css" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/images/preview.png" beforeDir="false" afterPath="$PROJECT_DIR$/images/preview.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/index.css" beforeDir="false" afterPath="$PROJECT_DIR$/index.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/index.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/libs/jquery-ui.css" beforeDir="false" afterPath="$PROJECT_DIR$/libs/jquery-ui.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/libs/pell.js" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/main.js" beforeDir="false" afterPath="$PROJECT_DIR$/main.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/burgs-and-states.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/burgs-and-states.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/coa-renderer.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/coa-renderer.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/cultures-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/cultures-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/fonts.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/fonts.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/heightmap-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/heightmap-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/load.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/load.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/military-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/military-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/names-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/names-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/relief-icons.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/relief-icons.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/religions-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/religions-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/river-generator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/river-generator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/save.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/save.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/3d.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/3d.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/battle-screen.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/battle-screen.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/burg-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/burg-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/burgs-overview.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/burgs-overview.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/cultures-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/cultures-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/editors.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/editors.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/general.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/general.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/heightmap-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/heightmap-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/labels-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/labels-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/layers.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/layers.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/markers-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/markers-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/measurers.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/measurers.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/military-overview.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/military-overview.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/notes-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/notes-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/options.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/options.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/provinces-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/provinces-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/rivers-creator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/rivers-creator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/rivers-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/rivers-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/rivers-overview.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/rivers-overview.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/states-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/states-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/style.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/style.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/tools.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/tools.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/units-editor.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/units-editor.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/ui/world-configurator.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/ui/world-configurator.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/modules/utils.js" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/modules/voronoi.js" beforeDir="false" afterPath="$PROJECT_DIR$/modules/voronoi.js" afterDir="false" />
</list>
<list id="1a815da3-372f-4aee-83db-d66764da8a8c" name="cap10bill-stuff" comment="cap10bill-stuff" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="CodeStyleSettingsInfer">
<option name="done" value="true" />
</component>
<component name="ComposerSettings">
<execution />
</component>
<component name="Git.Merge.Settings">
<option name="BRANCH" value="master" />
</component>
<component name="Git.Settings">
<favorite-branches>
<branch-storage>
<map>
<entry type="REMOTE">
<value>
<list>
<branch-info repo="$PROJECT_DIR$" source="origin/dev-economics" />
<branch-info repo="$PROJECT_DIR$" source="origin/cap10bill" />
</list>
</value>
</entry>
</map>
</branch-storage>
</favorite-branches>
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="dev-economics" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="UPDATE_TYPE" value="REBASE" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="GitToolBoxStore">
<option name="projectConfigVersion" value="2" />
<option name="recentBranches">
<RecentBranches>
<option name="branchesForRepo">
<list>
<RecentBranchesForRepo>
<option name="branches">
<list>
<RecentBranch>
<option name="branchName" value="cap10bill" />
<option name="lastUsedInstant" value="1638217190" />
</RecentBranch>
<RecentBranch>
<option name="branchName" value="master" />
<option name="lastUsedInstant" value="1638217189" />
</RecentBranch>
</list>
</option>
<option name="repositoryRootUrl" value="file://$PROJECT_DIR$" />
</RecentBranchesForRepo>
</list>
</option>
</RecentBranches>
</option>
</component>
<component name="KubernetesApiPersistence">
<option name="context" value="gke_fhir-poc-330418_us-west2_aidbox-clin-10300" />
<option name="namespace" value="default" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="mavenHome" value="$APPLICATION_HOME_DIR$/plugins/maven/lib/maven3" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectCodeStyleSettingsMigration">
<option name="version" value="1" />
</component>
<component name="ProjectId" id="20KLG9LLdl9Lkg5IYeUuWUNLYgk" />
<component name="ProjectViewState">
<option name="abbreviatePackageNames" value="true" />
<option name="autoscrollFromSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="aspect.path.notification.shown" value="true" />
<property name="dart.analysis.tool.window.visible" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/../../../../Program Files/KDiff3/kdiff3.exe" />
<property name="nodejs_package_manager_path" value="npm" />
<property name="settings.editor.selected.configurable" value="diff.external" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="53f140fa-3e36-41e1-9b6f-3a4b888fc394" name="Default Changelist" comment="" />
<changelist id="1a815da3-372f-4aee-83db-d66764da8a8c" name="cap10bill-stuff" comment="" />
<created>1635788235045</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1635788235045</updated>
<workItem from="1635788236544" duration="16834000" />
<workItem from="1636783758094" duration="4249000" />
<workItem from="1637180980802" duration="114000" />
<workItem from="1638216431637" duration="928000" />
<workItem from="1638217433198" duration="16527000" />
<workItem from="1638411446516" duration="5354000" />
<workItem from="1638493341355" duration="906000" />
</task>
<task id="LOCAL-00001" summary="For Other branch">
<created>1639061594876</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1639061594876</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="dev-economics-merge" />
</list>
</value>
</entry>
</map>
</option>
</State>
</value>
</entry>
</map>
</option>
<option name="RECENT_FILTERS">
<map>
<entry key="Branch">
<value>
<list>
<RecentGroup>
<option name="FILTER_VALUES">
<option value="master" />
</option>
</RecentGroup>
<RecentGroup>
<option name="FILTER_VALUES">
<option value="origin/dev-economics" />
</option>
</RecentGroup>
<RecentGroup>
<option name="FILTER_VALUES">
<option value="origin/vite-migration" />
</option>
</RecentGroup>
</list>
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="For Other branch" />
<option name="LAST_COMMIT_MESSAGE" value="For Other branch" />
</component>
<component name="WindowStateProjectService">
<state x="638" y="80" key="#com.intellij.ide.macro.MacrosDialog" timestamp="1639369375598">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="638" y="80" key="#com.intellij.ide.macro.MacrosDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639369375598" />
<state x="574" y="185" width="667" height="706" key="#com.intellij.tools.ToolEditorDialog" timestamp="1639369607692">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="574" y="185" width="667" height="706" key="#com.intellij.tools.ToolEditorDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639369607692" />
<state x="513" y="0" key="CommitChangelistDialog2" timestamp="1639061594894">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="513" y="0" key="CommitChangelistDialog2/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639061594894" />
<state x="92" y="92" width="1736" height="856" key="DiffContextDialog" timestamp="1639368893441">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="92" y="92" width="1736" height="856" key="DiffContextDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639368893441" />
<state x="687" y="215" key="FileChooserDialogImpl" timestamp="1639369646456">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="687" y="215" key="FileChooserDialogImpl/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639369646456" />
<state x="52" y="100" width="1854" height="868" key="MergeDialog" timestamp="1639367643275">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="52" y="100" width="1854" height="868" key="MergeDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639367643275" />
<state x="499" y="220" key="MultipleFileMergeDialog" timestamp="1639377574037">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="499" y="220" key="MultipleFileMergeDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639377574037" />
<state x="589" y="149" key="RollbackChangesDialog" timestamp="1638860880630">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="589" y="149" key="RollbackChangesDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1638860880630" />
<state x="260" y="0" key="SettingsEditor" timestamp="1639372442707">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="260" y="0" key="SettingsEditor/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639372442707" />
<state x="92" y="92" width="1736" height="856" key="ShowDiffWithBranchDialog" timestamp="1639368289613">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="92" y="92" width="1736" height="856" key="ShowDiffWithBranchDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639368289613" />
<state x="787" y="376" width="374" height="287" key="VCS.EditChangelistDialog" timestamp="1639061484548">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="787" y="376" width="374" height="287" key="VCS.EditChangelistDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639061484548" />
<state x="598" y="189" key="VcsDiffUtil.ChangesDialog" timestamp="1639368226337">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="598" y="189" key="VcsDiffUtil.ChangesDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639368226337" />
<state x="623" y="395" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1639369945245">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="623" y="395" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1639369945245" />
<state x="613" y="160" key="git4idea.branch.GitSmartOperationDialog" timestamp="1638860788027">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="613" y="160" key="git4idea.branch.GitSmartOperationDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1638860788027" />
<state x="735" y="414" key="git4idea.rebase.GitUnstructuredEditor" timestamp="1638865361273">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="735" y="414" key="git4idea.rebase.GitUnstructuredEditor/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1638865361273" />
<state x="673" y="389" key="git4idea.ui.GitResetDialog" timestamp="1638860917078">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="673" y="389" key="git4idea.ui.GitResetDialog/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1638860917078" />
<state x="539" y="5" width="840" height="1034" key="search.everywhere.popup" timestamp="1638860833149">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="539" y="5" width="840" height="1034" key="search.everywhere.popup/0.0.1920.1040/-1920.0.1920.1040@0.0.1920.1040" timestamp="1638860833149" />
</component>
</project>

View file

@ -1,10 +1,10 @@
# Fantasy Map Generator
Azgaar's _Fantasy Map Generator_ is a free client-side web application generating interactive and highly customizable svg maps based on voronoi diagram.
Azgaar's _Fantasy Map Generator_ is a free web application generating interactive and highly customizable svg maps based on voronoi diagram.
Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator).
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance. The current progress is tracked in [Trello](https://trello.com/b/7x832DG4/fantasy-map-generator). Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com).
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840629213659136/preview1.png)](https://i.redd.it/8bf81ir2cy631.png)
@ -12,18 +12,20 @@ Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840632296734720/preview3.png)](https://cdn.discordapp.com/attachments/515359096925454350/593891237984206848/The_Wichin_Island_-_diplomacy.png)
Join our [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) and [Discord server](https://discordapp.com/invite/X7E84HU) to share the created maps, discuss the Generator, suggest ideas and get a most recent updates. You may also contact me directly via [email](mailto:azgaar.fmg@yandex.by). For bug reports please use the project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or Discord "Bugs" channel. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips).
Join our [Discord server](https://discordapp.com/invite/X7E84HU) and [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) to share your creations, discuss the Generator, suggest ideas and get the most recent updates.
Contact me via [email](mailto:azgaar.fmg@yandex.by) if you have non-public suggestions. For bug reports please use [GitHub issues](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or _#bugs_ channel on Discord. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips).
Electron desktop application is available in [releases](https://github.com/Azgaar/Fantasy-Map-Generator/releases). Download archive for your architecture, unzip and run.
Pull requests are welcomed. The Tool codebase is messy and requires re-design, but I will appreciate if you start with minor changes.
Pull requests are highly welcomed. The codebase is messy and requires re-design, but I will appreciate if you start with minor changes. Check out the [data model](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model) before contributing.
You can support the project on [Patreon](https://www.patreon.com/azgaar).
_Inspiration:_
* Martin O'Leary's [_Generating fantasy maps_](https://mewo2.com/notes/terrain)
- Martin O'Leary's [_Generating fantasy maps_](https://mewo2.com/notes/terrain)
* Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation)
- Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation)
* Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)
- Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)

View file

@ -1,5 +1,9 @@
Azgaar's Fantasy Map Generator
This is an open-source software available under MIT license
Developed by Azgaar (azgaar.fmg@yandex.com) and contributors
Minsk, 2017-2021. MIT License
https://github.com/Azgaar/Fantasy-Map-Generator
To run the tool unzip ALL files and open index.html in browser

BIN
_maps/.DS_Store vendored Normal file

Binary file not shown.

49
dropbox.html Normal file
View file

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<script type="text/javascript" src="https://unpkg.com/dropbox@10.8.0/dist/Dropbox-sdk.min.js"></script>
<title>FMG Dropbox Auth</title>
</head>
<body>
<script>
/*
open this page in a new window without query parameter to start auth
window.opener.setDropBoxToken(token) will be called on the opener
window.
*/
const REDIRECT_URI = window.location.origin + window.location.pathname;
const dbxAuth = new Dropbox.DropboxAuth({clientId: "pdr9ae64ip0qno4"});
const spObj = new URLSearchParams(window.location.search);
const searchParams = Object.fromEntries(spObj.entries());
if (searchParams.code) getToken();
else doAuth(); // start authentication
function doAuth() {
dbxAuth
.getAuthenticationUrl(REDIRECT_URI, undefined, "code", "offline", undefined, undefined, true)
.then(authUrl => {
window.sessionStorage.clear();
window.sessionStorage.setItem("codeVerifier", dbxAuth.codeVerifier);
window.location.href = authUrl;
})
.catch(error => console.error(error));
}
function getToken() {
dbxAuth.setCodeVerifier(window.sessionStorage.getItem("codeVerifier"));
dbxAuth
.getAccessTokenFromCode(REDIRECT_URI, searchParams.code)
.then(resp => {
const token = resp.result.access_token;
window.opener.Cloud.providers.dropbox.setDropBoxToken(token);
})
.catch(error => {
console.error(error);
});
}
</script>
</body>
</html>

175
fonts.css
View file

@ -1,175 +0,0 @@
@font-face {
font-family: 'Amatic SC';
font-style: normal;
font-weight: 700;
src: local('Amatic SC Bold'), local('AmaticSC-Bold'), url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Architects Daughter';
font-style: normal;
font-weight: 400;
src: local('Architects Daughter Regular'), local('ArchitectsDaughter-Regular'), url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Bitter';
font-style: normal;
font-weight: 400;
src: local('Bitter Regular'), local('Bitter-Regular'), url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Caesar Dressing';
font-style: normal;
font-weight: 400;
src: local('Caesar Dressing'), local('CaesarDressing-Regular'), url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
src: local('Cinzel Regular'), local('Cinzel-Regular'), url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Comfortaa';
font-style: normal;
font-weight: 700;
src: local('Comfortaa Bold'), local('Comfortaa-Bold'), url(https://fonts.gstatic.com/s/comfortaa/v12/fND5XPYKrF2tQDwwfWZJI-gdm0LZdjqr5-oayXSOefg.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Dancing Script';
font-style: normal;
font-weight: 700;
src: local('Dancing Script Bold'), local('DancingScript-Bold'), url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Fredericka the Great';
font-style: normal;
font-weight: 400;
src: local('Fredericka the Great'), local('FrederickatheGreat'), url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Gloria Hallelujah';
font-style: normal;
font-weight: 400;
src: local('Gloria Hallelujah'), local('GloriaHallelujah'), url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Great Vibes';
font-style: normal;
font-weight: 400;
src: local('Great Vibes'), local('GreatVibes-Regular'), url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'IM Fell English';
font-style: normal;
font-weight: 400;
src: local('IM FELL English Roman'), local('IM_FELL_English_Roman'), url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Kaushan Script';
font-style: normal;
font-weight: 400;
src: local('Kaushan Script'), local('KaushanScript-Regular'), url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'MedievalSharp';
font-style: normal;
font-weight: 400;
src: local('MedievalSharp'), url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Metamorphous';
font-style: normal;
font-weight: 400;
src: local('Metamorphous'), url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Montez';
font-style: normal;
font-weight: 400;
src: local('Montez Regular'), local('Montez-Regular'), url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Nova Script';
font-style: normal;
font-weight: 400;
src: local('Nova Script Regular'), local('NovaScript-Regular'), url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Orbitron';
font-style: normal;
font-weight: 400;
src: local('Orbitron Regular'), local('Orbitron-Regular'), url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Satisfy';
font-style: normal;
font-weight: 400;
src: local('Satisfy Regular'), local('Satisfy-Regular'), url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Shadows Into Light';
font-style: normal;
font-weight: 400;
src: local('Shadows Into Light'), local('ShadowsIntoLight'), url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Uncial Antiqua';
font-style: normal;
font-weight: 400;
src: local('Uncial Antiqua'), local('UncialAntiqua-Regular'), url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Underdog';
font-style: normal;
font-weight: 400;
src: local('Underdog'), local('Underdog-Regular'), url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2) format('woff2');
unicode-range: 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;
}
@font-face {
font-family: 'Yellowtail';
font-style: normal;
font-weight: 400;
src: local('Yellowtail Regular'), local('Yellowtail-Regular'), url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
unicode-range: 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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

341
index.css

File diff suppressed because one or more lines are too long

2514
index.css.orig Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

4752
index.html.orig Normal file

File diff suppressed because one or more lines are too long

530
libs/jquery-ui.css vendored
View file

@ -3,81 +3,79 @@
* Copyright jQuery Foundation and other contributors; Licensed MIT */
.ui-draggable-handle {
-ms-touch-action: none;
touch-action: none;
-ms-touch-action: none;
touch-action: none;
}
.ui-helper-hidden {
display: none;
display: none;
}
.ui-helper-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.ui-helper-reset {
margin: 0;
padding: 0;
border: 0;
outline: 0;
line-height: 1.3;
text-decoration: none;
font-size: 100%;
list-style: none;
margin: 0;
padding: 0;
border: 0;
outline: 0;
line-height: 1.3;
text-decoration: none;
font-size: 100%;
list-style: none;
}
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
content: "";
display: table;
border-collapse: collapse;
content: "";
display: table;
border-collapse: collapse;
}
.ui-helper-clearfix:after {
clear: both;
clear: both;
}
.ui-helper-zfix {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
opacity: 0;
filter:Alpha(Opacity=0); /* support: IE8 */
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
opacity: 0;
filter: Alpha(Opacity=0); /* support: IE8 */
}
.ui-front {
z-index: 100;
z-index: 100;
}
/* Interaction Cues
----------------------------------*/
.ui-state-disabled {
cursor: default !important;
pointer-events: none;
cursor: default !important;
pointer-events: none;
}
/* Icons
----------------------------------*/
.ui-icon {
display: inline-block;
vertical-align: middle;
margin-top: -.25em;
position: relative;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
display: inline-block;
vertical-align: middle;
margin-top: -0.25em;
position: relative;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
}
.ui-widget-icon-block {
left: 50%;
margin-left: -8px;
display: block;
left: 50%;
margin-left: -8px;
display: block;
}
/* Misc visuals
@ -85,102 +83,102 @@
/* Overlays */
.ui-widget-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ui-resizable {
position: relative;
position: relative;
}
.ui-resizable-handle {
position: absolute;
font-size: 0.1px;
display: block;
-ms-touch-action: none;
touch-action: none;
position: absolute;
font-size: 0.1px;
display: block;
-ms-touch-action: none;
touch-action: none;
}
.ui-resizable-disabled .ui-resizable-handle,
.ui-resizable-autohide .ui-resizable-handle {
display: none;
display: none;
}
.ui-resizable-n {
cursor: n-resize;
height: 7px;
width: 100%;
top: -5px;
left: 0;
cursor: n-resize;
height: 7px;
width: 100%;
top: -5px;
left: 0;
}
.ui-resizable-s {
cursor: s-resize;
height: 7px;
width: 100%;
bottom: -5px;
left: 0;
cursor: s-resize;
height: 7px;
width: 100%;
bottom: -5px;
left: 0;
}
.ui-resizable-e {
cursor: e-resize;
width: 7px;
right: -5px;
top: 0;
height: 100%;
cursor: e-resize;
width: 7px;
right: -5px;
top: 0;
height: 100%;
}
.ui-resizable-w {
cursor: w-resize;
width: 7px;
left: -5px;
top: 0;
height: 100%;
cursor: w-resize;
width: 7px;
left: -5px;
top: 0;
height: 100%;
}
.ui-resizable-se {
cursor: se-resize;
width: 12px;
height: 12px;
right: 1px;
bottom: 1px;
cursor: se-resize;
width: 12px;
height: 12px;
right: 1px;
bottom: 1px;
}
.ui-resizable-sw {
cursor: sw-resize;
width: 9px;
height: 9px;
left: -5px;
bottom: -5px;
cursor: sw-resize;
width: 9px;
height: 9px;
left: -5px;
bottom: -5px;
}
.ui-resizable-nw {
cursor: nw-resize;
width: 9px;
height: 9px;
left: -5px;
top: -5px;
cursor: nw-resize;
width: 9px;
height: 9px;
left: -5px;
top: -5px;
}
.ui-resizable-ne {
cursor: ne-resize;
width: 9px;
height: 9px;
right: -5px;
top: -5px;
cursor: ne-resize;
width: 9px;
height: 9px;
right: -5px;
top: -5px;
}
.ui-sortable-handle {
-ms-touch-action: none;
touch-action: none;
-ms-touch-action: none;
touch-action: none;
}
.ui-button {
padding: .4em 1em;
display: inline-block;
position: relative;
line-height: normal;
margin-right: .1em;
cursor: pointer;
vertical-align: middle;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
padding: 0.4em 1em;
display: inline-block;
position: relative;
line-height: normal;
margin-right: 0.1em;
cursor: pointer;
vertical-align: middle;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Support: IE <= 11 */
overflow: visible;
/* Support: IE <= 11 */
overflow: visible;
}
.ui-button,
@ -188,128 +186,126 @@
.ui-button:visited,
.ui-button:hover,
.ui-button:active {
text-decoration: none;
text-decoration: none;
}
/* to make room for the icon, a width needs to be set here */
.ui-button-icon-only {
width: 2em;
box-sizing: border-box;
white-space: nowrap;
width: 2em;
box-sizing: border-box;
white-space: nowrap;
}
/* button icon element(s) */
.ui-button-icon-only .ui-icon {
position: absolute;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
}
.ui-button.ui-icon-notext .ui-icon {
padding: 0;
width: 2.1em;
height: 2.1em;
text-indent: -9999px;
white-space: nowrap;
padding: 0;
width: 2.1em;
height: 2.1em;
text-indent: -9999px;
white-space: nowrap;
}
input.ui-button.ui-icon-notext .ui-icon {
width: auto;
height: auto;
text-indent: 0;
white-space: normal;
padding: .4em 1em;
width: auto;
height: auto;
text-indent: 0;
white-space: normal;
padding: 0.4em 1em;
}
/* workarounds */
/* Support: Firefox 5 - 40 */
input.ui-button::-moz-focus-inner,
button.ui-button::-moz-focus-inner {
border: 0;
padding: 0;
border: 0;
padding: 0;
}
.ui-controlgroup {
vertical-align: middle;
display: inline-block;
vertical-align: middle;
display: inline-block;
}
.ui-controlgroup > .ui-controlgroup-item {
float: left;
margin-left: 0;
margin-right: 0;
float: left;
margin-left: 0;
margin-right: 0;
}
.ui-controlgroup > .ui-controlgroup-item:focus,
.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
z-index: 9999;
z-index: 9999;
}
.ui-controlgroup-vertical > .ui-controlgroup-item {
display: block;
float: none;
width: 100%;
margin-top: 0;
margin-bottom: 0;
text-align: left;
display: block;
float: none;
width: 100%;
margin-top: 0;
margin-bottom: 0;
text-align: left;
}
.ui-controlgroup-vertical .ui-controlgroup-item {
box-sizing: border-box;
box-sizing: border-box;
}
.ui-controlgroup .ui-controlgroup-label {
padding: .4em 1em;
padding: 0.4em 1em;
}
.ui-controlgroup .ui-controlgroup-label span {
font-size: 80%;
font-size: 80%;
}
.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
border-left: none;
border-left: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
border-top: none;
border-top: none;
}
.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
border-right: none;
border-right: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
border-bottom: none;
border-bottom: none;
}
/* Spinner specific style fixes */
.ui-controlgroup-vertical .ui-spinner-input {
/* Support: IE8 only, Android < 4.4 only */
width: 75%;
width: calc( 100% - 2.4em );
/* Support: IE8 only, Android < 4.4 only */
width: 75%;
width: calc(100% - 2.4em);
}
.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
border-top-style: solid;
border-top-style: solid;
}
.ui-checkboxradio-label .ui-icon-background {
box-shadow: inset 1px 1px 1px #ccc;
border-radius: .12em;
border: none;
box-shadow: inset 1px 1px 1px #ccc;
border-radius: 0.12em;
border: none;
}
.ui-checkboxradio-radio-label .ui-icon-background {
width: 16px;
height: 16px;
border-radius: 1em;
overflow: visible;
border: none;
width: 16px;
height: 16px;
border-radius: 1em;
overflow: visible;
border: none;
}
.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
background-image: none;
width: 8px;
height: 8px;
border-width: 4px;
border-style: solid;
background-image: none;
width: 8px;
height: 8px;
border-width: 4px;
border-style: solid;
}
.ui-checkboxradio-disabled {
pointer-events: none;
pointer-events: none;
}
body .ui-dialog {
position: absolute;
position: absolute;
top: 0;
left: 0;
outline: 0;
@ -317,30 +313,30 @@ body .ui-dialog {
background-color: inherit;
}
.ui-dialog .ui-dialog-titlebar {
padding: .4em 1em;
position: relative;
font-size: 1.2em;
padding: 0.4em 1em;
position: relative;
font-size: 1.2em;
min-width: 150px;
}
.ui-dialog .ui-dialog-title {
float: left;
margin: .1em 0;
white-space: nowrap;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
float: left;
margin: 0.1em 0;
white-space: nowrap;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-dialog .ui-dialog-titlebar button {
position: absolute;
right: .5em;
.ui-dialog .ui-dialog-titlebar button {
position: absolute;
right: 0.5em;
top: 53%;
padding: 0;
width: 1.8em;
height: 1.8em;
color: #ffffff;
background: none;
font-size: .75em;
font-size: 0.75em;
border: 1px solid #c5c5c5;
}
@ -349,113 +345,107 @@ body .ui-dialog {
}
.ui-dialog .ui-dialog-titlebar button.ui-dialog-titlebar-close {
margin: -1em 0 0;
margin: -1em 0 0;
}
.ui-dialog .ui-dialog-titlebar button:active {
.ui-dialog .ui-dialog-titlebar button:active {
border: 1px solid #5d4651;
color: #5d4651;
}
.ui-dialog .ui-dialog-content {
position: relative;
border: 0;
padding: .5em 1em;
background: none;
overflow-y: auto;
overflow-x: hidden;
position: relative;
border: 0;
padding: 0.5em 1em;
background: none;
overflow-y: auto;
overflow-x: hidden;
}
.ui-dialog .ui-dialog-buttonpane {
text-align: left;
border-width: 1px 0 0 0;
background-image: none;
margin-top: .5em;
padding: .3em 1em .5em .4em;
text-align: left;
border-width: 1px 0 0 0;
background-image: none;
margin-top: 0.5em;
padding: 0.3em 1em 0.5em 0.4em;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
float: right;
float: right;
}
.ui-dialog .ui-dialog-buttonpane button {
margin: .5em .4em .5em 0;
cursor: pointer;
margin: 0.5em 0.4em 0.5em 0;
cursor: pointer;
}
.ui-dialog .ui-resizable-n {
height: 2px;
top: 0;
height: 2px;
top: 0;
}
.ui-dialog .ui-resizable-e {
width: 2px;
right: 0;
width: 2px;
right: 0;
}
.ui-dialog .ui-resizable-s {
height: 2px;
bottom: 0;
height: 2px;
bottom: 0;
}
.ui-dialog .ui-resizable-w {
width: 2px;
left: 0;
width: 2px;
left: 0;
}
.ui-dialog .ui-resizable-se,
.ui-dialog .ui-resizable-sw,
.ui-dialog .ui-resizable-ne,
.ui-dialog .ui-resizable-nw {
width: 7px;
height: 7px;
width: 7px;
height: 7px;
}
.ui-dialog .ui-resizable-se {
right: 0;
bottom: 0;
right: 0;
bottom: 0;
}
.ui-dialog .ui-resizable-sw {
left: 0;
bottom: 0;
left: 0;
bottom: 0;
}
.ui-dialog .ui-resizable-ne {
right: 0;
top: 0;
right: 0;
top: 0;
}
.ui-dialog .ui-resizable-nw {
left: 0;
top: 0;
left: 0;
top: 0;
}
.ui-draggable .ui-dialog-titlebar {
cursor: move;
cursor: move;
}
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Arial,Helvetica,sans-serif;
font-family: Arial, Helvetica, sans-serif;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
font-family: Arial, Helvetica, sans-serif;
font-size: 1em;
}
.ui-widget button[class^="icon-"] {
padding: 1px 6px;
padding: 1px 6px;
}
.ui-widget.ui-widget-content {
border: 1px solid #5e4fa2;
border: 1px solid #5e4fa2;
color: #333333;
}
.ui-widget-content {
border: 1px solid #dddddd;
color: #333333;
border: 1px solid #dddddd;
color: #333333;
}
.ui-widget-content a {
color: #333333;
}
.ui-widget-header {
border-bottom: 1px solid #5d4651;
background: #916e7f;
color: #ffffff;
font-weight: bold;
color: #333333;
}
.ui-widget-header a {
color: #333333;
color: #333333;
}
/* Interaction states
@ -469,10 +459,10 @@ body .ui-dialog {
works properly when clicked or hovered */
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
border: 1px solid #c5c5c5;
background: #f6f6f6;
font-weight: normal;
color: #454545;
border: 1px solid #c5c5c5;
background: #f6f6f6;
font-weight: normal;
color: #454545;
}
.ui-state-default a,
.ui-state-default a:link,
@ -481,10 +471,10 @@ a.ui-button,
a:link.ui-button,
a:visited.ui-button,
.ui-button {
color: #454545;
color: #454545;
}
.ui-button:active {
color: #5d4651;
color: #5d4651;
border-color: #5d4651;
}
.ui-state-hover a,
@ -497,12 +487,12 @@ a:visited.ui-button,
.ui-state-focus a:visited,
a.ui-button:hover,
a.ui-button:focus {
color: #2b2b2b;
text-decoration: none;
color: #2b2b2b;
text-decoration: none;
}
.ui-visual-focus {
box-shadow: 0 0 3px 1px rgb(94, 158, 214);
box-shadow: 0 0 3px 1px rgb(94, 158, 214);
}
/* Interaction Cues
@ -510,68 +500,68 @@ a.ui-button:focus {
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #dad55e;
background: #fffa90;
color: #777620;
border: 1px solid #dad55e;
background: #fffa90;
color: #777620;
}
.ui-state-checked {
border: 1px solid #dad55e;
background: #fffa90;
border: 1px solid #dad55e;
background: #fffa90;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: #777620;
color: #777620;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #f1a899;
background: #fddfdf;
color: #5f3f3f;
border: 1px solid #f1a899;
background: #fddfdf;
color: #5f3f3f;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #5f3f3f;
color: #5f3f3f;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
color: #5f3f3f;
color: #5f3f3f;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
font-weight: bold;
font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
opacity: .7;
filter:Alpha(Opacity=70); /* support: IE8 */
font-weight: normal;
opacity: 0.7;
filter: Alpha(Opacity=70); /* support: IE8 */
font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
opacity: .35;
filter:Alpha(Opacity=35); /* support: IE8 */
background-image: none;
opacity: 0.35;
filter: Alpha(Opacity=35); /* support: IE8 */
background-image: none;
}
.ui-state-disabled .ui-icon {
filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
filter: Alpha(Opacity=35); /* support: IE8 - See #6059 */
}
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay {
background: #aaaaaa;
opacity: .3;
filter: Alpha(Opacity=30); /* support: IE8 */
background: #aaaaaa;
opacity: 0.3;
filter: Alpha(Opacity=30); /* support: IE8 */
}
.ui-widget-shadow {
-webkit-box-shadow: 0px 0px 5px #666666;
box-shadow: 0px 0px 5px #666666;
-webkit-box-shadow: 0px 0px 5px #666666;
box-shadow: 0px 0px 5px #666666;
}

View file

@ -1,157 +0,0 @@
"use strict";
window.Pell = (function () {
const defaultParagraphSeparatorString = "defaultParagraphSeparator";
const formatBlock = "formatBlock";
const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener);
const appendChild = (parent, child) => parent.appendChild(child);
const createElement = tag => document.createElement(tag);
const queryCommandState = command => document.queryCommandState(command);
const queryCommandValue = command => document.queryCommandValue(command);
const exec = (command, value = null) => document.execCommand(command, false, value);
const defaultActions = {
bold: {
icon: "<b>B</b>",
title: "Bold",
state: () => queryCommandState("bold"),
result: () => exec("bold")
},
italic: {
icon: "<i>I</i>",
title: "Italic",
state: () => queryCommandState("italic"),
result: () => exec("italic")
},
underline: {
icon: "<u>U</u>",
title: "Underline",
state: () => queryCommandState("underline"),
result: () => exec("underline")
},
strikethrough: {
icon: "<strike>S</strike>",
title: "Strike-through",
state: () => queryCommandState("strikeThrough"),
result: () => exec("strikeThrough")
},
heading1: {
icon: "<b>H<sub>1</sub></b>",
title: "Heading 1",
result: () => exec(formatBlock, "<h1>")
},
heading2: {
icon: "<b>H<sub>2</sub></b>",
title: "Heading 2",
result: () => exec(formatBlock, "<h2>")
},
paragraph: {
icon: "&#182;",
title: "Paragraph",
result: () => exec(formatBlock, "<p>")
},
quote: {
icon: "&#8220; &#8221;",
title: "Quote",
result: () => exec(formatBlock, "<blockquote>")
},
olist: {
icon: "&#35;",
title: "Ordered List",
result: () => exec("insertOrderedList")
},
ulist: {
icon: "&#8226;",
title: "Unordered List",
result: () => exec("insertUnorderedList")
},
code: {
icon: "&lt;/&gt;",
title: "Code",
result: () => exec(formatBlock, "<pre>")
},
line: {
icon: "&#8213;",
title: "Horizontal Line",
result: () => exec("insertHorizontalRule")
},
link: {
icon: "&#128279;",
title: "Link",
result: () => navigator.clipboard.readText().then(url => exec("createLink", url))
},
image: {
icon: "&#128247;",
title: "Image",
result: () => {
navigator.clipboard.readText().then(url => exec("insertImage", url));
exec("enableObjectResizing");
}
}
};
const defaultClasses = {
actionbar: "pell-actionbar",
button: "pell-button",
content: "pell-content",
selected: "pell-button-selected"
};
const init = settings => {
const actions = settings.actions
? settings.actions.map(action => {
if (typeof action === "string") return defaultActions[action];
else if (defaultActions[action.name]) return {...defaultActions[action.name], ...action};
return action;
})
: Object.keys(defaultActions).map(action => defaultActions[action]);
const classes = {...defaultClasses, ...settings.classes};
const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || "div";
const actionbar = createElement("div");
actionbar.className = classes.actionbar;
appendChild(settings.element, actionbar);
const content = (settings.element.content = createElement("div"));
content.contentEditable = true;
content.className = classes.content;
content.oninput = ({target: {firstChild}}) => {
if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`);
else if (content.innerHTML === "<br>") content.innerHTML = "";
settings.onChange(content.innerHTML);
};
content.onkeydown = event => {
if (event.key === "Enter" && queryCommandValue(formatBlock) === "blockquote") {
setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0);
}
};
appendChild(settings.element, content);
actions.forEach(action => {
const button = createElement("button");
button.className = classes.button;
button.innerHTML = action.icon;
button.title = action.title;
button.setAttribute("type", "button");
button.onclick = () => action.result() && content.focus();
if (action.state) {
const handler = () => button.classList[action.state() ? "add" : "remove"](classes.selected);
addEventListener(content, "keyup", handler);
addEventListener(content, "mouseup", handler);
addEventListener(button, "click", handler);
}
appendChild(actionbar, button);
});
if (settings.styleWithCSS) exec("styleWithCSS");
exec(defaultParagraphSeparatorString, defaultParagraphSeparator);
return settings.element;
};
return {exec, init};
})();

36
libs/umami.js Normal file
View file

@ -0,0 +1,36 @@
(window => {
const noTrack = !location.hostname || window.localStorage.getItem("noTrack");
const {
screen: {width, height},
navigator: {language},
location: {hostname, pathname, search},
document: {referrer}
} = window;
const website = "4f6fd0ae-646a-4946-a9da-7aad63284e48";
const root = "https://fmg-stats.herokuapp.com";
const screen = `${width}x${height}`;
const url = `${pathname}${search}`;
const post = (url, data) => {
const req = new XMLHttpRequest();
req.open("POST", url, true);
req.setRequestHeader("Content-Type", "application/json");
req.send(JSON.stringify(data));
};
const collect = (type, params) => {
if (noTrack) return;
const payload = {website, hostname, screen, language, cache: false};
Object.keys(params).forEach(key => {
payload[key] = params[key];
});
post(`${root}/api/collect`, {type, payload});
};
collect("pageview", {url, referrer});
window.track = (event_type = "reach", event_value = "") => collect("event", {event_type, event_value, url});
})(window);

602
main.js
View file

@ -2,11 +2,11 @@
// https://github.com/Azgaar/Fantasy-Map-Generator
'use strict';
const version = '1.652'; // generator version
const version = '1.71'; // generator version
document.title += ' v' + version;
// Logging constants
const PRODUCTION = window.location.host;
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
const DEBUG = localStorage.getItem('debug');
const INFO = DEBUG || !PRODUCTION;
const TIME = DEBUG || !PRODUCTION;
@ -112,18 +112,17 @@ legend.on('mousemove', () => tip('Drag to change the position. Click to hide the
// main data variables
let grid = {}; // initial grapg based on jittered square grid and data
let pack = {}; // packed graph and data
let seed,
mapId,
mapHistory = [],
elSelected,
modules = {},
notes = [];
let seed;
let mapId;
let mapHistory = [];
let elSelected;
let modules = {};
let notes = [];
let rulers = new Rulers();
let customization = 0; // 0 - no; 1 = heightmap draw; 2 - states draw; 3 - add state/burg; 4 - cultures draw
let customization = 0;
let biomesData = applyDefaultBiomesSystem();
let nameBases = Names.getNameBases(); // cultures-related data
const fonts = ['Almendra+SC', 'Georgia', 'Arial', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact']; // default fonts
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
@ -149,28 +148,36 @@ function zoomed() {
const zoom = d3.zoom().scaleExtent([1, 20]).on('zoom', zoomed);
// default options
let options = {pinNotes: false}; // options object
let options = {
pinNotes: false,
showMFCGMap: true,
winds: [225, 45, 225, 315, 135, 315],
stateLabelsMode: "auto"
};
let mapCoordinates = {}; // map coordinates on globe
options.winds = [225, 45, 225, 315, 135, 315]; // default wind directions
let populationRate = +document.getElementById('populationRateInput').value;
let urbanization = +document.getElementById('urbanizationInput').value;
let urbanDensity = +document.getElementById("urbanDensityInput").value;
applyStoredOptions();
let graphWidth = +mapWidthInput.value,
graphHeight = +mapHeightInput.value; // voronoi graph extention, cannot be changed arter generation
let svgWidth = graphWidth,
svgHeight = graphHeight; // svg canvas resolution, can be changed
// voronoi graph extention, cannot be changed arter generation
let graphWidth = +mapWidthInput.value;
let graphHeight = +mapHeightInput.value;
landmass.append('rect').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
oceanPattern.append('rect').attr('fill', 'url(#oceanic)').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
oceanLayers.append('rect').attr('id', 'oceanBase').attr('x', 0).attr('y', 0).attr('width', graphWidth).attr('height', graphHeight);
void (function removeLoading() {
// svg canvas resolution, can be changed
let svgWidth = graphWidth;
let svgHeight = graphHeight;
// remove loading screen
d3.select('#loading').transition().duration(4000).style('opacity', 0).remove();
d3.select('#initial').transition().duration(4000).attr('opacity', 0).remove();
d3.select('#optionsContainer').transition().duration(3000).style('opacity', 1);
d3.select('#tooltip').transition().duration(4000).style('opacity', 1);
})();
// decide which map should be loaded or generated on page load
void (function checkLoadParameters() {
@ -219,39 +226,8 @@ void (function checkLoadParameters() {
generateMapOnLoad();
})();
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: 'GET', mode: 'cors'})
.then((response) => {
if (response.ok) return response.blob();
throw new Error('Cannot load map from URL');
})
.then((blob) => uploadMap(blob))
.catch((error) => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
}
function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, 'link provided')}.
${random ? `A new random map is generated. ` : ''}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$('#alert').dialog({
title: 'Loading error',
width: '32em',
buttons: {
OK: function () {
$(this).dialog('close');
}
}
});
}
function generateMapOnLoad() {
applyStyleOnLoad(); // apply default of previously selected style
applyStyleOnLoad(); // apply default or previously selected style
generate(); // generate map
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
applyPreset(); // apply saved layers preset
@ -262,10 +238,11 @@ function focusOn() {
const url = new URL(window.location.href);
const params = url.searchParams;
if (params.get('from') === 'MFCG' && document.referrer) {
if (params.get('seed').length === 13) {
const fromMGCG = params.get("from") === "MFCG" && document.referrer;
if (fromMGCG) {
// show back burg from MFCG
params.set('burg', params.get('seed').slice(-4));
const burgSeed = params.get("seed").slice(-4);
params.set("burg", burgSeed);
} else {
// select burg for MFCG
findBurgForMFCG(params);
@ -273,23 +250,33 @@ function focusOn() {
}
}
const s = +params.get('scale') || 8;
let x = +params.get('x');
let y = +params.get('y');
const scaleParam = params.get("scale");
const cellParam = params.get("cell");
const burgParam = params.get("burg");
const c = +params.get('cell');
if (c) {
x = pack.cells.p[c][0];
y = pack.cells.p[c][1];
if (scaleParam || cellParam || burgParam) {
const scale = +scaleParam || 8;
if (cellParam) {
const cell = +params.get("cell");
const [x, y] = pack.cells.p[cell];
zoomTo(x, y, scale, 1600);
return;
}
if (burgParam) {
const burg = isNaN(+burgParam) ? pack.burgs.find(burg => burg.name === burgParam) : pack.burgs[+burgParam];
if (!burg) return;
const {x, y} = burg;
zoomTo(x, y, scale, 1600);
return;
}
const x = +params.get("x") || graphWidth / 2;
const y = +params.get("y") || graphHeight / 2;
zoomTo(x, y, scale, 1600);
}
const b = +params.get('burg');
if (b && pack.burgs[b]) {
x = pack.burgs[b].x;
y = pack.burgs[b].y;
}
if (x && y) zoomTo(x, y, s, 1600);
}
// find burg for MFCG and focus on it
@ -334,7 +321,6 @@ function findBurgForMFCG(params) {
else if (p[0] === 'shantytown') b.shanty = +p[1];
else b[p[0]] = +p[1]; // other parameters
}
b.MFCGlink = document.referrer; // set direct link to MFCG
if (params.get('name') && params.get('name') != 'null') b.name = params.get('name');
const label = burgLabels.select("[data-id='" + burgId + "']");
@ -421,13 +407,19 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>Main changes:
<li>Ability to add river selecting its cells</li>
<li>Keep river course on edit</li>
<li>Refactor river rendering code</li>
<li>Ability to limit military units by biome, state, culture and religion</li>
<li>New marker types</li>
<li>New markers editor</li>
<li>Markers overview screen</li>
<li>Markers regeneration menu</li>
<li>Burg editor update</li>
<li>Editable theme color</li>
<li>Add font dialog</li>
<li>Save to Dropbox</li>
</ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
<span>Thanks for all supporters on ${patreon}!</i></span>`;
<span>Thanks for all supporters on <a href="https://www.patreon.com/azgaar" target="_blank">Patreon</a>!</i></span>`;
$('#alert').dialog({
resizable: false,
@ -479,10 +471,8 @@ function resetZoom(d = 1000) {
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
}
// calculate x,y extreme points of viewBox
// calculate x y extreme points of viewBox
function getViewBoxExtent() {
// x = trX / scale * -1 + graphWidth / scale
// y = trY / scale * -1 + graphHeight / scale
return [
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
@ -536,19 +526,20 @@ function invokeActiveZooming() {
}
// rescale map markers
+markers.attr("rescale") &&
if (+markers.attr('rescale') && markers.style('display') !== 'none') {
markers.selectAll('use').each(function () {
const x = +this.dataset.x,
y = +this.dataset.y,
desired = +this.dataset.size;
const size = Math.max(desired * 5 + 25 / scale, 1);
d3.select(this)
.attr('x', x - size / 2)
.attr('y', y - size)
.attr('width', size)
.attr('height', size);
pack.markers?.forEach(marker => {
const {i, x, y, size = 30, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
}
// rescale rulers to have always the same size
if (ruler.style('display') !== 'none') {
@ -678,7 +669,7 @@ function generate() {
Lakes.generateName();
Military.generate();
addMarkers();
Markers.generate();
addZones();
Names.getMapName();
@ -687,11 +678,12 @@ function generate() {
INFO && console.groupEnd('Generated Map ' + seed);
} catch (error) {
ERROR && console.error(error);
const parsedError = parseError(error);
clearMainTip();
alertMessage.innerHTML = `An error is occured on map generation. Please retry.
<br>If error is critical, clear the stored data and try again.
<p id="errorBox">${parseError(error)}</p>`;
<p id="errorBox">${parsedError}</p>`;
$('#alert').dialog({
resizable: false,
title: 'Generation error',
@ -702,7 +694,7 @@ function generate() {
localStorage.setItem('version', version);
},
Regenerate: function () {
regenerateMap();
regenerateMap("generation error");
$(this).dialog('close');
},
Ignore: function () {
@ -731,6 +723,7 @@ function generateSeed() {
// Place points to calculate Voronoi diagram
function placePoints() {
TIME && console.time('placePoints');
Math.random = aleaPRNG(seed); // reset PRNG
const cellsDesired = +pointsInput.dataset.cells;
const spacing = (grid.spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2)); // spacing between points before jirrering
@ -955,11 +948,11 @@ function calculateMapCoordinates() {
const size = +document.getElementById('mapSizeOutput').value;
const latShift = +document.getElementById('latitudeOutput').value;
const latT = (size / 100) * 180;
const latN = 90 - ((180 - latT) * latShift) / 100;
const latS = latN - latT;
const latT = rn((size / 100) * 180, 1);
const latN = rn(90 - ((180 - latT) * latShift) / 100, 1);
const latS = rn(latN - latT, 1);
const lon = Math.min(((graphWidth / graphHeight) * latT) / 2, 180);
const lon = rn(Math.min(((graphWidth / graphHeight) * latT) / 2, 180));
mapCoordinates = {latT, latN, latS, lonT: lon * 2, lonW: -lon, lonE: lon};
}
@ -979,7 +972,7 @@ function calculateTemperatures() {
const lat = Math.abs(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [0; 90]
const initTemp = tEq - int(lat / 90) * tDelta;
for (let i = r; i < r + grid.cellsX; i++) {
cells.temp[i] = Math.max(Math.min(initTemp - convertToFriendly(cells.h[i]), 127), -128);
cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127);
}
});
@ -998,43 +991,45 @@ function calculateTemperatures() {
function generatePrecipitation() {
TIME && console.time('generatePrecipitation');
prec.selectAll('*').remove();
const cells = grid.cells;
const {cells, cellsX, cellsY} = grid;
cells.prec = new Uint8Array(cells.i.length); // precipitation array
const modifier = precInput.value / 100; // user's input
const cellsX = grid.cellsX,
cellsY = grid.cellsY;
let westerly = [],
easterly = [],
southerly = 0,
northerly = 0;
{
// latitude bands
// x4 = 0-5 latitude: wet through the year (rising zone)
// x2 = 5-20 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 20-30 latitude: dry all year (sinking zone)
// x2 = 30-50 latitude: wet winter (rising zone), dry summer (sinking zone)
// x3 = 50-60 latitude: wet all year (rising zone)
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-90 latitude: dry all year (sinking zone)
}
const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5]; // by 5d step
const westerly = [];
const easterly = [];
let southerly = 0;
let northerly = 0;
// difine wind directions based on cells latitude and prevailing winds there
// precipitation modifier per latitude band
// x4 = 0-5 latitude: wet through the year (rising zone)
// x2 = 5-20 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 20-30 latitude: dry all year (sinking zone)
// x2 = 30-50 latitude: wet winter (rising zone), dry summer (sinking zone)
// x3 = 50-60 latitude: wet all year (rising zone)
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 latitude: dry all year (sinking zone)
// x0.5 = 85-90 latitude: dry all year (sinking zone)
const lalitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const band = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = lalitudeModifier[band];
const tier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
if (options.winds[tier] > 40 && options.winds[tier] < 140) westerly.push([c, latMod, tier]);
else if (options.winds[tier] > 220 && options.winds[tier] < 320) easterly.push([c + cellsX - 1, latMod, tier]);
if (options.winds[tier] > 100 && options.winds[tier] < 260) northerly++;
else if (options.winds[tier] > 280 || options.winds[tier] < 80) southerly++;
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = lalitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
if (isWest) westerly.push([c, latMod, windTier]);
if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
if (isNorth) northerly++;
if (isSouth) southerly++;
});
// distribute winds by direction
if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX);
if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX);
const vertT = southerly + northerly;
if (northerly) {
const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0;
@ -1042,6 +1037,7 @@ function generatePrecipitation() {
const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
}
if (southerly) {
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
const latModS = mapCoordinates.latT > 60 ? d3.mean(lalitudeModifier) : lalitudeModifier[bandS];
@ -1049,20 +1045,34 @@ function generatePrecipitation() {
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
}
function getWindDirections(tier) {
const angle = options.winds[tier];
const isWest = angle > 40 && angle < 140;
const isEast = angle > 220 && angle < 320;
const isNorth = angle > 100 && angle < 260;
const isSouth = angle > 280 || angle < 80;
return {isWest, isEast, isNorth, isSouth};
}
function passWind(source, maxPrec, next, steps) {
const maxPrecInit = maxPrec;
for (let first of source) {
if (first[0]) {
maxPrec = Math.min(maxPrecInit * first[1], 255);
first = first[0];
}
let humidity = maxPrec - cells.h[first]; // initial water amount
if (humidity <= 0) continue; // if first cell in row is too elevated cosdired wind dry
for (let s = 0, current = first; s < steps; s++, current += next) {
// no flux on permafrost
if (cells.temp[current] < -5) continue;
// water cell
if (cells.temp[current] < -5) continue; // no flux in permafrost
if (cells.h[current] < 20) {
// water cell
if (cells.h[current + next] >= 20) {
cells.prec[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
} else {
@ -1073,20 +1083,20 @@ function generatePrecipitation() {
}
// land cell
const precipitation = getPrecipitation(humidity, current, next);
const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION;
const precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity;
cells.prec[current] += precipitation;
const evaporation = precipitation > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
humidity = Math.min(Math.max(humidity - precipitation + evaporation, 0), maxPrec);
humidity = isPassable ? minmax(humidity - precipitation + evaporation, 0, maxPrec) : 0;
}
}
}
function getPrecipitation(humidity, i, n) {
if (cells.h[i + n] > 85) return humidity; // 85 is max passable height
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
return Math.min(Math.max(normalLoss + diff * mod, 1), humidity);
return minmax(normalLoss + diff * mod, 1, humidity);
}
void (function drawWindDirection() {
@ -1400,22 +1410,21 @@ function reMarkFeatures() {
// assign biome id for each cell
function defineBiomes() {
TIME && console.time('defineBiomes');
const cells = pack.cells,
f = pack.features,
temp = grid.cells.temp,
prec = grid.cells.prec;
const {cells} = pack;
const {temp, prec} = grid.cells;
cells.biome = new Uint8Array(cells.i.length); // biomes array
for (const i of cells.i) {
const t = temp[cells.g[i]]; // cell temperature
const h = cells.h[i]; // cell height
const m = h < 20 ? 0 : calculateMoisture(i); // cell moisture
cells.biome[i] = getBiomeId(m, t, h);
const temperature = temp[cells.g[i]];
const height = cells.h[i];
const moisture = height < 20 ? 0 : calculateMoisture(i);
cells.biome[i] = getBiomeId(moisture, temperature, height);
}
function calculateMoisture(i) {
let moist = prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
const n = cells.c[i]
.filter(isLand)
.map((c) => prec[cells.g[c]])
@ -1428,12 +1437,13 @@ function defineBiomes() {
// assign biome id to a cell
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome, including sea ice
if (height < 20) return 0; // marine biome: liquid water cells
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24))) return 12; // wetland biome
const m = Math.min((moisture / 5) | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
if (height < 20) return 0; // marine biome: all water cells
if (temperature < -5) return 11; // permafrost biome
if (moisture > 40 && temperature > -2 && (height < 25 || (moisture > 24 && height > 24 && height < 60))) return 12; // wetland biome
const moistureBand = Math.min((moisture / 5) | 0, 4); // [0-4]
const temperatureBand = Math.min(Math.max(20 - temperature, 0), 25); // [0-25]
return biomesData.biomesMartix[moistureBand][temperatureBand];
}
// assess cells suitability to calculate population and rang cells for culture center and burgs placement
@ -1486,295 +1496,6 @@ function rankCells() {
TIME && console.timeEnd('rankCells');
}
// generate some markers
function addMarkers(number = 1) {
if (!number) return;
TIME && console.time('addMarkers');
const cells = pack.cells,
states = pack.states;
void (function addVolcanoes() {
let mounts = Array.from(cells.i)
.filter((i) => cells.h[i] > 70)
.sort((a, b) => cells.h[b] - cells.h[a]);
let count = mounts.length < 10 ? 0 : Math.ceil((mounts.length / 300) * number);
if (count) addMarker('volcano', '🌋', 52, 50, 13);
while (count && mounts.length) {
const cell = mounts.splice(biased(0, mounts.length - 1, 5), 1);
const x = cells.p[cell][0],
y = cells.p[cell][1];
const id = appendMarker(cell, 'volcano');
const proper = Names.getCulture(cells.culture[cell]);
const name = P(0.3) ? 'Mount ' + proper : Math.random() > 0.3 ? proper + ' Volcano' : proper;
notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight([x, y])}`});
count--;
}
})();
void (function addHotSprings() {
let springs = Array.from(cells.i)
.filter((i) => cells.h[i] > 50)
.sort((a, b) => cells.h[b] - cells.h[a]);
let count = springs.length < 30 ? 0 : Math.ceil((springs.length / 1000) * number);
if (count) addMarker('hot_springs', '♨️', 50, 52, 12.5);
while (count && springs.length) {
const cell = springs.splice(biased(1, springs.length - 1, 3), 1);
const id = appendMarker(cell, 'hot_springs');
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(30, 15, 20, 100));
notes.push({id, name: proper + ' Hot Springs', legend: `A hot springs area. Temperature: ${temp}`});
count--;
}
})();
void (function addMines() {
let hills = Array.from(cells.i).filter((i) => cells.h[i] > 47 && cells.burg[i]);
let count = !hills.length ? 0 : Math.ceil((hills.length / 7) * number);
if (!count) return;
addMarker('mine', '⛏️', 48, 50, 13.5);
const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
while (count && hills.length) {
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
const id = appendMarker(cell, 'mine');
const resource = rw(resources);
const burg = pack.burgs[cells.burg[cell]];
const name = `${burg.name}${resource} mining town`;
const population = rn(burg.population * populationRate * urbanization);
const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
notes.push({id, name, legend});
count--;
}
})();
void (function addBridges() {
const meanRoad = d3.mean(cells.road.filter((r) => r));
const meanFlux = d3.mean(cells.fl.filter((fl) => fl));
let bridges = Array.from(cells.i)
.filter((i) => cells.burg[i] && cells.h[i] >= 20 && cells.r[i] && cells.fl[i] > meanFlux && cells.road[i] > meanRoad)
.sort((a, b) => cells.road[b] + cells.fl[b] / 10 - (cells.road[a] + cells.fl[a] / 10));
let count = !bridges.length ? 0 : Math.ceil((bridges.length / 12) * number);
if (count) addMarker('bridge', '🌉', 50, 50, 14);
while (count && bridges.length) {
const cell = bridges.splice(0, 1);
const id = appendMarker(cell, 'bridge');
const burg = pack.burgs[cells.burg[cell]];
const river = pack.rivers.find((r) => r.i === pack.cells.r[cell]);
const riverName = river ? `${river.name} ${river.type}` : 'river';
const name = river && P(0.2) ? river.name : burg.name;
notes.push({id, name: `${name} Bridge`, legend: `A stone bridge over the ${riverName} near ${burg.name}`});
count--;
}
})();
void (function addInns() {
const maxRoad = d3.max(cells.road) * 0.9;
let taverns = Array.from(cells.i).filter((i) => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
if (!taverns.length) return;
const count = Math.ceil(4 * number);
addMarker('inn', '🍻', 50, 50, 14.5);
const color = ['Dark', 'Light', 'Bright', 'Golden', 'White', 'Black', 'Red', 'Pink', 'Purple', 'Blue', 'Green', 'Yellow', 'Amber', 'Orange', 'Brown', 'Grey'];
const animal = [
'Antelope',
'Ape',
'Badger',
'Bear',
'Beaver',
'Bison',
'Boar',
'Buffalo',
'Cat',
'Crane',
'Crocodile',
'Crow',
'Deer',
'Dog',
'Eagle',
'Elk',
'Fox',
'Goat',
'Goose',
'Hare',
'Hawk',
'Heron',
'Horse',
'Hyena',
'Ibis',
'Jackal',
'Jaguar',
'Lark',
'Leopard',
'Lion',
'Mantis',
'Marten',
'Moose',
'Mule',
'Narwhal',
'Owl',
'Panther',
'Rat',
'Raven',
'Rook',
'Scorpion',
'Shark',
'Sheep',
'Snake',
'Spider',
'Swan',
'Tiger',
'Turtle',
'Wolf',
'Wolverine',
'Camel',
'Falcon',
'Hound',
'Ox'
];
const adj = [
'New',
'Good',
'High',
'Old',
'Great',
'Big',
'Major',
'Happy',
'Main',
'Huge',
'Far',
'Beautiful',
'Fair',
'Prime',
'Ancient',
'Golden',
'Proud',
'Lucky',
'Fat',
'Honest',
'Giant',
'Distant',
'Friendly',
'Loud',
'Hungry',
'Magical',
'Superior',
'Peaceful',
'Frozen',
'Divine',
'Favorable',
'Brave',
'Sunny',
'Flying'
];
for (let i = 0; i < taverns.length && i < count; i++) {
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
const id = appendMarker(cell, 'inn');
const type = P(0.3) ? 'inn' : 'tavern';
const name = P(0.5) ? ra(color) + ' ' + ra(animal) : P(0.6) ? ra(adj) + ' ' + ra(animal) : ra(adj) + ' ' + capitalize(type);
notes.push({id, name: 'The ' + name, legend: `A big and famous roadside ${type}`});
}
})();
void (function addLighthouses() {
const lands = cells.i.filter((i) => cells.harbor[i] > 6 && cells.c[i].some((c) => cells.h[c] < 20 && cells.road[c]));
const lighthouses = Array.from(lands).map((i) => [i, cells.v[i][cells.c[i].findIndex((c) => cells.h[c] < 20 && cells.road[c])]]);
if (lighthouses.length) addMarker('lighthouse', '🚨', 50, 50, 16);
const count = Math.ceil(4 * number);
for (let i = 0; i < lighthouses.length && i < count; i++) {
const cell = lighthouses[i][0],
vertex = lighthouses[i][1];
const id = appendMarker(cell, 'lighthouse');
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + ' Lighthouse' + name, legend: `A lighthouse to keep the navigation safe`});
}
})();
void (function addWaterfalls() {
const waterfalls = cells.i.filter((i) => cells.r[i] && cells.h[i] > 70);
if (waterfalls.length) addMarker('waterfall', '⟱', 50, 54, 16.5);
const count = Math.ceil(3 * number);
for (let i = 0; i < waterfalls.length && i < count; i++) {
const cell = waterfalls[i];
const id = appendMarker(cell, 'waterfall');
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + ' Waterfall' + name, legend: `An extremely beautiful waterfall`});
}
})();
void (function addBattlefields() {
let battlefields = Array.from(cells.i).filter((i) => cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
let count = battlefields.length < 100 ? 0 : Math.ceil((battlefields.length / 500) * number);
if (count) addMarker('battlefield', '⚔️', 50, 52, 12);
while (count && battlefields.length) {
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
const id = appendMarker(cell, 'battlefield');
const campaign = ra(states[cells.state[cell]].campaigns);
const date = generateDate(campaign.start, campaign.end);
const name = Names.getCulture(cells.culture[cell]) + ' Battlefield';
const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
notes.push({id, name, legend});
count--;
}
})();
function addMarker(id, icon, x, y, size) {
const markers = svg.select('#defs-markers');
if (markers.select('#marker_' + id).size()) return;
const symbol = markers
.append('symbol')
.attr('id', 'marker_' + id)
.attr('viewBox', '0 0 30 30');
symbol.append('path').attr('d', 'M6,19 l9,10 L24,19').attr('fill', '#000000').attr('stroke', 'none');
symbol.append('circle').attr('cx', 15).attr('cy', 15).attr('r', 10).attr('fill', '#ffffff').attr('stroke', '#000000').attr('stroke-width', 1);
symbol
.append('text')
.attr('x', x + '%')
.attr('y', y + '%')
.attr('fill', '#000000')
.attr('stroke', '#3200ff')
.attr('stroke-width', 0)
.attr('font-size', size + 'px')
.attr('dominant-baseline', 'central')
.text(icon);
}
function appendMarker(cell, type) {
const x = cells.p[cell][0],
y = cells.p[cell][1];
const id = getNextId('markerElement');
const name = '#marker_' + type;
markers
.append('use')
.attr('id', id)
.attr('xlink:href', name)
.attr('data-id', name)
.attr('data-x', x)
.attr('data-y', y)
.attr('x', x - 15)
.attr('y', y - 30)
.attr('data-size', 1)
.attr('width', 30)
.attr('height', 30);
return id;
}
TIME && console.timeEnd('addMarkers');
}
// regenerate some zones
function addZones(number = 1) {
TIME && console.time('addZones');
@ -1823,9 +1544,18 @@ function addZones(number = 1) {
});
}
const invasion = rw({Invasion: 4, Occupation: 3, Raid: 2, Conquest: 2, Subjugation: 1, Foray: 1, Skirmishes: 1, Incursion: 2, Pillaging: 1, Intervention: 1});
const name = getAdjective(invader.name) + ' ' + invasion;
data.push({name, type: 'Invasion', cells: cellsArray, fill: 'url(#hatch1)'});
const invasion = rw({
Invasion: 4,
Occupation: 3,
Raid: 2,
Conquest: 2,
Subjugation: 1,
Foray: 1,
Skirmishes: 1,
Incursion: 2,
Pillaging: 1,
Intervention: 1
});
}
function addRebels() {
@ -2133,7 +1863,7 @@ function addZones(number = 1) {
// show map stats on generation complete
function showStatistics() {
const template = templateInput.value;
const template = templateInput.options[templateInput.selectedIndex].text;
const templateRandom = locked('template') ? '' : '(random)';
const stats = ` Seed: ${seed}
Canvas size: ${graphWidth}x${graphHeight}

1909
main.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -415,7 +415,7 @@ window.BurgsAndStates = (function () {
function getRiverCost(r, i, type) {
if (type === 'River') return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
@ -479,6 +479,7 @@ window.BurgsAndStates = (function () {
const {cells, features, states} = pack;
const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1));
const mode = options.stateLabelsMode || "auto";
for (const s of states) {
if (!s.i || s.removed || !s.cells || (list && !list.includes(s.i))) continue;
@ -585,7 +586,8 @@ window.BurgsAndStates = (function () {
paths.forEach((p) => {
const id = p[0];
const s = states[p[0]];
const state = states[p[0]];
const {name, fullName} = state;
if (list) {
t.select('#textPath_stateLabel' + id).remove();
@ -599,22 +601,7 @@ window.BurgsAndStates = (function () {
.attr('id', 'textPath_stateLabel' + id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
let lines = [];
let ratio = 100;
if (pathLength < s.name.length) {
// only short name will fit
lines = splitInTwo(s.name);
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 50);
} else if (pathLength > s.fullName.length * 2.5) {
// full name will fit in one line
lines = [s.fullName];
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 70), 170), 70);
} else {
// try miltilined label
lines = splitInTwo(s.fullName);
ratio = Math.max(Math.min(rn((pathLength / lines[0].length) * 60), 150), 70);
}
const [lines, ratio] = getLines(mode, name, fullName, pathLength);
// prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) {
@ -646,7 +633,7 @@ window.BurgsAndStates = (function () {
.node();
el.insertAdjacentHTML('afterbegin', spans.join(''));
if (lines.length < 2) return;
if (mode === "full" || lines.length === 1) return;
// check whether multilined label is generally inside the state. If no, replace with short name label
const cs = pack.cells.state;
@ -657,14 +644,14 @@ window.BurgsAndStates = (function () {
const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id;
const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id;
const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id;
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside => exit
// use one-line name
const name = pathLength > s.fullName.length * 1.8 ? s.fullName : s.name;
example.text(name);
// move to one-line name
const text = pathLength > fullName.length * 1.8 ? fullName : name;
example.text(text);
const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `<tspan x="${left}px">${name}</tspan>`;
ratio = Math.max(Math.min(rn((pathLength / name.length) * 60), 130), 40);
el.innerHTML = `<tspan x="${left}px">${text}</tspan>`;
el.setAttribute('font-size', ratio + '%');
});
@ -673,13 +660,34 @@ window.BurgsAndStates = (function () {
})();
TIME && console.timeEnd('drawStateLabels');
function getLines(mode, name, fullName, pathLength) {
// short name
if (mode === "short" || (mode === "auto" && pathLength < name.length)) {
const lines = splitInTwo(name);
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 60), 50, 150)];
}
// full name: one line
if (pathLength > fullName.length * 2.5) {
const lines = [fullName];
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 70), 70, 170)];
}
// full name: two lines
const lines = splitInTwo(fullName);
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 60), 70, 150)];
}
};
// calculate states data like area, population etc.
const collectStatistics = function () {
TIME && console.time('collectStatistics');
const cells = pack.cells,
states = pack.states;
const {cells, states} = pack;
states.forEach((s) => {
if (s.removed) return;
s.cells = s.area = s.burgs = s.rural = s.urban = 0;
@ -737,21 +745,28 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd('assignColors');
};
// generate historical conflicts of each state
const generateCampaigns = function () {
const wars = {War: 6, Conflict: 2, Campaign: 4, Invasion: 2, Rebellion: 2, Conquest: 2, Intervention: 1, Expedition: 1, Crusade: 1};
const wars = {War: 6, Conflict: 2, Campaign: 4, Invasion: 2, Rebellion: 2, Conquest: 2, Intervention: 1, Expedition: 1, Crusade: 1};
const generateCampaign = state => {
const neighbors = state.neighbors.length ? state.neighbors : [0];
pack.states.forEach((s) => {
if (!s.i || s.removed) return;
const n = s.neighbors.length ? s.neighbors : [0];
s.campaigns = n
return neighbors
.map((i) => {
const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(s.culture);
const start = gauss(options.year - 100, 150, 1, options.year - 6);
const end = start + gauss(4, 5, 1, options.year - start - 1);
return {name: getAdjective(name) + ' ' + rw(wars), start, end};
})
.sort((a, b) => a.start - b.start);
const name = i && P(0.8) ? pack.states[i].name : Names.getCultureShort(state.culture);
const start = gauss(options.year - 100, 150, 1, options.year - 6);
const end = start + gauss(4, 5, 1, options.year - start - 1);
return {name: getAdjective(name) + " " + rw(wars), start, end};
})
.sort((a, b) => a.start - b.start);
};
// generate historical conflicts of each state
const generateCampaigns = function () {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.campaigns = generateCampaign(s);
});
};
@ -950,6 +965,13 @@ window.BurgsAndStates = (function () {
const union = {Union: 3, League: 4, Confederation: 1, 'United Kingdom': 1, 'United Republic': 1, 'United Provinces': 2, Commonwealth: 1, Heptarchy: 1}; // weighted random
const theocracy = {Theocracy: 20, Brotherhood: 1, Thearchy: 2, See: 1, 'Holy State': 1};
const anarchy = {'Free Territory': 2, Council: 3, Commune: 1, Community: 1};
"Most Serene Republic": 2,
Tetrarchy: 1,
Triumvirate: 1,
Diarchy: 1,
"Trade Company": 4,
Junta: 1
}; // weighted random
for (const s of states) {
if (list && !list.includes(s.i)) continue;
@ -974,6 +996,7 @@ window.BurgsAndStates = (function () {
// Default name depends on exponent tier, some culture bases have special names for tiers
if (s.diplomacy) {
if (form === 'Duchy' && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes('Vassal')) return 'Marches'; // some vassal dutchies on borderland
if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals
if (P(0.3) && s.diplomacy.includes('Vassal')) return 'Protectorate'; // some vassals
}
@ -1078,16 +1101,16 @@ window.BurgsAndStates = (function () {
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
Math.random = aleaPRNG(localSeed);
const cells = pack.cells,
states = pack.states,
burgs = pack.burgs;
const {cells, states, burgs} = pack;
const provinces = (pack.provinces = [0]);
cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value;
if (states.length < 2 || !percentage) {
states.forEach((s) => (s.provinces = []));
return;
} // no provinces
const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** 0.5; // max growth
const forms = {
@ -1100,7 +1123,6 @@ window.BurgsAndStates = (function () {
};
// generate provinces for a selected burgs
Math.random = aleaPRNG(localSeed);
states.forEach((s) => {
s.provinces = [];
if (!s.i || s.removed) return;

File diff suppressed because it is too large Load diff

139
modules/cloud.js Normal file
View file

@ -0,0 +1,139 @@
'use strict';
/*
Cloud provider implementations (Dropbox only as now)
provider Interface:
name: name of the provider
async auth(): authenticate and get access tokens from provider
async save(filename): save map file to provider as filename
async load(filename): load filename from provider
async list(): list available filenames at provider
async getLink(filePath): get shareable link for file
restore(): restore access tokens from storage if possible
*/
window.Cloud = (function () {
// helpers to use in providers for token handling
const lSKey = (x) => `auth-${x}`;
const setToken = (prov, key) => localStorage.setItem(lSKey(prov), key);
const getToken = (prov) => localStorage.getItem(lSKey(prov));
/**********************************************************/
/* Dropbox provider */
/**********************************************************/
const DBP = {
name: 'dropbox',
clientId: 'pdr9ae64ip0qno4',
authWindow: undefined,
token: null, // Access token
api: null,
restore() {
this.token = getToken(this.name);
if (this.token) this.connect(this.token);
},
async call(name, param) {
try {
return await this.api[name](param);
} catch (e) {
if (e.name !== 'DropboxResponseError') throw e;
// retry with auth
await this.auth();
return await this.api[name](param);
}
},
connect(token) {
const clientId = this.clientId;
const auth = new Dropbox.DropboxAuth({clientId});
auth.setAccessToken(token);
this.api = new Dropbox.Dropbox({auth});
},
async save(fileName, contents) {
if (!this.api) await this.auth();
const resp = this.call('filesUpload', {path: '/' + fileName, contents});
DEBUG && console.log('Dropbox response:', resp);
return true;
},
async load(path) {
if (!this.api) await this.auth();
const resp = await this.call('filesDownload', {path});
const blob = resp.result.fileBlob;
if (!blob) throw new Error('Invalid response from dropbox.');
return blob;
},
async list() {
if (!this.api) return null;
const resp = await this.call('filesListFolder', {path: ''});
return resp.result.entries.map((e) => ({name: e.name, path: e.path_lower}));
},
auth() {
const url = window.location.origin + window.location.pathname + 'dropbox.html';
this.authWindow = window.open(url, 'auth', 'width=640,height=480');
// child window expected to call
// window.opener.Cloud.providers.dropbox.setDropBoxToken (see below)
return new Promise((resolve, reject) => {
const watchDog = () => {
this.authWindow.close();
reject(new Error('Timeout. No auth for dropbox.'));
};
setTimeout(watchDog, 120 * 1000);
window.addEventListener('dropboxauth', (e) => {
clearTimeout(watchDog);
resolve();
});
});
},
// Callback function for auth window.
setDropBoxToken(token) {
DEBUG && console.log('Access token:', token);
setToken(this.name, token);
this.connect(token);
this.authWindow.close();
window.dispatchEvent(new Event('dropboxauth'));
},
async getLink(path) {
if (!this.api) await this.auth();
let resp;
// already exists?
resp = await this.call('sharingListSharedLinks', {path});
if (resp.result.links.length) return resp.result.links[0].url;
// create new
resp = await this.call('sharingCreateSharedLinkWithSettings', {
path,
settings: {
require_password: false,
audience: 'public',
access: 'viewer',
requested_visibility: 'public',
allow_download: true
}
});
DEBUG && console.log('Dropbox link object:', resp.result);
return resp.result.url;
}
};
// register providers here:
const providers = {
dropbox: DBP
};
// restore all providers at startup
for (const p of Object.values(providers)) p.restore();
return {providers};
})();

View file

@ -1,4 +1,4 @@
"use strict";
'use strict';
window.COA = (function () {
const tinctures = {
@ -45,8 +45,38 @@ window.COA = (function () {
const charges = {
// categories selection
types: {conventional: 30, crosses: 10, animals: 2, animalHeads: 1, birds: 2, fantastic: 3, plants: 1, agriculture: 1, arms: 3, bodyparts: 1, people: 1, architecture: 1, miscellaneous: 3, inescutcheon: 3},
single: {conventional: 12, crosses: 8, plants: 2, animals: 10, animalHeads: 2, birds: 4, fantastic: 7, agriculture: 1, arms: 6, bodyparts: 1, people: 2, architecture: 1, miscellaneous: 10, inescutcheon: 5},
types: {
conventional: 30,
crosses: 10,
animals: 2,
animalHeads: 1,
birds: 2,
fantastic: 3,
plants: 1,
agriculture: 1,
arms: 3,
bodyparts: 1,
people: 1,
architecture: 1,
miscellaneous: 3,
inescutcheon: 3
},
single: {
conventional: 12,
crosses: 8,
plants: 2,
animals: 10,
animalHeads: 2,
birds: 4,
fantastic: 7,
agriculture: 1,
arms: 6,
bodyparts: 1,
people: 2,
architecture: 1,
miscellaneous: 10,
inescutcheon: 5
},
semy: {conventional: 12, crosses: 3, plants: 1},
// generic categories
conventional: {
@ -193,86 +223,86 @@ window.COA = (function () {
Capital: {crown: 4, orb: 1, lute: 1, castle: 3, tower: 1},
Сathedra: {chalice: 1, orb: 1, crosier: 2, lamb: 1, monk: 2, angel: 3, crossLatin: 2, crossPatriarchal: 1, crossOrthodox: 1, crossCalvary: 1},
// specific cases
natural: {fountain: "azure", garb: "or", raven: "sable"}, // charges to mainly use predefined colours
natural: {fountain: 'azure', garb: 'or', raven: 'sable'}, // charges to mainly use predefined colours
sinister: [
// charges that can be sinister
"crossGamma",
"lionRampant",
"lionPassant",
"wolfRampant",
"wolfPassant",
"wolfStatant",
"wolfHeadErased",
"greyhoundСourant",
"boarRampant",
"horseRampant",
"horseSalient",
"bullPassant",
"bearRampant",
"bearPassant",
"goat",
"lamb",
"elephant",
"eagle",
"raven",
"cock",
"parrot",
"swan",
"swanErased",
"heron",
"pike",
"dragonPassant",
"dragonRampant",
"wyvern",
"wyvernWithWingsDisplayed",
"griffinPassant",
"griffinRampant",
"unicornRampant",
"pegasus",
"serpent",
"hatchet",
"lochaberAxe",
"hand",
"wing",
"wingSword",
"lute",
"harp",
"bow",
"head",
"headWreathed",
"knight",
"lymphad",
"log",
"crosier",
"dolphin",
"sabre",
"monk",
"owl",
"axe",
"camel",
"fasces",
"lionPassantGuardant",
"helmet"
'crossGamma',
'lionRampant',
'lionPassant',
'wolfRampant',
'wolfPassant',
'wolfStatant',
'wolfHeadErased',
'greyhoundСourant',
'boarRampant',
'horseRampant',
'horseSalient',
'bullPassant',
'bearRampant',
'bearPassant',
'goat',
'lamb',
'elephant',
'eagle',
'raven',
'cock',
'parrot',
'swan',
'swanErased',
'heron',
'pike',
'dragonPassant',
'dragonRampant',
'wyvern',
'wyvernWithWingsDisplayed',
'griffinPassant',
'griffinRampant',
'unicornRampant',
'pegasus',
'serpent',
'hatchet',
'lochaberAxe',
'hand',
'wing',
'wingSword',
'lute',
'harp',
'bow',
'head',
'headWreathed',
'knight',
'lymphad',
'log',
'crosier',
'dolphin',
'sabre',
'monk',
'owl',
'axe',
'camel',
'fasces',
'lionPassantGuardant',
'helmet'
],
reversed: [
// charges that can be reversed
"goutte",
"mullet",
"mullet7",
"crescent",
"crossTau",
"cancer",
"sword",
"sabresCrossed",
"hand",
"horseshoe",
"bowWithArrow",
"arrow",
"arrowsSheaf",
"rake",
"crossTriquetra",
"crossLatin",
"crossTau"
'goutte',
'mullet',
'mullet7',
'crescent',
'crossTau',
'cancer',
'sword',
'sabresCrossed',
'hand',
'horseshoe',
'bowWithArrow',
'arrow',
'arrowsSheaf',
'rake',
'crossTriquetra',
'crossLatin',
'crossTau'
]
};
@ -412,7 +442,28 @@ window.COA = (function () {
perBendSinister: lines,
perChevron: lines,
perChevronReversed: lines,
perCross: {straight: 20, wavy: 5, engrailed: 4, invecked: 3, rayonne: 1, embattled: 1, raguly: 1, urdy: 1, indented: 2, dentilly: 1, bevilled: 1, angled: 1, embattledGhibellin: 1, embattledGrady: 1, dovetailedIndented: 1, dovetailed: 1, potenty: 1, potentyDexter: 1, potentySinister: 1, nebuly: 1},
perCross: {
straight: 20,
wavy: 5,
engrailed: 4,
invecked: 3,
rayonne: 1,
embattled: 1,
raguly: 1,
urdy: 1,
indented: 2,
dentilly: 1,
bevilled: 1,
angled: 1,
embattledGhibellin: 1,
embattledGrady: 1,
dovetailedIndented: 1,
dovetailed: 1,
potenty: 1,
potentyDexter: 1,
potentySinister: 1,
nebuly: 1
},
perPile: lines
};
@ -471,7 +522,7 @@ window.COA = (function () {
};
const generate = function (parent, kinship, dominion, type) {
if (!parent || parent === "custom") {
if (!parent || parent === 'custom') {
parent = null;
kinship = 0;
dominion = 0;
@ -479,127 +530,127 @@ window.COA = (function () {
let usedPattern = null,
usedTinctures = [];
const t1 = P(kinship) ? parent.t1 : getTincture("field");
if (t1.includes("-")) usedPattern = t1;
const t1 = P(kinship) ? parent.t1 : getTincture('field');
if (t1.includes('-')) usedPattern = t1;
const coa = {t1};
let charge = P(usedPattern ? 0.5 : 0.93) ? true : false; // 80% for charge
const linedOrdinary = (charge && P(0.3)) || P(0.5) ? (parent?.ordinaries && P(kinship) ? parent.ordinaries[0].ordinary : rw(ordinaries.lined)) : null;
const ordinary = (!charge && P(0.65)) || P(0.3) ? (linedOrdinary ? linedOrdinary : rw(ordinaries.straight)) : null; // 36% for ordinary
const rareDivided = ["chief", "terrace", "chevron", "quarter", "flaunches"].includes(ordinary);
const rareDivided = ['chief', 'terrace', 'chevron', 'quarter', 'flaunches'].includes(ordinary);
const divisioned = rareDivided ? P(0.03) : charge && ordinary ? P(0.03) : charge ? P(0.3) : ordinary ? P(0.7) : P(0.995); // 33% for division
const division = divisioned ? (parent?.division && P(kinship - 0.1) ? parent.division.division : rw(divisions.variants)) : null;
if (charge) charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== "Generic" && P(0.2) ? rw(charges[type]) : selectCharge();
if (charge) charge = parent?.charges && P(kinship - 0.1) ? parent.charges[0].charge : type && type !== 'Generic' && P(0.2) ? rw(charges[type]) : selectCharge();
if (division) {
const t = getTincture("division", usedTinctures, P(0.98) ? coa.t1 : null);
const t = getTincture('division', usedTinctures, P(0.98) ? coa.t1 : null);
coa.division = {division, t};
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(0.7)) ? "straight" : rw(divisions[division]);
if (divisions[division]) coa.division.line = usedPattern || (ordinary && P(0.7)) ? 'straight' : rw(divisions[division]);
}
if (ordinary) {
coa.ordinaries = [{ordinary, t: getTincture("charge", usedTinctures, coa.t1)}];
if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? "straight" : rw(lines);
if (division && !charge && !usedPattern && P(0.5) && ordinary !== "bordure" && ordinary !== "orle") {
if (P(0.8)) coa.ordinaries[0].divided = "counter";
coa.ordinaries = [{ordinary, t: getTincture('charge', usedTinctures, coa.t1)}];
if (linedOrdinary) coa.ordinaries[0].line = usedPattern || (division && P(0.7)) ? 'straight' : rw(lines);
if (division && !charge && !usedPattern && P(0.5) && ordinary !== 'bordure' && ordinary !== 'orle') {
if (P(0.8)) coa.ordinaries[0].divided = 'counter';
// 40%
else if (P(0.6)) coa.ordinaries[0].divided = "field";
else if (P(0.6)) coa.ordinaries[0].divided = 'field';
// 6%
else coa.ordinaries[0].divided = "division"; // 4%
else coa.ordinaries[0].divided = 'division'; // 4%
}
}
if (charge) {
let p = "e",
t = "gules";
let p = 'e',
t = 'gules';
const ordinaryT = coa.ordinaries ? coa.ordinaries[0].t : null;
if (positions.ordinariesOn[ordinary] && P(0.8)) {
// place charge over ordinary (use tincture of field type)
p = rw(positions.ordinariesOn[ordinary]);
while (charges.natural[charge] === ordinaryT) charge = selectCharge();
t = !usedPattern && P(0.3) ? coa.t1 : getTincture("charge", [], ordinaryT);
t = !usedPattern && P(0.3) ? coa.t1 : getTincture('charge', [], ordinaryT);
} else if (positions.ordinariesOff[ordinary] && P(0.95)) {
// place charge out of ordinary (use tincture of ordinary type)
p = rw(positions.ordinariesOff[ordinary]);
while (charges.natural[charge] === coa.t1) charge = selectCharge();
t = !usedPattern && P(0.3) ? ordinaryT : getTincture("charge", usedTinctures, coa.t1);
t = !usedPattern && P(0.3) ? ordinaryT : getTincture('charge', usedTinctures, coa.t1);
} else if (positions.divisions[division]) {
// place charge in fields made by division
p = rw(positions.divisions[division]);
while (charges.natural[charge] === coa.t1) charge = selectCharge();
t = getTincture("charge", ordinaryT ? usedTinctures.concat(ordinaryT) : usedTinctures, coa.t1);
t = getTincture('charge', ordinaryT ? usedTinctures.concat(ordinaryT) : usedTinctures, coa.t1);
} else if (positions[charge]) {
// place charge-suitable position
p = rw(positions[charge]);
while (charges.natural[charge] === coa.t1) charge = selectCharge();
t = getTincture("charge", usedTinctures, coa.t1);
t = getTincture('charge', usedTinctures, coa.t1);
} else {
// place in standard position (use new tincture)
p = usedPattern ? "e" : charges.conventional[charge] ? rw(positions.conventional) : rw(positions.complex);
p = usedPattern ? 'e' : charges.conventional[charge] ? rw(positions.conventional) : rw(positions.complex);
while (charges.natural[charge] === coa.t1) charge = selectCharge();
t = getTincture("charge", usedTinctures.concat(ordinaryT), coa.t1);
t = getTincture('charge', usedTinctures.concat(ordinaryT), coa.t1);
}
if (charges.natural[charge]) t = charges.natural[charge]; // natural tincture
coa.charges = [{charge, t, p}];
if (p === "ABCDEFGHIKL" && P(0.95)) {
if (p === 'ABCDEFGHIKL' && P(0.95)) {
// add central charge if charge is in bordure
coa.charges[0].charge = rw(charges.conventional);
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.t1);
coa.charges.push({charge, t, p: "e"});
} else if (P(0.8) && charge === "inescutcheon") {
const t = getTincture('charge', usedTinctures, coa.t1);
coa.charges.push({charge, t, p: 'e'});
} else if (P(0.8) && charge === 'inescutcheon') {
// add charge to inescutcheon
const charge = selectCharge(charges.types);
const t2 = getTincture("charge", [], t);
const t2 = getTincture('charge', [], t);
coa.charges.push({charge, t: t2, p, size: 0.5});
} else if (division && !ordinary) {
const allowCounter = !usedPattern && (!coa.line || coa.line === "straight");
const allowCounter = !usedPattern && (!coa.line || coa.line === 'straight');
// dimidiation: second charge at division basic positons
if (P(0.3) && ["perPale", "perFess"].includes(division) && coa.line === "straight") {
coa.charges[0].divided = "field";
if (P(0.3) && ['perPale', 'perFess'].includes(division) && coa.line === 'straight') {
coa.charges[0].divided = 'field';
if (P(0.95)) {
const p2 = p === "e" || P(0.5) ? "e" : rw(positions.divisions[division]);
const p2 = p === 'e' || P(0.5) ? 'e' : rw(positions.divisions[division]);
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2, divided: "division"});
const t = getTincture('charge', usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2, divided: 'division'});
}
} else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
} else if (allowCounter && P(0.4)) coa.charges[0].divided = 'counter';
// counterchanged, 40%
else if (["perPale", "perFess", "perBend", "perBendSinister"].includes(division) && P(0.8)) {
else if (['perPale', 'perFess', 'perBend', 'perBendSinister'].includes(division) && P(0.8)) {
// place 2 charges in division standard positions
const [p1, p2] = division === "perPale" ? ["p", "q"] : division === "perFess" ? ["k", "n"] : division === "perBend" ? ["l", "m"] : ["j", "o"]; // perBendSinister
const [p1, p2] = division === 'perPale' ? ['p', 'q'] : division === 'perFess' ? ['k', 'n'] : division === 'perBend' ? ['l', 'm'] : ['j', 'o']; // perBendSinister
coa.charges[0].p = p1;
const charge = selectCharge(charges.single);
const t = getTincture("charge", usedTinctures, coa.division.t);
const t = getTincture('charge', usedTinctures, coa.division.t);
coa.charges.push({charge, t, p: p2});
} else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
} else if (['perCross', 'perSaltire'].includes(division) && P(0.5)) {
// place 4 charges in division standard positions
const [p1, p2, p3, p4] = division === "perCross" ? ["j", "l", "m", "o"] : ["b", "d", "f", "h"];
const [p1, p2, p3, p4] = division === 'perCross' ? ['j', 'l', 'm', 'o'] : ['b', 'd', 'f', 'h'];
coa.charges[0].p = p1;
const c2 = selectCharge(charges.single);
const t2 = getTincture("charge", [], coa.division.t);
const t2 = getTincture('charge', [], coa.division.t);
const c3 = selectCharge(charges.single);
const t3 = getTincture("charge", [], coa.division.t);
const t3 = getTincture('charge', [], coa.division.t);
const c4 = selectCharge(charges.single);
const t4 = getTincture("charge", [], coa.t1);
const t4 = getTincture('charge', [], coa.t1);
coa.charges.push({charge: c2, t: t2, p: p2}, {charge: c3, t: t3, p: p3}, {charge: c4, t: t4, p: p4});
} else if (allowCounter && p.length > 1) coa.charges[0].divided = "counter"; // counterchanged, 40%
} else if (allowCounter && p.length > 1) coa.charges[0].divided = 'counter'; // counterchanged, 40%
}
coa.charges.forEach(c => defineChargeAttributes(c));
coa.charges.forEach((c) => defineChargeAttributes(c));
function defineChargeAttributes(c) {
// define size
c.size = (c.size || 1) * getSize(c.p, ordinary, division);
// clean-up position
c.p = [...new Set(c.p)].join("");
c.p = [...new Set(c.p)].join('');
// define orientation
if (P(0.02) && charges.sinister.includes(c.charge)) c.sinister = 1;
@ -610,51 +661,51 @@ window.COA = (function () {
// dominions have canton with parent coa
if (P(dominion) && parent.charges) {
const invert = isSameType(parent.t1, coa.t1);
const t = invert ? getTincture("division", usedTinctures, coa.t1) : parent.t1;
const canton = {ordinary: "canton", t};
const t = invert ? getTincture('division', usedTinctures, coa.t1) : parent.t1;
const canton = {ordinary: 'canton', t};
if (coa.charges) {
coa.charges.forEach((charge, i) => {
if (charge.size === 1.5) charge.size = 1.4;
if (charge.p.includes("a")) charge.p = charge.p.replaceAll("a", "");
if (charge.p.includes("j")) charge.p = charge.p.replaceAll("j", "");
if (charge.p.includes("y")) charge.p = charge.p.replaceAll("y", "");
if (charge.p.includes('a')) charge.p = charge.p.replaceAll('a', '');
if (charge.p.includes('j')) charge.p = charge.p.replaceAll('j', '');
if (charge.p.includes('y')) charge.p = charge.p.replaceAll('y', '');
if (!charge.p) coa.charges.splice(i, 1);
});
}
let charge = parent.charges[0].charge;
if (charge === "inescutcheon" && parent.charges[1]) charge = parent.charges[1].charge;
if (charge === 'inescutcheon' && parent.charges[1]) charge = parent.charges[1].charge;
let t2 = invert ? parent.t1 : parent.charges[0].t;
if (isSameType(t, t2)) t2 = getTincture("charge", usedTinctures, t);
if (isSameType(t, t2)) t2 = getTincture('charge', usedTinctures, t);
if (!coa.charges) coa.charges = [];
coa.charges.push({charge, t: t2, p: "y", size: 0.5});
coa.charges.push({charge, t: t2, p: 'y', size: 0.5});
coa.ordinaries ? coa.ordinaries.push(canton) : (coa.ordinaries = [canton]);
}
function selectCharge(set) {
const type = set ? rw(set) : ordinary || divisioned ? rw(charges.types) : rw(charges.single);
return type === "inescutcheon" ? "inescutcheon" : rw(charges[type]);
return type === 'inescutcheon' ? 'inescutcheon' : rw(charges[type]);
}
// select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
function getTincture(element, fields = [], RoT) {
const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
const base = RoT ? (RoT.includes('-') ? RoT.split('-')[1] : RoT) : null;
let type = rw(tinctures[element]); // metals, colours, stains, patterns
if (RoT && type !== "patterns") type = getType(base) === "metals" ? "colours" : "metals"; // follow RoT
if (type === "metals" && fields.includes("or") && fields.includes("argent")) type = "colours"; // exclude metals overuse
if (RoT && type !== 'patterns') type = getType(base) === 'metals' ? 'colours' : 'metals'; // follow RoT
if (type === 'metals' && fields.includes('or') && fields.includes('argent')) type = 'colours'; // exclude metals overuse
let tincture = rw(tinctures[type]);
while (tincture === base || fields.includes(tincture)) {
tincture = rw(tinctures[type]);
} // follow RoT
if (type !== "patterns" && element !== "charge") usedTinctures.push(tincture); // add field tincture
if (type !== 'patterns' && element !== 'charge') usedTinctures.push(tincture); // add field tincture
if (type === "patterns") {
if (type === 'patterns') {
usedPattern = tincture;
tincture = definePattern(tincture, element);
}
@ -663,72 +714,72 @@ window.COA = (function () {
}
function getType(t) {
const tincture = t.includes("-") ? t.split("-")[1] : t;
if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
const tincture = t.includes('-') ? t.split('-')[1] : t;
if (Object.keys(tinctures.metals).includes(tincture)) return 'metals';
if (Object.keys(tinctures.colours).includes(tincture)) return 'colours';
if (Object.keys(tinctures.stains).includes(tincture)) return 'stains';
}
function isSameType(t1, t2) {
return type(t1) === type(t2);
function type(tincture) {
if (Object.keys(tinctures.metals).includes(tincture)) return "metals";
if (Object.keys(tinctures.colours).includes(tincture)) return "colours";
if (Object.keys(tinctures.stains).includes(tincture)) return "stains";
else return "pattern";
if (Object.keys(tinctures.metals).includes(tincture)) return 'metals';
if (Object.keys(tinctures.colours).includes(tincture)) return 'colours';
if (Object.keys(tinctures.stains).includes(tincture)) return 'stains';
else return 'pattern';
}
}
function definePattern(pattern, element, size = "") {
function definePattern(pattern, element, size = '') {
let t1 = null,
t2 = null;
if (P(0.1)) size = "-small";
else if (P(0.1)) size = "-smaller";
else if (P(0.01)) size = "-big";
else if (P(0.005)) size = "-smallest";
if (P(0.1)) size = '-small';
else if (P(0.1)) size = '-smaller';
else if (P(0.01)) size = '-big';
else if (P(0.005)) size = '-smallest';
// apply standard tinctures
if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
t1 = "azure";
t2 = "argent";
} else if (P(0.8) && pattern === "ermine") {
t1 = "argent";
t2 = "sable";
} else if (pattern === "pappellony") {
if (P(0.5) && ['vair', 'vairInPale', 'vairEnPointe'].includes(pattern)) {
t1 = 'azure';
t2 = 'argent';
} else if (P(0.8) && pattern === 'ermine') {
t1 = 'argent';
t2 = 'sable';
} else if (pattern === 'pappellony') {
if (P(0.2)) {
t1 = "gules";
t2 = "or";
t1 = 'gules';
t2 = 'or';
} else if (P(0.2)) {
t1 = "argent";
t2 = "sable";
t1 = 'argent';
t2 = 'sable';
} else if (P(0.2)) {
t1 = "azure";
t2 = "argent";
t1 = 'azure';
t2 = 'argent';
}
} else if (pattern === "masoned") {
} else if (pattern === 'masoned') {
if (P(0.3)) {
t1 = "gules";
t2 = "argent";
t1 = 'gules';
t2 = 'argent';
} else if (P(0.3)) {
t1 = "argent";
t2 = "sable";
t1 = 'argent';
t2 = 'sable';
} else if (P(0.1)) {
t1 = "or";
t2 = "sable";
t1 = 'or';
t2 = 'sable';
}
} else if (pattern === "fretty") {
if (t2 === "sable" || P(0.35)) {
t1 = "argent";
t2 = "gules";
} else if (pattern === 'fretty') {
if (t2 === 'sable' || P(0.35)) {
t1 = 'argent';
t2 = 'gules';
} else if (P(0.25)) {
t1 = "sable";
t2 = "or";
t1 = 'sable';
t2 = 'or';
} else if (P(0.15)) {
t1 = "gules";
t2 = "argent";
t1 = 'gules';
t2 = 'argent';
}
} else if (pattern === "semy") pattern += "_of_" + selectCharge(charges.semy);
} else if (pattern === 'semy') pattern += '_of_' + selectCharge(charges.semy);
if (!t1 || !t2) {
const startWithMetal = P(0.7);
@ -737,7 +788,7 @@ window.COA = (function () {
}
// division should not be the same tincture as base field
if (element === "division") {
if (element === 'division') {
if (usedTinctures.includes(t1)) t1 = replaceTincture(t1);
if (usedTinctures.includes(t2)) t2 = replaceTincture(t2);
}
@ -755,12 +806,12 @@ window.COA = (function () {
}
function getSize(p, o = null, d = null) {
if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
if (p === "e") return 1.5;
if (p === "jln" || p === "jlh") return 0.7;
if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p)) return 0.5;
if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross") return 0.6;
if (p === 'e' && (o === 'bordure' || o === 'orle')) return 1.1;
if (p === 'e') return 1.5;
if (p === 'jln' || p === 'jlh') return 0.7;
if (p === 'abcpqh' || p === 'ez' || p === 'be') return 0.5;
if (['a', 'b', 'c', 'd', 'f', 'g', 'h', 'i', 'bh', 'df'].includes(p)) return 0.5;
if (['j', 'l', 'm', 'o', 'jlmo'].includes(p) && d === 'perCross') return 0.6;
if (p.length > 10) return 0.18; // >10 (bordure)
if (p.length > 7) return 0.3; // 8, 9, 10
if (p.length > 4) return 0.4; // 5, 6, 7
@ -772,18 +823,18 @@ window.COA = (function () {
};
const getShield = function (culture, state) {
const emblemShape = document.getElementById("emblemShape");
const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || "Diversiform";
if (shapeGroup !== "Diversiform") return emblemShape.value;
const emblemShape = document.getElementById('emblemShape');
const shapeGroup = emblemShape.selectedOptions[0]?.parentNode.label || 'Diversiform';
if (shapeGroup !== 'Diversiform') return emblemShape.value;
if (emblemShape.value === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
if (emblemShape.value === 'state' && state && pack.states[state].coa) return pack.states[state].coa.shield;
if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater";
console.error('Shield shape is not defined on culture level', pack.cultures[culture]);
return 'heater';
};
const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
const copy = coa => JSON.parse(JSON.stringify(coa));
const toString = (coa) => JSON.stringify(coa).replaceAll('#', '%23');
const copy = (coa) => JSON.parse(JSON.stringify(coa));
return {generate, toString, copy, getShield, shields};
})();

View file

@ -1,17 +1,17 @@
"use strict";
'use strict';
window.COArenderer = (function () {
const colors = {
argent: "#fafafa",
or: "#ffe066",
gules: "#d7374a",
sable: "#333333",
azure: "#377cd7",
vert: "#26c061",
purpure: "#522d5b",
murrey: "#85185b",
sanguine: "#b63a3a",
tenné: "#cc7f19"
argent: '#fafafa',
or: '#ffe066',
gules: '#d7374a',
sable: '#333333',
azure: '#377cd7',
vert: '#26c061',
purpure: '#522d5b',
murrey: '#85185b',
sanguine: '#b63a3a',
tenné: '#cc7f19'
};
const shieldPositions = {
@ -1396,142 +1396,165 @@ window.COArenderer = (function () {
};
const shieldBox = {
heater: "0 10 200 200",
spanish: "0 10 200 200",
french: "0 10 200 200",
heater: '0 10 200 200',
spanish: '0 10 200 200',
french: '0 10 200 200',
horsehead: "0 10 200 200",
horsehead2: "0 10 200 200",
polish: "0 0 200 200",
hessen: "0 5 200 200",
swiss: "0 10 200 200",
horsehead: '0 10 200 200',
horsehead2: '0 10 200 200',
polish: '0 0 200 200',
hessen: '0 5 200 200',
swiss: '0 10 200 200',
boeotian: "0 0 200 200",
roman: "0 0 200 200",
kite: "0 0 200 200",
oldFrench: "0 10 200 200",
renaissance: "0 5 200 200",
baroque: "0 10 200 200",
boeotian: '0 0 200 200',
roman: '0 0 200 200',
kite: '0 0 200 200',
oldFrench: '0 10 200 200',
renaissance: '0 5 200 200',
baroque: '0 10 200 200',
targe: "0 0 200 200",
targe2: "0 0 200 200",
pavise: "0 0 200 200",
wedged: "0 10 200 200",
targe: '0 0 200 200',
targe2: '0 0 200 200',
pavise: '0 0 200 200',
wedged: '0 10 200 200',
flag: "0 0 200 200",
pennon: "2.5 0 200 200",
guidon: "2.5 0 200 200",
banner: "0 10 200 200",
dovetail: "0 10 200 200",
gonfalon: "0 10 200 200",
pennant: "0 0 200 200",
flag: '0 0 200 200',
pennon: '2.5 0 200 200',
guidon: '2.5 0 200 200',
banner: '0 10 200 200',
dovetail: '0 10 200 200',
gonfalon: '0 10 200 200',
pennant: '0 0 200 200',
round: "0 0 200 200",
oval: "0 0 200 200",
vesicaPiscis: "0 0 200 200",
square: "0 0 200 200",
diamond: "0 0 200 200",
no: "0 0 200 200",
round: '0 0 200 200',
oval: '0 0 200 200',
vesicaPiscis: '0 0 200 200',
square: '0 0 200 200',
diamond: '0 0 200 200',
no: '0 0 200 200',
fantasy1: "0 0 200 200",
fantasy2: "0 5 200 200",
fantasy3: "0 5 200 200",
fantasy4: "0 5 200 200",
fantasy5: "0 0 200 200",
fantasy1: '0 0 200 200',
fantasy2: '0 5 200 200',
fantasy3: '0 5 200 200',
fantasy4: '0 5 200 200',
fantasy5: '0 0 200 200',
noldor: "0 0 200 200",
gondor: "0 5 200 200",
easterling: "0 0 200 200",
erebor: "0 0 200 200",
ironHills: "0 5 200 200",
urukHai: "0 0 200 200",
moriaOrc: "0 0 200 200"
noldor: '0 0 200 200',
gondor: '0 5 200 200',
easterling: '0 0 200 200',
erebor: '0 0 200 200',
ironHills: '0 5 200 200',
urukHai: '0 0 200 200',
moriaOrc: '0 0 200 200'
};
const shieldPaths = {
heater: "m25,25 h150 v50 a150,150,0,0,1,-75,125 a150,150,0,0,1,-75,-125 z",
spanish: "m25,25 h150 v100 a75,75,0,0,1,-150,0 z",
french: "m 25,25 h 150 v 139.15 c 0,41.745 -66,18.15 -75,36.3 -9,-18.15 -75,5.445 -75,-36.3 v 0 z",
horsehead: "m 20,40 c 0,60 40,80 40,100 0,10 -4,15 -0.35,30 C 65,185.7 81,200 100,200 c 19.1,0 35.3,-14.6 40.5,-30.4 C 144.2,155 140,150 140,140 140,120 180,100 180,40 142.72,40 150,15 100,15 55,15 55,40 20,40 Z",
horsehead2: "M60 20c-5 20-10 35-35 55 25 35 35 65 30 100 20 0 35 10 45 26 10-16 30-26 45-26-5-35 5-65 30-100a87 87 0 01-35-55c-25 3-55 3-80 0z",
polish: "m 90.3,6.3 c -12.7,0 -20.7,10.9 -40.5,14 0,11.8 -4.9,23.5 -11.4,31.1 0,0 12.7,6 12.7,19.3 C 51.1,90.8 30,90.8 30,90.8 c 0,0 -3.6,7.4 -3.6,22.4 0,34.3 23.1,60.2 40.7,68.2 17.6,8 27.7,11.4 32.9,18.6 5.2,-7.3 15.3,-10.7 32.8,-18.6 17.6,-8 40.7,-33.9 40.7,-68.2 0,-15 -3.6,-22.4 -3.6,-22.4 0,0 -21.1,0 -21.1,-20.1 0,-13.3 12.7,-19.3 12.7,-19.3 C 155.1,43.7 150.2,32.1 150.2,20.3 130.4,17.2 122.5,6.3 109.7,6.3 102.5,6.3 100,10 100,10 c 0,0 -2.5,-3.7 -9.7,-3.7 z",
hessen: "M170 20c4 5 8 13 15 20 0 0-10 0-10 15 0 100-15 140-75 145-65-5-75-45-75-145 0-15-10-15-10-15l15-20c0 15 10-5 70-5s70 20 70 5z",
swiss: "m 25,20 c -0.1,0 25.2,8.5 37.6,8.5 C 75.1,28.5 99.1,20 100,20 c 0.6,0 24.9,8.5 37.3,8.5 C 149.8,28.5 174.4,20 175,20 l -0.3,22.6 C 173.2,160.3 100,200 100,200 100,200 26.5,160.9 25.2,42.6 Z",
boeotian: "M150 115c-5 0-10-5-10-15s5-15 10-15c10 0 7 10 15 10 10 0 0-30 0-30-10-25-30-55-65-55S45 40 35 65c0 0-10 30 0 30 8 0 5-10 15-10 5 0 10 5 10 15s-5 15-10 15c-10 0-7-10-15-10-10 0 0 30 0 30 10 25 30 55 65 55s55-30 65-55c0 0 10-30 0-30-8 0-5 10-15 10z",
roman: "m 160,170 c -40,20 -80,20 -120,0 V 30 C 80,10 120,10 160,30 Z",
kite: "m 53.3,46.4 c 0,4.1 1,12.3 1,12.3 7.1,55.7 45.7,141.3 45.7,141.3 0,0 38.6,-85.6 45.7,-141.2 0,0 1,-8.1 1,-12.3 C 146.7,20.9 125.8,0.1 100,0.1 74.2,0.1 53.3,20.9 53.3,46.4 Z",
oldFrench: "m25,25 h150 v75 a100,100,0,0,1,-75,100 a100,100,0,0,1,-75,-100 z",
renaissance: "M 25,33.9 C 33.4,50.3 36.2,72.9 36.2,81.7 36.2,109.9 25,122.6 25,141 c 0,29.4 24.9,44.1 40.2,47.7 15.3,3.7 29.3,0 34.8,11.3 5.5,-11.3 19.6,-7.6 34.8,-11.3 C 150.1,185 175,170.3 175,141 c 0,-18.4 -11.2,-31.1 -11.2,-59.3 0,-8.8 2.8,-31.3 11.2,-47.7 L 155.7,14.4 C 138.2,21.8 119.3,25.7 100,25.7 c -19.3,0 -38.2,-3.9 -55.7,-11.3 z",
baroque: "m 100,25 c 18,0 50,2 75,14 v 37 l -2.7,3.2 c -4.9,5.4 -6.6,9.6 -6.7,16.2 0,6.5 2,11.6 6.9,17.2 l 2.8,3.1 v 10.2 c 0,17.7 -2.2,27.7 -7.8,35.9 -5,7.3 -11.7,11.3 -32.3,19.4 -12.6,5 -20.2,8.8 -28.6,14.5 C 103.3,198 100,200 100,200 c 0,0 -2.8,-2.3 -6.4,-4.7 C 85.6,189.8 78,186 65,180.9 32.4,168.1 26.9,160.9 25.8,129.3 L 25,116 l 3.3,-3.3 c 4.8,-5.2 7,-10.7 7,-17.3 0,-6.8 -1.8,-11.1 -6.5,-16.1 L 25,76 V 39 C 50,27 82,25 100,25 Z",
targe: "m 20,35 c 15,0 115,-60 155,-10 -5,10 -15,15 -10,50 5,45 10,70 -10,90 C 125,195 75,195 50,175 25,150 30,130 35,85 50,95 65,85 65,70 65,50 50,45 40,50 30,55 27,65 30,70 23,73 20,70 14,70 11,60 20,45 20,35 Z",
targe2: "m 84,32.2 c 6.2,-1 19.5,-31.4 94.1,-20.2 -30.57,33.64 -21.66,67.37 -11.2,95 20.2,69.5 -41.17549,84.7 -66.88,84.7 C 74.32,191.7071 8.38,168.95 32,105.9 36.88,92.88 31,89 31,82.6 35.15,82.262199 56.79,86.17 56.5,69.8 56.20,52.74 42.2,47.9 25.9,55.2 25.9,51.4 39.8,6.7 84,32.2 Z",
pavise: "M95 7L39.9 37.3a10 10 0 00-5.1 9.5L46 180c.4 5.2 3.7 10 9 10h90c5.3 0 9.6-4.8 10-10l10.6-133.2a10 10 0 00-5-9.5L105 7c-4.2-2.3-6.2-2.3-10 0z",
wedged: "m 51.2,19 h 96.4 c 3.1,12.7 10.7,20.9 26.5,20.8 C 175.7,94.5 165.3,144.3 100,200 43.5,154.2 22.8,102.8 25.1,39.7 37,38.9 47.1,34.7 51.2,19 Z",
round: "m 185,100 a 85,85 0 0 1 -85,85 85,85 0 0 1 -85,-85 85,85 0 0 1 85,-85 85,85 0 0 1 85,85",
oval: "m 32.3,99.5 a 67.7,93.7 0 1 1 0,1.3 z",
vesicaPiscis: "M 100,0 C 63.9,20.4 41,58.5 41,100 c 0,41.5 22.9,79.6 59,100 36.1,-20.4 59,-58.5 59,-100 C 159,58.5 136.1,20.4 100,0 Z",
square: "M 25,25 H 175 V 175 H 25 Z",
diamond: "M 25,100 100,200 175,100 100,0 Z",
no: "m0,0 h200 v200 h-200 z",
flag: "M 10,40 h180 v120 h-180 Z",
pennon: "M 10,40 l190,60 -190,60 Z",
guidon: "M 10,40 h190 l-65,60 65,60 h-190 Z",
banner: "m 25,25 v 170 l 25,-40 25,40 25,-40 25,40 25,-40 25,40 V 25 Z",
dovetail: "m 25,25 v 175 l 75,-40 75,40 V 25 Z",
gonfalon: "m 25,25 v 125 l 75,50 75,-50 V 25 Z",
pennant: "M 25,15 100,200 175,15 Z",
fantasy1: "M 100,5 C 85,30 40,35 15,40 c 40,35 20,90 40,115 15,25 40,30 45,45 5,-15 30,-20 45,-45 20,-25 0,-80 40,-115 C 160,35 115,30 100,5 Z",
fantasy2: "m 152,21 c 0,0 -27,14 -52,-4 C 75,35 48,21 48,21 50,45 30,55 30,75 60,75 60,115 32,120 c 3,40 53,50 68,80 15,-30 65,-40 68,-80 -28,-5 -28,-45 2,-45 C 170,55 150,45 152,21 Z",
fantasy3: "M 167,67 C 165,0 35,0 33,67 c 32,-7 27,53 -3,43 -5,45 60,65 70,90 10,-25 75,-47.51058 70,-90 -30,10 -35,-50 -3,-43 z",
fantasy4: "M100 9C55 48 27 27 13 39c23 50 3 119 49 150 14 9 28 11 38 11s27-4 38-11c55-39 24-108 49-150-14-12-45 7-87-30z",
fantasy5: "M 100,0 C 75,25 30,25 30,25 c 0,69 20,145 70,175 50,-30 71,-106 70,-175 0,0 -45,0 -70,-25 z",
noldor: "m 55,75 h 2 c 3,-25 38,-10 3,20 15,50 30,75 40,105 10,-30 25,-55 40,-105 -35,-30 0,-45 3,-20 h 2 C 150,30 110,20 100,0 90,20 50,30 55,75 Z",
gondor: "m 100,200 c 15,-15 38,-35 45,-60 h 5 V 30 h -5 C 133,10 67,10 55,30 h -5 v 110 h 5 c 7,25 30,45 45,60 z",
easterling: "M 160,185 C 120,170 80,170 40,185 V 15 c 40,15 80,15 120,0 z",
erebor: "M25 135 V60 l22-13 16-37 h75 l15 37 22 13 v75l-22 18-16 37 H63l-16-37z",
ironHills: "m 30,25 60,-10 10,10 10,-10 60,10 -5,125 -65,50 -65,-50 z",
urukHai: "M 30,60 C 40,60 60,50 60,20 l -5,-3 45,-17 75,40 -5,5 -35,155 -5,-35 H 70 v 35 z",
moriaOrc: "M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z"
heater: 'm25,25 h150 v50 a150,150,0,0,1,-75,125 a150,150,0,0,1,-75,-125 z',
spanish: 'm25,25 h150 v100 a75,75,0,0,1,-150,0 z',
french: 'm 25,25 h 150 v 139.15 c 0,41.745 -66,18.15 -75,36.3 -9,-18.15 -75,5.445 -75,-36.3 v 0 z',
horsehead:
'm 20,40 c 0,60 40,80 40,100 0,10 -4,15 -0.35,30 C 65,185.7 81,200 100,200 c 19.1,0 35.3,-14.6 40.5,-30.4 C 144.2,155 140,150 140,140 140,120 180,100 180,40 142.72,40 150,15 100,15 55,15 55,40 20,40 Z',
horsehead2: 'M60 20c-5 20-10 35-35 55 25 35 35 65 30 100 20 0 35 10 45 26 10-16 30-26 45-26-5-35 5-65 30-100a87 87 0 01-35-55c-25 3-55 3-80 0z',
polish:
'm 90.3,6.3 c -12.7,0 -20.7,10.9 -40.5,14 0,11.8 -4.9,23.5 -11.4,31.1 0,0 12.7,6 12.7,19.3 C 51.1,90.8 30,90.8 30,90.8 c 0,0 -3.6,7.4 -3.6,22.4 0,34.3 23.1,60.2 40.7,68.2 17.6,8 27.7,11.4 32.9,18.6 5.2,-7.3 15.3,-10.7 32.8,-18.6 17.6,-8 40.7,-33.9 40.7,-68.2 0,-15 -3.6,-22.4 -3.6,-22.4 0,0 -21.1,0 -21.1,-20.1 0,-13.3 12.7,-19.3 12.7,-19.3 C 155.1,43.7 150.2,32.1 150.2,20.3 130.4,17.2 122.5,6.3 109.7,6.3 102.5,6.3 100,10 100,10 c 0,0 -2.5,-3.7 -9.7,-3.7 z',
hessen: 'M170 20c4 5 8 13 15 20 0 0-10 0-10 15 0 100-15 140-75 145-65-5-75-45-75-145 0-15-10-15-10-15l15-20c0 15 10-5 70-5s70 20 70 5z',
swiss:
'm 25,20 c -0.1,0 25.2,8.5 37.6,8.5 C 75.1,28.5 99.1,20 100,20 c 0.6,0 24.9,8.5 37.3,8.5 C 149.8,28.5 174.4,20 175,20 l -0.3,22.6 C 173.2,160.3 100,200 100,200 100,200 26.5,160.9 25.2,42.6 Z',
boeotian:
'M150 115c-5 0-10-5-10-15s5-15 10-15c10 0 7 10 15 10 10 0 0-30 0-30-10-25-30-55-65-55S45 40 35 65c0 0-10 30 0 30 8 0 5-10 15-10 5 0 10 5 10 15s-5 15-10 15c-10 0-7-10-15-10-10 0 0 30 0 30 10 25 30 55 65 55s55-30 65-55c0 0 10-30 0-30-8 0-5 10-15 10z',
roman: 'm 160,170 c -40,20 -80,20 -120,0 V 30 C 80,10 120,10 160,30 Z',
kite: 'm 53.3,46.4 c 0,4.1 1,12.3 1,12.3 7.1,55.7 45.7,141.3 45.7,141.3 0,0 38.6,-85.6 45.7,-141.2 0,0 1,-8.1 1,-12.3 C 146.7,20.9 125.8,0.1 100,0.1 74.2,0.1 53.3,20.9 53.3,46.4 Z',
oldFrench: 'm25,25 h150 v75 a100,100,0,0,1,-75,100 a100,100,0,0,1,-75,-100 z',
renaissance:
'M 25,33.9 C 33.4,50.3 36.2,72.9 36.2,81.7 36.2,109.9 25,122.6 25,141 c 0,29.4 24.9,44.1 40.2,47.7 15.3,3.7 29.3,0 34.8,11.3 5.5,-11.3 19.6,-7.6 34.8,-11.3 C 150.1,185 175,170.3 175,141 c 0,-18.4 -11.2,-31.1 -11.2,-59.3 0,-8.8 2.8,-31.3 11.2,-47.7 L 155.7,14.4 C 138.2,21.8 119.3,25.7 100,25.7 c -19.3,0 -38.2,-3.9 -55.7,-11.3 z',
baroque:
'm 100,25 c 18,0 50,2 75,14 v 37 l -2.7,3.2 c -4.9,5.4 -6.6,9.6 -6.7,16.2 0,6.5 2,11.6 6.9,17.2 l 2.8,3.1 v 10.2 c 0,17.7 -2.2,27.7 -7.8,35.9 -5,7.3 -11.7,11.3 -32.3,19.4 -12.6,5 -20.2,8.8 -28.6,14.5 C 103.3,198 100,200 100,200 c 0,0 -2.8,-2.3 -6.4,-4.7 C 85.6,189.8 78,186 65,180.9 32.4,168.1 26.9,160.9 25.8,129.3 L 25,116 l 3.3,-3.3 c 4.8,-5.2 7,-10.7 7,-17.3 0,-6.8 -1.8,-11.1 -6.5,-16.1 L 25,76 V 39 C 50,27 82,25 100,25 Z',
targe:
'm 20,35 c 15,0 115,-60 155,-10 -5,10 -15,15 -10,50 5,45 10,70 -10,90 C 125,195 75,195 50,175 25,150 30,130 35,85 50,95 65,85 65,70 65,50 50,45 40,50 30,55 27,65 30,70 23,73 20,70 14,70 11,60 20,45 20,35 Z',
targe2:
'm 84,32.2 c 6.2,-1 19.5,-31.4 94.1,-20.2 -30.57,33.64 -21.66,67.37 -11.2,95 20.2,69.5 -41.17549,84.7 -66.88,84.7 C 74.32,191.7071 8.38,168.95 32,105.9 36.88,92.88 31,89 31,82.6 35.15,82.262199 56.79,86.17 56.5,69.8 56.20,52.74 42.2,47.9 25.9,55.2 25.9,51.4 39.8,6.7 84,32.2 Z',
pavise: 'M95 7L39.9 37.3a10 10 0 00-5.1 9.5L46 180c.4 5.2 3.7 10 9 10h90c5.3 0 9.6-4.8 10-10l10.6-133.2a10 10 0 00-5-9.5L105 7c-4.2-2.3-6.2-2.3-10 0z',
wedged: 'm 51.2,19 h 96.4 c 3.1,12.7 10.7,20.9 26.5,20.8 C 175.7,94.5 165.3,144.3 100,200 43.5,154.2 22.8,102.8 25.1,39.7 37,38.9 47.1,34.7 51.2,19 Z',
round: 'm 185,100 a 85,85 0 0 1 -85,85 85,85 0 0 1 -85,-85 85,85 0 0 1 85,-85 85,85 0 0 1 85,85',
oval: 'm 32.3,99.5 a 67.7,93.7 0 1 1 0,1.3 z',
vesicaPiscis: 'M 100,0 C 63.9,20.4 41,58.5 41,100 c 0,41.5 22.9,79.6 59,100 36.1,-20.4 59,-58.5 59,-100 C 159,58.5 136.1,20.4 100,0 Z',
square: 'M 25,25 H 175 V 175 H 25 Z',
diamond: 'M 25,100 100,200 175,100 100,0 Z',
no: 'm0,0 h200 v200 h-200 z',
flag: 'M 10,40 h180 v120 h-180 Z',
pennon: 'M 10,40 l190,60 -190,60 Z',
guidon: 'M 10,40 h190 l-65,60 65,60 h-190 Z',
banner: 'm 25,25 v 170 l 25,-40 25,40 25,-40 25,40 25,-40 25,40 V 25 Z',
dovetail: 'm 25,25 v 175 l 75,-40 75,40 V 25 Z',
gonfalon: 'm 25,25 v 125 l 75,50 75,-50 V 25 Z',
pennant: 'M 25,15 100,200 175,15 Z',
fantasy1: 'M 100,5 C 85,30 40,35 15,40 c 40,35 20,90 40,115 15,25 40,30 45,45 5,-15 30,-20 45,-45 20,-25 0,-80 40,-115 C 160,35 115,30 100,5 Z',
fantasy2: 'm 152,21 c 0,0 -27,14 -52,-4 C 75,35 48,21 48,21 50,45 30,55 30,75 60,75 60,115 32,120 c 3,40 53,50 68,80 15,-30 65,-40 68,-80 -28,-5 -28,-45 2,-45 C 170,55 150,45 152,21 Z',
fantasy3: 'M 167,67 C 165,0 35,0 33,67 c 32,-7 27,53 -3,43 -5,45 60,65 70,90 10,-25 75,-47.51058 70,-90 -30,10 -35,-50 -3,-43 z',
fantasy4: 'M100 9C55 48 27 27 13 39c23 50 3 119 49 150 14 9 28 11 38 11s27-4 38-11c55-39 24-108 49-150-14-12-45 7-87-30z',
fantasy5: 'M 100,0 C 75,25 30,25 30,25 c 0,69 20,145 70,175 50,-30 71,-106 70,-175 0,0 -45,0 -70,-25 z',
noldor: 'm 55,75 h 2 c 3,-25 38,-10 3,20 15,50 30,75 40,105 10,-30 25,-55 40,-105 -35,-30 0,-45 3,-20 h 2 C 150,30 110,20 100,0 90,20 50,30 55,75 Z',
gondor: 'm 100,200 c 15,-15 38,-35 45,-60 h 5 V 30 h -5 C 133,10 67,10 55,30 h -5 v 110 h 5 c 7,25 30,45 45,60 z',
easterling: 'M 160,185 C 120,170 80,170 40,185 V 15 c 40,15 80,15 120,0 z',
erebor: 'M25 135 V60 l22-13 16-37 h75 l15 37 22 13 v75l-22 18-16 37 H63l-16-37z',
ironHills: 'm 30,25 60,-10 10,10 10,-10 60,10 -5,125 -65,50 -65,-50 z',
urukHai: 'M 30,60 C 40,60 60,50 60,20 l -5,-3 45,-17 75,40 -5,5 -35,155 -5,-35 H 70 v 35 z',
moriaOrc:
'M45 35c5 3 7 10 13 9h19c4-2 7-4 9-9 6 1 9 9 16 11 7-2 14 0 21 0 6-3 6-10 10-15 2-5 1-10-2-15-2-4-5-14-4-16 3 6 7 11 12 14 7 3 3 12 7 16 3 6 4 12 9 18 2 4 6 8 5 14 0 6-1 12 3 18-3 6-2 13-1 20 1 6-2 12-1 18 0 6-3 13 0 18 8 4 0 8-5 7-4 3-9 3-13 9-5 5-5 13-8 19 0 6 0 15-7 16-1 6-7 6-10 12-1-6 0-6-2-9l2-19c2-4 5-12-3-12-4-5-11-5-15 1l-13-18c-3-4-2 9-3 12 2 2-4-6-7-5-8-2-8 7-11 11-2 4-5 10-8 9 3-10 3-16 1-23-1-4 2-9-4-11 0-6 1-13-2-19-4-2-9-6-13-7V91c4-7-5-13 0-19-3-7 2-11 2-18-1-6 1-12 3-17v-1z'
};
const lines = {
straight: "m 0,100 v15 h 200 v -15 z",
engrailed: "m 0,95 a 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 v 20 H 0 Z",
invecked: "M0,102.5 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 v12.5 H0 z",
embattled: "M 0,105 H 2.5 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 2.5 v 10 H 0 Z",
wavy: "m 200,115 v -15 c -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 v 15 z",
raguly: "m 200,95 h -3 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 H 97 l -5,10 H 82 L 87,95 H 77 l -5,10 H 62 L 67,95 H 57 l -5,10 H 42 L 47,95 H 37 l -5,10 H 22 L 27,95 H 17 l -5,10 H 2 L 7,95 H 0 v 20 h 200 z",
dancetty: "m 0,105 10,-15 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 10,15 v 10 H 0 Z",
dentilly: "M 180,105 170,95 v 10 L 160,95 v 10 L 150,95 v 10 L 140,95 v 10 L 130,95 v 10 L 120,95 v 10 L 110,95 v 10 L 100,95 v 10 L 90,95 v 10 L 80,95 v 10 L 70,95 v 10 L 60,95 v 10 L 50,95 v 10 L 40,95 v 10 L 30,95 v 10 L 20,95 v 10 L 10,95 v 10 L 0,95 v 20 H 200 V 105 L 190,95 v 10 L 180,95 Z",
angled: "m 0,95 h 100 v 10 h 100 v 10 H 0 Z",
urdy: "m 200,90 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 L 0,90 v 25 h 200",
indented: "m 100,95 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 v 20 H 0 V 95 l 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 z",
bevilled: "m 0,92.5 h 110 l -20,15 H 200 V 115 H 0 Z",
nowy: "m 0,95 h 80 c 0,0 0.1,20.1 20,20 19.9,-0.1 20,-20 20,-20 h 80 v 20 H 0 Z",
nowyReversed: "m 200,105 h -80 c 0,0 -0.1,-20.1 -20,-20 -19.9,0.1 -20,20 -20,20 H 0 v 10 h 200 z",
potenty: "m 3,95 v 5 h 5 v 5 H 0 v 10 h 200 l 0.5,-10 H 193 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 H 100.5 93 v -5 h 5 V 95 H 83 v 5 h 5 v 5 H 73 v -5 h 5 V 95 H 63 v 5 h 5 v 5 H 53 v -5 h 5 V 95 H 43 v 5 h 5 v 5 H 33 v -5 h 5 V 95 H 23 v 5 h 5 v 5 H 13 v -5 h 5 v -5 z",
potentyDexter: "m 200,105 h -2 v -10 0 0 h -10 v 5 h 5 v 5 H 183 V 95 h -10 v 5 h 5 v 5 H 168 V 95 h -10 v 5 h 5 v 5 H 153 V 95 h -10 v 5 h 5 v 5 H 138 V 95 h -10 v 5 h 5 v 5 H 123 V 95 h -10 v 5 h 5 v 5 h -10 v 0 0 -10 H 98 v 5 h 5 v 5 H 93 V 95 H 83 v 5 h 5 v 5 H 78 V 95 H 68 v 5 h 5 v 5 H 63 V 95 H 53 v 5 h 5 v 5 H 48 V 95 H 38 v 5 h 5 v 5 H 33 V 95 H 23 v 5 h 5 v 5 H 18 V 95 H 8 v 5 h 5 v 5 H 3 V 95 H 0 v 20 h 200 z",
potentySinister: "m 2.5,95 v 10 H 0 v 10 h 202.5 v -15 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 z",
embattledGhibellin: "M 200,200 V 100 l -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 v 15 h 200",
embattledNotched: "m 200,105 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 H 90 V 95 l -5,5 -5,-5 v 10 H 75 V 95 l -5,5 -5,-5 v 10 H 60 V 95 l -5,5 -5,-5 v 10 H 45 V 95 l -5,5 -5,-5 v 10 H 30 V 95 l -5,5 -5,-5 v 10 H 15 V 95 l -5,5 -5,-5 v 10 H 0 v 10 h 200",
embattledGrady: "m 0,95 v 20 H 200 V 95 h -2.5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 z",
dovetailed: "m 200,95 h -7 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 H 93 l 4,10 H 83 L 87,95 H 73 l 4,10 H 63 L 67,95 H 53 l 4,10 H 43 L 47,95 H 33 l 4,10 H 23 L 27,95 H 13 l 4,10 H 3 L 7,95 H 0 v 20 h 200",
dovetailedIndented: "m 200,100 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 v 15 h 200",
straight: 'm 0,100 v15 h 200 v -15 z',
engrailed:
'm 0,95 a 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 6.25,6.25 0 0 0 12.5,0 v 20 H 0 Z',
invecked:
'M0,102.5 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 a6.25,6.25,0,0,1,12.5,0 v12.5 H0 z',
embattled: 'M 0,105 H 2.5 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 15 V 95 h 15 v 10 h 2.5 v 10 H 0 Z',
wavy: 'm 200,115 v -15 c -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 -8.9,3.5 -16,3.1 -25,0 -8.9,-3.5 -16,-3.1 -25,0 -8.9,3.5 -16,3.2 -25,0 -8.9,-3.5 -16,-3.2 -25,0 v 15 z',
raguly:
'm 200,95 h -3 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 h -10 l -5,10 h -10 l 5,-10 H 97 l -5,10 H 82 L 87,95 H 77 l -5,10 H 62 L 67,95 H 57 l -5,10 H 42 L 47,95 H 37 l -5,10 H 22 L 27,95 H 17 l -5,10 H 2 L 7,95 H 0 v 20 h 200 z',
dancetty: 'm 0,105 10,-15 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 15,20 15,-20 10,15 v 10 H 0 Z',
dentilly:
'M 180,105 170,95 v 10 L 160,95 v 10 L 150,95 v 10 L 140,95 v 10 L 130,95 v 10 L 120,95 v 10 L 110,95 v 10 L 100,95 v 10 L 90,95 v 10 L 80,95 v 10 L 70,95 v 10 L 60,95 v 10 L 50,95 v 10 L 40,95 v 10 L 30,95 v 10 L 20,95 v 10 L 10,95 v 10 L 0,95 v 20 H 200 V 105 L 190,95 v 10 L 180,95 Z',
angled: 'm 0,95 h 100 v 10 h 100 v 10 H 0 Z',
urdy: 'm 200,90 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,6 -5,-6 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 l -5,-5 -5,5 v 10 l -5,5 -5,-5 V 95 L 0,90 v 25 h 200',
indented:
'm 100,95 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 v 20 H 0 V 95 l 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 5,-10 5,10 z',
bevilled: 'm 0,92.5 h 110 l -20,15 H 200 V 115 H 0 Z',
nowy: 'm 0,95 h 80 c 0,0 0.1,20.1 20,20 19.9,-0.1 20,-20 20,-20 h 80 v 20 H 0 Z',
nowyReversed: 'm 200,105 h -80 c 0,0 -0.1,-20.1 -20,-20 -19.9,0.1 -20,20 -20,20 H 0 v 10 h 200 z',
potenty:
'm 3,95 v 5 h 5 v 5 H 0 v 10 h 200 l 0.5,-10 H 193 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 h -15 v -5 h 5 v -5 h -15 v 5 h 5 v 5 H 100.5 93 v -5 h 5 V 95 H 83 v 5 h 5 v 5 H 73 v -5 h 5 V 95 H 63 v 5 h 5 v 5 H 53 v -5 h 5 V 95 H 43 v 5 h 5 v 5 H 33 v -5 h 5 V 95 H 23 v 5 h 5 v 5 H 13 v -5 h 5 v -5 z',
potentyDexter:
'm 200,105 h -2 v -10 0 0 h -10 v 5 h 5 v 5 H 183 V 95 h -10 v 5 h 5 v 5 H 168 V 95 h -10 v 5 h 5 v 5 H 153 V 95 h -10 v 5 h 5 v 5 H 138 V 95 h -10 v 5 h 5 v 5 H 123 V 95 h -10 v 5 h 5 v 5 h -10 v 0 0 -10 H 98 v 5 h 5 v 5 H 93 V 95 H 83 v 5 h 5 v 5 H 78 V 95 H 68 v 5 h 5 v 5 H 63 V 95 H 53 v 5 h 5 v 5 H 48 V 95 H 38 v 5 h 5 v 5 H 33 V 95 H 23 v 5 h 5 v 5 H 18 V 95 H 8 v 5 h 5 v 5 H 3 V 95 H 0 v 20 h 200 z',
potentySinister:
'm 2.5,95 v 10 H 0 v 10 h 202.5 v -15 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 h -10 v 10 h -10 v -5 h 5 v -5 z',
embattledGhibellin:
'M 200,200 V 100 l -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 -5,-5 v 10 l -5,-5 -5,5 V 95 l -5,5 v 15 h 200',
embattledNotched:
'm 200,105 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 h -5 V 95 l -5,5 -5,-5 v 10 H 90 V 95 l -5,5 -5,-5 v 10 H 75 V 95 l -5,5 -5,-5 v 10 H 60 V 95 l -5,5 -5,-5 v 10 H 45 V 95 l -5,5 -5,-5 v 10 H 30 V 95 l -5,5 -5,-5 v 10 H 15 V 95 l -5,5 -5,-5 v 10 H 0 v 10 h 200',
embattledGrady:
'm 0,95 v 20 H 200 V 95 h -2.5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 h -5 v 5 h -5 v 5 h -5 v -5 h -5 v -5 z',
dovetailed:
'm 200,95 h -7 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 h -14 l 4,10 h -14 l 4,-10 H 93 l 4,10 H 83 L 87,95 H 73 l 4,10 H 63 L 67,95 H 53 l 4,10 H 43 L 47,95 H 33 l 4,10 H 23 L 27,95 H 13 l 4,10 H 3 L 7,95 H 0 v 20 h 200',
dovetailedIndented:
'm 200,100 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 -7,-5 4,10 -7,-5 -7,5 4,-10 -7,5 v 15 h 200',
nebuly:
"m 13.1,89.8 c -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.2,4.5 -7.3,4.5 -0.5,0 -2.2,-0.2 -2.2,-0.2 V 115 h 200 v -10.1 c -3.7,-0.2 -6.7,-2.2 -6.7,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 -1.5,4.1 -4.2,4.4 -8.8,4.5 -4.7,-0.1 -8.7,-1.5 -8.9,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 z",
'm 13.1,89.8 c -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.2,4.5 -7.3,4.5 -0.5,0 -2.2,-0.2 -2.2,-0.2 V 115 h 200 v -10.1 c -3.7,-0.2 -6.7,-2.2 -6.7,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.8,-1.9 1.8,-3.1 0,-2.5 -3.2,-4.5 -7.2,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 -1.5,4.1 -4.2,4.4 -8.8,4.5 -4.7,-0.1 -8.7,-1.5 -8.9,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 -4.1,0 -7.3,2 -7.3,4.5 0,1.2 0.7,2.3 1.8,3.1 1.2,0.7 1.9,1.8 1.9,3 0,2.5 -3.3,4.5 -7.3,4.5 -4,0 -7.3,-2 -7.3,-4.5 0,-1.2 0.7,-2.3 1.9,-3 1.2,-0.8 1.9,-1.9 1.9,-3.1 0,-2.5 -3.3,-4.5 -7.3,-4.5 z',
rayonne:
"M0 115l-.1-6 .2.8c1.3-1 2.3-2.5 2.9-4.4.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4A9 9 0 015.5 90c-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 2.1 3.1 3.1 4.6 1 1.6 2.4 3.1 2.7 4.8.3 1.7.3 3.3 0 5.2 1.3-1 2.6-2.7 3.2-4.6.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.75 2.79 2.72 4.08 4.45 5.82L200 115z",
'M0 115l-.1-6 .2.8c1.3-1 2.3-2.5 2.9-4.4.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4A9 9 0 015.5 90c-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 2.1 3.1 3.1 4.6 1 1.6 2.4 3.1 2.7 4.8.3 1.7.3 3.3 0 5.2 1.3-1 2.6-2.7 3.2-4.6.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.5 2 1.7 3.6 3.1 4.6a9 9 0 013.1 4.6c.5 2 .4 3.9-.3 5.4a9 9 0 003.1-4.6c.5-2 .4-3.9-.3-5.4-.7-1.5-.8-3.4-.3-5.4.5-2 1.7-3.6 3.1-4.6-.7 1.5-.8 3.4-.3 5.4.75 2.79 2.72 4.08 4.45 5.82L200 115z',
seaWaves:
"m 28.83,94.9 c -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.33,-2.03 -2.19,-3.56 -4.45,-3.56 -4.24,0 -6.91,3.13 -8.5,5.13 V 115 h 200 v -14.89 c -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -6.6,3.09 -8.19,5.09 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 z",
'm 28.83,94.9 c -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.33,-2.03 -2.19,-3.56 -4.45,-3.56 -4.24,0 -6.91,3.13 -8.5,5.13 V 115 h 200 v -14.89 c -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.44,-3.6 3.6,-3.6 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -6.6,3.09 -8.19,5.09 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.2,-3.55 -4.46,-3.55 -4.25,0 -7.16,3.17 -8.75,5.18 -1.59,2.01 -4.5,5.18 -8.75,5.18 -2.16,0 -3.91,-1.63 -3.91,-3.64 0,-2.01 1.75,-3.64 3.91,-3.64 0.7,0 1.36,0.17 1.93,0.48 -0.34,-2.01 -2.21,-3.55 -4.46,-3.55 z',
dragonTeeth:
"M 9.4,85 C 6.5,88.1 4.1,92.9 3,98.8 1.9,104.6 2.3,110.4 3.8,115 2.4,113.5 0,106.6 0,109.3 v 5.7 h 200 v -5.7 c -1.1,-2.4 -2,-5.1 -2.6,-8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -1.4,-1.5 -2.8,-3.9 -3.8,-6.1 -1.1,-2.4 -2.3,-6.1 -2.6,-7.7 -0.2,-5.9 0.2,-11.7 1.7,-16.3 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 C 63,95.4 63.4,89.6 64.9,85 c -2.9,3.1 -5.3,7.9 -6.3,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.6,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 C 18.6,95.4 19,89.6 20.5,85 17.6,88.1 15.2,92.9 14.1,98.8 13,104.6 13.4,110.4 14.9,115 12,111.9 9.6,107.1 8.6,101.2 7.5,95.4 7.9,89.6 9.4,85 Z",
firTrees: "m 3.9,90 -4,7 2,-0.5 L 0,100 v 15 h 200 v -15 l -1.9,-3.5 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 z",
flechy: "m 0,100 h 85 l 15,-15 15,15 h 85 v 15 H 0 Z",
barby: "m 0,100 h 85 l 15,15 15,-15 h 85 v 15 H 0 Z",
enclavy: "M 0,100 H 85 V 85 h 30 v 15 h 85 v 15 H 0 Z",
escartely: "m 0,100 h 85 v 15 h 30 v -15 h 85 v 15 H 0 Z",
arched: "m 100,95 c 40,-0.2 100,20 100,20 H 0 c 0,0 60,-19.8 100,-20 z",
archedReversed: "m 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z"
'M 9.4,85 C 6.5,88.1 4.1,92.9 3,98.8 1.9,104.6 2.3,110.4 3.8,115 2.4,113.5 0,106.6 0,109.3 v 5.7 h 200 v -5.7 c -1.1,-2.4 -2,-5.1 -2.6,-8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.9 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.9 -0.7,11.6 0.8,16.2 -1.4,-1.5 -2.8,-3.9 -3.8,-6.1 -1.1,-2.4 -2.3,-6.1 -2.6,-7.7 -0.2,-5.9 0.2,-11.7 1.7,-16.3 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.9,-16.2 -3,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -2.9,-3.1 -5.3,-7.9 -6.4,-13.8 C 63,95.4 63.4,89.6 64.9,85 c -2.9,3.1 -5.3,7.9 -6.3,13.8 -1.1,5.8 -0.7,11.6 0.8,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.6,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 -1.1,-5.8 -0.7,-11.6 0.8,-16.2 -2.9,3.1 -5.3,7.9 -6.4,13.8 -1.1,5.8 -0.7,11.6 0.9,16.2 -3,-3.1 -5.3,-7.9 -6.4,-13.8 C 18.6,95.4 19,89.6 20.5,85 17.6,88.1 15.2,92.9 14.1,98.8 13,104.6 13.4,110.4 14.9,115 12,111.9 9.6,107.1 8.6,101.2 7.5,95.4 7.9,89.6 9.4,85 Z',
firTrees:
'm 3.9,90 -4,7 2,-0.5 L 0,100 v 15 h 200 v -15 l -1.9,-3.5 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4.1,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 -4,-7 -4,7 2,-0.5 -4,7 2,-0.5 -4,7 -4,-7 2,0.5 -4,-7 2,0.5 z',
flechy: 'm 0,100 h 85 l 15,-15 15,15 h 85 v 15 H 0 Z',
barby: 'm 0,100 h 85 l 15,15 15,-15 h 85 v 15 H 0 Z',
enclavy: 'M 0,100 H 85 V 85 h 30 v 15 h 85 v 15 H 0 Z',
escartely: 'm 0,100 h 85 v 15 h 30 v -15 h 85 v 15 H 0 Z',
arched: 'm 100,95 c 40,-0.2 100,20 100,20 H 0 c 0,0 60,-19.8 100,-20 z',
archedReversed: 'm 0,85 c 0,0 60,20.2 100,20 40,-0.2 100,-20 100,-20 v 30 H 0 Z'
};
const templates = {
@ -1548,14 +1571,20 @@ window.COArenderer = (function () {
gyronny: `<polygon points="0,0 200,200 200,100 0,100"/><polygon points="200,0 0,200 100,200 100,0"/>`,
chevronny: `<path d="M0,80 100,-15 200,80 200,120 100,25 0,120z M0,160 100,65 200,160 200,200 100,105 0,200z M0,240 100,145 200,240 0,240z"/>`,
// lined divisions
perFessLined: line => `<path d="${line}"/><rect x="0" y="115" width="200" height="85" shape-rendering="crispedges"/>`,
perPaleLined: line => `<path d="${line}" transform="rotate(-90 100 100)"/><rect x="115" y="0" width="85" height="200" shape-rendering="crispedges"/>`,
perBendLined: line => `<path d="${line}" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perBendSinisterLined: line => `<path d="${line}" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perChevronLined: line => `<rect x="15" y="115" width="200" height="200" transform="translate(70 70) rotate(45 100 100)"/><path d="${line}" transform="translate(129 71) rotate(-45 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(71 71) rotate(45 100 100)"/>`,
perChevronReversedLined: line => `<rect x="15" y="115" width="200" height="200" transform="translate(-70 -70) rotate(225.001 100 100)"/><path d="${line}" transform="translate(-70.7 -70.7) rotate(225 100 100) scale(1 1)"/><path d="${line}" transform="translate(270.7 -70.7) rotate(-225 -100 100) scale(-1 1)"/>`,
perCrossLined: line => `<rect x="100" y="0" width="100" height="92.5"/><rect x="0" y="107.5" width="100" height="92.5"/><path d="${line}" transform="translate(0 50) scale(.5001)"/><path d="${line}" transform="translate(200 150) scale(-.5)"/>`,
perPileLined: line => `<path d="${line}" transform="translate(161.66 10) rotate(66.66 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(38.33 10) rotate(-66.66 100 100)"/><polygon points="-2.15,0 84.15,200 115.85,200 202.15,0 200,200 0,200"/>`,
perFessLined: (line) => `<path d="${line}"/><rect x="0" y="115" width="200" height="85" shape-rendering="crispedges"/>`,
perPaleLined: (line) => `<path d="${line}" transform="rotate(-90 100 100)"/><rect x="115" y="0" width="85" height="200" shape-rendering="crispedges"/>`,
perBendLined: (line) =>
`<path d="${line}" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perBendSinisterLined: (line) =>
`<path d="${line}" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)"/><rect x="0" y="115" width="200" height="85" transform="translate(-10 -10) rotate(-45 110 110) scale(1.1)" shape-rendering="crispedges"/>`,
perChevronLined: (line) =>
`<rect x="15" y="115" width="200" height="200" transform="translate(70 70) rotate(45 100 100)"/><path d="${line}" transform="translate(129 71) rotate(-45 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(71 71) rotate(45 100 100)"/>`,
perChevronReversedLined: (line) =>
`<rect x="15" y="115" width="200" height="200" transform="translate(-70 -70) rotate(225.001 100 100)"/><path d="${line}" transform="translate(-70.7 -70.7) rotate(225 100 100) scale(1 1)"/><path d="${line}" transform="translate(270.7 -70.7) rotate(-225 -100 100) scale(-1 1)"/>`,
perCrossLined: (line) =>
`<rect x="100" y="0" width="100" height="92.5"/><rect x="0" y="107.5" width="100" height="92.5"/><path d="${line}" transform="translate(0 50) scale(.5001)"/><path d="${line}" transform="translate(200 150) scale(-.5)"/>`,
perPileLined: (line) =>
`<path d="${line}" transform="translate(161.66 10) rotate(66.66 -100 100) scale(-1 1)"/><path d="${line}" transform="translate(38.33 10) rotate(-66.66 100 100)"/><polygon points="-2.15,0 84.15,200 115.85,200 202.15,0 200,200 0,200"/>`,
// straight ordinaries
fess: `<rect x="0" y="75" width="200" height="50"/>`,
pale: `<rect x="75" y="0" width="50" height="200"/>`,
@ -1591,85 +1620,182 @@ window.COArenderer = (function () {
pilesInPoint: `<path d="M15,0 100,200 60,0Z M80,0 100,200 120,0Z M140,0 100,200 185,0Z"/>`,
label: `<path d="m 46,54.8 6.6,-15.6 95.1,0 5.9,15.5 -16.8,0.1 4.5,-11.8 L 104,43 l 4.3,11.9 -16.8,0 4.3,-11.8 -37.2,0 4.5,11.8 -16.9,0 z"/>`,
// lined ordinaries
fessLined: line => `<path d="${line}" transform="translate(0 -25)"/><path d="${line}" transform="translate(0 25) rotate(180 100 100)"/><rect x="0" y="88" width="200" height="24" stroke="none"/>`,
paleLined: line => `<path d="${line}" transform="rotate(-90 100 100) translate(0 -25)"/><path d="${line}" transform="rotate(90 100 100) translate(0 -25)"/><rect x="88" y="0" width="24" height="200" stroke="none"/>`,
bendLined: line => `<path d="${line}" transform="translate(8 -18) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-28 18) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendSinisterLined: line => `<path d="${line}" transform="translate(-28 -18) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(8 18) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
chiefLined: line => `<path d="${line}" transform="translate(0,-25) rotate(180.00001 100 100)"/><rect width="200" height="62" stroke="none"/>`,
barLined: line => `<path d="${line}" transform="translate(0,-12.5)"/><path d="${line}" transform="translate(0,12.5) rotate(180.00001 100 100)"/><rect x="0" y="94" width="200" height="12" stroke="none"/>`,
gemelleLined: line => `<path d="${line}" transform="translate(0,-22.5)"/><path d="${line}" transform="translate(0,22.5) rotate(180.00001 100 100)"/>`,
fessCotissedLined: line => `<path d="${line}" transform="translate(0 15) scale(1 .5)"/><path d="${line}" transform="translate(0 85) rotate(180 100 50) scale(1 .5)"/><rect x="0" y="80" width="200" height="40"/>`,
fessDoubleCotissedLined: line => `<rect x="0" y="85" width="200" height="30"/><rect x="0" y="72.5" width="200" height="7.5"/><rect x="0" y="120" width="200" height="7.5"/><path d="${line}" transform="translate(0 10) scale(1 .5)"/><path d="${line}" transform="translate(0 90) rotate(180 100 50) scale(1 .5)"/>`,
bendletLined: line => `<path d="${line}" transform="translate(2 -12) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-22 12) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendletSinisterLined: line => `<path d="${line}" transform="translate(-22 -12) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(2 12) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
terraceLined: line => `<path d="${line}" transform="translate(0,50)"/><rect x="0" y="164" width="200" height="36" stroke="none"/>`,
crossLined: line => `<path d="${line}" transform="translate(0,-14.5)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-14.5)"/>`,
crossPartedLined: line => `<path d="${line}" transform="translate(0,-20)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-20)"/>`,
saltireLined: line => `<path d="${line}" transform="translate(0 -10) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 10) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 -10) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(0 10) rotate(-225 110 100) scale(1.1 1)"/>`,
saltirePartedLined: line => `<path d="${line}" transform="translate(3 -13) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 13) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 -13) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(3 13) rotate(-225 110 100) scale(1.1 1)"/>`
fessLined: (line) =>
`<path d="${line}" transform="translate(0 -25)"/><path d="${line}" transform="translate(0 25) rotate(180 100 100)"/><rect x="0" y="88" width="200" height="24" stroke="none"/>`,
paleLined: (line) =>
`<path d="${line}" transform="rotate(-90 100 100) translate(0 -25)"/><path d="${line}" transform="rotate(90 100 100) translate(0 -25)"/><rect x="88" y="0" width="24" height="200" stroke="none"/>`,
bendLined: (line) =>
`<path d="${line}" transform="translate(8 -18) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-28 18) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendSinisterLined: (line) =>
`<path d="${line}" transform="translate(-28 -18) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(8 18) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="88" width="200" height="24" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
chiefLined: (line) => `<path d="${line}" transform="translate(0,-25) rotate(180.00001 100 100)"/><rect width="200" height="62" stroke="none"/>`,
barLined: (line) =>
`<path d="${line}" transform="translate(0,-12.5)"/><path d="${line}" transform="translate(0,12.5) rotate(180.00001 100 100)"/><rect x="0" y="94" width="200" height="12" stroke="none"/>`,
gemelleLined: (line) => `<path d="${line}" transform="translate(0,-22.5)"/><path d="${line}" transform="translate(0,22.5) rotate(180.00001 100 100)"/>`,
fessCotissedLined: (line) =>
`<path d="${line}" transform="translate(0 15) scale(1 .5)"/><path d="${line}" transform="translate(0 85) rotate(180 100 50) scale(1 .5)"/><rect x="0" y="80" width="200" height="40"/>`,
fessDoubleCotissedLined: (line) =>
`<rect x="0" y="85" width="200" height="30"/><rect x="0" y="72.5" width="200" height="7.5"/><rect x="0" y="120" width="200" height="7.5"/><path d="${line}" transform="translate(0 10) scale(1 .5)"/><path d="${line}" transform="translate(0 90) rotate(180 100 50) scale(1 .5)"/>`,
bendletLined: (line) =>
`<path d="${line}" transform="translate(2 -12) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-22 12) rotate(225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(45 110 100) scale(1.1 1)" stroke="none"/>`,
bendletSinisterLined: (line) =>
`<path d="${line}" transform="translate(-22 -12) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(2 12) rotate(-225 110 100) scale(1.1 1)"/><rect x="0" y="94" width="200" height="12" transform="translate(-10 0) rotate(-45 110 100) scale(1.1 1)" stroke="none"/>`,
terraceLined: (line) => `<path d="${line}" transform="translate(0,50)"/><rect x="0" y="164" width="200" height="36" stroke="none"/>`,
crossLined: (line) =>
`<path d="${line}" transform="translate(0,-14.5)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-14.5)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-14.5)"/>`,
crossPartedLined: (line) =>
`<path d="${line}" transform="translate(0,-20)"/><path d="${line}" transform="rotate(180 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-90 100 100) translate(0,-20)"/><path d="${line}" transform="rotate(-270 100 100) translate(0,-20)"/>`,
saltireLined: (line) =>
`<path d="${line}" transform="translate(0 -10) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 10) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-20 -10) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(0 10) rotate(-225 110 100) scale(1.1 1)"/>`,
saltirePartedLined: (line) =>
`<path d="${line}" transform="translate(3 -13) rotate(45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 13) rotate(225 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(-23 -13) rotate(-45 110 100) scale(1.1 1)"/><path d="${line}" transform="translate(3 13) rotate(-225 110 100) scale(1.1 1)"/>`
};
const patterns = {
semy: (p, c1, c2, size, chargeId) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="200" fill="${c1}" stroke="none"/><g fill="${c2}"><use transform="translate(-100 -50)" href="#${chargeId}"/><use transform="translate(100 -50)" href="#${chargeId}"/><use transform="translate(0 50)" href="#${chargeId}"/></g></pattern>`,
vair: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.25}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="25" fill="${c1}" stroke="none"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c2}" stroke="none"/><path d="m25,25 l-6.25,6.25 v12.5 l-6.25,6.25 l-6.25,-6.25 v-12.5 l-6.25,-6.25 z" fill="${c1}"/><path d="M0 50 h25" fill="none"/></pattern>`,
counterVair: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.25}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="50" fill="${c2}" stroke="none"/><path d="m 12.5,0 6.25,6.25 v 12.5 L 25,25 18.75,31.25 v 12.5 L 12.5,50 6.25,43.75 V 31.25 L 0,25 6.25,18.75 V 6.25 Z" fill="${c1}"/></pattern>`,
vairInPale: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 25 25"><rect width="25" height="25" fill="${c1}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}" stroke="#000" stroke-width=".2"/></pattern>`,
vairEnPointe: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.25}" viewBox="0 0 25 50"><rect width="25" height="25" fill="${c2}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c1}"/><rect x="0" y="25" width="25" height="25" fill="${c1}" stroke-width="1" stroke="${c1}"/><path d="m12.5,25 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/></pattern>`,
vairAncien: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><path fill="${c2}" stroke="none" d="m 0,90 c 10,0 25,-5 25,-40 0,-25 10,-40 25,-40 15,0 25,15 25,40 0,35 15,40 25,40 v 10 H 0 Z"/><path fill="none" stroke="#000" d="M 0,90 c 10,0 25,-5 25,-40 0,-35 15,-40 25,-40 10,0 25,5 25,40 0,35 15,40 25,40 M0,100 h100"/></pattern>`,
potent: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="100" fill="${c1}" stroke="none"/><rect y="100" width="200" height="100" fill="${c2}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}"/><path d="m25 100v50h50v50h50v-50h50v-50z" fill="${c1}"/><path d="m0 0h200 M0 100h200" fill="none"/></pattern>`,
counterPotent: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m25 50h50v-50h50v50h50v100h-50v50h-50v-50h-50v-50z" fill="${c2}"/><path d="m0 0h200 M0 100h200 M0 200h200"/></pattern>`,
potentInPale: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.0625}" viewBox="0 0 200 100" stroke-width="1"><rect width="200" height="100" fill="${c1}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}" stroke="#000"/><path d="m0 0h200 M0 100h200" fill="none" stroke="#000"/></pattern>`,
potentEnPointe: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m0 0h25v50h50v50h50v-50h50v-50h25v100h-25v50h-50v50h-50v-50h-50v-50h-25v-100" fill="${c2}"/></pattern>`,
semy: (p, c1, c2, size, chargeId) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="200" fill="${c1}" stroke="none"/><g fill="${c2}"><use transform="translate(-100 -50)" href="#${chargeId}"/><use transform="translate(100 -50)" href="#${chargeId}"/><use transform="translate(0 50)" href="#${chargeId}"/></g></pattern>`,
vair: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="25" fill="${c1}" stroke="none"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c2}" stroke="none"/><path d="m25,25 l-6.25,6.25 v12.5 l-6.25,6.25 l-6.25,-6.25 v-12.5 l-6.25,-6.25 z" fill="${c1}"/><path d="M0 50 h25" fill="none"/></pattern>`,
counterVair: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50" stroke="#000" stroke-width=".2"><rect width="25" height="50" fill="${c2}" stroke="none"/><path d="m 12.5,0 6.25,6.25 v 12.5 L 25,25 18.75,31.25 v 12.5 L 12.5,50 6.25,43.75 V 31.25 L 0,25 6.25,18.75 V 6.25 Z" fill="${c1}"/></pattern>`,
vairInPale: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 25 25"><rect width="25" height="25" fill="${c1}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}" stroke="#000" stroke-width=".2"/></pattern>`,
vairEnPointe: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 25 50"><rect width="25" height="25" fill="${c2}"/><path d="m12.5,0 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c1}"/><rect x="0" y="25" width="25" height="25" fill="${c1}" stroke-width="1" stroke="${c1}"/><path d="m12.5,25 l6.25,6.25 v12.5 l6.25,6.25 h-25 l6.25,-6.25 v-12.5 z" fill="${c2}"/></pattern>`,
vairAncien: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><path fill="${c2}" stroke="none" d="m 0,90 c 10,0 25,-5 25,-40 0,-25 10,-40 25,-40 15,0 25,15 25,40 0,35 15,40 25,40 v 10 H 0 Z"/><path fill="none" stroke="#000" d="M 0,90 c 10,0 25,-5 25,-40 0,-35 15,-40 25,-40 10,0 25,5 25,40 0,35 15,40 25,40 M0,100 h100"/></pattern>`,
potent: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="#000"><rect width="200" height="100" fill="${c1}" stroke="none"/><rect y="100" width="200" height="100" fill="${c2}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}"/><path d="m25 100v50h50v50h50v-50h50v-50z" fill="${c1}"/><path d="m0 0h200 M0 100h200" fill="none"/></pattern>`,
counterPotent: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m25 50h50v-50h50v50h50v100h-50v50h-50v-50h-50v-50z" fill="${c2}"/><path d="m0 0h200 M0 100h200 M0 200h200"/></pattern>`,
potentInPale: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.0625
}" viewBox="0 0 200 100" stroke-width="1"><rect width="200" height="100" fill="${c1}" stroke="none"/><path d="m25 50h50v-50h50v50h50v50h-150z" fill="${c2}" stroke="#000"/><path d="m0 0h200 M0 100h200" fill="none" stroke="#000"/></pattern>`,
potentEnPointe: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" stroke="none"><rect width="200" height="200" fill="${c1}"/><path d="m0 0h25v50h50v50h50v-50h50v-50h25v100h-25v50h-50v50h-50v-50h-50v-50h-25v-100" fill="${c2}"/></pattern>`,
ermine: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 200 200" fill="${c2}"><rect width="200" height="200" fill="${c1}"/><g stroke="none" fill="${c2}"><g transform="translate(-100 -50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g><g transform="translate(100 -50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g><g transform="translate(0 50)"><path d="m100 81.1c-4.25 17.6-12.7 29.8-21.2 38.9 3.65-0.607 7.9-3.04 11.5-5.47-2.42 4.86-4.86 8.51-7.3 12.7 1.82-0.607 6.07-4.86 12.7-10.9 1.21 8.51 2.42 17.6 4.25 23.6 1.82-5.47 3.04-15.2 4.25-23.6 3.65 3.65 7.3 7.9 12.7 10.9l-7.9-13.3c3.65 1.82 7.9 4.86 11.5 6.07-9.11-9.11-17-21.2-20.6-38.9z"/><path d="m82.4 81.7c-0.607-0.607-6.07 2.42-9.72-4.25 7.9 6.68 15.2-7.3 21.8 1.82 1.82 4.25-6.68 10.9-12.1 2.42z"/><path d="m117 81.7c0.607-1.21 6.07 2.42 9.11-4.86-7.3 7.3-15.2-7.3-21.2 2.42-1.82 4.25 6.68 10.9 12.1 2.42z"/><path d="m101 66.5c-1.02-0.607 3.58-4.25-3.07-8.51 5.63 7.9-10.2 10.9-1.54 17.6 3.58 2.42 12.2-2.42 4.6-9.11z"/></g></g></pattern>`,
chequy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.25}" height="${size * 0.25}" viewBox="0 0 50 50" fill="${c2}"><rect width="50" height="50"/><rect width="25" height="25" fill="${c1}"/><rect x="25" y="25" width="25" height="25" fill="${c1}"/></pattern>`,
lozengy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><polygon points="25,0 50,25 25,50 0,25" fill="${c2}"/></pattern>`,
fusily: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.25}" viewBox="0 0 50 100"><rect width="50" height="100" fill="${c2}"/><polygon points="25,0 50,50 25,100 0,50" fill="${c1}"/></pattern>`,
pally: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.5}" height="${size * 0.125}" viewBox="0 0 100 25"><rect width="100" height="25" fill="${c2}"/><rect x="25" y="0" width="25" height="25" fill="${c1}"/><rect x="75" y="0" width="25" height="25" fill="${c1}"/></pattern>`,
barry: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.5}" viewBox="0 0 25 100"><rect width="25" height="100" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c1}"/><rect x="0" y="75" width="25" height="25" fill="${c1}"/></pattern>`,
gemelles: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><rect y="5" width="50" height="10" fill="${c2}"/><rect y="40" width="50" height="10" fill="${c2}"/></pattern>`,
bendy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.5}" height="${size * 0.5}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><polygon points="0,25 75,100 25,100 0,75" fill="${c2}"/><polygon points="25,0 75,0 100,25 100,75" fill="${c2}"/></pattern>`,
bendySinister: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.5}" height="${size * 0.5}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c2}"/><polygon points="0,25 25,0 75,0 0,75" fill="${c1}"/><polygon points="25,100 100,25 100,75 75,100" fill="${c1}"/></pattern>`,
palyBendy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.6258}" height="${size * 0.3576}" viewBox="0 0 175 100"><rect y="0" x="0" width="175" height="100" fill="${c2}"/><g fill="${c1}"><path d="m0 20 35 30v50l-35-30z"/><path d="m35 0 35 30v50l-35-30z"/><path d="m70 0h23l12 10v50l-35-30z"/><path d="m70 80 23 20h-23z"/><path d="m105 60 35 30v10h-35z"/><path d="m105 0h35v40l-35-30z"/><path d="m 140,40 35,30 v 30 h -23 l -12,-10z"/><path d="M 175,0 V 20 L 152,0 Z"/></g></pattern>`,
barryBendy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.3572}" height="${size * 0.6251}" viewBox="0 0 100 175"><rect width="100" height="175" fill="${c2}"/><g fill="${c1}"><path d="m20 0 30 35h50l-30-35z"/><path d="m0 35 30 35h50l-30-35z"/><path d="m0 70v23l10 12h50l-30-35z"/><path d="m80 70 20 23v-23z"/><path d="m60 105 30 35h10v-35z"/><path d="m0 105v35h40l-30-35z"/><path d="m 40,140 30,35 h 30 v -23 l -10,-12 z"/><path d="m0 175h20l-20-23z"/></g></pattern>`,
pappellony: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><circle cx="0" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="100" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="50" cy="1" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/></pattern>`,
pappellony2: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 100 100" stroke="#000" stroke-width="2"><rect width="100" height="100" fill="${c1}" stroke="none"/><circle cy="50" r="49" fill="${c2}"/><circle cx="100" cy="50" r="49" fill="${c2}"/><circle cx="50" cy="0" r="49" fill="${c1}"/></pattern>`,
scaly: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 100 100" stroke="#000"><rect width="100" height="100" fill="${c1}" stroke="none"/><path d="M 0,84 C -40,84 -50,49 -50,49 -50,79 -27,99 0,99 27,99 50,79 50,49 50,49 40,84 0,84 Z" fill="${c2}"/><path d="M 100,84 C 60,84 50,49 50,49 c 0,30 23,50 50,50 27,0 50,-20 50,-50 0,0 -10,35 -50,35 z" fill="${c2}"/><path d="M 50,35 C 10,35 0,0 0,0 0,30 23,50 50,50 77,50 100,30 100,0 100,0 90,35 50,35 Z" fill="${c2}"/></pattern>`,
chequy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.25}" height="${
size * 0.25
}" viewBox="0 0 50 50" fill="${c2}"><rect width="50" height="50"/><rect width="25" height="25" fill="${c1}"/><rect x="25" y="25" width="25" height="25" fill="${c1}"/></pattern>`,
lozengy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><polygon points="25,0 50,25 25,50 0,25" fill="${c2}"/></pattern>`,
fusily: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 50 100"><rect width="50" height="100" fill="${c2}"/><polygon points="25,0 50,50 25,100 0,50" fill="${c1}"/></pattern>`,
pally: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.125
}" viewBox="0 0 100 25"><rect width="100" height="25" fill="${c2}"/><rect x="25" y="0" width="25" height="25" fill="${c1}"/><rect x="75" y="0" width="25" height="25" fill="${c1}"/></pattern>`,
barry: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.5
}" viewBox="0 0 25 100"><rect width="25" height="100" fill="${c2}"/><rect x="0" y="25" width="25" height="25" fill="${c1}"/><rect x="0" y="75" width="25" height="25" fill="${c1}"/></pattern>`,
gemelles: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 50 50"><rect width="50" height="50" fill="${c1}"/><rect y="5" width="50" height="10" fill="${c2}"/><rect y="40" width="50" height="10" fill="${c2}"/></pattern>`,
bendy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.5
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><polygon points="0,25 75,100 25,100 0,75" fill="${c2}"/><polygon points="25,0 75,0 100,25 100,75" fill="${c2}"/></pattern>`,
bendySinister: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.5}" height="${
size * 0.5
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c2}"/><polygon points="0,25 25,0 75,0 0,75" fill="${c1}"/><polygon points="25,100 100,25 100,75 75,100" fill="${c1}"/></pattern>`,
palyBendy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.6258}" height="${
size * 0.3576
}" viewBox="0 0 175 100"><rect y="0" x="0" width="175" height="100" fill="${c2}"/><g fill="${c1}"><path d="m0 20 35 30v50l-35-30z"/><path d="m35 0 35 30v50l-35-30z"/><path d="m70 0h23l12 10v50l-35-30z"/><path d="m70 80 23 20h-23z"/><path d="m105 60 35 30v10h-35z"/><path d="m105 0h35v40l-35-30z"/><path d="m 140,40 35,30 v 30 h -23 l -12,-10z"/><path d="M 175,0 V 20 L 152,0 Z"/></g></pattern>`,
barryBendy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.3572}" height="${
size * 0.6251
}" viewBox="0 0 100 175"><rect width="100" height="175" fill="${c2}"/><g fill="${c1}"><path d="m20 0 30 35h50l-30-35z"/><path d="m0 35 30 35h50l-30-35z"/><path d="m0 70v23l10 12h50l-30-35z"/><path d="m80 70 20 23v-23z"/><path d="m60 105 30 35h10v-35z"/><path d="m0 105v35h40l-30-35z"/><path d="m 40,140 30,35 h 30 v -23 l -10,-12 z"/><path d="m0 175h20l-20-23z"/></g></pattern>`,
pappellony: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100"><rect width="100" height="100" fill="${c1}"/><circle cx="0" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="100" cy="51" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/><circle cx="50" cy="1" r="45" stroke="${c2}" fill="${c1}" stroke-width="10"/></pattern>`,
pappellony2: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" stroke="#000" stroke-width="2"><rect width="100" height="100" fill="${c1}" stroke="none"/><circle cy="50" r="49" fill="${c2}"/><circle cx="100" cy="50" r="49" fill="${c2}"/><circle cx="50" cy="0" r="49" fill="${c1}"/></pattern>`,
scaly: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" stroke="#000"><rect width="100" height="100" fill="${c1}" stroke="none"/><path d="M 0,84 C -40,84 -50,49 -50,49 -50,79 -27,99 0,99 27,99 50,79 50,49 50,49 40,84 0,84 Z" fill="${c2}"/><path d="M 100,84 C 60,84 50,49 50,49 c 0,30 23,50 50,50 27,0 50,-20 50,-50 0,0 -10,35 -50,35 z" fill="${c2}"/><path d="M 50,35 C 10,35 0,0 0,0 0,30 23,50 50,50 77,50 100,30 100,0 100,0 90,35 50,35 Z" fill="${c2}"/></pattern>`,
plumetty: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.25
}" viewBox="0 0 50 100" stroke-width=".8"><rect width="50" height="100" fill="${c2}" stroke="none"/><path fill="${c1}" stroke="none" d="M 25,100 C 44,88 49.5,74 50,50 33.5,40 25,25 25,4e-7 25,25 16.5,40 0,50 0.5,74 6,88 25,100 Z"/><path fill="none" stroke="${c2}" d="m17 40c5.363 2.692 10.7 2.641 16 0m-19 7c7.448 4.105 14.78 3.894 22 0m-27 7c6-2 10.75 3.003 16 3 5.412-0.0031 10-5 16-3m-35 9c4-7 12 3 19 2 7 1 15-9 19-2m-35 6c6-2 11 3 16 3s10-5 16-3m-30 7c8 0 8 3 14 3s7-3 14-3m-25 8c7.385 4.048 14.72 3.951 22 0m-19 8c5.455 2.766 10.78 2.566 16 0m-8 6v-78"/><g fill="none" stroke="${c1}"><path d="m42 90c2.678 1.344 5.337 2.004 8 2m-11 5c3.686 2.032 7.344 3.006 10.97 3m0.0261-1.2e-4v-30"/><path d="m0 92c2.689 0.0045 5.328-0.6687 8-2m-8 10c3.709-0.0033 7.348-1.031 11-3m-11 3v-30"/><path d="m0 7c5.412-0.0031 10-5 16-3m-16 11c7 1 15-9 19-2m-19 9c5 0 10-5 16-3m-16 10c6 0 7-3 14-3m-14.02 11c3.685-0.002185 7.357-1.014 11.02-3m-11 10c2.694-0.01117 5.358-0.7036 7.996-2m-8 6v-48"/><path d="m34 4c6-2 10.75 3.003 16 3m-19 6c4-7 12 3 19 2m-16 4c6-2 11 3 16 3m-14 4c8 0 8 3 14 3m-11 5c3.641 1.996 7.383 2.985 11 3m-8 5c2.762 1.401 5.303 2.154 8.002 2.112m-0.00154 3.888v-48"/></g></pattern>`,
masoned: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.125}" height="${size * 0.125}" viewBox="0 0 100 100" fill="none"><rect width="100" height="100" fill="${c1}"/><rect width="100" height="50" stroke="${c2}" stroke-width="4"/><line x1="50" y1="50" x2="50" y2="100" stroke="${c2}" stroke-width="5"/></pattern>`,
fretty: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.2}" height="${size * 0.2}" viewBox="0 0 140 140" stroke="#000" stroke-width="2"><rect width="140" height="140" fill="${c1}" stroke="none"/><path d="m-15 5 150 150 20-20-150-150z" fill="${c2}"/><path d="m10 150 140-140-20-20-140 140z" fill="${c2}" stroke="none"/><path d="m0 120 20 20 120-120-20-20z" fill="none"/></pattern>`,
grillage: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.25}" height="${size * 0.25}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect width="200" height="200" fill="${c1}" stroke="none"/><path d="m205 65v-30h-210v30z" fill="${c2}"/><path d="m65-5h-30v210h30z" fill="${c2}"/><path d="m205 165v-30h-210v30z" fill="${c2}"/><path d="m165,65h-30v140h30z" fill="${c2}"/><path d="m 165,-5h-30v40h30z" fill="${c2}"/></pattern>`,
chainy: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.167}" height="${size * 0.167}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect x="-6.691e-6" width="200" height="200" fill="${c1}" stroke="none"/><path d="m155-5-20-20-160 160 20 20z" fill="${c2}"/><path d="m45 205 160-160 20 20-160 160z" fill="${c2}"/><path d="m45-5 20-20 160 160-20 20-160-160" fill="${c2}"/><path d="m-5 45-20 20 160 160 20-20-160-160" fill="${c2}"/></pattern>`,
masoned: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.125}" height="${
size * 0.125
}" viewBox="0 0 100 100" fill="none"><rect width="100" height="100" fill="${c1}"/><rect width="100" height="50" stroke="${c2}" stroke-width="4"/><line x1="50" y1="50" x2="50" y2="100" stroke="${c2}" stroke-width="5"/></pattern>`,
fretty: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.2}" height="${
size * 0.2
}" viewBox="0 0 140 140" stroke="#000" stroke-width="2"><rect width="140" height="140" fill="${c1}" stroke="none"/><path d="m-15 5 150 150 20-20-150-150z" fill="${c2}"/><path d="m10 150 140-140-20-20-140 140z" fill="${c2}" stroke="none"/><path d="m0 120 20 20 120-120-20-20z" fill="none"/></pattern>`,
grillage: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.25}" height="${
size * 0.25
}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect width="200" height="200" fill="${c1}" stroke="none"/><path d="m205 65v-30h-210v30z" fill="${c2}"/><path d="m65-5h-30v210h30z" fill="${c2}"/><path d="m205 165v-30h-210v30z" fill="${c2}"/><path d="m165,65h-30v140h30z" fill="${c2}"/><path d="m 165,-5h-30v40h30z" fill="${c2}"/></pattern>`,
chainy: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.167}" height="${
size * 0.167
}" viewBox="0 0 200 200" stroke="#000" stroke-width="2"><rect x="-6.691e-6" width="200" height="200" fill="${c1}" stroke="none"/><path d="m155-5-20-20-160 160 20 20z" fill="${c2}"/><path d="m45 205 160-160 20 20-160 160z" fill="${c2}"/><path d="m45-5 20-20 160 160-20 20-160-160" fill="${c2}"/><path d="m-5 45-20 20 160 160 20-20-160-160" fill="${c2}"/></pattern>`,
maily: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.167}" height="${
size * 0.167
}" viewBox="0 0 200 200" stroke="#000" stroke-width="1.2"><path fill="${c1}" stroke="none" d="M0 0h200v200H0z"/><g fill="${c2}"><path d="m80-2c-5.27e-4 2.403-0.1094 6.806-0.3262 9.199 5.014-1.109 10.1-1.768 15.19-2.059 0.09325-1.712 0.1401-5.426 0.1406-7.141z"/><path d="m100 5a95 95 0 0 0-95 95 95 95 0 0 0 95 95 95 95 0 0 0 95-95 95 95 0 0 0-95-95zm0 15a80 80 0 0 1 80 80 80 80 0 0 1-80 80 80 80 0 0 1-80-80 80 80 0 0 1 80-80z"/><path d="m92.8 20.33c-5.562 0.4859-11.04 1.603-16.34 3.217-7.793 25.31-27.61 45.12-52.91 52.91-5.321 1.638-10.8 2.716-16.34 3.217-2.394 0.2168-6.796 0.3256-9.199 0.3262v15c1.714-4.79e-4 5.429-0.04737 7.141-0.1406 5.109-0.2761 10.19-0.9646 15.19-2.059 36.24-7.937 64.54-36.24 72.47-72.47z"/><path d="m202 80c-2.403-5.31e-4 -6.806-0.1094-9.199-0.3262 1.109 5.014 1.768 10.1 2.059 15.19 1.712 0.09326 5.426 0.1401 7.141 0.1406z"/><path d="m179.7 92.8c-0.4859-5.562-1.603-11.04-3.217-16.34-25.31-7.793-45.12-27.61-52.91-52.91-1.638-5.321-2.716-10.8-3.217-16.34-0.2168-2.394-0.3256-6.796-0.3262-9.199h-15c4.8e-4 1.714 0.0474 5.429 0.1406 7.141 0.2761 5.109 0.9646 10.19 2.059 15.19 7.937 36.24 36.24 64.54 72.47 72.47z"/><path d="m120 202c5.3e-4 -2.403 0.1094-6.806 0.3262-9.199-5.014 1.109-10.1 1.768-15.19 2.059-0.0933 1.712-0.1402 5.426-0.1406 7.141z"/><path d="m107.2 179.7c5.562-0.4859 11.04-1.603 16.34-3.217 7.793-25.31 27.61-45.12 52.91-52.91 5.321-1.638 10.8-2.716 16.34-3.217 2.394-0.2168 6.796-0.3256 9.199-0.3262v-15c-1.714 4.7e-4 -5.429 0.0474-7.141 0.1406-5.109 0.2761-10.19 0.9646-15.19 2.059-36.24 7.937-64.54 36.24-72.47 72.47z"/><path d="m -2,120 c 2.403,5.4e-4 6.806,0.1094 9.199,0.3262 -1.109,-5.014 -1.768,-10.1 -2.059,-15.19 -1.712,-0.0933 -5.426,-0.1402 -7.141,-0.1406 z"/><path d="m 20.33,107.2 c 0.4859,5.562 1.603,11.04 3.217,16.34 25.31,7.793 45.12,27.61 52.91,52.91 1.638,5.321 2.716,10.8 3.217,16.34 0.2168,2.394 0.3256,6.796 0.3262,9.199 L 95,202 c -4.8e-4,-1.714 -0.0472,-5.44 -0.1404,-7.152 -0.2761,-5.109 -0.9646,-10.19 -2.059,-15.19 -7.937,-36.24 -36.24,-64.54 -72.47,-72.47 z"/></g></pattern>`,
honeycombed: (p, c1, c2, size) => `<pattern id="${p}" width="${size * 0.143}" height="${size * 0.24514}" viewBox="0 0 70 120"><rect width="70" height="120" fill="${c1}"/><path d="M 70,0 V 20 L 35,40 m 35,80 V 100 L 35,80 M 0,120 V 100 L 35,80 V 40 L 0,20 V 0" stroke="${c2}" fill="none" stroke-width="3"/></pattern>`
honeycombed: (p, c1, c2, size) =>
`<pattern id="${p}" width="${size * 0.143}" height="${
size * 0.24514
}" viewBox="0 0 70 120"><rect width="70" height="120" fill="${c1}"/><path d="M 70,0 V 20 L 35,40 m 35,80 V 100 L 35,80 M 0,120 V 100 L 35,80 V 40 L 0,20 V 0" stroke="${c2}" fill="none" stroke-width="3"/></pattern>`
};
const draw = async function (id, coa) {
const {shield, division, ordinaries = [], charges = []} = coa;
const ordinariesRegular = ordinaries.filter(o => !o.above);
const ordinariesAboveCharges = ordinaries.filter(o => o.above);
const ordinariesRegular = ordinaries.filter((o) => !o.above);
const ordinariesAboveCharges = ordinaries.filter((o) => o.above);
const shieldPath = shieldPaths[shield];
const tDiv = division ? (division.t.includes("-") ? division.t.split("-")[1] : division.t) : null;
const tDiv = division ? (division.t.includes('-') ? division.t.split('-')[1] : division.t) : null;
const positions = shieldPositions[shield];
const sizeModifier = shieldSize[shield] || 1;
const viewBox = shieldBox[shield] || "0 0 200 200";
const viewBox = shieldBox[shield] || '0 0 200 200';
const shieldClip = `<clipPath id="${shield}_${id}"><path d="${shieldPath}"/></clipPath>`;
const divisionClip = division ? `<clipPath id="divisionClip_${id}">${getTemplate(division.division, division.line)}</clipPath>` : "";
const divisionClip = division ? `<clipPath id="divisionClip_${id}">${getTemplate(division.division, division.line)}</clipPath>` : '';
const loadedCharges = await getCharges(coa, id, shieldPath);
const loadedPatterns = getPatterns(coa, id);
const blacklight = `<radialGradient id="backlight_${id}" cx="100%" cy="100%" r="150%"><stop stop-color="#fff" stop-opacity=".3" offset="0"/><stop stop-color="#fff" stop-opacity=".15" offset=".25"/><stop stop-color="#000" stop-opacity="0" offset="1"/></radialGradient>`;
const field = `<rect x="0" y="0" width="200" height="200" fill="${clr(coa.t1)}"/>`;
const divisionGroup = division ? templateDivision() : "";
const divisionGroup = division ? templateDivision() : '';
const overlay = `<path d="${shieldPath}" fill="url(#backlight_${id})" stroke="#333"/>`;
const svg = `<svg id="${id}" width="200" height="200" viewBox="${viewBox}">
@ -1678,67 +1804,67 @@ window.COArenderer = (function () {
${overlay}</svg>`;
// insert coa svg to defs
document.getElementById("coas").insertAdjacentHTML("beforeend", svg);
document.getElementById('coas').insertAdjacentHTML('beforeend', svg);
return true;
function templateDivision() {
let svg = "";
let svg = '';
// In field part
for (const ordinary of ordinariesRegular) {
if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
if (ordinary.divided === 'field') svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === 'counter') svg += templateOrdinary(ordinary, tDiv);
}
for (const charge of charges) {
if (charge.divided === "field") svg += templateCharge(charge, charge.t);
else if (charge.divided === "counter") svg += templateCharge(charge, tDiv);
if (charge.divided === 'field') svg += templateCharge(charge, charge.t);
else if (charge.divided === 'counter') svg += templateCharge(charge, tDiv);
}
for (const ordinary of ordinariesAboveCharges) {
if (ordinary.divided === "field") svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, tDiv);
if (ordinary.divided === 'field') svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === 'counter') svg += templateOrdinary(ordinary, tDiv);
}
// In division part
svg += `<g clip-path="url(#divisionClip_${id})"><rect x="0" y="0" width="200" height="200" fill="${clr(division.t)}"/>`;
for (const ordinary of ordinariesRegular) {
if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
if (ordinary.divided === 'division') svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === 'counter') svg += templateOrdinary(ordinary, coa.t1);
}
for (const charge of charges) {
if (charge.divided === "division") svg += templateCharge(charge, charge.t);
else if (charge.divided === "counter") svg += templateCharge(charge, coa.t1);
if (charge.divided === 'division') svg += templateCharge(charge, charge.t);
else if (charge.divided === 'counter') svg += templateCharge(charge, coa.t1);
}
for (const ordinary of ordinariesAboveCharges) {
if (ordinary.divided === "division") svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === "counter") svg += templateOrdinary(ordinary, coa.t1);
if (ordinary.divided === 'division') svg += templateOrdinary(ordinary, ordinary.t);
else if (ordinary.divided === 'counter') svg += templateOrdinary(ordinary, coa.t1);
}
return (svg += `</g>`);
}
function templateAboveAll() {
let svg = "";
let svg = '';
ordinariesRegular
.filter(o => !o.divided)
.forEach(ordinary => {
.filter((o) => !o.divided)
.forEach((ordinary) => {
svg += templateOrdinary(ordinary, ordinary.t);
});
charges
.filter(o => !o.divided || !division)
.forEach(charge => {
.filter((o) => !o.divided || !division)
.forEach((charge) => {
svg += templateCharge(charge, charge.t);
});
ordinariesAboveCharges
.filter(o => !o.divided)
.forEach(ordinary => {
.filter((o) => !o.divided)
.forEach((ordinary) => {
svg += templateOrdinary(ordinary, ordinary.t);
});
@ -1748,17 +1874,17 @@ window.COArenderer = (function () {
function templateOrdinary(ordinary, tincture) {
const fill = clr(tincture);
let svg = `<g fill="${fill}" stroke="none">`;
if (ordinary.ordinary === "bordure") svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="16.7%"/>`;
else if (ordinary.ordinary === "orle") svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="5%" transform="scale(.85)" transform-origin="center">`;
if (ordinary.ordinary === 'bordure') svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="16.7%"/>`;
else if (ordinary.ordinary === 'orle') svg += `<path d="${shieldPath}" fill="none" stroke="${fill}" stroke-width="5%" transform="scale(.85)" transform-origin="center">`;
else svg += getTemplate(ordinary.ordinary, ordinary.line);
return svg + `</g>`;
}
function templateCharge(charge, tincture) {
const fill = clr(tincture);
const chargePositions = [...new Set(charge.p)].filter(position => positions[position]);
const chargePositions = [...new Set(charge.p)].filter((position) => positions[position]);
let svg = "";
let svg = '';
svg += `<g fill="${fill}" stroke="#000">`;
for (const p of chargePositions) {
const transform = getElTransform(charge, p);
@ -1780,69 +1906,69 @@ window.COArenderer = (function () {
};
async function getCharges(coa, id, shieldPath) {
let charges = coa.charges ? coa.charges.map(charge => charge.charge) : []; // add charges
let charges = coa.charges ? coa.charges.map((charge) => charge.charge) : []; // add charges
if (semy(coa.t1)) charges.push(semy(coa.t1)); // add field semy charge
if (semy(coa.division?.t)) charges.push(semy(coa.division.t)); // add division semy charge
const uniqueCharges = [...new Set(charges)];
const fetchedCharges = await Promise.all(
uniqueCharges.map(async charge => {
if (charge === "inescutcheon") return `<g id="inescutcheon_${id}"><path transform="translate(66 66) scale(.34)" d="${shieldPath}"/></g>`;
uniqueCharges.map(async (charge) => {
if (charge === 'inescutcheon') return `<g id="inescutcheon_${id}"><path transform="translate(66 66) scale(.34)" d="${shieldPath}"/></g>`;
const fetched = await fetchCharge(charge, id);
return fetched;
})
);
return fetchedCharges.join("");
return fetchedCharges.join('');
}
const url = PRODUCTION ? "./charges/" : "http://armoria.herokuapp.com/charges/"; // on local machine fetch files from server
const url = location.hostname ? './charges/' : 'http://armoria.herokuapp.com/charges/'; // on local machine fetch files from server
async function fetchCharge(charge, id) {
const fetched = fetch(url + charge + ".svg")
.then(res => {
const fetched = fetch(url + charge + '.svg')
.then((res) => {
if (res.ok) return res.text();
else throw new Error("Cannot fetch charge");
else throw new Error('Cannot fetch charge');
})
.then(text => {
const html = document.createElement("html");
.then((text) => {
const html = document.createElement('html');
html.innerHTML = text;
const g = html.querySelector("g");
g.setAttribute("id", charge + "_" + id);
const g = html.querySelector('g');
g.setAttribute('id', charge + '_' + id);
return g.outerHTML;
})
.catch(err => console.error(err));
.catch((err) => console.error(err));
return fetched;
}
function getPatterns(coa, id) {
const isPattern = string => string.includes("-");
const isPattern = (string) => string.includes('-');
let patternsToAdd = [];
if (coa.t1.includes("-")) patternsToAdd.push(coa.t1); // add field pattern
if (coa.t1.includes('-')) patternsToAdd.push(coa.t1); // add field pattern
if (coa.division && isPattern(coa.division.t)) patternsToAdd.push(coa.division.t); // add division pattern
if (coa.ordinaries) coa.ordinaries.filter(ordinary => isPattern(ordinary.t)).forEach(ordinary => patternsToAdd.push(ordinary.t)); // add ordinaries pattern
if (coa.charges) coa.charges.filter(charge => isPattern(charge.t)).forEach(charge => patternsToAdd.push(charge.t)); // add charges pattern
if (!patternsToAdd.length) return "";
if (coa.ordinaries) coa.ordinaries.filter((ordinary) => isPattern(ordinary.t)).forEach((ordinary) => patternsToAdd.push(ordinary.t)); // add ordinaries pattern
if (coa.charges) coa.charges.filter((charge) => isPattern(charge.t)).forEach((charge) => patternsToAdd.push(charge.t)); // add charges pattern
if (!patternsToAdd.length) return '';
return [...new Set(patternsToAdd)]
.map(patternString => {
const [pattern, t1, t2, size] = patternString.split("-");
.map((patternString) => {
const [pattern, t1, t2, size] = patternString.split('-');
const charge = semy(patternString);
if (charge) return patterns.semy(patternString, clr(t1), clr(t2), getSizeMod(size), charge + "_" + id);
if (charge) return patterns.semy(patternString, clr(t1), clr(t2), getSizeMod(size), charge + '_' + id);
return patterns[pattern](patternString, clr(t1), clr(t2), getSizeMod(size), charge);
})
.join("");
.join('');
}
function getSizeMod(size) {
if (size === "small") return 0.8;
if (size === "smaller") return 0.5;
if (size === "smallest") return 0.25;
if (size === "big") return 1.6;
if (size === 'small') return 0.8;
if (size === 'smaller') return 0.5;
if (size === 'smallest') return 0.25;
if (size === 'big') return 1.6;
return 1;
}
function getTemplate(id, line) {
const linedId = id + "Lined";
if (!line || line === "straight" || !templates[linedId]) return templates[id];
const linedId = id + 'Lined';
if (!line || line === 'straight' || !templates[linedId]) return templates[id];
const linePath = lines[line];
return templates[linedId](linePath);
}
@ -1862,8 +1988,8 @@ window.COArenderer = (function () {
// render coa if does not exist
const trigger = async function (id, coa) {
if (coa === "custom") {
console.warn("Cannot render custom emblem", coa);
if (coa === 'custom') {
console.warn('Cannot render custom emblem', coa);
return;
}
if (!coa) {
@ -1874,15 +2000,15 @@ window.COArenderer = (function () {
};
const add = function (type, i, coa, x, y) {
const id = type + "COA" + i;
const g = document.getElementById(type + "Emblems");
const id = type + 'COA' + i;
const g = document.getElementById(type + 'Emblems');
if (emblems.selectAll("use").size()) {
const size = +g.getAttribute("font-size") || 50;
if (emblems.selectAll('use').size()) {
const size = +g.getAttribute('font-size') || 50;
const use = `<use data-i="${i}" x="${x - size / 2}" y="${y - size / 2}" width="1em" height="1em" href="#${id}"/>`;
g.insertAdjacentHTML("beforeend", use);
g.insertAdjacentHTML('beforeend', use);
}
if (layerIsOn("toggleEmblems")) trigger(id, coa);
if (layerIsOn('toggleEmblems')) trigger(id, coa);
};
return {trigger, add, shieldPaths};

View file

@ -1,30 +1,30 @@
"use strict";
'use strict';
window.Cultures = (function () {
let cells;
const generate = function () {
TIME && console.time("generateCultures");
TIME && console.time('generateCultures');
cells = pack.cells;
cells.culture = new Uint16Array(cells.i.length); // cell cultures
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
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) {
count = Math.floor(populated.length / 50);
if (!count) {
WARN && console.warn(`There are no populated cells. Cannot generate cultures`);
pack.cultures = [{name: "Wildlands", i: 0, base: 1, shield: "round"}];
pack.cultures = [{name: 'Wildlands', i: 0, base: 1, shield: 'round'}];
alertMessage.innerHTML = `
The climate is harsh and people cannot live in this world.<br>
No cultures, states and burgs will be created.<br>
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({
$('#alert').dialog({
resizable: false,
title: "Extreme climate warning",
title: 'Extreme climate warning',
buttons: {
Ok: function () {
$(this).dialog("close");
$(this).dialog('close');
}
}
});
@ -35,12 +35,12 @@ window.Cultures = (function () {
There are only ${populated.length} populated cells and it's insufficient livable area.<br>
Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br>
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({
$('#alert').dialog({
resizable: false,
title: "Extreme climate warning",
title: 'Extreme climate warning',
buttons: {
Ok: function () {
$(this).dialog("close");
$(this).dialog('close');
}
}
});
@ -50,11 +50,11 @@ window.Cultures = (function () {
const cultures = (pack.cultures = selectCultures(count));
const centers = d3.quadtree();
const colors = getColors(count);
const emblemShape = document.getElementById("emblemShape").value;
const emblemShape = document.getElementById('emblemShape').value;
const codes = [];
cultures.forEach(function (c, i) {
const cell = (c.center = placeCenter(c.sort ? c.sort : i => cells.s[i]));
const cell = (c.center = placeCenter(c.sort ? c.sort : (i) => cells.s[i]));
centers.add(cells.p[cell]);
c.i = i + 1;
delete c.odd;
@ -66,7 +66,7 @@ window.Cultures = (function () {
c.code = abbreviate(c.name, codes);
codes.push(c.code);
cells.culture[cell] = i + 1;
if (emblemShape === "random") c.shield = getRandomShield();
if (emblemShape === 'random') c.shield = getRandomShield();
});
function placeCenter(v) {
@ -82,20 +82,20 @@ window.Cultures = (function () {
}
// the first culture with id 0 is for wildlands
cultures.unshift({name: "Wildlands", i: 0, base: 1, origin: null, shield: "round"});
cultures.unshift({name: 'Wildlands', i: 0, base: 1, origin: null, shield: 'round'});
// make sure all bases exist in nameBases
if (!nameBases.length) {
ERROR && console.error("Name base is empty, default nameBases will be applied");
ERROR && console.error('Name base is empty, default nameBases will be applied');
nameBases = Names.getNameBases();
}
cultures.forEach(c => (c.base = c.base % nameBases.length));
cultures.forEach((c) => (c.base = c.base % nameBases.length));
function selectCultures(c) {
let def = getDefault(c);
if (c === def.length) return def;
if (def.every(d => d.odd === 1)) return def.splice(0, c);
if (def.every((d) => d.odd === 1)) return def.splice(0, c);
const count = Math.min(c, def.length);
const cultures = [];
@ -113,28 +113,28 @@ window.Cultures = (function () {
// set culture type based on culture center position
function defineCultureType(i) {
if (cells.h[i] < 70 && [1, 2, 4].includes(cells.biome[i])) return "Nomadic"; // high penalty in forest biomes and near coastline
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
if (cells.h[i] < 70 && [1, 2, 4].includes(cells.biome[i])) return 'Nomadic'; // high penalty in forest biomes and near coastline
if (cells.h[i] > 50) return 'Highland'; // no penalty for hills and moutains, high for other elevations
const f = pack.features[cells.f[cells.haven[i]]]; // opposite feature
if (f.type === "lake" && f.cells > 5) return "Lake"; // low water cross penalty and high for growth not along coastline
if ((cells.harbor[i] && f.type !== "lake" && P(0.1)) || (cells.harbor[i] === 1 && P(0.6)) || (pack.features[cells.f[i]].group === "isle" && P(0.4))) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
if (cells.t[i] > 2 && [3, 7, 8, 9, 10, 12].includes(cells.biome[i])) return "Hunting"; // high penalty in non-native biomes
return "Generic";
if (f.type === 'lake' && f.cells > 5) return 'Lake'; // low water cross penalty and high for growth not along coastline
if ((cells.harbor[i] && f.type !== 'lake' && P(0.1)) || (cells.harbor[i] === 1 && P(0.6)) || (pack.features[cells.f[i]].group === 'isle' && P(0.4))) return 'Naval'; // low water cross penalty and high for non-along-coastline growth
if (cells.r[i] && cells.fl[i] > 100) return 'River'; // no River cross penalty, penalty for non-River growth
if (cells.t[i] > 2 && [3, 7, 8, 9, 10, 12].includes(cells.biome[i])) return 'Hunting'; // high penalty in non-native biomes
return 'Generic';
}
function defineCultureExpansionism(type) {
let base = 1; // Generic
if (type === "Lake") base = 0.8;
else if (type === "Naval") base = 1.5;
else if (type === "River") base = 0.9;
else if (type === "Nomadic") base = 1.5;
else if (type === "Hunting") base = 0.7;
else if (type === "Highland") base = 1.2;
if (type === 'Lake') base = 0.8;
else if (type === 'Naval') base = 1.5;
else if (type === 'River') base = 0.9;
else if (type === 'Nomadic') base = 1.5;
else if (type === 'Hunting') base = 0.7;
else if (type === 'Highland') base = 1.2;
return rn(((Math.random() * powerInput.value) / 2 + 1) * base, 1);
}
TIME && console.timeEnd("generateCultures");
TIME && console.timeEnd('generateCultures');
};
const add = function (center) {
@ -149,22 +149,22 @@ window.Cultures = (function () {
} else {
// add random culture besed on one of the current ones
culture = rand(pack.cultures.length - 1);
name = Names.getCulture(culture, 5, 8, "");
name = Names.getCulture(culture, 5, 8, '');
base = pack.cultures[culture].base;
}
const code = abbreviate(
name,
pack.cultures.map(c => c.code)
pack.cultures.map((c) => c.code)
);
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
// define emblem shape
let shield = culture.shield;
const emblemShape = document.getElementById("emblemShape").value;
if (emblemShape === "random") shield = getRandomShield();
const emblemShape = document.getElementById('emblemShape').value;
if (emblemShape === 'random') shield = getRandomShield();
pack.cultures.push({name, color, base, center, i, expansionism: 1, type: "Generic", cells: 0, area: 0, rural: 0, urban: 0, origin: 0, code, shield});
pack.cultures.push({name, color, base, center, i, expansionism: 1, type: 'Generic', cells: 0, area: 0, rural: 0, urban: 0, origin: 0, code, shield});
};
const getDefault = function (count) {
@ -175,156 +175,156 @@ window.Cultures = (function () {
t = cells.t,
h = cells.h,
temp = grid.cells.temp;
const n = cell => Math.ceil((s[cell] / sMax) * 3); // normalized cell score
const n = (cell) => Math.ceil((s[cell] / sMax) * 3); // normalized cell score
const td = (cell, goal) => {
const d = Math.abs(temp[cells.g[cell]] - goal);
return d ? d + 1 : 1;
}; // temperature difference fee
const bd = (cell, biomes, fee = 4) => (biomes.includes(cells.biome[cell]) ? 1 : fee); // biome difference fee
const sf = (cell, fee = 4) => (cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== "lake" ? 1 : fee); // not on sea coast fee
const sf = (cell, fee = 4) => (cells.haven[cell] && pack.features[cells.f[cells.haven[cell]]].type !== 'lake' ? 1 : fee); // not on sea coast fee
if (culturesSet.value === "european") {
if (culturesSet.value === 'european') {
return [
{name: "Shwazen", base: 0, odd: 1, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "swiss"},
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "wedged"},
{name: "Luari", base: 2, odd: 1, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "french"},
{name: "Tallian", base: 3, odd: 1, sort: i => n(i) / td(i, 15), shield: "horsehead"},
{name: "Astellian", base: 4, odd: 1, sort: i => n(i) / td(i, 16), shield: "spanish"},
{name: "Slovan", base: 5, odd: 1, sort: i => (n(i) / td(i, 6)) * t[i], shield: "polish"},
{name: "Norse", base: 6, odd: 1, sort: i => n(i) / td(i, 5), shield: "heater"},
{name: "Elladan", base: 7, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 15) / t[i], shield: "roman"},
{name: "Soumi", base: 9, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"},
{name: "Portuzian", base: 13, odd: 1, sort: i => n(i) / td(i, 17) / sf(i), shield: "renaissance"},
{name: "Vengrian", base: 15, odd: 1, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "horsehead2"},
{name: "Turchian", base: 16, odd: 0.05, sort: i => n(i) / td(i, 14), shield: "round"},
{name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "oldFrench"},
{name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "oval"}
{name: 'Shwazen', base: 0, odd: 1, sort: (i) => n(i) / td(i, 10) / bd(i, [6, 8]), shield: 'swiss'},
{name: 'Angshire', base: 1, odd: 1, sort: (i) => n(i) / td(i, 10) / sf(i), shield: 'wedged'},
{name: 'Luari', base: 2, odd: 1, sort: (i) => n(i) / td(i, 12) / bd(i, [6, 8]), shield: 'french'},
{name: 'Tallian', base: 3, odd: 1, sort: (i) => n(i) / td(i, 15), shield: 'horsehead'},
{name: 'Astellian', base: 4, odd: 1, sort: (i) => n(i) / td(i, 16), shield: 'spanish'},
{name: 'Slovan', base: 5, odd: 1, sort: (i) => (n(i) / td(i, 6)) * t[i], shield: 'polish'},
{name: 'Norse', base: 6, odd: 1, sort: (i) => n(i) / td(i, 5), shield: 'heater'},
{name: 'Elladan', base: 7, odd: 1, sort: (i) => (n(i) / td(i, 18)) * h[i], shield: 'boeotian'},
{name: 'Romian', base: 8, odd: 0.2, sort: (i) => n(i) / td(i, 15) / t[i], shield: 'roman'},
{name: 'Soumi', base: 9, odd: 1, sort: (i) => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: 'pavise'},
{name: 'Portuzian', base: 13, odd: 1, sort: (i) => n(i) / td(i, 17) / sf(i), shield: 'renaissance'},
{name: 'Vengrian', base: 15, odd: 1, sort: (i) => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: 'horsehead2'},
{name: 'Turchian', base: 16, odd: 0.05, sort: (i) => n(i) / td(i, 14), shield: 'round'},
{name: 'Euskati', base: 20, odd: 0.05, sort: (i) => (n(i) / td(i, 15)) * h[i], shield: 'oldFrench'},
{name: 'Keltan', base: 22, odd: 0.05, sort: (i) => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: 'oval'}
];
}
if (culturesSet.value === "oriental") {
if (culturesSet.value === 'oriental') {
return [
{name: "Koryo", base: 10, odd: 1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 1, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Turchian", base: 16, odd: 1, sort: i => n(i) / td(i, 12), shield: "round"},
{name: "Berberan", base: 17, odd: 0.2, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "oval"},
{name: "Eurabic", base: 18, odd: 1, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "oval"},
{name: "Efratic", base: 23, odd: 0.1, sort: i => (n(i) / td(i, 22)) * t[i], shield: "round"},
{name: "Tehrani", base: 24, odd: 1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"},
{name: "Maui", base: 25, odd: 0.2, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "vesicaPiscis"},
{name: "Carnatic", base: 26, odd: 0.5, sort: i => n(i) / td(i, 26), shield: "round"},
{name: "Vietic", base: 29, odd: 0.8, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"},
{name: "Guantzu", base: 30, odd: 0.5, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}
{name: 'Koryo', base: 10, odd: 1, sort: (i) => n(i) / td(i, 12) / t[i], shield: 'round'},
{name: 'Hantzu', base: 11, odd: 1, sort: (i) => n(i) / td(i, 13), shield: 'banner'},
{name: 'Yamoto', base: 12, odd: 1, sort: (i) => n(i) / td(i, 15) / t[i], shield: 'round'},
{name: 'Turchian', base: 16, odd: 1, sort: (i) => n(i) / td(i, 12), shield: 'round'},
{name: 'Berberan', base: 17, odd: 0.2, sort: (i) => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: 'oval'},
{name: 'Eurabic', base: 18, odd: 1, sort: (i) => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: 'oval'},
{name: 'Efratic', base: 23, odd: 0.1, sort: (i) => (n(i) / td(i, 22)) * t[i], shield: 'round'},
{name: 'Tehrani', base: 24, odd: 1, sort: (i) => (n(i) / td(i, 18)) * h[i], shield: 'round'},
{name: 'Maui', base: 25, odd: 0.2, sort: (i) => n(i) / td(i, 24) / sf(i) / t[i], shield: 'vesicaPiscis'},
{name: 'Carnatic', base: 26, odd: 0.5, sort: (i) => n(i) / td(i, 26), shield: 'round'},
{name: 'Vietic', base: 29, odd: 0.8, sort: (i) => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: 'banner'},
{name: 'Guantzu', base: 30, odd: 0.5, sort: (i) => n(i) / td(i, 17), shield: 'banner'},
{name: 'Ulus', base: 31, odd: 1, sort: (i) => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: 'banner'}
];
}
if (culturesSet.value === "english") {
const getName = () => Names.getBase(1, 5, 9, "", 0);
if (culturesSet.value === 'english') {
const getName = () => Names.getBase(1, 5, 9, '', 0);
return [
{name: getName(), base: 1, odd: 1, shield: "heater"},
{name: getName(), base: 1, odd: 1, shield: "wedged"},
{name: getName(), base: 1, odd: 1, shield: "swiss"},
{name: getName(), base: 1, odd: 1, shield: "oldFrench"},
{name: getName(), base: 1, odd: 1, shield: "swiss"},
{name: getName(), base: 1, odd: 1, shield: "spanish"},
{name: getName(), base: 1, odd: 1, shield: "hessen"},
{name: getName(), base: 1, odd: 1, shield: "fantasy5"},
{name: getName(), base: 1, odd: 1, shield: "fantasy4"},
{name: getName(), base: 1, odd: 1, shield: "fantasy1"}
{name: getName(), base: 1, odd: 1, shield: 'heater'},
{name: getName(), base: 1, odd: 1, shield: 'wedged'},
{name: getName(), base: 1, odd: 1, shield: 'swiss'},
{name: getName(), base: 1, odd: 1, shield: 'oldFrench'},
{name: getName(), base: 1, odd: 1, shield: 'swiss'},
{name: getName(), base: 1, odd: 1, shield: 'spanish'},
{name: getName(), base: 1, odd: 1, shield: 'hessen'},
{name: getName(), base: 1, odd: 1, shield: 'fantasy5'},
{name: getName(), base: 1, odd: 1, shield: 'fantasy4'},
{name: getName(), base: 1, odd: 1, shield: 'fantasy1'}
];
}
if (culturesSet.value === "antique") {
if (culturesSet.value === 'antique') {
return [
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 15) / sf(i), shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 16) / sf(i), shield: "roman"}, // Roman
{name: "Roman", base: 8, odd: 1, sort: i => n(i) / td(i, 17) / t[i], shield: "roman"}, // Roman
{name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"}, // Greek
{name: "Hellenic", base: 7, odd: 1, sort: i => (n(i) / td(i, 19) / sf(i)) * h[i], shield: "boeotian"}, // Greek
{name: "Macedonian", base: 7, odd: 0.5, sort: i => (n(i) / td(i, 12)) * h[i], shield: "round"}, // Greek
{name: "Celtic", base: 22, odd: 1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "round"},
{name: "Germanic", base: 0, odd: 1, sort: i => n(i) / td(i, 10) ** 0.5 / bd(i, [6, 8]), shield: "round"},
{name: "Persian", base: 24, odd: 0.8, sort: i => (n(i) / td(i, 18)) * h[i], shield: "oval"}, // Iranian
{name: "Scythian", base: 24, odd: 0.5, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [4]), shield: "round"}, // Iranian
{name: "Cantabrian", base: 20, odd: 0.5, sort: i => (n(i) / td(i, 16)) * h[i], shield: "oval"}, // Basque
{name: "Estian", base: 9, odd: 0.2, sort: i => (n(i) / td(i, 5)) * t[i], shield: "pavise"}, // Finnic
{name: "Carthaginian", base: 17, odd: 0.3, sort: i => n(i) / td(i, 19) / sf(i), shield: "oval"}, // Berber
{name: "Mesopotamian", base: 23, odd: 0.2, sort: i => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield: "oval"} // Mesopotamian
{name: 'Roman', base: 8, odd: 1, sort: (i) => n(i) / td(i, 14) / t[i], shield: 'roman'}, // Roman
{name: 'Roman', base: 8, odd: 1, sort: (i) => n(i) / td(i, 15) / sf(i), shield: 'roman'}, // Roman
{name: 'Roman', base: 8, odd: 1, sort: (i) => n(i) / td(i, 16) / sf(i), shield: 'roman'}, // Roman
{name: 'Roman', base: 8, odd: 1, sort: (i) => n(i) / td(i, 17) / t[i], shield: 'roman'}, // Roman
{name: 'Hellenic', base: 7, odd: 1, sort: (i) => (n(i) / td(i, 18) / sf(i)) * h[i], shield: 'boeotian'}, // Greek
{name: 'Hellenic', base: 7, odd: 1, sort: (i) => (n(i) / td(i, 19) / sf(i)) * h[i], shield: 'boeotian'}, // Greek
{name: 'Macedonian', base: 7, odd: 0.5, sort: (i) => (n(i) / td(i, 12)) * h[i], shield: 'round'}, // Greek
{name: 'Celtic', base: 22, odd: 1, sort: (i) => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: 'round'},
{name: 'Germanic', base: 0, odd: 1, sort: (i) => n(i) / td(i, 10) ** 0.5 / bd(i, [6, 8]), shield: 'round'},
{name: 'Persian', base: 24, odd: 0.8, sort: (i) => (n(i) / td(i, 18)) * h[i], shield: 'oval'}, // Iranian
{name: 'Scythian', base: 24, odd: 0.5, sort: (i) => n(i) / td(i, 11) ** 0.5 / bd(i, [4]), shield: 'round'}, // Iranian
{name: 'Cantabrian', base: 20, odd: 0.5, sort: (i) => (n(i) / td(i, 16)) * h[i], shield: 'oval'}, // Basque
{name: 'Estian', base: 9, odd: 0.2, sort: (i) => (n(i) / td(i, 5)) * t[i], shield: 'pavise'}, // Finnic
{name: 'Carthaginian', base: 17, odd: 0.3, sort: (i) => n(i) / td(i, 19) / sf(i), shield: 'oval'}, // Berber
{name: 'Mesopotamian', base: 23, odd: 0.2, sort: (i) => n(i) / td(i, 22) / bd(i, [1, 2, 3]), shield: 'oval'} // Mesopotamian
];
}
if (culturesSet.value === "highFantasy") {
if (culturesSet.value === 'highFantasy') {
return [
// fantasy races
{name: "Quenian (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "gondor"}, // Elves
{name: "Eldar (Elfish)", base: 33, odd: 1, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "noldor"}, // Elves
{name: "Trow (Dark Elfish)", base: 34, odd: 0.9, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves
{name: "Lothian (Dark Elfish)", base: 34, odd: 0.3, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "wedged"}, // Dark Elves
{name: "Dunirr (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "ironHills"}, // Dwarfs
{name: "Khazadur (Dwarven)", base: 35, odd: 1, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarfs
{name: "Kobold (Goblin)", base: 36, odd: 1, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin
{name: "Uruk (Orkish)", base: 37, odd: 1, sort: i => h[i] * t[i], shield: "urukHai"}, // Orc
{name: "Ugluk (Orkish)", base: 37, odd: 0.5, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "moriaOrc"}, // Orc
{name: "Yotunn (Giants)", base: 38, odd: 0.7, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name: "Rake (Drakonic)", base: 39, odd: 0.7, sort: i => -s[i], shield: "fantasy2"}, // Draconic
{name: "Arago (Arachnid)", base: 40, odd: 0.7, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid
{name: "Aj'Snaga (Serpents)", base: 41, odd: 0.7, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"}, // Serpents
{name: 'Quenian (Elfish)', base: 33, odd: 1, sort: (i) => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: 'gondor'}, // Elves
{name: 'Eldar (Elfish)', base: 33, odd: 1, sort: (i) => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: 'noldor'}, // Elves
{name: 'Trow (Dark Elfish)', base: 34, odd: 0.9, sort: (i) => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: 'hessen'}, // Dark Elves
{name: 'Lothian (Dark Elfish)', base: 34, odd: 0.3, sort: (i) => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: 'wedged'}, // Dark Elves
{name: 'Dunirr (Dwarven)', base: 35, odd: 1, sort: (i) => n(i) + h[i], shield: 'ironHills'}, // Dwarfs
{name: 'Khazadur (Dwarven)', base: 35, odd: 1, sort: (i) => n(i) + h[i], shield: 'erebor'}, // Dwarfs
{name: 'Kobold (Goblin)', base: 36, odd: 1, sort: (i) => t[i] - s[i], shield: 'moriaOrc'}, // Goblin
{name: 'Uruk (Orkish)', base: 37, odd: 1, sort: (i) => h[i] * t[i], shield: 'urukHai'}, // Orc
{name: 'Ugluk (Orkish)', base: 37, odd: 0.5, sort: (i) => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: 'moriaOrc'}, // Orc
{name: 'Yotunn (Giants)', base: 38, odd: 0.7, sort: (i) => td(i, -10), shield: 'pavise'}, // Giant
{name: 'Rake (Drakonic)', base: 39, odd: 0.7, sort: (i) => -s[i], shield: 'fantasy2'}, // Draconic
{name: 'Arago (Arachnid)', base: 40, odd: 0.7, sort: (i) => t[i] - s[i], shield: 'horsehead2'}, // Arachnid
{name: "Aj'Snaga (Serpents)", base: 41, odd: 0.7, sort: (i) => n(i) / bd(i, [12], 10), shield: 'fantasy1'}, // Serpents
// fantasy human
{name: "Anor (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 10), shield: "fantasy5"},
{name: "Dail (Human)", base: 32, odd: 1, sort: i => n(i) / td(i, 13), shield: "roman"},
{name: "Rohand (Human)", base: 16, odd: 1, sort: i => n(i) / td(i, 16), shield: "round"},
{name: "Dulandir (Human)", base: 31, odd: 1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "easterling"}
{name: 'Anor (Human)', base: 32, odd: 1, sort: (i) => n(i) / td(i, 10), shield: 'fantasy5'},
{name: 'Dail (Human)', base: 32, odd: 1, sort: (i) => n(i) / td(i, 13), shield: 'roman'},
{name: 'Rohand (Human)', base: 16, odd: 1, sort: (i) => n(i) / td(i, 16), shield: 'round'},
{name: 'Dulandir (Human)', base: 31, odd: 1, sort: (i) => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: 'easterling'}
];
}
if (culturesSet.value === "darkFantasy") {
if (culturesSet.value === 'darkFantasy') {
return [
// common real-world English
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"},
{name: "Enlandic", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"},
{name: "Westen", base: 1, odd: 1, sort: i => n(i) / td(i, 10), shield: "heater"},
{name: "Nortumbic", base: 1, odd: 1, sort: i => n(i) / td(i, 7), shield: "heater"},
{name: "Mercian", base: 1, odd: 1, sort: i => n(i) / td(i, 9), shield: "heater"},
{name: "Kentian", base: 1, odd: 1, sort: i => n(i) / td(i, 12), shield: "heater"},
{name: 'Angshire', base: 1, odd: 1, sort: (i) => n(i) / td(i, 10) / sf(i), shield: 'heater'},
{name: 'Enlandic', base: 1, odd: 1, sort: (i) => n(i) / td(i, 12), shield: 'heater'},
{name: 'Westen', base: 1, odd: 1, sort: (i) => n(i) / td(i, 10), shield: 'heater'},
{name: 'Nortumbic', base: 1, odd: 1, sort: (i) => n(i) / td(i, 7), shield: 'heater'},
{name: 'Mercian', base: 1, odd: 1, sort: (i) => n(i) / td(i, 9), shield: 'heater'},
{name: 'Kentian', base: 1, odd: 1, sort: (i) => n(i) / td(i, 12), shield: 'heater'},
// rare real-world western
{name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5) / sf(i), shield: "oldFrench"},
{name: "Schwarzen", base: 0, odd: 0.3, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "gonfalon"},
{name: "Luarian", base: 2, odd: 0.3, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"},
{name: "Hetallian", base: 3, odd: 0.3, sort: i => n(i) / td(i, 15), shield: "oval"},
{name: "Astellian", base: 4, odd: 0.3, sort: i => n(i) / td(i, 16), shield: "spanish"},
{name: 'Norse', base: 6, odd: 0.7, sort: (i) => n(i) / td(i, 5) / sf(i), shield: 'oldFrench'},
{name: 'Schwarzen', base: 0, odd: 0.3, sort: (i) => n(i) / td(i, 10) / bd(i, [6, 8]), shield: 'gonfalon'},
{name: 'Luarian', base: 2, odd: 0.3, sort: (i) => n(i) / td(i, 12) / bd(i, [6, 8]), shield: 'oldFrench'},
{name: 'Hetallian', base: 3, odd: 0.3, sort: (i) => n(i) / td(i, 15), shield: 'oval'},
{name: 'Astellian', base: 4, odd: 0.3, sort: (i) => n(i) / td(i, 16), shield: 'spanish'},
// rare real-world exotic
{name: "Kiswaili", base: 28, odd: 0.05, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"},
{name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"},
{name: "Koryo", base: 10, odd: 0.05, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 0.05, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 0.05, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Guantzu", base: 30, odd: 0.05, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 0.05, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"},
{name: "Turan", base: 16, odd: 0.05, sort: i => n(i) / td(i, 12), shield: "round"},
{name: "Berberan", base: 17, odd: 0.05, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"},
{name: "Eurabic", base: 18, odd: 0.05, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"},
{name: "Slovan", base: 5, odd: 0.05, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"},
{name: "Keltan", base: 22, odd: 0.1, sort: i => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: "vesicaPiscis"},
{name: "Elladan", base: 7, odd: 0.2, sort: i => (n(i) / td(i, 18) / sf(i)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.2, sort: i => n(i) / td(i, 14) / t[i], shield: "roman"},
{name: 'Kiswaili', base: 28, odd: 0.05, sort: (i) => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: 'vesicaPiscis'},
{name: 'Yoruba', base: 21, odd: 0.05, sort: (i) => n(i) / td(i, 15) / bd(i, [5, 7]), shield: 'vesicaPiscis'},
{name: 'Koryo', base: 10, odd: 0.05, sort: (i) => n(i) / td(i, 12) / t[i], shield: 'round'},
{name: 'Hantzu', base: 11, odd: 0.05, sort: (i) => n(i) / td(i, 13), shield: 'banner'},
{name: 'Yamoto', base: 12, odd: 0.05, sort: (i) => n(i) / td(i, 15) / t[i], shield: 'round'},
{name: 'Guantzu', base: 30, odd: 0.05, sort: (i) => n(i) / td(i, 17), shield: 'banner'},
{name: 'Ulus', base: 31, odd: 0.05, sort: (i) => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: 'banner'},
{name: 'Turan', base: 16, odd: 0.05, sort: (i) => n(i) / td(i, 12), shield: 'round'},
{name: 'Berberan', base: 17, odd: 0.05, sort: (i) => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: 'round'},
{name: 'Eurabic', base: 18, odd: 0.05, sort: (i) => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: 'round'},
{name: 'Slovan', base: 5, odd: 0.05, sort: (i) => (n(i) / td(i, 6)) * t[i], shield: 'round'},
{name: 'Keltan', base: 22, odd: 0.1, sort: (i) => n(i) / td(i, 11) ** 0.5 / bd(i, [6, 8]), shield: 'vesicaPiscis'},
{name: 'Elladan', base: 7, odd: 0.2, sort: (i) => (n(i) / td(i, 18) / sf(i)) * h[i], shield: 'boeotian'},
{name: 'Romian', base: 8, odd: 0.2, sort: (i) => n(i) / td(i, 14) / t[i], shield: 'roman'},
// fantasy races
{name: "Eldar", base: 33, odd: 0.5, sort: i => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: "fantasy5"}, // Elves
{name: "Trow", base: 34, odd: 0.8, sort: i => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: "hessen"}, // Dark Elves
{name: "Durinn", base: 35, odd: 0.8, sort: i => n(i) + h[i], shield: "erebor"}, // Dwarven
{name: "Kobblin", base: 36, odd: 0.8, sort: i => t[i] - s[i], shield: "moriaOrc"}, // Goblin
{name: "Uruk", base: 37, odd: 0.8, sort: i => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: "urukHai"}, // Orc
{name: "Yotunn", base: 38, odd: 0.8, sort: i => td(i, -10), shield: "pavise"}, // Giant
{name: "Drake", base: 39, odd: 0.9, sort: i => -s[i], shield: "fantasy2"}, // Draconic
{name: "Rakhnid", base: 40, odd: 0.9, sort: i => t[i] - s[i], shield: "horsehead2"}, // Arachnid
{name: "Aj'Snaga", base: 41, odd: 0.9, sort: i => n(i) / bd(i, [12], 10), shield: "fantasy1"} // Serpents
{name: 'Eldar', base: 33, odd: 0.5, sort: (i) => (n(i) / bd(i, [6, 7, 8, 9], 10)) * t[i], shield: 'fantasy5'}, // Elves
{name: 'Trow', base: 34, odd: 0.8, sort: (i) => (n(i) / bd(i, [7, 8, 9, 12], 10)) * t[i], shield: 'hessen'}, // Dark Elves
{name: 'Durinn', base: 35, odd: 0.8, sort: (i) => n(i) + h[i], shield: 'erebor'}, // Dwarven
{name: 'Kobblin', base: 36, odd: 0.8, sort: (i) => t[i] - s[i], shield: 'moriaOrc'}, // Goblin
{name: 'Uruk', base: 37, odd: 0.8, sort: (i) => (h[i] * t[i]) / bd(i, [1, 2, 10, 11]), shield: 'urukHai'}, // Orc
{name: 'Yotunn', base: 38, odd: 0.8, sort: (i) => td(i, -10), shield: 'pavise'}, // Giant
{name: 'Drake', base: 39, odd: 0.9, sort: (i) => -s[i], shield: 'fantasy2'}, // Draconic
{name: 'Rakhnid', base: 40, odd: 0.9, sort: (i) => t[i] - s[i], shield: 'horsehead2'}, // Arachnid
{name: "Aj'Snaga", base: 41, odd: 0.9, sort: (i) => n(i) / bd(i, [12], 10), shield: 'fantasy1'} // Serpents
];
}
if (culturesSet.value === "random") {
if (culturesSet.value === 'random') {
return d3.range(count).map(function () {
const rnd = rand(nameBases.length - 1);
const name = Names.getBaseShort(rnd);
@ -334,44 +334,44 @@ window.Cultures = (function () {
// all-world
return [
{name: "Shwazen", base: 0, odd: 0.7, sort: i => n(i) / td(i, 10) / bd(i, [6, 8]), shield: "hessen"},
{name: "Angshire", base: 1, odd: 1, sort: i => n(i) / td(i, 10) / sf(i), shield: "heater"},
{name: "Luari", base: 2, odd: 0.6, sort: i => n(i) / td(i, 12) / bd(i, [6, 8]), shield: "oldFrench"},
{name: "Tallian", base: 3, odd: 0.6, sort: i => n(i) / td(i, 15), shield: "horsehead2"},
{name: "Astellian", base: 4, odd: 0.6, sort: i => n(i) / td(i, 16), shield: "spanish"},
{name: "Slovan", base: 5, odd: 0.7, sort: i => (n(i) / td(i, 6)) * t[i], shield: "round"},
{name: "Norse", base: 6, odd: 0.7, sort: i => n(i) / td(i, 5), shield: "heater"},
{name: "Elladan", base: 7, odd: 0.7, sort: i => (n(i) / td(i, 18)) * h[i], shield: "boeotian"},
{name: "Romian", base: 8, odd: 0.7, sort: i => n(i) / td(i, 15), shield: "roman"},
{name: "Soumi", base: 9, odd: 0.3, sort: i => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: "pavise"},
{name: "Koryo", base: 10, odd: 0.1, sort: i => n(i) / td(i, 12) / t[i], shield: "round"},
{name: "Hantzu", base: 11, odd: 0.1, sort: i => n(i) / td(i, 13), shield: "banner"},
{name: "Yamoto", base: 12, odd: 0.1, sort: i => n(i) / td(i, 15) / t[i], shield: "round"},
{name: "Portuzian", base: 13, odd: 0.4, sort: i => n(i) / td(i, 17) / sf(i), shield: "spanish"},
{name: "Nawatli", base: 14, odd: 0.1, sort: i => h[i] / td(i, 18) / bd(i, [7]), shield: "square"},
{name: "Vengrian", base: 15, odd: 0.2, sort: i => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: "wedged"},
{name: "Turchian", base: 16, odd: 0.2, sort: i => n(i) / td(i, 13), shield: "round"},
{name: "Berberan", base: 17, odd: 0.1, sort: i => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: "round"},
{name: "Eurabic", base: 18, odd: 0.2, sort: i => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: "round"},
{name: "Inuk", base: 19, odd: 0.05, sort: i => td(i, -1) / bd(i, [10, 11]) / sf(i), shield: "square"},
{name: "Euskati", base: 20, odd: 0.05, sort: i => (n(i) / td(i, 15)) * h[i], shield: "spanish"},
{name: "Yoruba", base: 21, odd: 0.05, sort: i => n(i) / td(i, 15) / bd(i, [5, 7]), shield: "vesicaPiscis"},
{name: "Keltan", base: 22, odd: 0.05, sort: i => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: "vesicaPiscis"},
{name: "Efratic", base: 23, odd: 0.05, sort: i => (n(i) / td(i, 22)) * t[i], shield: "diamond"},
{name: "Tehrani", base: 24, odd: 0.1, sort: i => (n(i) / td(i, 18)) * h[i], shield: "round"},
{name: "Maui", base: 25, odd: 0.05, sort: i => n(i) / td(i, 24) / sf(i) / t[i], shield: "round"},
{name: "Carnatic", base: 26, odd: 0.05, sort: i => n(i) / td(i, 26), shield: "round"},
{name: "Inqan", base: 27, odd: 0.05, sort: i => h[i] / td(i, 13), shield: "square"},
{name: "Kiswaili", base: 28, odd: 0.1, sort: i => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: "vesicaPiscis"},
{name: "Vietic", base: 29, odd: 0.1, sort: i => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: "banner"},
{name: "Guantzu", base: 30, odd: 0.1, sort: i => n(i) / td(i, 17), shield: "banner"},
{name: "Ulus", base: 31, odd: 0.1, sort: i => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: "banner"}
{name: 'Shwazen', base: 0, odd: 0.7, sort: (i) => n(i) / td(i, 10) / bd(i, [6, 8]), shield: 'hessen'},
{name: 'Angshire', base: 1, odd: 1, sort: (i) => n(i) / td(i, 10) / sf(i), shield: 'heater'},
{name: 'Luari', base: 2, odd: 0.6, sort: (i) => n(i) / td(i, 12) / bd(i, [6, 8]), shield: 'oldFrench'},
{name: 'Tallian', base: 3, odd: 0.6, sort: (i) => n(i) / td(i, 15), shield: 'horsehead2'},
{name: 'Astellian', base: 4, odd: 0.6, sort: (i) => n(i) / td(i, 16), shield: 'spanish'},
{name: 'Slovan', base: 5, odd: 0.7, sort: (i) => (n(i) / td(i, 6)) * t[i], shield: 'round'},
{name: 'Norse', base: 6, odd: 0.7, sort: (i) => n(i) / td(i, 5), shield: 'heater'},
{name: 'Elladan', base: 7, odd: 0.7, sort: (i) => (n(i) / td(i, 18)) * h[i], shield: 'boeotian'},
{name: 'Romian', base: 8, odd: 0.7, sort: (i) => n(i) / td(i, 15), shield: 'roman'},
{name: 'Soumi', base: 9, odd: 0.3, sort: (i) => (n(i) / td(i, 5) / bd(i, [9])) * t[i], shield: 'pavise'},
{name: 'Koryo', base: 10, odd: 0.1, sort: (i) => n(i) / td(i, 12) / t[i], shield: 'round'},
{name: 'Hantzu', base: 11, odd: 0.1, sort: (i) => n(i) / td(i, 13), shield: 'banner'},
{name: 'Yamoto', base: 12, odd: 0.1, sort: (i) => n(i) / td(i, 15) / t[i], shield: 'round'},
{name: 'Portuzian', base: 13, odd: 0.4, sort: (i) => n(i) / td(i, 17) / sf(i), shield: 'spanish'},
{name: 'Nawatli', base: 14, odd: 0.1, sort: (i) => h[i] / td(i, 18) / bd(i, [7]), shield: 'square'},
{name: 'Vengrian', base: 15, odd: 0.2, sort: (i) => (n(i) / td(i, 11) / bd(i, [4])) * t[i], shield: 'wedged'},
{name: 'Turchian', base: 16, odd: 0.2, sort: (i) => n(i) / td(i, 13), shield: 'round'},
{name: 'Berberan', base: 17, odd: 0.1, sort: (i) => (n(i) / td(i, 19) / bd(i, [1, 2, 3], 7)) * t[i], shield: 'round'},
{name: 'Eurabic', base: 18, odd: 0.2, sort: (i) => (n(i) / td(i, 26) / bd(i, [1, 2], 7)) * t[i], shield: 'round'},
{name: 'Inuk', base: 19, odd: 0.05, sort: (i) => td(i, -1) / bd(i, [10, 11]) / sf(i), shield: 'square'},
{name: 'Euskati', base: 20, odd: 0.05, sort: (i) => (n(i) / td(i, 15)) * h[i], shield: 'spanish'},
{name: 'Yoruba', base: 21, odd: 0.05, sort: (i) => n(i) / td(i, 15) / bd(i, [5, 7]), shield: 'vesicaPiscis'},
{name: 'Keltan', base: 22, odd: 0.05, sort: (i) => (n(i) / td(i, 11) / bd(i, [6, 8])) * t[i], shield: 'vesicaPiscis'},
{name: 'Efratic', base: 23, odd: 0.05, sort: (i) => (n(i) / td(i, 22)) * t[i], shield: 'diamond'},
{name: 'Tehrani', base: 24, odd: 0.1, sort: (i) => (n(i) / td(i, 18)) * h[i], shield: 'round'},
{name: 'Maui', base: 25, odd: 0.05, sort: (i) => n(i) / td(i, 24) / sf(i) / t[i], shield: 'round'},
{name: 'Carnatic', base: 26, odd: 0.05, sort: (i) => n(i) / td(i, 26), shield: 'round'},
{name: 'Inqan', base: 27, odd: 0.05, sort: (i) => h[i] / td(i, 13), shield: 'square'},
{name: 'Kiswaili', base: 28, odd: 0.1, sort: (i) => n(i) / td(i, 29) / bd(i, [1, 3, 5, 7]), shield: 'vesicaPiscis'},
{name: 'Vietic', base: 29, odd: 0.1, sort: (i) => n(i) / td(i, 25) / bd(i, [7], 7) / t[i], shield: 'banner'},
{name: 'Guantzu', base: 30, odd: 0.1, sort: (i) => n(i) / td(i, 17), shield: 'banner'},
{name: 'Ulus', base: 31, odd: 0.1, sort: (i) => (n(i) / td(i, 5) / bd(i, [2, 4, 10], 7)) * t[i], shield: 'banner'}
];
};
// expand cultures across the map (Dijkstra-like algorithm)
const expand = function () {
TIME && console.time("expandCultures");
TIME && console.time('expandCultures');
cells = pack.cells;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
@ -407,41 +407,41 @@ window.Cultures = (function () {
});
}
TIME && console.timeEnd("expandCultures");
TIME && console.timeEnd('expandCultures');
};
function getBiomeCost(c, biome, type) {
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
if (type === 'Hunting') return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
if (type === 'Nomadic' && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
return biomesData.cost[biome] * 2; // general non-native biome penalty
}
function getHeightCost(i, h, type) {
const f = pack.features[cells.f[i]],
a = cells.area[i];
if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures
if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
if (type === 'Lake' && f.type === 'lake') return 10; // no lake crossing penalty for Lake cultures
if (type === 'Naval' && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
if (type === 'Nomadic' && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
if (h < 20) return a * 6; // general sea/lake crossing penalty
if (type === "Highland" && h < 44) return 3000; // giant penalty for highlanders on lowlands
if (type === "Highland" && h < 62) return 200; // giant penalty for highlanders on lowhills
if (type === "Highland") return 0; // no penalty for highlanders on highlands
if (type === 'Highland' && h < 44) return 3000; // giant penalty for highlanders on lowlands
if (type === 'Highland' && h < 62) return 200; // giant penalty for highlanders on lowhills
if (type === 'Highland') return 0; // no penalty for highlanders on highlands
if (h >= 67) return 200; // general mountains crossing penalty
if (h >= 44) return 30; // general hills crossing penalty
return 0;
}
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (type === 'River') return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100); // river penalty from 20 to 100 based on flux
return minmax(cells.fl[i] / 10, 20, 100); // river penalty from 20 to 100 based on flux
}
function getTypeCost(t, type) {
if (t === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
if (t === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
if (t !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
if (t === 1) return type === 'Naval' || type === 'Lake' ? 0 : type === 'Nomadic' ? 60 : 20; // penalty for coastline
if (t === 2) return type === 'Naval' || type === 'Nomadic' ? 30 : 0; // low penalty for land level 2 for Navals and nomads
if (t !== -1) return type === 'Naval' || type === 'Lake' ? 100 : 0; // penalty for mainland for navals
return 0;
}

494
modules/export.js Normal file
View file

@ -0,0 +1,494 @@
'use strict';
// Functions to export map to image or data files
// download map as SVG
async function saveSVG() {
TIME && console.time('saveSVG');
const url = await getMapURL('svg', {fullMap: true});
const link = document.createElement('a');
link.download = getFileName() + '.svg';
link.href = url;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
TIME && console.timeEnd('saveSVG');
}
// download map as PNG
async function savePNG() {
TIME && console.time('savePNG');
const url = await getMapURL('png');
const link = document.createElement('a');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + '.png';
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
}, 1000);
});
};
TIME && console.timeEnd('savePNG');
}
// download map as JPEG
async function saveJPEG() {
TIME && console.time('saveJPEG');
const url = await getMapURL('png');
const canvas = document.createElement('canvas');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = async function () {
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL('image/jpeg', quality);
const link = document.createElement('a');
link.download = getFileName() + '.jpeg';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
};
TIME && console.timeEnd('saveJPEG');
}
// download map as png tiles
async function saveTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL('tiles', {debug: true, fullMap: true});
const zip = new JSZip();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = graphWidth;
canvas.height = graphHeight;
const imgSchema = new Image();
imgSchema.src = urlSchema;
imgSchema.onload = function () {
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => zip.file(`fmg_tile_schema.png`, blob));
};
// download tiles
const url = await getMapURL('tiles', {fullMap: true});
const tilesX = +document.getElementById('tileColsInput').value;
const tilesY = +document.getElementById('tileRowsInput').value;
const scale = +document.getElementById('tileScaleInput').value;
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
let loaded = 0;
const img = new Image();
img.src = url;
img.onload = function () {
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob((blob) => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
function downloadZip() {
const name = `${getFileName()}.zip`;
zip.generateAsync({type: 'blob'}).then((blob) => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true);
});
}
});
}
// parse map svg to object url
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false, fullMap = false} = options;
if (fullMap) drawScaleBar(1);
const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (!debug) clone.select('#debug')?.remove();
const cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox && type === 'mesh') clone.select('#oceanPattern')?.remove();
if (globe) clone.select('#scaleBar')?.remove();
if (noLabels) {
clone.select('#labels #states')?.remove();
clone.select('#labels #burgLabels')?.remove();
clone.select('#icons #burgIcons')?.remove();
}
if (noWater) {
clone.select('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').attr('opacity', 0);
}
if (fullMap) {
// reset transform to show the whole map
clone.attr('width', graphWidth).attr('height', graphHeight);
clone.select('#viewbox').attr('transform', null);
drawScaleBar(scale);
}
if (type === 'svg') removeUnusedElements(clone);
if (customization && type === 'mesh') updateMeshCells(clone);
inlineStyle(clone);
// remove unused filters
const filters = cloneEl.querySelectorAll('filter');
for (let i = 0; i < filters.length; i++) {
const id = filters[i].id;
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
if (cloneEl.getAttribute('filter') === 'url(#' + id + ')') continue;
filters[i].remove();
}
// remove unused patterns
const patterns = cloneEl.querySelectorAll('pattern');
for (let i = 0; i < patterns.length; i++) {
const id = patterns[i].id;
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
patterns[i].remove();
}
// remove unused symbols
const symbols = cloneEl.querySelectorAll('symbol');
for (let i = 0; i < symbols.length; i++) {
const id = symbols[i].id;
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
symbols[i].remove();
}
// add displayed emblems
if (layerIsOn('toggleEmblems') && emblems.selectAll('use').size()) {
cloneEl
.getElementById('emblems')
?.querySelectorAll('use')
.forEach((el) => {
const href = el.getAttribute('href') || el.getAttribute('xlink:href');
if (!href) return;
const emblem = document.getElementById(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true));
});
} else {
cloneDefs.querySelector('#defs-emblems')?.remove();
}
// replace ocean pattern href to base64
if (location.hostname && cloneEl.getElementById('oceanicPattern')) {
const el = cloneEl.getElementById('oceanicPattern');
const url = el.getAttribute('href');
await new Promise((resolve) => {
getBase64(url, (base64) => {
el.setAttribute('href', base64);
resolve();
});
});
}
// add relief icons
if (cloneEl.getElementById('terrain')) {
const uniqueElements = new Set();
const terrainNodes = cloneEl.getElementById('terrain').childNodes;
for (let i = 0; i < terrainNodes.length; i++) {
const href = terrainNodes[i].getAttribute('href') || terrainNodes[i].getAttribute('xlink:href');
uniqueElements.add(href);
}
const defsRelief = svgDefs.getElementById('defs-relief');
for (const terrain of [...uniqueElements]) {
const element = defsRelief.querySelector(terrain);
if (element) cloneDefs.appendChild(element.cloneNode(true));
}
}
// add wind rose
if (cloneEl.getElementById('compass')) {
const rose = svgDefs.getElementById('rose');
if (rose) cloneDefs.appendChild(rose.cloneNode(true));
}
// add port icon
if (cloneEl.getElementById('anchors')) {
const anchor = svgDefs.getElementById('icon-anchor');
if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
}
// add grid pattern
if (cloneEl.getElementById('gridOverlay')?.hasChildNodes()) {
const type = cloneEl.getElementById('gridOverlay').getAttribute('type');
const pattern = svgDefs.getElementById('pattern_' + type);
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
if (!cloneEl.getElementById('hatching').children.length) cloneEl.getElementById('hatching')?.remove(); // remove unused hatching group
if (!cloneEl.getElementById('fogging-cont')) cloneEl.getElementById('fog')?.remove(); // remove unused fog
if (!cloneEl.getElementById('regions')) cloneEl.getElementById('statePaths')?.remove(); // removed unused statePaths
if (!cloneEl.getElementById('labels')) cloneEl.getElementById('textPaths')?.remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById('armies'))
cloneEl.insertAdjacentHTML(
'afterbegin',
'<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>'
);
// add xlink: for href to support svg1.1
if (type === 'svg') {
cloneEl.querySelectorAll('[href]').forEach((el) => {
const href = el.getAttribute('href');
el.removeAttribute('href');
el.setAttribute('xlink:href', href);
});
}
const usedFonts = getUsedFonts(cloneEl);
const fontsToLoad = usedFonts.filter((font) => font.src);
if (fontsToLoad.length) {
const dataURLfonts = await loadFontsAsDataURI(fontsToLoad);
const fontFaces = dataURLfonts
.map(({family, src, unicodeRange = '', variant = 'normal'}) => {
return `@font-face {font-family: "${family}"; src: ${src}; unicode-range: ${unicodeRange}; font-variant: ${variant};}`;
})
.join('\n');
const style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = fontFaces;
cloneEl.querySelector('defs').appendChild(style);
}
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
return url;
}
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
function removeUnusedElements(clone) {
if (!terrain.selectAll('use').size()) clone.select('#defs-relief')?.remove();
for (let empty = 1; empty; ) {
empty = 0;
clone.selectAll('g').each(function () {
if (!this.hasChildNodes() || this.style.display === 'none' || this.classList.contains('hidden')) {
empty++;
this.remove();
}
if (this.hasAttribute('display') && this.style.display === 'inline') this.removeAttribute('display');
});
}
}
function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter((i) => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
clone.select('#heights').attr('filter', 'url(#blur1)');
clone
.select('#heights')
.selectAll('polygon')
.data(data)
.join('polygon')
.attr('points', (d) => getGridPolygon(d))
.attr('id', (d) => 'cell' + d)
.attr('stroke', (d) => getColor(grid.cells.h[d], scheme));
}
// for each g element get inline style
function inlineStyle(clone) {
const emptyG = clone.append('g').node();
const defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll('g, #ruler *, #scaleBar > text').each(function () {
const compStyle = window.getComputedStyle(this);
let style = '';
for (let i = 0; i < compStyle.length; i++) {
const key = compStyle[i];
const value = compStyle.getPropertyValue(key);
// Firefox mask hack
if (key === 'mask-image' && value !== defaultStyles.getPropertyValue(key)) {
style += "mask-image: url('#land');";
continue;
}
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
for (const key in compStyle) {
const value = compStyle.getPropertyValue(key);
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
if (style != '') this.setAttribute('style', style);
});
emptyG.remove();
}
function saveGeoJSON_Cells() {
const json = {type: 'FeatureCollection', features: []};
const cells = pack.cells;
const getPopulation = (i) => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = (i) => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
cells.i.forEach((i) => {
const coordinates = getCellCoordinates(cells.v[i]);
const height = getHeight(i);
const biome = cells.biome[i];
const type = pack.features[cells.f[i]].type;
const population = getPopulation(i);
const state = cells.state[i];
const province = cells.province[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const neighbors = cells.c[i];
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
const feature = {type: 'Feature', geometry: {type: 'Polygon', coordinates}, properties};
json.features.push(feature);
});
const name = getFileName('Cells') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Routes() {
const json = {type: 'FeatureCollection', features: []};
routes.selectAll('g > path').each(function () {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, type}};
json.features.push(feature);
});
const name = getFileName('Routes') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Rivers() {
const json = {type: 'FeatureCollection', features: []};
rivers.selectAll('path').each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find((r) => r.i === +id.slice(5));
const name = river ? river.name : '';
const type = river ? river.type : '';
const i = river ? river.i : '';
const basin = river ? river.basin : '';
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, i, basin, name, type, width, increment}};
json.features.push(feature);
});
const name = getFileName('Rivers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Markers() {
const features = pack.markers.map((marker) => {
const {i, type, icon, x, y, size, fill, stroke} = marker;
const coordinates = getQGIScoordinates(x, y);
const id = `marker${i}`;
const note = notes.find((note) => note.id === id);
const properties = {id, type, icon, ...note, size, fill, stroke};
return {type: 'Feature', geometry: {type: 'Point', coordinates}, properties};
});
const json = {type: 'FeatureCollection', features};
const fileName = getFileName('Markers') + '.geojson';
downloadFile(JSON.stringify(json), fileName, 'application/json');
}
function getCellCoordinates(vertices) {
const p = pack.vertices.p;
const coordinates = vertices.map((n) => getQGIScoordinates(p[n][0], p[n][1]));
return [coordinates.concat([coordinates[0]])];
}
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getQGIScoordinates(p.x, p.y));
}
return points;
}
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] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x, y]);
}
return points;
}

View file

@ -1,141 +1,271 @@
// helper finctions to work with fonts
'use strict';
async function addFonts(url) {
$("head").append('<link rel="stylesheet" type="text/css" href="' + url + '">');
const fonts = [
{family: 'Arial'},
{family: 'Times New Roman'},
{family: 'Georgia'},
{family: 'Garamond'},
{family: 'Lucida Sans Unicode'},
{family: 'Courier New'},
{family: 'Verdana'},
{family: 'Impact'},
{family: 'Comic Sans MS'},
{family: 'Papyrus'},
{
family: 'Almendra SC',
src: 'url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Amatic SC',
src: 'url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Architects Daughter',
src: 'url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.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: 'Bitter',
src: 'url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.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: 'Caesar Dressing',
src: 'url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Cinzel',
src: 'url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.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: 'Dancing Script',
src: 'url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.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: 'Fredericka the Great',
src: 'url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Gloria Hallelujah',
src: 'url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.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: 'Great Vibes',
src: 'url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.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: 'IM Fell English',
src: 'url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215'
},
{
family: 'Kaushan Script',
src: 'url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)',
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: 'MedievalSharp',
src: 'url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Metamorphous',
src: 'url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Montez',
src: 'url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.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: 'Nova Script',
src: 'url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Orbitron',
src: 'url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.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: 'Satisfy',
src: 'url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.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: 'Shadows Into Light',
src: 'url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.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: 'Uncial Antiqua',
src: 'url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Underdog',
src: 'url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)',
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
},
{
family: 'Yellowtail',
src: 'url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.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'
}
];
declareDefaultFonts(); // execute once on load
function declareFont(font) {
const {family, src, ...rest} = font;
if (!src) return;
const fontFace = new FontFace(family, src, {...rest, display: 'block'});
document.fonts.add(fontFace);
addFontOption(family);
}
function declareDefaultFonts() {
fonts.forEach((font) => {
if (font.src) declareFont(font);
else addFontOption(font.family);
});
}
function getUsedFonts(svg) {
const usedFontFamilies = new Set();
const labelGroups = svg.querySelectorAll('#labels g');
for (const labelGroup of labelGroups) {
const font = labelGroup.getAttribute('font-family');
if (font) usedFontFamilies.add(font);
}
const provinceFont = provs.attr('font-family');
if (provinceFont) usedFontFamilies.add(provinceFont);
const legend = svg.querySelector('#legend');
const legendFont = legend?.getAttribute('font-family');
if (legendFont) usedFontFamilies.add(legendFont);
const usedFonts = fonts.filter((font) => usedFontFamilies.has(font.family));
return usedFonts;
}
function addFontOption(family) {
const options = document.getElementById('styleSelectFont');
// const existingOption = options.querySelector(`[value="${family}"]`);
// if (existingOption) return;
const option = document.createElement('option');
option.value = family;
option.innerText = family;
option.style.fontFamily = family;
options.add(option);
}
async function fetchGoogleFont(family) {
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, '+')}`;
try {
const resp = await fetch(url);
const text = await resp.text();
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
let FontRule = rule_1 => {
let family = rule_1.style.getPropertyValue("font-family");
let font = family.replace(/['"]+/g, "").replace(/ /g, "+");
let weight = rule_1.style.getPropertyValue("font-weight");
if (weight && weight !== "400") font += ":" + weight;
if (fonts.indexOf(font) == -1) {
fonts.push(font);
fetched++;
}
};
let fetched = 0;
for (let r of styleSheet.cssRules) {
FontRule(r);
}
document.head.removeChild(s);
return fetched;
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
const fonts = fontFaceRules.map((fontFace) => {
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
const src = `url(${srcURL})`;
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
const font = {family, src};
if (unicodeRange) font.unicodeRange = unicodeRange;
if (variant && variant !== 'normal') font.variant = variant;
return font;
});
return fonts;
} catch (err) {
return ERROR && console.error(err);
ERROR && console.error(err);
return null;
}
}
function loadUsedFonts() {
const fontsInUse = getFontsList(svg);
const fontsToLoad = fontsInUse.filter(font => !fonts.includes(font));
if (fontsToLoad?.length) {
const url = "https://fonts.googleapis.com/css?family=" + fontsToLoad.join("|");
addFonts(url);
}
}
function getFontsList(svg) {
const fontsInUse = [];
svg.selectAll("#labels > g").each(function () {
if (!this.hasChildNodes()) return;
const font = this.dataset.font;
if (font) fontsInUse.push(font);
});
if (legend?.node()?.hasChildNodes()) fontsInUse.push(legend.attr("data-font"));
return [...new Set(fontsInUse)];
}
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
function GFontToDataURI(url) {
if (!url) return Promise.resolve();
return fetch(url) // first fecth the embed stylesheet page
.then(resp => resp.text()) // we only need the text of it
.then(text => {
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
const styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
const FontRule = rule => {
const src = rule.style.getPropertyValue("src");
const url = src ? src.split("url(")[1].split(")")[0] : "";
return {rule, src, url: url.substring(url.length - 1, 1)};
};
const fontProms = [];
for (const r of styleSheet.cssRules) {
let fR = FontRule(r);
if (!fR.url) continue;
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then(resp => resp.blob())
.then(blob => {
return new Promise(resolve => {
let f = new FileReader();
f.onload = e => resolve(f.result);
f.readAsDataURL(blob);
});
})
.then(dataURL => fR.rule.cssText.replace(fR.url, dataURL))
);
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
// fetch default fonts if not done before
function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) {
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous", "Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez", "Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
fontsToAdd.forEach(function (f) {
if (fonts.indexOf(f) === -1) fonts.push(f);
});
updateFontOptions();
}
}
function fetchFonts(url) {
return new Promise((resolve, reject) => {
if (url === "") return tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
if (url.indexOf("http") === -1) {
url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
url = "https://fonts.googleapis.com/css?family=" + url;
}
addFonts(url).then(fetched => {
if (fetched === undefined) return tip("Cannot fetch font for this value!", false, "error");
if (fetched === 0) return tip("Already in the fonts list!", false, "error");
updateFontOptions();
if (fetched === 1) {
tip("Font " + fonts[fonts.length - 1] + " is fetched");
} else if (fetched > 1) {
tip(fetched + " fonts are added to the list");
}
resolve(fetched);
});
function readBlobAsDataURL(blob) {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Update font list for Label and Burg Editors
function updateFontOptions() {
styleSelectFont.innerHTML = "";
for (let i = 0; i < fonts.length; i++) {
const opt = document.createElement("option");
opt.value = i;
const font = fonts[i].split(":")[0].replace(/\+/g, " ");
opt.style.fontFamily = opt.innerHTML = font;
styleSelectFont.add(opt);
}
async function loadFontsAsDataURI(fonts) {
const promises = fonts.map(async (font) => {
const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
const resp = await fetch(url);
const blob = await resp.blob();
const dataURL = await readBlobAsDataURL(blob);
return {...font, src: `url('${dataURL}')`};
});
return await Promise.all(promises);
}
async function addGoogleFont(family) {
const fontRanges = await fetchGoogleFont(family);
if (!fontRanges) return tip('Cannot fetch Google font for this value', true, 'error', 4000);
tip(`Google font ${family} is loading...`, true, 'warn', 4000);
const promises = fontRanges.map((range) => {
const {src, unicodeRange, variant} = range;
const fontFace = new FontFace(family, src, {unicodeRange, variant, display: 'block'});
return fontFace.load();
});
Promise.all(promises)
.then((fontFaces) => {
fontFaces.forEach((fontFace) => document.fonts.add(fontFace));
fonts.push(...fontRanges);
tip(`Google font ${family} is added to the list`, true, 'success', 4000);
addFontOption(family);
document.getElementById('styleSelectFont').value = family;
changeFont();
})
.catch((err) => {
tip(`Failed to load Google font ${family}`, true, 'error', 4000);
console.error(err);
});
}
function addLocalFont(family) {
fonts.push({family});
const fontFace = new FontFace(family, `local(${family})`, {display: 'block'});
document.fonts.add(fontFace);
tip(`Local font ${family} is added to the fonts list`, true, 'success', 4000);
addFontOption(family);
document.getElementById('styleSelectFont').value = family;
changeFont();
}
function addWebFont(family, url) {
const src = `url('${url}')`;
fonts.push({family, src});
const fontFace = new FontFace(family, src, {display: 'block'});
document.fonts.add(fontFace);
tip(`Font ${family} is added to the list`, true, 'success', 4000);
addFontOption(family);
document.getElementById('styleSelectFont').value = family;
changeFont();
}

View file

@ -1,38 +1,38 @@
"use strict";
'use strict';
window.HeightmapGenerator = (function () {
let cells, p;
const generate = function () {
TIME && console.time("generateHeightmap");
TIME && console.time('generateHeightmap');
cells = grid.cells;
p = grid.points;
cells.h = new Uint8Array(grid.points.length);
const template = document.getElementById("templateInput").value;
const template = document.getElementById('templateInput').value;
const templateString = HeightmapTemplates[template];
const steps = templateString.split("\n");
const steps = templateString.split('\n');
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
for (const step of steps) {
const elements = step.trim().split(" ");
const elements = step.trim().split(' ');
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
addStep(...elements);
}
TIME && console.timeEnd("generateHeightmap");
TIME && console.timeEnd('generateHeightmap');
};
function addStep(a1, a2, a3, a4, a5) {
if (a1 === "Hill") return addHill(a2, a3, a4, a5);
if (a1 === "Pit") return addPit(a2, a3, a4, a5);
if (a1 === "Range") return addRange(a2, a3, a4, a5);
if (a1 === "Trough") return addTrough(a2, a3, a4, a5);
if (a1 === "Strait") return addStrait(a2, a3);
if (a1 === "Add") return modify(a3, +a2, 1);
if (a1 === "Multiply") return modify(a3, 0, +a2);
if (a1 === "Smooth") return smooth(a2);
if (a1 === 'Hill') return addHill(a2, a3, a4, a5);
if (a1 === 'Pit') return addPit(a2, a3, a4, a5);
if (a1 === 'Range') return addRange(a2, a3, a4, a5);
if (a1 === 'Trough') return addTrough(a2, a3, a4, a5);
if (a1 === 'Strait') return addStrait(a2, a3);
if (a1 === 'Add') return modify(a3, +a2, 1);
if (a1 === 'Multiply') return modify(a3, 0, +a2);
if (a1 === 'Smooth') return smooth(a2);
}
function getBlobPower() {
@ -201,13 +201,13 @@ window.HeightmapGenerator = (function () {
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
frontier.forEach((i) => {
cells.h[i] = lim(cells.h[i] + h * (Math.random() * 0.3 + 0.85));
});
h = h ** power - 1;
if (h < 2) break;
frontier.forEach(f => {
cells.c[f].forEach(i => {
frontier.forEach((f) => {
cells.c[f].forEach((i) => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -295,13 +295,13 @@ window.HeightmapGenerator = (function () {
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
frontier.forEach((i) => {
cells.h[i] = lim(cells.h[i] - h * (Math.random() * 0.3 + 0.85));
});
h = h ** power - 1;
if (h < 2) break;
frontier.forEach(f => {
cells.c[f].forEach(i => {
frontier.forEach((f) => {
cells.c[f].forEach((i) => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -323,11 +323,11 @@ window.HeightmapGenerator = (function () {
}
};
const addStrait = function (width, direction = "vertical") {
const addStrait = function (width, direction = 'vertical') {
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return;
const used = new Uint8Array(cells.h.length);
const vert = direction === "vertical";
const vert = direction === 'vertical';
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5;
@ -377,34 +377,36 @@ window.HeightmapGenerator = (function () {
};
const modify = function (range, add, mult, power) {
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
grid.cells.h = grid.cells.h.map(h => (h >= min && h <= max ? mod(h) : h));
const min = range === 'land' ? 20 : range === 'all' ? 0 : +range.split('-')[0];
const max = range === 'land' || range === 'all' ? 100 : +range.split('-')[1];
const isLand = min === 20;
function mod(v) {
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
if (mult !== 1) v = min === 20 ? (v - 20) * mult + 20 : v * mult;
if (power) v = min === 20 ? (v - 20) ** power + 20 : v ** power;
return lim(v);
}
grid.cells.h = grid.cells.h.map((h) => {
if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
return lim(h);
});
};
const smooth = function (fr = 2, add = 0) {
cells.h = cells.h.map((h, i) => {
const a = [h];
cells.c[i].forEach(c => a.push(cells.h[c]));
cells.c[i].forEach((c) => a.push(cells.h[c]));
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
});
};
function getPointInRange(range, length) {
if (typeof range !== "string") {
ERROR && console.error("Range should be a string");
if (typeof range !== 'string') {
ERROR && console.error('Range should be a string');
return;
}
const min = range.split("-")[0] / 100 || 0;
const max = range.split("-")[1] / 100 || min;
const min = range.split('-')[0] / 100 || 0;
const max = range.split('-')[1] / 100 || min;
return rand(min * length, max * length);
}

View file

@ -1,4 +1,4 @@
"use strict";
'use strict';
window.HeightmapTemplates = (function () {
const volcano = `Hill 1 90-100 44-56 40-60

View file

@ -1,18 +1,18 @@
"use strict";
'use strict';
window.Lakes = (function () {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
pack.features.forEach(f => {
if (f.type !== "lake") return;
pack.features.forEach((f) => {
if (f.type !== 'lake') return;
// default flux: sum of precipitation around lake
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
// temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map((c) => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
@ -31,16 +31,16 @@ window.Lakes = (function () {
// get array of land cells aroound lake
const getShoreline = function (lake) {
const uniqueCells = new Set();
lake.vertices.forEach(v => pack.vertices.c[v].forEach(c => pack.cells.h[c] >= 20 && uniqueCells.add(c)));
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 prepareLakeData = (h) => {
const cells = pack.cells;
const ELEVATION_LIMIT = +document.getElementById("lakeElevationLimitOutput").value;
const ELEVATION_LIMIT = +document.getElementById('lakeElevationLimitOutput').value;
pack.features.forEach(f => {
if (f.type !== "lake") return;
pack.features.forEach((f) => {
if (f.type !== 'lake') return;
delete f.flux;
delete f.inlets;
delete f.outlet;
@ -74,7 +74,7 @@ window.Lakes = (function () {
if (h[n] < 20) {
const nFeature = pack.features[cells.f[n]];
if (nFeature.type === "ocean" || f.height > nFeature.height) {
if (nFeature.type === 'ocean' || f.height > nFeature.height) {
deep = false;
break;
}
@ -91,25 +91,25 @@ window.Lakes = (function () {
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
if (feature.type !== 'lake') continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.outCell;
delete feature.closed;
feature.height = rn(feature.height, 3);
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
const inlets = feature.inlets?.filter((r) => pack.rivers.find((river) => river.i === r));
if (!inlets || !inlets.length) delete feature.inlets;
else feature.inlets = inlets;
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
const outlet = feature.outlet && pack.rivers.find((river) => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
};
const defineGroup = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
if (feature.type !== 'lake') continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
@ -121,29 +121,29 @@ window.Lakes = (function () {
const generateName = function () {
Math.random = aleaPRNG(seed);
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
if (feature.type !== 'lake') continue;
feature.name = getName(feature);
}
};
const getName = function (feature) {
const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
const landCell = pack.cells.c[feature.firstCell].find((c) => pack.cells.h[c] >= 20);
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
};
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (feature.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.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";
if (!feature.outlet && feature.evaporation > feature.flux) return 'salt';
return "freshwater";
return 'freshwater';
}
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};

View file

@ -1,5 +1,5 @@
// Functions to save and load the map
'use strict';
// Functions to load and parse .map files
function quickLoad() {
ldb.get('lastMap', (blob) => {
@ -12,6 +12,37 @@ function quickLoad() {
});
}
async function loadFromDropbox() {
const mapPath = document.getElementById("loadFromDropboxSelect")?.value;
DEBUG && console.log("Loading map from Dropbox:", mapPath);
const blob = await Cloud.providers.dropbox.load(mapPath);
uploadMap(blob);
}
async function createSharableDropboxLink() {
const mapFile = document.querySelector("#loadFromDropbox select").value;
const sharableLink = document.getElementById("sharableLink");
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
let url;
try {
url = await Cloud.providers.dropbox.getLink(mapFile);
} catch {
tip("Dropbox API error. Can not create link.", true, "error", 2000);
return;
}
const fmg = window.location.href.split("?")[0];
const reallink = `${fmg}?maplink=${url}`;
// voodoo magic required by the yellow god of CORS
const link = reallink.replace("www.dropbox.com/s/", "dl.dropboxusercontent.com/1/view/");
const shortLink = link.slice(0, 50) + "...";
sharableLinkContainer.style.display = "block";
sharableLink.innerText = shortLink;
sharableLink.setAttribute("href", link);
}
function loadMapPrompt(blob) {
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
@ -46,52 +77,110 @@ function loadMapPrompt(blob) {
}
}
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: "GET", mode: "cors"})
.then(response => {
if (response.ok) return response.blob();
throw new Error("Cannot load map from URL");
})
.then(blob => uploadMap(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
});
}
function showUploadErrorMessage(error, URL, random) {
ERROR && console.error(error);
alertMessage.innerHTML = `Cannot load map from the ${link(URL, "link provided")}.
${random ? `A new random map is generated. ` : ""}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$("#alert").dialog({
title: "Loading error",
width: "32em",
buttons: {
OK: function () {
$(this).dialog("close");
}
}
});
}
function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
const fileReader = new FileReader();
fileReader.onload = function (fileLoadedEvent) {
if (callback) callback();
document.getElementById('coas').innerHTML = ''; // remove auto-generated emblems
const result = fileLoadedEvent.target.result;
const [mapData, mapVersion] = parseLoadedResult(result);
const dataLoaded = fileLoadedEvent.target.result;
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = mapVersion === currentVersion;
const data = dataLoaded.split('\r\n');
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
const isNewer = mapVersion > currentVersion;
const isOutdated = mapVersion < currentVersion;
const mapVersion = data[0].split('|')[0] || data[0];
if (mapVersion === version) {
parseLoadedData(data);
return;
}
const archive = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'archived version');
const parsed = parseFloat(mapVersion);
let message = '',
load = false;
if (isNaN(parsed) || data.length < 26 || !data[5]) {
message = `The file you are trying to load is outdated or not a valid .map file.
<br>Please try to open it using an ${archive}`;
} else if (parsed < 0.7) {
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}`;
} else {
load = true;
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
}
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Version conflict',
width: '38em',
buttons: {
OK: function () {
$(this).dialog('close');
if (load) parseLoadedData(data);
}
}
});
if (isUpdated) return parseLoadedData(mapData);
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
};
fileReader.readAsText(file, 'UTF-8');
fileReader.readAsText(file, "UTF-8");
}
function parseLoadedResult(result) {
try {
// data can be in FMG internal format or base64 encoded
const isDelimited = result.substr(0, 10).includes("|");
const decoded = isDelimited ? result : decodeURIComponent(atob(result));
const mapData = decoded.split("\r\n");
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
return [mapData, mapVersion];
} catch (error) {
console.error(error);
return [null, null];
}
}
function showUploadMessage(type, mapData, mapVersion) {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
let message, title, canBeLoaded;
if (type === "invalid") {
message = `The file does not look like a valid <i>.map</i> file.<br>Please check the data format`;
title = "Invalid file";
canBeLoaded = false;
} else if (type === "ancient") {
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
title = "Ancient file";
canBeLoaded = false;
} else if (type === "newer") {
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
title = "Newer file";
canBeLoaded = false;
} else if (type === "outdated") {
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>Click OK to get map <b>auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
title = "Outdated file";
canBeLoaded = true;
}
alertMessage.innerHTML = message;
const buttons = {
OK: function () {
$(this).dialog('close');
if (canBeLoaded) parseLoadedData(mapData);
}
};
$("#alert").dialog({title, buttons});
}
function parseLoadedData(data) {
@ -133,8 +222,8 @@ function parseLoadedData(data) {
if (settings[11]) barPosY.value = settings[11];
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
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[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
@ -142,13 +231,27 @@ function parseLoadedData(data) {
if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21];
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];
})();
void (function applyOptionsToUI() {
stateLabelsModeInput.value = options.stateLabelsMode;
})();
void (function parseConfiguration() {
if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]);
if (data[34]) {
const usedFonts = JSON.parse(data[34]);
usedFonts.forEach(usedFont => {
const {family: usedFamily, unicodeRange: usedRange, variant: usedVariant} = usedFont;
const defaultFont = fonts.find(({family, unicodeRange, variant}) => family === usedFamily && unicodeRange === usedRange && variant === usedVariant);
if (!defaultFont) fonts.push(usedFont);
declareFont(usedFont);
});
}
const biomes = data[3].split('|');
biomesData = applyDefaultBiomesSystem();
@ -222,8 +325,6 @@ function parseLoadedData(data) {
burgLabels = labels.select('#burgLabels');
})();
loadUsedFonts();
void (function parseGridData() {
grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points);
@ -245,7 +346,9 @@ function parseLoadedData(data) {
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: 'No religion'}];
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.resources = data[35] ? JSON.parse(data[35]) : [];
// TODO: ***** both 35?
pack.resources = data[36] ? JSON.parse(data[36]) : [];
pack.markers = data[35] ? JSON.parse(data[35]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(','));
@ -339,23 +442,27 @@ function parseLoadedData(data) {
// 1.0 adds a legend box
legend = svg.append('g').attr('id', 'legend');
legend
.attr('font-family', 'Almendra SC')
.attr('data-font', 'Almendra+SC')
.attr('font-size', 13)
.attr('data-size', 13)
.attr('data-x', 99)
.attr('data-y', 93)
.attr('stroke-width', 2.5)
.attr('stroke', '#812929')
.attr('stroke-dasharray', '0 4 10 4')
.attr("font-family", "Almendra SC")
.attr("font-size", 13)
.attr("data-size", 13)
.attr("data-x", 99)
.attr("data-y", 93)
.attr("stroke-width", 2.5)
.attr("stroke", "#812929")
.attr("stroke-dasharray", "0 4 10 4")
.attr("stroke-linecap", "round");
.attr('stroke-linecap', 'round');
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append('g').attr('id', 'stateBorders');
provinceBorders = borders.append('g').attr('id', 'provinceBorders');
borders.attr('opacity', null).attr('stroke', null).attr('stroke-width', null).attr('stroke-dasharray', null).attr('stroke-linecap', null).attr('filter', null);
stateBorders.attr('opacity', 0.8).attr('stroke', '#56566d').attr('stroke-width', 1).attr('stroke-dasharray', '2').attr('stroke-linecap', 'butt');
provinceBorders.attr('opacity', 0.8).attr('stroke', '#56566d').attr('stroke-width', 0.5).attr('stroke-dasharray', '1').attr('stroke-linecap', 'butt');
borders
.attr("opacity", null)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", null)
.attr("filter", null);
// 1.0 adds state relations, provinces, forms and full names
provs = viewbox.insert('g', '#borders').attr('id', 'provs').attr('opacity', 0.6);
@ -377,7 +484,7 @@ function parseLoadedData(data) {
zones.attr('opacity', 0.6).attr('stroke', null).attr('stroke-width', 0).attr('stroke-dasharray', null).attr('stroke-linecap', 'butt');
addZones();
if (!markers.selectAll('*').size()) {
addMarkers();
Markers.generate();
turnButtonOn('toggleMarkers');
}
@ -729,29 +836,41 @@ function parseLoadedData(data) {
if (version < 1.65) {
// v 1.65 changed rivers data
rivers.attr('style', null); // remove style to unhide layer
d3.select('#rivers').attr('style', null); // remove style to unhide layer
const {cells, rivers} = pack;
for (const river of pack.rivers) {
for (const river of rivers) {
const node = document.getElementById('river' + river.i);
if (node && !river.cells) {
const riverCells = new Set();
const riverCells = [];
const riverPoints = [];
const length = node.getTotalLength() / 2;
if (!length) continue;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const x = (p1.x + p2.x) / 2;
const y = (p1.y + p2.y) / 2;
const cell = findCell(x, y, 6);
if (cell) riverCells.add(cell);
for (let i = 0; i <= segments; i++) {
const shift = increment * i;
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
const x = rn((x1 + x2) / 2, 1);
const y = rn((y1 + y2) / 2, 1);
const cell = findCell(x, y);
riverPoints.push([x, y]);
riverCells.push(cell);
}
river.cells = Array.from(riverCells);
river.cells = riverCells;
river.points = riverPoints;
}
pack.cells.i.forEach((i) => {
if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
river.widthFactor = 1;
cells.i.forEach((i) => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
if (riverInWater) cells.r[i] = 0;
});
}
}
@ -764,6 +883,59 @@ function parseLoadedData(data) {
// ecomonics:
// calculate salesTax for all states
if (version < 1.7) {
// v 1.7 changed markers data
const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("markers");
const markerElements = markersGroup.querySelectorAll("use");
const rescale = +markersGroup.getAttribute("rescale");
pack.markers = Array.from(markerElements).map((el, i) => {
const id = el.getAttribute("id");
const note = notes.find(note => note.id === id);
if (note) note.id = `marker${i}`;
let x = +el.dataset.x;
let y = +el.dataset.y;
const transform = el.getAttribute("transform");
if (transform) {
const [dx, dy] = parseTransform(transform);
if (dx) x += +dx;
if (dy) y += +dy;
}
const cell = findCell(x, y);
const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1);
const href = el.href.baseVal;
const type = href.replace("#marker_", "");
const symbol = defs.querySelector(`symbol${href}`);
const text = symbol.querySelector("text");
const circle = symbol.querySelector("circle");
const icon = text.innerHTML;
const px = Number(text.getAttribute("font-size")?.replace("px", ""));
const dx = Number(text.getAttribute("x")?.replace("%", ""));
const dy = Number(text.getAttribute("y")?.replace("%", ""));
const fill = circle.getAttribute("fill");
const stroke = circle.getAttribute("stroke");
const marker = {i, icon, type, x, y, size, cell};
if (size && size !== 30) marker.size = size;
if (!isNaN(px) && px !== 12) marker.px = px;
if (!isNaN(dx) && dx !== 50) marker.dx = dx;
if (!isNaN(dy) && dy !== 50) marker.dy = dy;
if (fill && fill !== "#ffffff") marker.fill = fill;
if (stroke && stroke !== "#000000") marker.stroke = stroke;
if (circle.getAttribute("opacity") === "0") marker.pin = "no";
return marker;
});
markersGroup.style.display = null;
defs.remove();
markerElements.forEach(el => el.remove());
if (layerIsOn("markers")) drawMarkers();
}
})();
void (function checkDataIntegrity() {
@ -891,7 +1063,7 @@ function parseLoadedData(data) {
},
'New map': function () {
$(this).dialog('close');
regenerateMap();
regenerateMap("loading error");
},
Cancel: function () {
$(this).dialog('close');

1204
modules/load.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,803 @@
'use strict';
window.Markers = (function () {
let config = [];
let occupied = [];
function getDefaultConfig() {
const culturesSet = document.getElementById('culturesSet').value;
const isFantasy = culturesSet.includes('Fantasy');
return [
{type: 'volcanoes', icon: '🌋', multiplier: 1, fn: addVolcanoes},
{type: 'hot-springs', icon: '♨️', multiplier: 1, fn: addHotSprings},
{type: 'mines', icon: '⛏️', multiplier: 1, fn: addMines},
{type: 'bridges', icon: '🌉', multiplier: 1, fn: addBridges},
{type: 'inns', icon: '🍻', multiplier: 1, fn: addInns},
{type: 'lighthouses', icon: '🚨', multiplier: 1, fn: addLighthouses},
{type: 'waterfalls', icon: '⟱', multiplier: 1, fn: addWaterfalls},
{type: 'battlefields', icon: '⚔️', multiplier: 1, fn: addBattlefields},
{type: 'dungeons', icon: '🗝️', multiplier: 1, fn: addDungeons},
{type: 'lake-monsters', icon: '🐉', multiplier: 1, fn: addLakeMonsters},
{type: 'sea-monsters', icon: '🦑', multiplier: 1, fn: addSeaMonsters},
{type: 'hill-monsters', icon: '👹', multiplier: 1, fn: addHillMonsters},
{type: 'sacred-mountains', icon: '🗻', multiplier: 1, fn: addSacredMountains},
{type: 'sacred-forests', icon: '🌳', multiplier: 1, fn: addSacredForests},
{type: 'sacred-pineries', icon: '🌲', multiplier: 1, fn: addSacredPineries},
{type: 'sacred-palm-groves', icon: '🌴', multiplier: 1, fn: addSacredPalmGroves},
{type: 'brigands', icon: '💰', multiplier: 1, fn: addBrigands},
{type: 'pirates', icon: '🏴‍☠️', multiplier: 1, fn: addPirates},
{type: 'statues', icon: '🗿', multiplier: 1, fn: addStatues},
{type: 'ruines', icon: '🏺', multiplier: 1, fn: addRuines},
{type: 'portals', icon: '🌀', multiplier: +isFantasy, fn: addPortals}
];
}
const getConfig = () => config;
const setConfig = (newConfig) => {
config = newConfig;
};
const generate = function () {
setConfig(getDefaultConfig());
pack.markers = [];
generateTypes();
};
const regenerate = () => {
pack.markers = pack.markers.filter(({i, lock, cell}) => {
if (lock) {
occupied[cell] = true;
return true;
}
const id = `marker${i}`;
document.getElementById(id)?.remove();
const index = notes.findIndex((note) => note.id === id);
if (index != -1) notes.splice(index, 1);
return false;
});
generateTypes();
};
function generateTypes() {
TIME && console.time('addMarkers');
config.forEach(({type, icon, multiplier, fn}) => {
if (multiplier === 0) return;
fn(type, icon, multiplier);
});
occupied = [];
TIME && console.timeEnd('addMarkers');
}
function getQuantity(array, min, each, multiplier) {
if (!array.length || array.length < min / multiplier) return 0;
const requestQty = Math.ceil((array.length / each) * multiplier);
return array.length < requestQty ? array.length : requestQty;
}
function extractAnyElement(array) {
const index = Math.floor(Math.random() * array.length);
return array.splice(index, 1);
}
function getMarkerCoordinates(cell) {
const {cells, burgs} = pack;
const burgId = cells.burg[cell];
if (burgId) {
const {x, y} = burgs[burgId];
return [x, y];
}
return cells.p[cell];
}
function addMarker({cell, type, icon, dx, dy, px}) {
const i = pack.markers.length;
const [x, y] = getMarkerCoordinates(cell);
const marker = {i, icon, type, x, y, cell};
if (dx) marker.dx = dx;
if (dy) marker.dy = dy;
if (px) marker.px = px;
pack.markers.push(marker);
occupied[cell] = true;
return 'marker' + i;
}
function addVolcanoes(type, icon, multiplier) {
const {cells} = pack;
let mountains = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] >= 70).sort((a, b) => cells.h[b] - cells.h[a]));
let quantity = getQuantity(mountains, 10, 500, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(mountains);
const id = addMarker({cell, icon, type, dx: 52, px: 13});
const proper = Names.getCulture(cells.culture[cell]);
const name = P(0.3) ? 'Mount ' + proper : Math.random() > 0.3 ? proper + ' Volcano' : proper;
notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight(cells.p[cell])}`});
quantity--;
}
}
function addHotSprings(type, icon, multiplier) {
const {cells} = pack;
let springs = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] > 50).sort((a, b) => cells.h[b] - cells.h[a]));
let quantity = getQuantity(springs, 30, 1200, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(springs);
const id = addMarker({cell, icon, type, dy: 52});
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(35, 15, 20, 100));
notes.push({id, name: proper + ' Hot Springs', legend: `A hot springs area. Average temperature: ${temp}`});
quantity--;
}
}
function addMines(type, icon, multiplier) {
const {cells} = pack;
let hillyBurgs = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] > 47 && cells.burg[i]));
let quantity = getQuantity(hillyBurgs, 1, 15, multiplier);
if (!quantity) return;
const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1};
while (quantity && hillyBurgs.length) {
const [cell] = extractAnyElement(hillyBurgs);
const id = addMarker({cell, icon, type, dx: 48, px: 13});
const resource = rw(resources);
const burg = pack.burgs[cells.burg[cell]];
const name = `${burg.name}${resource} mining town`;
const population = rn(burg.population * populationRate * urbanization);
const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
notes.push({id, name, legend});
quantity--;
}
}
function addBridges(type, icon, multiplier) {
const {cells, burgs} = pack;
const meanFlux = d3.mean(cells.fl.filter((fl) => fl));
let bridges = Array.from(cells.i.filter((i) => !occupied[i] && cells.burg[i] && cells.t[i] !== 1 && burgs[cells.burg[i]].population > 20 && cells.r[i] && cells.fl[i] > meanFlux));
let quantity = getQuantity(bridges, 1, 5, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(bridges);
const id = addMarker({cell, icon, type, px: 14});
const burg = pack.burgs[cells.burg[cell]];
const river = pack.rivers.find((r) => r.i === pack.cells.r[cell]);
const riverName = river ? `${river.name} ${river.type}` : 'river';
const name = river && P(0.2) ? river.name : burg.name;
const weightedAdjectives = {
stone: 10,
wooden: 1,
lengthy: 2,
formidable: 2,
rickety: 1,
beaten: 1,
weathered: 1
};
notes.push({id, name: `${name} Bridge`, legend: `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}`});
quantity--;
}
}
function addInns(type, icon, multiplier) {
const {cells} = pack;
let taverns = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10));
let quantity = getQuantity(taverns, 1, 100, multiplier);
if (!quantity) return;
const colors = ['Dark', 'Light', 'Bright', 'Golden', 'White', 'Black', 'Red', 'Pink', 'Purple', 'Blue', 'Green', 'Yellow', 'Amber', 'Orange', 'Brown', 'Grey'];
const animals = [
'Antelope',
'Ape',
'Badger',
'Bear',
'Beaver',
'Bison',
'Boar',
'Buffalo',
'Cat',
'Crane',
'Crocodile',
'Crow',
'Deer',
'Dog',
'Eagle',
'Elk',
'Fox',
'Goat',
'Goose',
'Hare',
'Hawk',
'Heron',
'Horse',
'Hyena',
'Ibis',
'Jackal',
'Jaguar',
'Lark',
'Leopard',
'Lion',
'Mantis',
'Marten',
'Moose',
'Mule',
'Narwhal',
'Owl',
'Panther',
'Rat',
'Raven',
'Rook',
'Scorpion',
'Shark',
'Sheep',
'Snake',
'Spider',
'Swan',
'Tiger',
'Turtle',
'Wolf',
'Wolverine',
'Camel',
'Falcon',
'Hound',
'Ox'
];
const adjectives = [
'New',
'Good',
'High',
'Old',
'Great',
'Big',
'Major',
'Happy',
'Main',
'Huge',
'Far',
'Beautiful',
'Fair',
'Prime',
'Ancient',
'Golden',
'Proud',
'Lucky',
'Fat',
'Honest',
'Giant',
'Distant',
'Friendly',
'Loud',
'Hungry',
'Magical',
'Superior',
'Peaceful',
'Frozen',
'Divine',
'Favorable',
'Brave',
'Sunny',
'Flying'
];
const methods = [
'Boiled',
'Grilled',
'Roasted',
'Spit-roasted',
'Stewed',
'Stuffed',
'Jugged',
'Mashed',
'Baked',
'Braised',
'Poached',
'Marinated',
'Pickled',
'Smoked',
'Dried',
'Dry-aged',
'Corned',
'Fried',
'Pan-fried',
'Deep-fried',
'Dressed',
'Steamed',
'Cured',
'Syrupped',
'Flame-Broiled'
];
const courses = [
'beef',
'pork',
'bacon',
'chicken',
'lamb',
'chevon',
'hare',
'rabbit',
'hart',
'deer',
'antlers',
'bear',
'buffalo',
'badger',
'beaver',
'turkey',
'pheasant',
'duck',
'goose',
'teal',
'quail',
'pigeon',
'seal',
'carp',
'bass',
'pike',
'catfish',
'sturgeon',
'escallop',
'pie',
'cake',
'pottage',
'pudding',
'onions',
'carrot',
'potato',
'beet',
'garlic',
'cabbage',
'eggplant',
'eggs',
'broccoli',
'zucchini',
'pepper',
'olives',
'pumpkin',
'spinach',
'peas',
'chickpea',
'beans',
'rice',
'pasta',
'bread',
'apples',
'peaches',
'pears',
'melon',
'oranges',
'mango',
'tomatoes',
'cheese',
'corn',
'rat tails',
'pig ears'
];
const types = ['hot', 'cold', 'fire', 'ice', 'smoky', 'misty', 'shiny', 'sweet', 'bitter', 'salty', 'sour', 'sparkling', 'smelly'];
const drinks = [
'wine',
'brandy',
'jinn',
'whisky',
'rom',
'beer',
'cider',
'mead',
'liquor',
'spirit',
'vodka',
'tequila',
'absinthe',
'nectar',
'milk',
'kvass',
'kumis',
'tea',
'water',
'juice',
'sap'
];
while (quantity) {
const [cell] = extractAnyElement(taverns);
const id = addMarker({cell, icon, type, px: 14});
const typeName = P(0.3) ? 'inn' : 'tavern';
const isAnimalThemed = P(0.7);
const animal = ra(animals);
const name = isAnimalThemed ? (P(0.6) ? ra(colors) + ' ' + animal : ra(adjectives) + ' ' + animal) : ra(adjectives) + ' ' + capitalize(type);
const meal = isAnimalThemed && P(0.3) ? animal : ra(courses);
const course = `${ra(methods)} ${meal}`.toLowerCase();
const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase();
const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here`;
notes.push({id, name: 'The ' + name, legend});
quantity--;
}
}
function addLighthouses(type, icon, multiplier) {
const {cells} = pack;
const lighthouses = Array.from(cells.i.filter((i) => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some((c) => cells.h[c] < 20 && cells.road[c])));
let quantity = getQuantity(lighthouses, 1, 2, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(lighthouses);
const id = addMarker({cell, icon, type, px: 14});
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + ' Lighthouse' + name, legend: `A lighthouse to serve as a beacon for ships in the open sea`});
quantity--;
}
}
function addWaterfalls(type, icon, multiplier) {
const {cells} = pack;
const waterfalls = Array.from(cells.i.filter((i) => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some((c) => cells.h[c] < 40 && cells.r[c])));
const quantity = getQuantity(waterfalls, 1, 5, multiplier);
if (!quantity) return;
const descriptions = [
'A gorgeous waterfall flows here',
'The rapids of an exceptionally beautiful waterfall',
'An impressive waterfall has cut through the land',
'The cascades of a stunning waterfall',
'A river drops down from a great height forming a wonderous waterfall',
'A breathtaking waterfall cuts through the landscape'
];
for (let i = 0; i < waterfalls.length && i < quantity; i++) {
const cell = waterfalls[i];
const id = addMarker({cell, icon, type, dy: 54, px: 16});
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + ' Waterfall' + name, legend: `${ra(descriptions)}`});
}
}
function addBattlefields(type, icon, multiplier) {
const {cells, states} = pack;
let battlefields = Array.from(cells.i.filter((i) => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25));
let quantity = getQuantity(battlefields, 50, 700, multiplier);
if (!quantity) return;
while (quantity && battlefields.length) {
const [cell] = extractAnyElement(battlefields);
const id = addMarker({cell, icon, type, dy: 52});
const state = states[cells.state[cell]];
if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state);
const campaign = ra(state.campaigns);
const date = generateDate(campaign.start, campaign.end);
const name = Names.getCulture(cells.culture[cell]) + ' Battlefield';
const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`;
notes.push({id, name, legend});
quantity--;
}
}
function addDungeons(type, icon, multiplier) {
const {cells} = pack;
let dungeons = Array.from(cells.i.filter((i) => !occupied[i] && cells.pop[i] && cells.pop[i] < 3));
let quantity = getQuantity(dungeons, 30, 200, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(dungeons);
const id = addMarker({cell, icon, type, dy: 51, px: 13});
const dungeonSeed = `${seed}${cell}`;
const name = 'Dungeon';
const legend = `<div>Undiscovered dungeon. See <a href="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" target="_blank">One page dungeon</a></div><iframe style="height: 33vh" src="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" sandbox="allow-scripts allow-same-origin"></iframe>`;
notes.push({id, name, legend});
quantity--;
}
}
function addLakeMonsters(type, icon, multiplier) {
const {features} = pack;
const lakes = features.filter((feature) => feature.type === 'lake' && feature.group === 'freshwater' && !occupied[feature.firstCell]);
let quantity = getQuantity(lakes, 2, 10, multiplier);
if (!quantity) return;
while (quantity) {
const [lake] = extractAnyElement(lakes);
const cell = lake.firstCell;
const id = addMarker({cell, icon, type, dy: 48});
const name = `${lake.name} Monster`;
const length = gauss(10, 5, 5, 100);
const legend = `Rumors say a relic monster of ${length} ${heightUnit.value} long inhabits ${lake.name} Lake. Truth or lie, folks are afraid to fish in the lake`;
notes.push({id, name, legend});
quantity--;
}
}
function addSeaMonsters(type, icon, multiplier) {
const {cells, features} = pack;
const sea = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] < 20 && cells.road[i] && features[cells.f[i]].type === 'ocean'));
let quantity = getQuantity(sea, 50, 700, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(sea);
const id = addMarker({cell, icon, type});
const name = `${Names.getCultureShort(0)} Monster`;
const length = gauss(25, 10, 10, 100);
const legend = `Old sailors tell stories of a gigantic sea monster inhabiting these dangerous waters. Rumors say it can be ${length} ${heightUnit.value} long`;
notes.push({id, name, legend});
quantity--;
}
}
function addHillMonsters(type, icon, multiplier) {
const {cells} = pack;
const hills = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] >= 50 && cells.pop[i]));
let quantity = getQuantity(hills, 30, 600, multiplier);
if (!quantity) return;
const adjectives = ['great', 'big', 'huge', 'prime', 'golden', 'proud', 'lucky', 'fat', 'giant', 'hungry', 'magical', 'superior', 'terrifying', 'horrifying', 'feared'];
const subjects = ['Locals', 'Elders', 'Inscriptions', 'Tipplers', 'Legends', 'Whispers', 'Rumors', 'Journeying folk', 'Tales'];
const species = ['Ogre', 'Troll', 'Cyclops', 'Giant', 'Monster', 'Beast', 'Dragon', 'Undead', 'Ghoul', 'Vampire', 'Hag', 'Banshee', 'Bearded Devil', 'Roc', 'Hydra', 'Warg'];
const modusOperandi = [
'steals cattle at night',
'prefers eating children',
"doesn't mind of human flesh",
'keeps the region at bay',
'eats kids whole',
'abducts young women',
'terrorizes the region',
'harasses travelers in the area',
'snatches people from homes',
'attacks anyone who dares to approach its lair',
'attacks unsuspecting victims'
];
while (quantity) {
const [cell] = extractAnyElement(hills);
const id = addMarker({cell, icon, type, dy: 54, px: 13});
const monster = ra(species);
const toponym = Names.getCulture(cells.culture[cell]);
const name = `${toponym} ${monster}`;
const legend = `${ra(subjects)} speak of a ${ra(adjectives)} ${monster} who inhabits ${toponym} hills and ${ra(modusOperandi)}`;
notes.push({id, name, legend});
quantity--;
}
}
function addSacredMountains(type, icon, multiplier) {
const {cells, cultures} = pack;
let lonelyMountains = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] >= 70 && cells.c[i].some((c) => cells.culture[c]) && cells.c[i].every((c) => cells.h[c] < 60)));
let quantity = getQuantity(lonelyMountains, 1, 5, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(lonelyMountains);
const id = addMarker({cell, icon, type, dy: 48});
const culture = cells.c[cell].map((c) => cells.culture[c]).find((c) => c);
const name = `${Names.getCulture(culture)} Mountain`;
const height = getFriendlyHeight(cells.p[cell]);
const legend = `A sacred mountain of ${cultures[culture].name} culture. Height: ${height}`;
notes.push({id, name, legend});
quantity--;
}
}
function addSacredForests(type, icon, multiplier) {
const {cells, cultures} = pack;
let temperateForests = Array.from(cells.i.filter((i) => !occupied[i] && cells.culture[i] && [6, 8].includes(cells.biome[i])));
let quantity = getQuantity(temperateForests, 30, 1000, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(temperateForests);
const id = addMarker({cell, icon, type});
const culture = cells.culture[cell];
const name = `${Names.getCulture(culture)} Forest`;
const legend = `A sacred forest of ${cultures[culture].name} culture`;
notes.push({id, name, legend});
quantity--;
}
}
function addSacredPineries(type, icon, multiplier) {
const {cells, cultures} = pack;
let borealForests = Array.from(cells.i.filter((i) => !occupied[i] && cells.culture[i] && cells.biome[i] === 9));
let quantity = getQuantity(borealForests, 30, 800, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(borealForests);
const id = addMarker({cell, icon, type, px: 13});
const culture = cells.culture[cell];
const name = `${Names.getCulture(culture)} Pinery`;
const legend = `A sacred pinery of ${cultures[culture].name} culture`;
notes.push({id, name, legend});
quantity--;
}
}
function addSacredPalmGroves(type, icon, multiplier) {
const {cells, cultures} = pack;
let oasises = Array.from(cells.i.filter((i) => !occupied[i] && cells.culture[i] && cells.biome[i] === 1 && cells.pop[i] > 1 && cells.road[i]));
let quantity = getQuantity(oasises, 1, 100, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(oasises);
const id = addMarker({cell, icon, type, px: 13});
const culture = cells.culture[cell];
const name = `${Names.getCulture(culture)} Palm Grove`;
const legend = `A sacred palm grove of ${cultures[culture].name} culture`;
notes.push({id, name, legend});
quantity--;
}
}
function addBrigands(type, icon, multiplier) {
const {cells} = pack;
let roads = Array.from(cells.i.filter((i) => !occupied[i] && cells.culture[i] && cells.road[i] > 4));
let quantity = getQuantity(roads, 50, 100, multiplier);
if (!quantity) return;
const animals = [
'Apes',
'Badgers',
'Bears',
'Beavers',
'Bisons',
'Boars',
'Cats',
'Crows',
'Dogs',
'Foxes',
'Hares',
'Hawks',
'Hyenas',
'Jackals',
'Jaguars',
'Leopards',
'Lions',
'Owls',
'Panthers',
'Rats',
'Ravens',
'Rooks',
'Scorpions',
'Sharks',
'Snakes',
'Spiders',
'Tigers',
'Wolfs',
'Wolverines',
'Falcons'
];
const types = {brigands: 4, bandits: 3, robbers: 1, highwaymen: 1};
while (quantity) {
const [cell] = extractAnyElement(roads);
const id = addMarker({cell, icon, type, px: 13});
const culture = cells.culture[cell];
const biome = cells.biome[cell];
const height = cells.p[cell];
const locality =
height >= 70 ? 'highlander' : [1, 2].includes(biome) ? 'desert' : [3, 4].includes(biome) ? 'mounted' : [5, 6, 7, 8, 9].includes(biome) ? 'forest' : biome === 12 ? 'swamp' : 'angry';
const name = `${Names.getCulture(culture)} ${ra(animals)}`;
const legend = `A gang of ${locality} ${rw(types)}`;
notes.push({id, name, legend});
quantity--;
}
}
function addPirates(type, icon, multiplier) {
const {cells} = pack;
let searoutes = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] < 20 && cells.road[i]));
let quantity = getQuantity(searoutes, 40, 300, multiplier);
if (!quantity) return;
while (quantity) {
const [cell] = extractAnyElement(searoutes);
const id = addMarker({cell, icon, type, dx: 51});
const name = `Pirates`;
const legend = `Pirate ships have been spotted in these waters`;
notes.push({id, name, legend});
quantity--;
}
}
function addStatues(type, icon, multiplier) {
const {cells} = pack;
let statues = Array.from(cells.i.filter((i) => !occupied[i] && cells.h[i] >= 20 && cells.h[i] < 40));
let quantity = getQuantity(statues, 80, 1200, multiplier);
if (!quantity) return;
const variants = ['Statue', 'Obelisk', 'Monument', 'Column', 'Monolith', 'Pillar', 'Megalith', 'Stele', 'Runestone', 'Sculpture', 'Effigy', 'Idol'];
const scripts = {
cypriot: '𐠁𐠂𐠃𐠄𐠅𐠈𐠊𐠋𐠌𐠍𐠎𐠏𐠐𐠑𐠒𐠓𐠔𐠕𐠖𐠗𐠘𐠙𐠚𐠛𐠜𐠝𐠞𐠟𐠠𐠡𐠢𐠣𐠤𐠥𐠦𐠧𐠨𐠩𐠪𐠫𐠬𐠭𐠮𐠯𐠰𐠱𐠲𐠳𐠴𐠵𐠷𐠸𐠼𐠿 ',
geez: 'ሀለሐመሠረሰቀበተኀነአከወዐዘየደገጠጰጸፀፈፐ ',
coptic: 'ⲲⲴⲶⲸⲺⲼⲾⳀⳁⳂⳃⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳤ⳥⳧⳩⳪ⳫⳬⳭⳲ⳹⳾ ',
tibetan: 'ༀ༁༂༃༄༅༆༇༈༉༊་༌༐༑༒༓༔༕༖༗༘༙༚༛༜༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳༴༵༶༷༸༹༺༻༼༽༾༿',
mongolian: '᠀᠐᠑᠒ᠠᠡᠦᠧᠨᠩᠪᠭᠮᠯᠰᠱᠲᠳᠵᠻᠼᠽᠾᠿᡀᡁᡆᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡙᡜᡝᡞᡟᡠᡡᡭᡮᡯᡰᡱᡲᡳᡴᢀᢁᢂᢋᢏᢐᢑᢒᢓᢛᢜᢞᢟᢠᢡᢢᢤᢥᢦ'
};
while (quantity) {
const [cell] = extractAnyElement(statues);
const id = addMarker({cell, icon, type});
const culture = cells.culture[cell];
const variant = ra(variants);
const name = `${Names.getCulture(culture)} ${variant}`;
const script = scripts[ra(Object.keys(scripts))];
const inscription = Array(rand(40, 100))
.fill(null)
.map(() => ra(script))
.join('');
const legend = `An ancient ${variant.toLowerCase()}. It has an inscription, but no one can translate it:
<div style="font-size: 1.8em; line-break: anywhere;">${inscription}</div>`;
notes.push({id, name, legend});
quantity--;
}
}
function addRuines(type, icon, multiplier) {
const {cells} = pack;
let ruins = Array.from(cells.i.filter((i) => !occupied[i] && cells.culture[i] && cells.h[i] >= 20 && cells.h[i] < 60));
let quantity = getQuantity(ruins, 80, 1200, multiplier);
if (!quantity) return;
const types = ['City', 'Town', 'Settlement', 'Pyramid', 'Fort', 'Stronghold', 'Temple', 'Sacred site', 'Mausoleum', 'Outpost', 'Fortification', 'Fortress', 'Castle'];
while (quantity) {
const [cell] = extractAnyElement(ruins);
const id = addMarker({cell, icon, type});
const ruinType = ra(types);
const name = `Ruined ${ruinType}`;
const legend = `Ruins of an ancient ${ruinType.toLowerCase()}. Untold riches may lie within.`;
notes.push({id, name, legend});
quantity--;
}
}
function addPortals(type, icon, multiplier) {
const {burgs} = pack;
let portals = burgs
.slice(1, Math.ceil(burgs.length / 10) + 1)
.filter(({cell}) => !occupied[cell])
.map((burg) => [burg.name, burg.cell]);
let quantity = getQuantity(portals, 16, 8, multiplier);
if (!quantity) return;
while (quantity) {
const [portal] = extractAnyElement(portals);
const [burgName, cell] = portal;
const id = addMarker({cell, icon, type, px: 14});
const name = `${burgName} Portal`;
const legend = `An element of the magic portal system connecting major cities. Portals installed centuries ago, but still work fine`;
notes.push({id, name, legend});
quantity--;
}
}
return {generate, regenerate, getConfig, setConfig};
})();

View file

@ -1,16 +1,15 @@
"use strict";
'use strict';
window.Military = (function () {
const generate = function () {
TIME && console.time("generateMilitaryForces");
const cells = pack.cells,
p = cells.p,
states = pack.states;
const valid = states.filter(s => s.i && !s.removed); // valid states
TIME && console.time('generateMilitaryForces');
const {cells, states} = pack;
const {p} = cells;
const valid = states.filter((s) => s.i && !s.removed); // valid states
if (!options.military) options.military = getDefaultOptions();
const expn = d3.sum(valid.map(s => s.expansionism)); // total expansion
const area = d3.sum(valid.map(s => s.area)); // total area
const expn = d3.sum(valid.map((s) => s.expansionism)); // total expansion
const area = d3.sum(valid.map((s) => s.area)); // total area
const rate = {x: 0, Ally: -0.2, Friendly: -0.1, Neutral: 0, Suspicion: 0.1, Enemy: 1, Unknown: 0, Rival: 0.5, Vassal: 0.5, Suzerain: -0.5};
const stateModifier = {
@ -19,7 +18,6 @@ window.Military = (function () {
mounted: {Nomadic: 2.3, Highland: 0.6, Lake: 0.7, Naval: 0.3, Hunting: 0.7, River: 0.8},
machinery: {Nomadic: 0.8, Highland: 1.4, Lake: 1.1, Naval: 1.4, Hunting: 0.4, River: 1.1},
naval: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.8, Hunting: 0.7, River: 1.2},
// non-default generic:
armored: {Nomadic: 1, Highland: 0.5, Lake: 1, Naval: 1, Hunting: 0.7, River: 1.1},
aviation: {Nomadic: 0.5, Highland: 0.5, Lake: 1.2, Naval: 1.2, Hunting: 0.6, River: 1.2},
magical: {Nomadic: 1, Highland: 2, Lake: 1, Naval: 1, Hunting: 1, River: 1}
@ -37,114 +35,136 @@ window.Military = (function () {
highland: {melee: 1.2, ranged: 2, mounted: 0.3, machinery: 3, naval: 1.0, armored: 0.8, aviation: 0.3, magical: 2}
};
valid.forEach(s => {
const temp = (s.temp = {}),
d = s.diplomacy;
const expansionRate = Math.min(Math.max(s.expansionism / expn / (s.area / area), 0.25), 4); // how much state expansionism is realized
const diplomacyRate = d.some(d => d === "Enemy") ? 1 : d.some(d => d === "Rival") ? 0.8 : d.some(d => d === "Suspicion") ? 0.5 : 0.1; // peacefulness
const neighborsRate = Math.min(
Math.max(
s.neighbors.map(n => (n ? pack.states[n].diplomacy[s.i] : "Suspicion")).reduce((s, r) => (s += rate[r]), 0.5),
0.3
),
3
); // neighbors rate
s.alert = Math.min(Math.max(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1), 5); // war alert rate (army modifier)
temp.platoons = [];
valid.forEach((s) => {
s.temp = {};
const d = s.diplomacy;
const expansionRate = minmax(s.expansionism / expn / (s.area / area), 0.25, 4); // how much state expansionism is realized
const diplomacyRate = d.some((d) => d === 'Enemy') ? 1 : d.some((d) => d === 'Rival') ? 0.8 : d.some((d) => d === 'Suspicion') ? 0.5 : 0.1; // peacefulness
const neighborsRateRaw = s.neighbors.map((n) => (n ? pack.states[n].diplomacy[s.i] : 'Suspicion')).reduce((s, r) => (s += rate[r]), 0.5);
const neighborsRate = minmax(neighborsRateRaw, 0.3, 3); // neighbors rate
s.alert = minmax(rn(expansionRate * diplomacyRate * neighborsRate, 2), 0.1, 5); // alert rate (area modifier)
s.temp.platoons = [];
// apply overall state modifiers for unit types based on state features
for (const unit of options.military) {
if (!stateModifier[unit.type]) continue;
let modifier = stateModifier[unit.type][s.type] || 1;
if (unit.type === "mounted" && s.formName.includes("Horde")) modifier *= 2;
else if (unit.type === "naval" && s.form === "Republic") modifier *= 1.2;
temp[unit.name] = modifier * s.alert;
if (unit.type === 'mounted' && s.formName.includes('Horde')) modifier *= 2;
else if (unit.type === 'naval' && s.form === 'Republic') modifier *= 1.2;
s.temp[unit.name] = modifier * s.alert;
}
});
const getType = cell => {
if ([1, 2, 3, 4].includes(cells.biome[cell])) return "nomadic";
if ([7, 8, 9, 12].includes(cells.biome[cell])) return "wetland";
if (cells.h[cell] >= 70) return "highland";
return "generic";
const getType = (cell) => {
if ([1, 2, 3, 4].includes(cells.biome[cell])) return 'nomadic';
if ([7, 8, 9, 12].includes(cells.biome[cell])) return 'wetland';
if (cells.h[cell] >= 70) return 'highland';
return 'generic';
};
function passUnitLimits(unit, biome, state, culture, religion) {
if (unit.biomes && !unit.biomes.includes(biome)) return false;
if (unit.states && !unit.states.includes(state)) return false;
if (unit.cultures && !unit.cultures.includes(culture)) return false;
if (unit.religions && !unit.religions.includes(religion)) return false;
return true;
}
for (const i of cells.i) {
if (!cells.pop[i]) continue;
const s = states[cells.state[i]]; // cell state
if (!s.i || s.removed) continue;
let m = cells.pop[i] / 100; // basic rural army in percentages
if (cells.culture[i] !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[i] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
const biome = cells.biome[i];
const state = cells.state[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const stateObj = states[state];
if (!state || stateObj.removed) continue;
let modifier = cells.pop[i] / 100; // basic rural army in percentages
if (culture !== stateObj.culture) modifier = stateObj.form === 'Union' ? modifier / 1.2 : modifier / 2; // non-dominant culture
if (religion !== cells.religion[stateObj.center]) modifier = stateObj.form === 'Theocracy' ? modifier / 2.2 : modifier / 1.4; // non-dominant religion
if (cells.f[i] !== cells.f[stateObj.center]) modifier = stateObj.type === 'Naval' ? modifier / 1.2 : modifier / 1.8; // different landmass
const type = getType(i);
for (const u of options.military) {
const perc = +u.rural;
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
for (const unit of options.military) {
const perc = +unit.rural;
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
const mod = type === "generic" ? 1 : cellTypeModifier[type][u.type]; // cell specific modifier
const army = m * perc * mod; // rural cell army
const t = rn(army * s.temp[u.name] * populationRate); // total troops
if (!t) continue;
let x = p[i][0],
y = p[i][1],
n = 0;
if (u.type === "naval") {
const cellTypeMod = type === 'generic' ? 1 : cellTypeModifier[type][unit.type]; // cell specific modifier
const army = modifier * perc * cellTypeMod; // rural cell army
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
if (!total) continue;
let [x, y] = p[i];
let n = 0;
if (unit.type === 'naval') {
let haven = cells.haven[i];
(x = p[haven][0]), (y = p[haven][1]);
[x, y] = p[haven];
n = 1;
} // place naval to sea
s.temp.platoons.push({cell: i, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
stateObj.temp.platoons.push({cell: i, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
for (const b of pack.burgs) {
if (!b.i || b.removed || !b.state || !b.population) continue;
const s = states[b.state]; // burg state
const biome = cells.biome[b.cell];
const state = b.state;
const culture = b.culture;
const religion = cells.religion[b.cell];
const stateObj = states[state];
let m = (b.population * urbanization) / 100; // basic urban army in percentages
if (b.capital) m *= 1.2; // capital has household troops
if (b.culture !== s.culture) m = s.form === "Union" ? m / 1.2 : m / 2; // non-dominant culture
if (cells.religion[b.cell] !== cells.religion[s.center]) m = s.form === "Theocracy" ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[b.cell] !== cells.f[s.center]) m = s.type === "Naval" ? m / 1.2 : m / 1.8; // different landmass
if (culture !== stateObj.culture) m = stateObj.form === 'Union' ? m / 1.2 : m / 2; // non-dominant culture
if (religion !== cells.religion[stateObj.center]) m = stateObj.form === 'Theocracy' ? m / 2.2 : m / 1.4; // non-dominant religion
if (cells.f[b.cell] !== cells.f[stateObj.center]) m = stateObj.type === 'Naval' ? m / 1.2 : m / 1.8; // different landmass
const type = getType(b.cell);
for (const u of options.military) {
if (u.type === "naval" && !b.port) continue; // only ports produce naval units
const perc = +u.urban;
if (isNaN(perc) || perc <= 0 || !s.temp[u.name]) continue;
for (const unit of options.military) {
if (unit.type === 'naval' && !b.port) continue; // only ports produce naval units
const perc = +unit.urban;
if (isNaN(perc) || perc <= 0 || !stateObj.temp[unit.name]) continue;
if (!passUnitLimits(unit, biome, state, culture, religion)) continue;
const mod = type === "generic" ? 1 : burgTypeModifier[type][u.type]; // cell specific modifier
const mod = type === 'generic' ? 1 : burgTypeModifier[type][unit.type]; // cell specific modifier
const army = m * perc * mod; // urban cell army
const t = rn(army * s.temp[u.name] * populationRate); // total troops
if (!t) continue;
let x = p[b.cell][0],
y = p[b.cell][1],
n = 0;
if (u.type === "naval") {
const total = rn(army * stateObj.temp[unit.name] * populationRate); // total troops
if (!total) continue;
let [x, y] = p[b.cell];
let n = 0;
if (unit.type === 'naval') {
let haven = cells.haven[b.cell];
(x = p[haven][0]), (y = p[haven][1]);
[x, y] = p[haven];
n = 1;
} // place naval in sea cell
s.temp.platoons.push({cell: b.cell, a: t, t, x, y, u: u.name, n, s: u.separate, type: u.type});
} // place naval to sea
stateObj.temp.platoons.push({cell: b.cell, a: total, t: total, x, y, u: unit.name, n, s: unit.separate, type: unit.type});
}
}
void (function removeExistingRegiments() {
armies.selectAll("g > g").each(function () {
const index = notes.findIndex(n => n.id === this.id);
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();
armies.selectAll('g').remove();
})();
const expected = 3 * populationRate; // expected regiment size
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.type === n1.type; // check if regiments can be merged
// get regiments for each state
valid.forEach(s => {
valid.forEach((s) => {
s.military = createRegiments(s.temp.platoons, s);
delete s.temp; // do not store temp data
drawRegiments(s.military, s.i);
@ -155,10 +175,10 @@ window.Military = (function () {
nodes.sort((a, b) => a.a - b.a); // form regiments in cells with most troops
const tree = d3.quadtree(
nodes,
d => d.x,
d => d.y
(d) => d.x,
(d) => d.y
);
nodes.forEach(n => {
nodes.forEach((n) => {
tree.remove(n);
const overlap = tree.find(n.x, n.y, 20);
if (overlap && overlap.t && mergeable(n, overlap)) {
@ -180,24 +200,24 @@ window.Military = (function () {
function merge(n0, n1) {
if (!n1.childen) n1.childen = [n0];
else n1.childen.push(n0);
if (n0.childen) n0.childen.forEach(n => n1.childen.push(n));
if (n0.childen) n0.childen.forEach((n) => n1.childen.push(n));
n1.t += n0.t;
n0.t = 0;
}
// parse regiments data
const regiments = nodes
.filter(n => n.t)
.filter((n) => n.t)
.sort((a, b) => b.t - a.t)
.map((r, i) => {
const u = {};
u[r.u] = r.a;
(r.childen || []).forEach(n => (u[n.u] = u[n.u] ? (u[n.u] += n.a) : n.a));
(r.childen || []).forEach((n) => (u[n.u] = u[n.u] ? (u[n.u] += n.a) : n.a));
return {i, a: r.t, cell: r.cell, x: r.x, y: r.y, bx: r.x, by: r.y, u, n: r.n, name, state: s.i};
});
// generate name for regiments
regiments.forEach(r => {
regiments.forEach((r) => {
r.name = getName(r, regiments);
r.icon = getEmblem(r);
generateNote(r, s);
@ -206,164 +226,175 @@ window.Military = (function () {
return regiments;
}
TIME && console.timeEnd("generateMilitaryForces");
TIME && console.timeEnd('generateMilitaryForces');
};
const getDefaultOptions = function () {
return [
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
{icon: "🏹", name: "archers", rural: 0.12, urban: 0.2, crew: 1, power: 1, type: "ranged", separate: 0},
{icon: "🐴", name: "cavalry", rural: 0.12, urban: 0.03, crew: 2, power: 2, type: "mounted", separate: 0},
{icon: "💣", name: "artillery", rural: 0, urban: 0.03, crew: 8, power: 12, type: "machinery", separate: 0},
{icon: "🌊", name: "fleet", rural: 0, urban: 0.015, crew: 100, power: 50, type: "naval", separate: 1}
{icon: '⚔️', name: 'infantry', rural: 0.25, urban: 0.2, crew: 1, power: 1, type: 'melee', separate: 0},
{icon: '🏹', name: 'archers', rural: 0.12, urban: 0.2, crew: 1, power: 1, type: 'ranged', separate: 0},
{icon: '🐴', name: 'cavalry', rural: 0.12, urban: 0.03, crew: 2, power: 2, type: 'mounted', separate: 0},
{icon: '💣', name: 'artillery', rural: 0, urban: 0.03, crew: 8, power: 12, type: 'machinery', separate: 0},
{icon: '🌊', name: 'fleet', rural: 0, urban: 0.015, crew: 100, power: 50, type: 'naval', separate: 1}
];
};
const drawRegiments = function (regiments, s) {
const size = +armies.attr("box-size");
const w = d => (d.n ? size * 4 : size * 6);
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 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 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);
.append('g')
.attr('id', 'army' + s)
.attr('fill', baseColor);
const g = army
.selectAll("g")
.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);
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", darkerColor)
.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);
.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);
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', darkerColor)
.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, s) {
const size = +armies.attr("box-size");
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" + s);
let army = armies.select('g#army' + s);
if (!army.size()) {
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
const baseColor = pack.states[s].color[0] === '#' ? pack.states[s].color : '#999';
army = armies
.append("g")
.attr("id", "army" + s)
.attr("fill", baseColor);
.append('g')
.attr('id', 'army' + s)
.attr('fill', baseColor);
}
const darkerColor = d3.color(army.attr("fill")).darker().hex();
const darkerColor = d3.color(army.attr('fill')).darker().hex();
const g = army
.append("g")
.attr("id", "regiment" + s + "-" + reg.i)
.attr("data-name", reg.name)
.attr("data-state", s)
.attr("data-id", reg.i);
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", darkerColor)
.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)
.append('g')
.attr('id', 'regiment' + s + '-' + reg.i)
.attr('data-name', reg.name)
.attr('data-state', s)
.attr('data-id', reg.i);
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', darkerColor)
.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);
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 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 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)")
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")
.attr('x', x1(x) - h)
.attr('y', y1(y));
el.select('.regimentIcon')
.transition(move)
.attr("x", x1(x) - size)
.attr("y", y);
.attr('x', x1(x) - size)
.attr('y', y);
};
// utilize si function to make regiment total text fit regiment box
const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
const getTotal = (reg) => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
const getName = function (r, regiments) {
const cells = pack.cells;
const proper = r.n ? null : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].name : cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : null;
const number = nth(regiments.filter(reg => reg.n === r.n && reg.i < r.i).length + 1);
const form = r.n ? "Fleet" : "Regiment";
const proper = r.n
? null
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
? pack.provinces[cells.province[r.cell]].name
: cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
? pack.burgs[cells.burg[r.cell]].name
: null;
const number = nth(regiments.filter((reg) => reg.n === r.n && reg.i < r.i).length + 1);
const form = r.n ? 'Fleet' : 'Regiment';
return `${number}${proper ? ` (${proper}) ` : ` `}${form}`;
};
// get default regiment emblem
const getEmblem = function (r) {
if (!r.n && !Object.values(r.u).length) return "🔰"; // "Newbie" regiment without troops
if (!r.n && pack.states[r.state].form === "Monarchy" && pack.cells.burg[r.cell] && pack.burgs[pack.cells.burg[r.cell]].capital) return "👑"; // "Royal" regiment based in capital
if (!r.n && !Object.values(r.u).length) return '🔰'; // "Newbie" regiment without troops
if (!r.n && pack.states[r.state].form === 'Monarchy' && pack.cells.burg[r.cell] && pack.burgs[pack.cells.burg[r.cell]].capital) return '👑'; // "Royal" regiment based in capital
const mainUnit = Object.entries(r.u).sort((a, b) => b[1] - a[1])[0][0]; // unit with more troops in regiment
const unit = options.military.find(u => u.name === mainUnit);
const unit = options.military.find((u) => u.name === mainUnit);
return unit.icon;
};
const generateNote = function (r, s) {
const cells = pack.cells;
const base = cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]] ? pack.burgs[cells.burg[r.cell]].name : cells.province[r.cell] && pack.provinces[cells.province[r.cell]] ? pack.provinces[cells.province[r.cell]].fullName : null;
const station = base ? `${r.name} is ${r.n ? "based" : "stationed"} in ${base}. ` : "";
const base =
cells.burg[r.cell] && pack.burgs[cells.burg[r.cell]]
? pack.burgs[cells.burg[r.cell]].name
: cells.province[r.cell] && pack.provinces[cells.province[r.cell]]
? pack.provinces[cells.province[r.cell]].fullName
: null;
const station = base ? `${r.name} is ${r.n ? 'based' : 'stationed'} in ${base}. ` : '';
const composition = r.a
? Object.keys(r.u)
.map(t => `${t}: ${r.u[t]}`)
.join("\r\n")
.map((t) => `${t}: ${r.u[t]}`)
.join('\r\n')
: null;
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : "";
const troops = composition ? `\r\n\r\nRegiment composition in ${options.year} ${options.eraShort}:\r\n${composition}.` : '';
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 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}`;
notes.push({id: `regiment${s.i}-${r.i}`, name: `${r.icon} ${r.name}`, legend});
};

View file

@ -1,4 +1,4 @@
"use strict";
'use strict';
window.Names = (function () {
let chains = [];
@ -6,34 +6,34 @@ window.Names = (function () {
// calculate Markov chain for a namesbase
const calculateChain = function (string) {
const chain = [];
const array = string.split(",");
const array = string.split(',');
for (const n of array) {
let name = n.trim().toLowerCase();
const basic = !/[^\u0000-\u007f]/.test(name); // basic chars and English rules can be applied
// split word into pseudo-syllables
for (let i = -1, syllable = ""; i < name.length; i += syllable.length || 1, syllable = "") {
let prev = name[i] || ""; // pre-onset letter
for (let i = -1, syllable = ''; i < name.length; i += syllable.length || 1, syllable = '') {
let prev = name[i] || ''; // pre-onset letter
let v = 0; // 0 if no vowels in syllable
for (let c = i + 1; name[c] && syllable.length < 5; c++) {
const that = name[c],
next = name[c + 1]; // next char
syllable += that;
if (syllable === " " || syllable === "-") break; // syllable starts with space or hyphen
if (!next || next === " " || next === "-") break; // no need to check
if (syllable === ' ' || syllable === '-') break; // syllable starts with space or hyphen
if (!next || next === ' ' || next === '-') break; // no need to check
if (vowel(that)) v = 1; // check if letter is vowel
// do not split some diphthongs
if (that === "y" && next === "e") continue; // 'ye'
if (that === 'y' && next === 'e') continue; // 'ye'
if (basic) {
// English-like
if (that === "o" && next === "o") continue; // 'oo'
if (that === "e" && next === "e") continue; // 'ee'
if (that === "a" && next === "e") continue; // 'ae'
if (that === "c" && next === "h") continue; // 'ch'
if (that === 'o' && next === 'o') continue; // 'oo'
if (that === 'e' && next === 'e') continue; // 'ee'
if (that === 'a' && next === 'e') continue; // 'ae'
if (that === 'c' && next === 'h') continue; // 'ch'
}
if (vowel(that) === next) break; // two same vowels in a row
@ -49,7 +49,7 @@ window.Names = (function () {
};
// update chain for specific base
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null);
const updateChain = (i) => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null);
// update chains for all used bases
const clearChains = () => (chains = []);
@ -57,39 +57,39 @@ window.Names = (function () {
// generate name using Markov's chain
const getBase = function (base, min, max, dupl) {
if (base === undefined) {
ERROR && console.error("Please define a base");
ERROR && console.error('Please define a base');
return;
}
if (!chains[base]) updateChain(base);
const data = chains[base];
if (!data || data[""] === undefined) {
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
ERROR && console.error("Namebase " + base + " is incorrect!");
return "ERROR";
if (!data || data[''] === undefined) {
tip('Namesbase ' + base + ' is incorrect. Please check in namesbase editor', false, 'error');
ERROR && console.error('Namebase ' + base + ' is incorrect!');
return 'ERROR';
}
if (!min) min = nameBases[base].min;
if (!max) max = nameBases[base].max;
if (dupl !== "") dupl = nameBases[base].d;
if (dupl !== '') dupl = nameBases[base].d;
let v = data[""],
let v = data[''],
cur = ra(v),
w = "";
w = '';
for (let i = 0; i < 20; i++) {
if (cur === "") {
if (cur === '') {
// end of word
if (w.length < min) {
cur = "";
w = "";
v = data[""];
cur = '';
w = '';
v = data[''];
} else break;
} else {
if (w.length + cur.length > max) {
// word too long
if (w.length < min) w += cur;
break;
} else v = data[last(cur)] || data[""];
} else v = data[last(cur)] || data[''];
}
w += cur;
@ -98,31 +98,29 @@ window.Names = (function () {
// parse word to get a final name
const l = last(w); // last letter
if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end
const basic = !/[^\u0000-\u007f]/.test(w); // true if word has only basic characters
if (l === "'" || l === ' ' || l === '-') w = w.slice(0, -1); // not allow some characters at the end
let name = [...w].reduce(function (r, c, i, d) {
if (c === d[i + 1] && !dupl.includes(c)) return r; // duplication is not allowed
if (!r.length) return c.toUpperCase();
if (r.slice(-1) === "-" && c === " ") return r; // remove space after hyphen
if (r.slice(-1) === " ") return r + c.toUpperCase(); // capitalize letter after space
if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen
if (c === "a" && d[i + 1] === "e") return r; // "ae" => "e"
if (basic && i + 1 < d.length && !vowel(c) && !vowel(d[i - 1]) && !vowel(d[i + 1])) return r; // remove consonant between 2 consonants
if (r.slice(-1) === '-' && c === ' ') return r; // remove space after hyphen
if (r.slice(-1) === ' ') return r + c.toUpperCase(); // capitalize letter after space
if (r.slice(-1) === '-') return r + c.toUpperCase(); // capitalize letter after hyphen
if (c === 'a' && d[i + 1] === 'e') return r; // "ae" => "e"
if (i + 2 < d.length && c === d[i + 1] && c === d[i + 2]) return r; // remove three same letters in a row
return r + c;
}, "");
}, '');
// join the word if any part has only 1 letter
if (name.split(" ").some(part => part.length < 2))
if (name.split(' ').some((part) => part.length < 2))
name = name
.split(" ")
.split(' ')
.map((p, i) => (i ? p.toLowerCase() : p))
.join("");
.join('');
if (name.length < 2) {
ERROR && console.error("Name is too short! Random name will be selected");
name = ra(nameBases[base].b.split(","));
ERROR && console.error('Name is too short! Random name will be selected');
name = ra(nameBases[base].b.split(','));
}
return name;
@ -130,84 +128,98 @@ window.Names = (function () {
// generate name for culture
const getCulture = function (culture, min, max, dupl) {
if (culture === undefined) {
ERROR && console.error("Please define a culture");
return;
}
if (culture === undefined) return ERROR && console.error('Please define a culture');
const base = pack.cultures[culture].base;
return getBase(base, min, max, dupl);
};
// generate short name for culture
const getCultureShort = function (culture) {
if (culture === undefined) {
ERROR && console.error("Please define a culture");
return;
}
if (culture === undefined) return ERROR && console.error('Please define a culture');
return getBaseShort(pack.cultures[culture].base);
};
// generate short name for base
const getBaseShort = function (base) {
if (nameBases[base] === undefined) {
tip(`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, false, "error");
tip(`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`, false, 'error');
base = 1;
}
const min = nameBases[base].min - 1;
const max = Math.max(nameBases[base].max - 2, min);
return getBase(base, min, max, "", 0);
return getBase(base, min, max, '', 0);
};
// generate state name based on capital or random name and culture-specific suffix
// prettier-ignore
const getState = function(name, culture, base) {
if (name === undefined) {ERROR && console.error("Please define a base name"); return;}
if (culture === undefined && base === undefined) {ERROR && console.error("Please define a culture"); return;}
const getState = function (name, culture, base) {
if (name === undefined) return ERROR && console.error('Please define a base name');
if (culture === undefined && base === undefined) return ERROR && console.error('Please define a culture');
if (base === undefined) base = pack.cultures[culture].base;
// exclude endings inappropriate for states name
if (name.includes(" ")) name = capitalize(name.replace(/ /g, "").toLowerCase()); // don't allow multiword state names
if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0,-4); // remove -berg for any
if (name.length > 5 && name.slice(-3) === "ton") name = name.slice(0,-3); // remove -ton for any
if (name.includes(' ')) name = capitalize(name.replace(/ /g, '').toLowerCase()); // don't allow multiword state names
if (name.length > 6 && name.slice(-4) === 'berg') name = name.slice(0, -4); // remove -berg for any
if (name.length > 5 && name.slice(-3) === 'ton') name = name.slice(0, -3); // remove -ton for any
if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0,-2); // remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; // Japanese ends on any vowel or -u
else if (base === 18 && P(.4)) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
if (base === 5 && ['sk', 'ev', 'ov'].includes(name.slice(-2))) name = name.slice(0, -2);
// remove -sk/-ev/-ov for Ruthenian
else if (base === 12) return vowel(name.slice(-1)) ? name : name + 'u';
// Japanese ends on any vowel or -u
else if (base === 18 && P(0.4)) name = vowel(name.slice(0, 1).toLowerCase()) ? 'Al' + name.toLowerCase() : 'Al ' + name; // Arabic starts with -Al
// no suffix for fantasy bases
if (base > 32 && base < 42) return name;
// define if suffix should be used
if (name.length > 3 && vowel(name.slice(-1))) {
if (vowel(name.slice(-2,-1)) && P(.85)) name = name.slice(0,-2); // 85% for vv
else if (P(.7)) name = name.slice(0,-1); // ~60% for cv
if (vowel(name.slice(-2, -1)) && P(0.85)) name = name.slice(0, -2);
// 85% for vv
else if (P(0.7)) name = name.slice(0, -1);
// ~60% for cv
else return name;
} else if (P(.4)) return name; // 60% for cc and vc
} else if (P(0.4)) return name; // 60% for cc and vc
// define suffix
let suffix = "ia"; // standard suffix
let suffix = 'ia'; // standard suffix
const rnd = Math.random(), l = name.length;
if (base === 3 && rnd < .03 && l < 7) suffix = "terra"; // Italian
else if (base === 4 && rnd < .03 && l < 7) suffix = "terra"; // Spanish
else if (base === 13 && rnd < .03 && l < 7) suffix = "terra"; // Portuguese
else if (base === 2 && rnd < .03 && l < 7) suffix = "terre"; // French
else if (base === 0 && rnd < .5 && l < 7) suffix = "land"; // German
else if (base === 1 && rnd < .4 && l < 7 ) suffix = "land"; // English
else if (base === 6 && rnd < .3 && l < 7) suffix = "land"; // Nordic
else if (base === 32 && rnd < .1 && l < 7) suffix = "land"; // generic Human
else if (base === 7 && rnd < .1) suffix = "eia"; // Greek
else if (base === 9 && rnd < .35) suffix = "maa"; // Finnic
else if (base === 15 && rnd < .4 && l < 6) suffix = "orszag"; // Hungarian
else if (base === 16) suffix = rnd < .6 ? "stan" : "ya"; // Turkish
else if (base === 10) suffix = "guk"; // Korean
else if (base === 11) suffix = " Guo"; // Chinese
else if (base === 14) suffix = rnd < .5 && l < 6 ? "tlan" : "co"; // Nahuatl
else if (base === 17 && rnd < .8) suffix = "a"; // Berber
else if (base === 18 && rnd < .8) suffix = "a"; // Arabic
const rnd = Math.random(),
l = name.length;
if (base === 3 && rnd < 0.03 && l < 7) suffix = 'terra';
// Italian
else if (base === 4 && rnd < 0.03 && l < 7) suffix = 'terra';
// Spanish
else if (base === 13 && rnd < 0.03 && l < 7) suffix = 'terra';
// Portuguese
else if (base === 2 && rnd < 0.03 && l < 7) suffix = 'terre';
// French
else if (base === 0 && rnd < 0.5 && l < 7) suffix = 'land';
// German
else if (base === 1 && rnd < 0.4 && l < 7) suffix = 'land';
// English
else if (base === 6 && rnd < 0.3 && l < 7) suffix = 'land';
// Nordic
else if (base === 32 && rnd < 0.1 && l < 7) suffix = 'land';
// generic Human
else if (base === 7 && rnd < 0.1) suffix = 'eia';
// Greek
else if (base === 9 && rnd < 0.35) suffix = 'maa';
// Finnic
else if (base === 15 && rnd < 0.4 && l < 6) suffix = 'orszag';
// Hungarian
else if (base === 16) suffix = rnd < 0.6 ? 'stan' : 'ya';
// Turkish
else if (base === 10) suffix = 'guk';
// Korean
else if (base === 11) suffix = ' Guo';
// Chinese
else if (base === 14) suffix = rnd < 0.5 && l < 6 ? 'tlan' : 'co';
// Nahuatl
else if (base === 17 && rnd < 0.8) suffix = 'a';
// Berber
else if (base === 18 && rnd < 0.8) suffix = 'a'; // Arabic
return validateSuffix(name, suffix);
}
};
function validateSuffix(name, suffix) {
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
@ -220,24 +232,24 @@ window.Names = (function () {
// generato name for the map
const getMapName = function (force) {
if (!force && locked("mapName")) return;
if (force && locked("mapName")) unlock("mapName");
if (!force && locked('mapName')) return;
if (force && locked('mapName')) unlock('mapName');
const base = P(0.7) ? 2 : P(0.5) ? rand(0, 6) : rand(0, 31);
if (!nameBases[base]) {
tip("Namebase is not found", false, "error");
return "";
tip('Namebase is not found', false, 'error');
return '';
}
const min = nameBases[base].min - 1;
const max = Math.max(nameBases[base].max - 3, min);
const baseName = getBase(base, min, max, "", 0);
const baseName = getBase(base, min, max, '', 0);
const name = P(0.7) ? addSuffix(baseName) : baseName;
mapName.value = name;
};
function addSuffix(name) {
const suffix = P(0.8) ? "ia" : "land";
if (suffix === "ia" && name.length > 6) name = name.slice(0, -(name.length - 3));
else if (suffix === "land" && name.length > 6) name = name.slice(0, -(name.length - 5));
const suffix = P(0.8) ? 'ia' : 'land';
if (suffix === 'ia' && name.length > 6) name = name.slice(0, -(name.length - 3));
else if (suffix === 'land' && name.length > 6) name = name.slice(0, -(name.length - 5));
return validateSuffix(name, suffix);
}
@ -250,7 +262,7 @@ window.Names = (function () {
{name: "English", i: 1, min: 6, max: 11, d: "", m: .1, b: "Abingdon,Albrighton,Alcester,Almondbury,Altrincham,Amersham,Andover,Appleby,Ashboume,Atherstone,Aveton,Axbridge,Aylesbury,Baldock,Bamburgh,Barton,Basingstoke,Berden,Bere,Berkeley,Berwick,Betley,Bideford,Bingley,Birmingham,Blandford,Blechingley,Bodmin,Bolton,Bootham,Boroughbridge,Boscastle,Bossinney,Bramber,Brampton,Brasted,Bretford,Bridgetown,Bridlington,Bromyard,Bruton,Buckingham,Bungay,Burton,Calne,Cambridge,Canterbury,Carlisle,Castleton,Caus,Charmouth,Chawleigh,Chichester,Chillington,Chinnor,Chipping,Chisbury,Cleobury,Clifford,Clifton,Clitheroe,Cockermouth,Coleshill,Combe,Congleton,Crafthole,Crediton,Cuddenbeck,Dalton,Darlington,Dodbrooke,Drax,Dudley,Dunstable,Dunster,Dunwich,Durham,Dymock,Exeter,Exning,Faringdon,Felton,Fenny,Finedon,Flookburgh,Fowey,Frampton,Gateshead,Gatton,Godmanchester,Grampound,Grantham,Guildford,Halesowen,Halton,Harbottle,Harlow,Hatfield,Hatherleigh,Haydon,Helston,Henley,Hertford,Heytesbury,Hinckley,Hitchin,Holme,Hornby,Horsham,Kendal,Kenilworth,Kilkhampton,Kineton,Kington,Kinver,Kirby,Knaresborough,Knutsford,Launceston,Leighton,Lewes,Linton,Louth,Luton,Lyme,Lympstone,Macclesfield,Madeley,Malborough,Maldon,Manchester,Manningtree,Marazion,Marlborough,Marshfield,Mere,Merryfield,Middlewich,Midhurst,Milborne,Mitford,Modbury,Montacute,Mousehole,Newbiggin,Newborough,Newbury,Newenden,Newent,Norham,Northleach,Noss,Oakham,Olney,Orford,Ormskirk,Oswestry,Padstow,Paignton,Penkneth,Penrith,Penzance,Pershore,Petersfield,Pevensey,Pickering,Pilton,Pontefract,Portsmouth,Preston,Quatford,Reading,Redcliff,Retford,Rockingham,Romney,Rothbury,Rothwell,Salisbury,Saltash,Seaford,Seasalter,Sherston,Shifnal,Shoreham,Sidmouth,Skipsea,Skipton,Solihull,Somerton,Southam,Southwark,Standon,Stansted,Stapleton,Stottesdon,Sudbury,Swavesey,Tamerton,Tarporley,Tetbury,Thatcham,Thaxted,Thetford,Thornbury,Tintagel,Tiverton,Torksey,Totnes,Towcester,Tregoney,Trematon,Tutbury,Uxbridge,Wallingford,Wareham,Warenmouth,Wargrave,Warton,Watchet,Watford,Wendover,Westbury,Westcheap,Weymouth,Whitford,Wickwar,Wigan,Wigmore,Winchelsea,Winkleigh,Wiscombe,Witham,Witheridge,Wiveliscombe,Woodbury,Yeovil"},
{name: "French", i: 2, min: 5, max: 13, d: "nlrs", m: .1, b: "Adon,Aillant,Amilly,Andonville,Ardon,Artenay,Ascheres,Ascoux,Attray,Aubin,Audeville,Aulnay,Autruy,Auvilliers,Auxy,Aveyron,Baccon,Bardon,Barville,Batilly,Baule,Bazoches,Beauchamps,Beaugency,Beaulieu,Beaune,Bellegarde,Boesses,Boigny,Boiscommun,Boismorand,Boisseaux,Bondaroy,Bonnee,Bonny,Bordes,Bou,Bougy,Bouilly,Boulay,Bouzonville,Bouzy,Boynes,Bray,Breteau,Briare,Briarres,Bricy,Bromeilles,Bucy,Cepoy,Cercottes,Cerdon,Cernoy,Cesarville,Chailly,Chaingy,Chalette,Chambon,Champoulet,Chanteau,Chantecoq,Chapell,Charme,Charmont,Charsonville,Chateau,Chateauneuf,Chatel,Chatenoy,Chatillon,Chaussy,Checy,Chevannes,Chevillon,Chevilly,Chevry,Chilleurs,Choux,Chuelles,Clery,Coinces,Coligny,Combleux,Combreux,Conflans,Corbeilles,Corquilleroy,Cortrat,Coudroy,Coullons,Coulmiers,Courcelles,Courcy,Courtemaux,Courtempierre,Courtenay,Cravant,Crottes,Dadonville,Dammarie,Dampierre,Darvoy,Desmonts,Dimancheville,Donnery,Dordives,Dossainville,Douchy,Dry,Echilleuses,Egry,Engenville,Epieds,Erceville,Ervauville,Escrennes,Escrignelles,Estouy,Faverelles,Fay,Feins,Ferolles,Ferrieres,Fleury,Fontenay,Foret,Foucherolles,Freville,Gatinais,Gaubertin,Gemigny,Germigny,Gidy,Gien,Girolles,Givraines,Gondreville,Grangermont,Greneville,Griselles,Guigneville,Guilly,Gyleslonains,Huetre,Huisseau,Ingrannes,Ingre,Intville,Isdes,Jargeau,Jouy,Juranville,Bussiere,Laas,Ladon,Lailly,Langesse,Leouville,Ligny,Lombreuil,Lorcy,Lorris,Loury,Louzouer,Malesherbois,Marcilly,Mardie,Mareau,Marigny,Marsainvilliers,Melleroy,Menestreau,Merinville,Messas,Meung,Mezieres,Migneres,Mignerette,Mirabeau,Montargis,Montbarrois,Montbouy,Montcresson,Montereau,Montigny,Montliard,Mormant,Morville,Moulinet,Moulon,Nancray,Nargis,Nesploy,Neuville,Neuvy,Nevoy,Nibelle,Nogent,Noyers,Ocre,Oison,Olivet,Ondreville,Onzerain,Orleans,Ormes,Orville,Oussoy,Outarville,Ouzouer,Pannecieres,Pannes,Patay,Paucourt,Pers,Pierrefitte,Pithiverais,Pithiviers,Poilly,Potier,Prefontaines,Presnoy,Pressigny,Puiseaux,Quiers,Ramoulu,Rebrechien,Rouvray,Rozieres,Rozoy,Ruan,Sandillon,Santeau,Saran,Sceaux,Seichebrieres,Semoy,Sennely,Sermaises,Sigloy,Solterre,Sougy,Sully,Sury,Tavers,Thignonville,Thimory,Thorailles,Thou,Tigy,Tivernon,Tournoisis,Trainou,Treilles,Trigueres,Trinay,Vannes,Varennes,Vennecy,Vieilles,Vienne,Viglain,Vignes,Villamblain,Villemandeur,Villemoutiers,Villemurlin,Villeneuve,Villereau,Villevoques,Villorceau,Vimory,Vitry,Vrigny,Ivre"},
{name: "Italian", i: 3, min: 5, max: 12, d: "cltr", m: .1, b: "Accumoli,Acquafondata,Acquapendente,Acuto,Affile,Agosta,Alatri,Albano,Allumiere,Alvito,Amaseno,Amatrice,Anagni,Anguillara,Anticoli,Antrodoco,Anzio,Aprilia,Aquino,Arce,Arcinazzo,Ardea,Ariccia,Arlena,Arnara,Arpino,Arsoli,Artena,Ascrea,Atina,Ausonia,Bagnoregio,Barbarano,Bassano,Bassiano,Bellegra,Belmonte,Blera,Bolsena,Bomarzo,Borbona,Borgo,Borgorose,Boville,Bracciano,Broccostella,Calcata,Camerata,Campagnano,Campodimele,Campoli,Canale,Canepina,Canino,Cantalice,Cantalupo,Canterano,Capena,Capodimonte,Capranica,Caprarola,Carbognano,Casalattico,Casalvieri,Casape,Casaprota,Casperia,Cassino,Castelforte,Castelliri,Castello,Castelnuovo,Castiglione,Castro,Castrocielo,Cave,Ceccano,Celleno,Cellere,Ceprano,Cerreto,Cervara,Cervaro,Cerveteri,Ciampino,Ciciliano,Cineto,Cisterna,Cittaducale,Cittareale,Civita,Civitavecchia,Civitella,Colfelice,Collalto,Colle,Colleferro,Collegiove,Collepardo,Collevecchio,Colli,Colonna,Concerviano,Configni,Contigliano,Corchiano,Coreno,Cori,Cottanello,Esperia,Fabrica,Faleria,Fara,Farnese,Ferentino,Fiamignano,Fiano,Filacciano,Filettino,Fiuggi,Fiumicino,Fondi,Fontana,Fonte,Fontechiari,Forano,Formello,Formia,Frascati,Frasso,Frosinone,Fumone,Gaeta,Gallese,Gallicano,Gallinaro,Gavignano,Genazzano,Genzano,Gerano,Giuliano,Gorga,Gradoli,Graffignano,Greccio,Grottaferrata,Grotte,Guarcino,Guidonia,Ischia,Isola,Itri,Jenne,Labico,Labro,Ladispoli,Lanuvio,Lariano,Latera,Lenola,Leonessa,Licenza,Longone,Lubriano,Maenza,Magliano,Mandela,Manziana,Marano,Marcellina,Marcetelli,Marino,Marta,Mazzano,Mentana,Micigliano,Minturno,Mompeo,Montalto,Montasola,Monte,Montebuono,Montefiascone,Monteflavio,Montelanico,Monteleone,Montelibretti,Montenero,Monterosi,Monterotondo,Montopoli,Montorio,Moricone,Morlupo,Morolo,Morro,Nazzano,Nemi,Nepi,Nerola,Nespolo,Nettuno,Norma,Olevano,Onano,Oriolo,Orte,Orvinio,Paganico,Palestrina,Paliano,Palombara,Pastena,Patrica,Percile,Pescorocchiano,Pescosolido,Petrella,Piansano,Picinisco,Pico,Piedimonte,Piglio,Pignataro,Pisoniano,Pofi,Poggio,Poli,Pomezia,Pontecorvo,Pontinia,Ponza,Ponzano,Posta,Pozzaglia,Priverno,Proceno,Prossedi,Riano,Rieti,Rignano,Riofreddo,Ripi,Rivodutri,Rocca,Roccagiovine,Roccagorga,Roccantica,Roccasecca,Roiate,Ronciglione,Roviano,Sabaudia,Sacrofano,Salisano,Sambuci,Santa,Santi,Santopadre,Saracinesco,Scandriglia,Segni,Selci,Sermoneta,Serrone,Settefrati,Sezze,Sgurgola,Sonnino,Sora,Soriano,Sperlonga,Spigno,Stimigliano,Strangolagalli,Subiaco,Supino,Sutri,Tarano,Tarquinia,Terelle,Terracina,Tessennano,Tivoli,Toffia,Tolfa,Torre,Torri,Torrice,Torricella,Torrita,Trevi,Trevignano,Trivigliano,Turania,Tuscania,Vacone,Valentano,Vallecorsa,Vallemaio,Vallepietra,Vallerano,Vallerotonda,Vallinfreda,Valmontone,Varco,Vasanello,Vejano,Velletri,Ventotene,Veroli,Vetralla,Vicalvi,Vico,Vicovaro,Vignanello,Viterbo,Viticuso,Vitorchiano,Vivaro,Zagarolo"},
{name: "Castillian", i: 4, min: 5, max: 11, d: "lr", m: 0, b: "Abanades,Ablanque,Adobes,Ajofrin,Alameda,Alaminos,Alarilla,Albalate,Albares,Albarreal,Albendiego,Alcabon,Alcanizo,Alcaudete,Alcocer,Alcolea,Alcoroches,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Alique,Almadrones,Almendral,Almoguera,Almonacid,Almorox,Alocen,Alovera,Alustante,Angon,Anguita,Anover,Anquela,Arbancon,Arbeteta,Arcicollar,Argecilla,Arges,Armallones,Armuna,Arroyo,Atanzon,Atienza,Aunon,Azuqueca,Azutan,Baides,Banos,Banuelos,Barcience,Bargas,Barriopedro,Belvis,Berninches,Borox,Brihuega,Budia,Buenaventura,Bujalaro,Burguillos,Burujon,Bustares,Cabanas,Cabanillas,Calera,Caleruela,Calzada,Camarena,Campillo,Camunas,Canizar,Canredondo,Cantalojas,Cardiel,Carmena,Carranque,Carriches,Casa,Casarrubios,Casas,Casasbuenas,Caspuenas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Cebolla,Cedillo,Cendejas,Centenera,Cervera,Checa,Chequilla,Chillaron,Chiloeches,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Ciruelos,Cobeja,Cobeta,Cobisa,Cogollor,Cogolludo,Condemios,Congostrina,Consuegra,Copernal,Corduente,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,El,Embid,Erustes,Escalona,Escalonilla,Escamilla,Escariche,Escopete,Espinosa,Espinoso,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galve,Galvez,Garciotum,Gascuena,Gerindote,Guadamur,Henche,Heras,Herreria,Herreruela,Hijes,Hinojosa,Hita,Hombrados,Hontanar,Hontoba,Horche,Hormigos,Huecas,Huermeces,Huerta,Hueva,Humanes,Illan,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Las,Layos,Ledanca,Lillo,Lominchar,Loranca,Los,Lucillos,Lupiana,Luzaga,Luzon,Madridejos,Magan,Majaelrayo,Malaga,Malaguilla,Malpica,Mandayona,Mantiel,Manzaneque,Maqueda,Maranchon,Marchamalo,Marjaliza,Marrupe,Mascaraque,Masegoso,Matarrubia,Matillas,Mazarete,Mazuecos,Medranda,Megina,Mejorada,Mentrida,Mesegar,Miedes,Miguel,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Mohedas,Molina,Monasterio,Mondejar,Montarron,Mora,Moratilla,Morenilla,Muduex,Nambroca,Navalcan,Negredo,Noblejas,Noez,Nombela,Noves,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palmaces,Palomeque,Pantoja,Pardos,Paredes,Pareja,Parrillas,Pastrana,Pelahustan,Penalen,Penalver,Pepino,Peralejos,Peralveche,Pinilla,Pioz,Piqueras,Polan,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Pulgar,Quer,Quero,Quintanar,Quismondo,Rebollosa,Recas,Renera,Retamoso,Retiendas,Riba,Rielves,Rillo,Riofrio,Robledillo,Robledo,Romanillos,Romanones,Rueda,Sacecorbo,Sacedon,Saelices,Salmeron,San,Santa,Santiuste,Santo,Sartajada,Sauca,Sayaton,Segurilla,Selas,Semillas,Sesena,Setiles,Sevilleja,Sienes,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Sotodosos,Talavera,Tamajon,Taragudo,Taravilla,Tartanedo,Tembleque,Tendilla,Terzaga,Tierzo,Tordellego,Tordelrabano,Tordesilos,Torija,Torralba,Torre,Torrecilla,Torrecuadrada,Torrejon,Torremocha,Torrico,Torrijos,Torrubia,Tortola,Tortuera,Tortuero,Totanes,Traid,Trijueque,Trillo,Turleque,Uceda,Ugena,Ujados,Urda,Utande,Valdarachas,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Vinuelas,Yebes,Yebra,Yelamos,Yeles,Yepes,Yuncler,Yunclillos,Yuncos,Yunquera,Zaorejas,Zarzuela,Zorita"},
{name: "Castillian", i: 4, min: 5, max: 11, d: "lr", m: 0, b: "Abanades,Ablanque,Adobes,Ajofrin,Alameda,Alaminos,Alarilla,Albalate,Albares,Albarreal,Albendiego,Alcabon,Alcanizo,Alcaudete,Alcocer,Alcolea,Alcoroches,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Alique,Almadrones,Almendral,Almoguera,Almonacid,Almorox,Alocen,Alovera,Alustante,Angon,Anguita,Anover,Anquela,Arbancon,Arbeteta,Arcicollar,Argecilla,Arges,Armallones,Armuna,Arroyo,Atanzon,Atienza,Aunon,Azuqueca,Azutan,Baides,Banos,Banuelos,Barcience,Bargas,Barriopedro,Belvis,Berninches,Borox,Brihuega,Budia,Buenaventura,Bujalaro,Burguillos,Burujon,Bustares,Cabanas,Cabanillas,Calera,Caleruela,Calzada,Camarena,Campillo,Camunas,Canizar,Canredondo,Cantalojas,Cardiel,Carmena,Carranque,Carriches,Casa,Casarrubios,Casas,Casasbuenas,Caspuenas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Cebolla,Cedillo,Cendejas,Centenera,Cervera,Checa,Chequilla,Chillaron,Chiloeches,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Ciruelos,Cobeja,Cobeta,Cobisa,Cogollor,Cogolludo,Condemios,Congostrina,Consuegra,Copernal,Corduente,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,El,Embid,Erustes,Escalona,Escalonilla,Escamilla,Escariche,Escopete,Espinosa,Espinoso,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galve,Galvez,Garciotum,Gascuena,Gerindote,Guadamur,Henche,Heras,Herreria,Herreruela,Hijes,Hinojosa,Hita,Hombrados,Hontanar,Hontoba,Horche,Hormigos,Huecas,Huermeces,Huerta,Hueva,Humanes,Illan,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Las,Layos,Ledanca,Lillo,Lominchar,Loranca,Los,Lucillos,Lupiana,Luzaga,Luzon,Madridejos,Magan,Majaelrayo,Malaga,Malaguilla,Malpica,Mandayona,Mantiel,Manzaneque,Maqueda,Maranchon,Marchamalo,Marjaliza,Marrupe,Mascaraque,Masegoso,Matarrubia,Matillas,Mazarete,Mazuecos,Medranda,Megina,Mejorada,Mentrida,Mesegar,Miedes,Miguel,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Mohedas,Molina,Monasterio,Mondejar,Montarron,Mora,Moratilla,Morenilla,Muduex,Nambroca,Navalcan,Negredo,Noblejas,Noez,Nombela,Noves,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palmaces,Palomeque,Pantoja,Pardos,Paredes,Pareja,Parrillas,Pastrana,Pelahustan,Penalen,Penalver,Pepino,Peralejos,Peralveche,Pinilla,Pioz,Piqueras,Polan,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Pulgar,Quer,Quero,Quintanar,Quismondo,Rebollosa,Recas,Renera,Retamoso,Retiendas,Riba,Rielves,Rillo,Riofrio,Robledillo,Robledo,Romanillos,Romanones,Rueda,Sacecorbo,Sacedon,Saelices,Salmeron,San,Santa,Santiuste,Santo,Sartajada,Sauca,Sayaton,Segurilla,Selas,Semillas,Sesena,Setiles,Sevilleja,Sienes,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Sotodasos,Talavera,Tamajon,Taragudo,Taravilla,Tartanedo,Tembleque,Tendilla,Terzaga,Tierzo,Tordellego,Tordelrabano,Tordesilos,Torija,Torralba,Torre,Torrecilla,Torrecuadrada,Torrejon,Torremocha,Torrico,Torrijos,Torrubia,Tortola,Tortuera,Tortuero,Totanes,Traid,Trijueque,Trillo,Turleque,Uceda,Ugena,Ujados,Urda,Utande,Valdarachas,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Vinuelas,Yebes,Yebra,Yelamos,Yeles,Yepes,Yuncler,Yunclillos,Yuncos,Yunquera,Zaorejas,Zarzuela,Zorita"},
{name: "Ruthenian", i: 5, min: 5, max: 10, d: "", m: 0, b: "Belgorod,Beloberezhye,Belyi,Belz,Berestiy,Berezhets,Berezovets,Berezutsk,Bobruisk,Bolonets,Borisov,Borovsk,Bozhesk,Bratslav,Bryansk,Brynsk,Buryn,Byhov,Chechersk,Chemesov,Cheremosh,Cherlen,Chern,Chernigov,Chernitsa,Chernobyl,Chernogorod,Chertoryesk,Chetvertnia,Demyansk,Derevesk,Devyagoresk,Dichin,Dmitrov,Dorogobuch,Dorogobuzh,Drestvin,Drokov,Drutsk,Dubechin,Dubichi,Dubki,Dubkov,Dveren,Galich,Glebovo,Glinsk,Goloty,Gomiy,Gorodets,Gorodische,Gorodno,Gorohovets,Goroshin,Gorval,Goryshon,Holm,Horobor,Hoten,Hotin,Hotmyzhsk,Ilovech,Ivan,Izborsk,Izheslavl,Kamenets,Kanev,Karachev,Karna,Kavarna,Klechesk,Klyapech,Kolomyya,Kolyvan,Kopyl,Korec,Kornik,Korochunov,Korshev,Korsun,Koshkin,Kotelno,Kovyla,Kozelsk,Kozelsk,Kremenets,Krichev,Krylatsk,Ksniatin,Kulatsk,Kursk,Kursk,Lebedev,Lida,Logosko,Lomihvost,Loshesk,Loshichi,Lubech,Lubno,Lubutsk,Lutsk,Luchin,Luki,Lukoml,Luzha,Lvov,Mtsensk,Mdin,Medniki,Melecha,Merech,Meretsk,Mescherskoe,Meshkovsk,Metlitsk,Mezetsk,Mglin,Mihailov,Mikitin,Mikulino,Miloslavichi,Mogilev,Mologa,Moreva,Mosalsk,Moschiny,Mozyr,Mstislav,Mstislavets,Muravin,Nemech,Nemiza,Nerinsk,Nichan,Novgorod,Novogorodok,Obolichi,Obolensk,Obolensk,Oleshsk,Olgov,Omelnik,Opoka,Opoki,Oreshek,Orlets,Osechen,Oster,Ostrog,Ostrov,Perelai,Peremil,Peremyshl,Pererov,Peresechen,Perevitsk,Pereyaslav,Pinsk,Ples,Polotsk,Pronsk,Proposhesk,Punia,Putivl,Rechitsa,Rodno,Rogachev,Romanov,Romny,Roslavl,Rostislavl,Rostovets,Rsha,Ruza,Rybchesk,Rylsk,Rzhavesk,Rzhev,Rzhischev,Sambor,Serensk,Serensk,Serpeysk,Shilov,Shuya,Sinech,Sizhka,Skala,Slovensk,Slutsk,Smedin,Sneporod,Snitin,Snovsk,Sochevo,Sokolec,Starica,Starodub,Stepan,Sterzh,Streshin,Sutesk,Svinetsk,Svisloch,Terebovl,Ternov,Teshilov,Teterin,Tiversk,Torchevsk,Toropets,Torzhok,Tripolye,Trubchevsk,Tur,Turov,Usvyaty,Uteshkov,Vasilkov,Velil,Velye,Venev,Venicha,Verderev,Vereya,Veveresk,Viazma,Vidbesk,Vidychev,Voino,Volodimer,Volok,Volyn,Vorobesk,Voronich,Voronok,Vorotynsk,Vrev,Vruchiy,Vselug,Vyatichsk,Vyatka,Vyshegorod,Vyshgorod,Vysokoe,Yagniatin,Yaropolch,Yasenets,Yuryev,Yuryevets,Zaraysk,Zhitomel,Zholvazh,Zizhech,Zubkov,Zudechev,Zvenigorod"},
{name: "Nordic", i: 6, min: 6, max: 10, d: "kln", m: .1, b: "Akureyri,Aldra,Alftanes,Andenes,Austbo,Auvog,Bakkafjordur,Ballangen,Bardal,Beisfjord,Bifrost,Bildudalur,Bjerka,Bjerkvik,Bjorkosen,Bliksvaer,Blokken,Blonduos,Bolga,Bolungarvik,Borg,Borgarnes,Bosmoen,Bostad,Bostrand,Botsvika,Brautarholt,Breiddalsvik,Bringsli,Brunahlid,Budardalur,Byggdakjarni,Dalvik,Djupivogur,Donnes,Drageid,Drangsnes,Egilsstadir,Eiteroga,Elvenes,Engavogen,Ertenvog,Eskifjordur,Evenes,Eyrarbakki,Fagernes,Fallmoen,Fellabaer,Fenes,Finnoya,Fjaer,Fjelldal,Flakstad,Flateyri,Flostrand,Fludir,Gardaber,Gardur,Gimstad,Givaer,Gjeroy,Gladstad,Godoya,Godoynes,Granmoen,Gravdal,Grenivik,Grimsey,Grindavik,Grytting,Hafnir,Halsa,Hauganes,Haugland,Hauknes,Hella,Helland,Hellissandur,Hestad,Higrav,Hnifsdalur,Hofn,Hofsos,Holand,Holar,Holen,Holkestad,Holmavik,Hopen,Hovden,Hrafnagil,Hrisey,Husavik,Husvik,Hvammstangi,Hvanneyri,Hveragerdi,Hvolsvollur,Igeroy,Indre,Inndyr,Innhavet,Innes,Isafjordur,Jarklaustur,Jarnsreykir,Junkerdal,Kaldvog,Kanstad,Karlsoy,Kavosen,Keflavik,Kjelde,Kjerstad,Klakk,Kopasker,Kopavogur,Korgen,Kristnes,Krutoga,Krystad,Kvina,Lande,Laugar,Laugaras,Laugarbakki,Laugarvatn,Laupstad,Leines,Leira,Leiren,Leland,Lenvika,Loding,Lodingen,Lonsbakki,Lopsmarka,Lovund,Luroy,Maela,Melahverfi,Meloy,Mevik,Misvaer,Mornes,Mosfellsber,Moskenes,Myken,Naurstad,Nesberg,Nesjahverfi,Nesset,Nevernes,Obygda,Ofoten,Ogskardet,Okervika,Oknes,Olafsfjordur,Oldervika,Olstad,Onstad,Oppeid,Oresvika,Orsnes,Orsvog,Osmyra,Overdal,Prestoya,Raudalaekur,Raufarhofn,Reipo,Reykholar,Reykholt,Reykjahlid,Rif,Rinoya,Rodoy,Rognan,Rosvika,Rovika,Salhus,Sanden,Sandgerdi,Sandoker,Sandset,Sandvika,Saudarkrokur,Selfoss,Selsoya,Sennesvik,Setso,Siglufjordur,Silvalen,Skagastrond,Skjerstad,Skonland,Skorvogen,Skrova,Sleneset,Snubba,Softing,Solheim,Solheimar,Sorarnoy,Sorfugloy,Sorland,Sormela,Sorvaer,Sovika,Stamsund,Stamsvika,Stave,Stokka,Stokkseyri,Storjord,Storo,Storvika,Strand,Straumen,Strendene,Sudavik,Sudureyri,Sundoya,Sydalen,Thingeyri,Thorlakshofn,Thorshofn,Tjarnabyggd,Tjotta,Tosbotn,Traelnes,Trofors,Trones,Tverro,Ulvsvog,Unnstad,Utskor,Valla,Vandved,Varmahlid,Vassos,Vevelstad,Vidrek,Vik,Vikholmen,Vogar,Vogehamn,Vopnafjordur"},
{name: "Greek", i: 7, min: 5, max: 11, d: "s", m: .1, b: "Abdera,Abila,Abydos,Acanthus,Acharnae,Actium,Adramyttium,Aegae,Aegina,Aegium,Aenus,Agrinion,Aigosthena,Akragas,Akrai,Akrillai,Akroinon,Akrotiri,Alalia,Alexandreia,Alexandretta,Alexandria,Alinda,Amarynthos,Amaseia,Ambracia,Amida,Amisos,Amnisos,Amphicaea,Amphigeneia,Amphipolis,Amphissa,Ankon,Antigona,Antipatrea,Antioch,Antioch,Antiochia,Andros,Apamea,Aphidnae,Apollonia,Argos,Arsuf,Artanes,Artemita,Argyroupoli,Asine,Asklepios,Aspendos,Assus,Astacus,Athenai,Athmonia,Aytos,Ancient,Baris,Bhrytos,Borysthenes,Berge,Boura,Bouthroton,Brauron,Byblos,Byllis,Byzantium,Bythinion,Callipolis,Cebrene,Chalcedon,Calydon,Carystus,Chamaizi,Chalcis,Chersonesos,Chios,Chytri,Clazomenae,Cleonae,Cnidus,Colosse,Corcyra,Croton,Cyme,Cyrene,Cythera,Decelea,Delos,Delphi,Demetrias,Dicaearchia,Dimale,Didyma,Dion,Dioscurias,Dodona,Dorylaion,Dyme,Edessa,Elateia,Eleusis,Eleutherna,Emporion,Ephesus,Ephyra,Epidamnos,Epidauros,Eresos,Eretria,Erythrae,Eubea,Gangra,Gaza,Gela,Golgi,Gonnos,Gorgippia,Gournia,Gortyn,Gythium,Hagios,Hagia,Halicarnassus,Halieis,Helike,Heliopolis,Hellespontos,Helorus,Hemeroskopeion,Heraclea,Hermione,Hermonassa,Hierapetra,Hierapolis,Himera,Histria,Hubla,Hyele,Ialysos,Iasus,Idalium,Imbros,Iolcus,Itanos,Ithaca,Juktas,Kallipolis,Kamares,Kameiros,Kannia,Kamarina,Kasmenai,Katane,Kerkinitida,Kepoi,Kimmerikon,Kios,Klazomenai,Knidos,Knossos,Korinthos,Kos,Kourion,Kume,Kydonia,Kynos,Kyrenia,Lamia,Lampsacus,Laodicea,Lapithos,Larissa,Lato,Laus,Lebena,Lefkada,Lekhaion,Leibethra,Leontinoi,Lepreum,Lessa,Lilaea,Lindus,Lissus,Epizephyrian,Madytos,Magnesia,Mallia,Mantineia,Marathon,Marmara,Maroneia,Masis,Massalia,Megalopolis,Megara,Mesembria,Messene,Metapontum,Methana,Methone,Methumna,Miletos,Misenum,Mochlos,Monastiraki,Morgantina,Mulai,Mukenai,Mylasa,Myndus,Myonia,Myra,Myrmekion,Mutilene,Myos,Nauplios,Naucratis,Naupactus,Naxos,Neapoli,Neapolis,Nemea,Nicaea,Nicopolis,Nirou,Nymphaion,Nysa,Oenoe,Oenus,Odessos,Olbia,Olous,Olympia,Olynthus,Opus,Orchomenus,Oricos,Orestias,Oreus,Oropus,Onchesmos,Pactye,Pagasae,Palaikastro,Pandosia,Panticapaeum,Paphos,Parium,Paros,Parthenope,Patrae,Pavlopetri,Pegai,Pelion,Peiraies,Pella,Percote,Pergamum,Petsofa,Phaistos,Phaleron,Phanagoria,Pharae,Pharnacia,Pharos,Phaselis,Philippi,Pithekussa,Philippopolis,Platanos,Phlius,Pherae,Phocaea,Pinara,Pisa,Pitane,Pitiunt,Pixous,Plataea,Poseidonia,Potidaea,Priapus,Priene,Prousa,Pseira,Psychro,Pteleum,Pydna,Pylos,Pyrgos,Rhamnus,Rhegion,Rhithymna,Rhodes,Rhypes,Rizinia,Salamis,Same,Samos,Scyllaeum,Selinus,Seleucia,Semasus,Sestos,Scidrus,Sicyon,Side,Sidon,Siteia,Sinope,Siris,Sklavokampos,Smyrna,Soli,Sozopolis,Sparta,Stagirus,Stratos,Stymphalos,Sybaris,Surakousai,Taras,Tanagra,Tanais,Tauromenion,Tegea,Temnos,Tenedos,Tenea,Teos,Thapsos,Thassos,Thebai,Theodosia,Therma,Thespiae,Thronion,Thoricus,Thurii,Thyreum,Thyria,Tiruns,Tithoraea,Tomis,Tragurion,Trapeze,Trapezus,Tripolis,Troizen,Troliton,Troy,Tylissos,Tyras,Tyros,Tyritake,Vasiliki,Vathypetros,Zakynthos,Zakros,Zankle"},

View file

@ -1,16 +1,16 @@
"use strict";
'use strict';
window.OceanLayers = (function () {
let cells, vertices, pointsN, used;
const OceanLayers = function OceanLayers() {
const outline = oceanLayers.attr("layers");
if (outline === "none") return;
TIME && console.time("drawOceanLayers");
const outline = oceanLayers.attr('layers');
if (outline === 'none') return;
TIME && console.time('drawOceanLayers');
lineGen.curve(d3.curveBasisClosed);
(cells = grid.cells), (pointsN = grid.cells.i.length), (vertices = grid.vertices);
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
const limits = outline === 'random' ? randomizeOutline() : outline.split(',').map((s) => +s);
const chains = [];
const opacity = rn(0.4 / limits.length, 2);
@ -26,28 +26,28 @@ window.OceanLayers = (function () {
const chain = connectVertices(start, t); // vertices chain to form a path
if (chain.length < 4) continue;
const relax = 1 + t * -2; // select only n-th point
const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some(c => c >= pointsN));
const relaxed = chain.filter((v, i) => !(i % relax) || vertices.c[v].some((c) => c >= pointsN));
if (relaxed.length < 4) continue;
const points = clipPoly(
relaxed.map(v => vertices.p[v]),
relaxed.map((v) => vertices.p[v]),
1
);
chains.push([t, points]);
}
for (const t of limits) {
const layer = chains.filter(c => c[0] === t);
let path = layer.map(c => round(lineGen(c[1]))).join("");
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").style("opacity", opacity);
const layer = chains.filter((c) => c[0] === t);
let path = layer.map((c) => round(lineGen(c[1]))).join('');
if (path) oceanLayers.append('path').attr('d', path).attr('fill', '#ecf2f9').style('opacity', opacity);
}
// find eligible cell 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 >= pointsN)); // map border cell
return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])];
if (cells.b[i]) return cells.v[i].find((v) => vertices.c[v].some((c) => c >= pointsN)); // map border cell
return cells.v[i][cells.c[i].findIndex((c) => cells.t[c] < t || !cells.t[c])];
}
TIME && console.timeEnd("drawOceanLayers");
TIME && console.timeEnd('drawOceanLayers');
};
function randomizeOutline() {
@ -71,7 +71,7 @@ window.OceanLayers = (function () {
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.t[c] === t).forEach(c => (used[c] = 1));
c.filter((c) => cells.t[c] === t).forEach((c) => (used[c] = 1));
const v = vertices.v[current]; // neighboring vertices
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t - 1;
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t - 1;
@ -80,7 +80,7 @@ window.OceanLayers = (function () {
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
ERROR && console.error('Next vertex is not found');
break;
}
}

View file

@ -1,13 +1,13 @@
"use strict";
'use strict';
window.ReliefIcons = (function () {
const ReliefIcons = function () {
TIME && console.time("drawRelief");
terrain.selectAll("*").remove();
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 density = terrain.attr('density') || 0.4;
const size = 2 * (terrain.attr('size') || 1);
const mod = 0.2 * size; // size modifier
const relief = [];
@ -19,8 +19,8 @@ window.ReliefIcons = (function () {
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]);
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);
@ -34,7 +34,7 @@ window.ReliefIcons = (function () {
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;
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)});
}
}
@ -51,8 +51,8 @@ window.ReliefIcons = (function () {
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 : Math.min(Math.max((h - 40) * mod, 3), 6);
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];
}
}
@ -60,39 +60,39 @@ window.ReliefIcons = (function () {
// sort relief icons by y+size
relief.sort((a, b) => a.y + a.s - (b.y + b.s));
let reliefHTML = "";
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");
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";
if (type === 'conifer' && temp < 0) type = 'coniferSnow';
return getIcon(type);
}
function getVariant(type) {
switch (type) {
case "mount":
case 'mount':
return rand(2, 7);
case "mountSnow":
case 'mountSnow':
return rand(1, 6);
case "hill":
case 'hill':
return rand(2, 5);
case "conifer":
case 'conifer':
return 2;
case "coniferSnow":
case 'coniferSnow':
return 1;
case "swamp":
case 'swamp':
return rand(2, 3);
case "cactus":
case 'cactus':
return rand(1, 3);
case "deadTree":
case 'deadTree':
return rand(1, 2);
default:
return 2;
@ -101,27 +101,27 @@ window.ReliefIcons = (function () {
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";
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
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

@ -287,7 +287,16 @@ window.Religions = (function () {
Heresy: {Heresy: 1}
};
const methods = {'Random + type': 3, 'Random + ism': 1, 'Supreme + ism': 5, 'Faith of + Supreme': 5, 'Place + ism': 1, 'Culture + ism': 2, 'Place + ian + type': 6, 'Culture + type': 4};
const methods = {
'Random + type': 3,
'Random + ism': 1,
'Supreme + ism': 5,
'Faith of + Supreme': 5,
'Place + ism': 1,
'Culture + ism': 2,
'Place + ian + type': 6,
'Culture + type': 4
};
const types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
@ -419,9 +428,20 @@ window.Religions = (function () {
const name = getCultName('Heresy', center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
religions.push({i: religions.length, name, color, culture, type: 'Heresy', form: r.form, deity: r.deity, expansion: 'global', expansionism, center, origin: r.i});
religions.push({
i: religions.length,
name,
color,
culture,
type: 'Heresy',
form: r.form,
deity: r.deity,
expansion: 'global',
expansionism,
center,
origin: r.i
});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
@ -454,7 +474,24 @@ window.Religions = (function () {
name,
religions.map((r) => r.code)
);
religions.push({i, name, color, culture, type, form: formName, deity, expansion, expansionism: 0, center, cells: 0, area: 0, rural: 0, urban: 0, origin: r, code});
religions.push({
i,
name,
color,
culture,
type,
form: formName,
deity,
expansion,
expansionism: 0,
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
origin: r,
code
});
cells.religion[center] = i;
};
@ -551,21 +588,19 @@ window.Religions = (function () {
};
function checkCenters() {
const cells = pack.cells,
religions = pack.religions;
const {cells, religions} = pack;
const codes = religions.map((r) => r.code);
religions
.filter((r) => r.i)
.forEach((r) => {
r.code = abbreviate(r.name, codes);
religions.forEach((r) => {
if (!r.i) return;
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter((i) => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
});
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter((i) => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
});
}
function updateCultures() {

View file

@ -0,0 +1,993 @@
'use strict';
window.Religions = (function () {
// name generation approach and relative chance to be selected
const approach = {
Number: 1,
Being: 3,
Adjective: 5,
<<<<<<< HEAD
'Color + Animal': 5,
'Adjective + Animal': 5,
'Adjective + Being': 5,
'Adjective + Genitive': 1,
'Color + Being': 3,
'Color + Genitive': 3,
'Being + of + Genitive': 2,
'Being + of the + Genitive': 1,
'Animal + of + Genitive': 1,
'Adjective + Being + of + Genitive': 2,
'Adjective + Animal + of + Genitive': 2
=======
"Color + Animal": 5,
"Adjective + Animal": 5,
"Adjective + Being": 5,
"Adjective + Genitive": 1,
"Color + Being": 3,
"Color + Genitive": 3,
"Being + of + Genitive": 2,
"Being + of the + Genitive": 1,
"Animal + of + Genitive": 1,
"Adjective + Being + of + Genitive": 2,
"Adjective + Animal + of + Genitive": 2
>>>>>>> master
};
// turn weighted array into simple array
const approaches = [];
for (const a in approach) {
for (let j = 0; j < approach[a]; j++) {
approaches.push(a);
}
}
const base = {
<<<<<<< HEAD
number: ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve'],
being: [
'God',
'Goddess',
'Lord',
'Lady',
'Deity',
'Creator',
'Maker',
'Overlord',
'Ruler',
'Chief',
'Master',
'Spirit',
'Ancestor',
'Father',
'Forebear',
'Forefather',
'Mother',
'Brother',
'Sister',
'Elder',
'Numen',
'Ancient',
'Virgin',
'Giver',
'Council',
'Guardian',
'Reaper'
],
animal: [
'Dragon',
'Wyvern',
'Phoenix',
'Unicorn',
'Sphinx',
'Centaur',
'Pegasus',
'Kraken',
'Basilisk',
'Chimera',
'Cyclope',
'Antelope',
'Ape',
'Badger',
'Bear',
'Beaver',
'Bison',
'Boar',
'Buffalo',
'Cat',
'Cobra',
'Crane',
'Crocodile',
'Crow',
'Deer',
'Dog',
'Eagle',
'Elk',
'Fox',
'Goat',
'Goose',
'Hare',
'Hawk',
'Heron',
'Horse',
'Hyena',
'Ibis',
'Jackal',
'Jaguar',
'Lark',
'Leopard',
'Lion',
'Mantis',
'Marten',
'Moose',
'Mule',
'Narwhal',
'Owl',
'Panther',
'Rat',
'Raven',
'Rook',
'Scorpion',
'Shark',
'Sheep',
'Snake',
'Spider',
'Swan',
'Tiger',
'Turtle',
'Viper',
'Vulture',
'Walrus',
'Wolf',
'Wolverine',
'Worm',
'Camel',
'Falcon',
'Hound',
'Ox',
'Serpent'
],
adjective: [
'New',
'Good',
'High',
'Old',
'Great',
'Big',
'Young',
'Major',
'Strong',
'Happy',
'Last',
'Main',
'Huge',
'Far',
'Beautiful',
'Wild',
'Fair',
'Prime',
'Crazy',
'Ancient',
'Proud',
'Secret',
'Lucky',
'Sad',
'Silent',
'Latter',
'Severe',
'Fat',
'Holy',
'Pure',
'Aggressive',
'Honest',
'Giant',
'Mad',
'Pregnant',
'Distant',
'Lost',
'Broken',
'Blind',
'Friendly',
'Unknown',
'Sleeping',
'Slumbering',
'Loud',
'Hungry',
'Wise',
'Worried',
'Sacred',
'Magical',
'Superior',
'Patient',
'Dead',
'Deadly',
'Peaceful',
'Grateful',
'Frozen',
'Evil',
'Scary',
'Burning',
'Divine',
'Bloody',
'Dying',
'Waking',
'Brutal',
'Unhappy',
'Calm',
'Cruel',
'Favorable',
'Blond',
'Explicit',
'Disturbing',
'Devastating',
'Brave',
'Sunny',
'Troubled',
'Flying',
'Sustainable',
'Marine',
'Fatal',
'Inherent',
'Selected',
'Naval',
'Cheerful',
'Almighty',
'Benevolent',
'Eternal',
'Immutable',
'Infallible'
],
genitive: [
'Day',
'Life',
'Death',
'Night',
'Home',
'Fog',
'Snow',
'Winter',
'Summer',
'Cold',
'Springs',
'Gates',
'Nature',
'Thunder',
'Lightning',
'War',
'Ice',
'Frost',
'Fire',
'Doom',
'Fate',
'Pain',
'Heaven',
'Justice',
'Light',
'Love',
'Time',
'Victory'
],
theGenitive: [
'World',
'Word',
'South',
'West',
'North',
'East',
'Sun',
'Moon',
'Peak',
'Fall',
'Dawn',
'Eclipse',
'Abyss',
'Blood',
'Tree',
'Earth',
'Harvest',
'Rainbow',
'Sea',
'Sky',
'Stars',
'Storm',
'Underworld',
'Wild'
],
color: ['Dark', 'Light', 'Bright', 'Golden', 'White', 'Black', 'Red', 'Pink', 'Purple', 'Blue', 'Green', 'Yellow', 'Amber', 'Orange', 'Brown', 'Grey']
=======
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: [
"God",
"Goddess",
"Lord",
"Lady",
"Deity",
"Creator",
"Maker",
"Overlord",
"Ruler",
"Chief",
"Master",
"Spirit",
"Ancestor",
"Father",
"Forebear",
"Forefather",
"Mother",
"Brother",
"Sister",
"Elder",
"Numen",
"Ancient",
"Virgin",
"Giver",
"Council",
"Guardian",
"Reaper"
],
animal: [
"Dragon",
"Wyvern",
"Phoenix",
"Unicorn",
"Sphinx",
"Centaur",
"Pegasus",
"Kraken",
"Basilisk",
"Chimera",
"Cyclope",
"Antelope",
"Ape",
"Badger",
"Bear",
"Beaver",
"Bison",
"Boar",
"Buffalo",
"Cat",
"Cobra",
"Crane",
"Crocodile",
"Crow",
"Deer",
"Dog",
"Eagle",
"Elk",
"Fox",
"Goat",
"Goose",
"Hare",
"Hawk",
"Heron",
"Horse",
"Hyena",
"Ibis",
"Jackal",
"Jaguar",
"Lark",
"Leopard",
"Lion",
"Mantis",
"Marten",
"Moose",
"Mule",
"Narwhal",
"Owl",
"Panther",
"Rat",
"Raven",
"Rook",
"Scorpion",
"Shark",
"Sheep",
"Snake",
"Spider",
"Swan",
"Tiger",
"Turtle",
"Viper",
"Vulture",
"Walrus",
"Wolf",
"Wolverine",
"Worm",
"Camel",
"Falcon",
"Hound",
"Ox",
"Serpent"
],
adjective: [
"New",
"Good",
"High",
"Old",
"Great",
"Big",
"Young",
"Major",
"Strong",
"Happy",
"Last",
"Main",
"Huge",
"Far",
"Beautiful",
"Wild",
"Fair",
"Prime",
"Crazy",
"Ancient",
"Proud",
"Secret",
"Lucky",
"Sad",
"Silent",
"Latter",
"Severe",
"Fat",
"Holy",
"Pure",
"Aggressive",
"Honest",
"Giant",
"Mad",
"Pregnant",
"Distant",
"Lost",
"Broken",
"Blind",
"Friendly",
"Unknown",
"Sleeping",
"Slumbering",
"Loud",
"Hungry",
"Wise",
"Worried",
"Sacred",
"Magical",
"Superior",
"Patient",
"Dead",
"Deadly",
"Peaceful",
"Grateful",
"Frozen",
"Evil",
"Scary",
"Burning",
"Divine",
"Bloody",
"Dying",
"Waking",
"Brutal",
"Unhappy",
"Calm",
"Cruel",
"Favorable",
"Blond",
"Explicit",
"Disturbing",
"Devastating",
"Brave",
"Sunny",
"Troubled",
"Flying",
"Sustainable",
"Marine",
"Fatal",
"Inherent",
"Selected",
"Naval",
"Cheerful",
"Almighty",
"Benevolent",
"Eternal",
"Immutable",
"Infallible"
],
genitive: [
"Day",
"Life",
"Death",
"Night",
"Home",
"Fog",
"Snow",
"Winter",
"Summer",
"Cold",
"Springs",
"Gates",
"Nature",
"Thunder",
"Lightning",
"War",
"Ice",
"Frost",
"Fire",
"Doom",
"Fate",
"Pain",
"Heaven",
"Justice",
"Light",
"Love",
"Time",
"Victory"
],
theGenitive: [
"World",
"Word",
"South",
"West",
"North",
"East",
"Sun",
"Moon",
"Peak",
"Fall",
"Dawn",
"Eclipse",
"Abyss",
"Blood",
"Tree",
"Earth",
"Harvest",
"Rainbow",
"Sea",
"Sky",
"Stars",
"Storm",
"Underworld",
"Wild"
],
color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]
>>>>>>> master
};
const forms = {
Folk: {Shamanism: 2, Animism: 2, 'Ancestor worship': 1, Polytheism: 2},
Organized: {Polytheism: 5, Dualism: 1, Monotheism: 4, 'Non-theism': 1},
Cult: {Cult: 1, 'Dark Cult': 1},
Heresy: {Heresy: 1}
};
<<<<<<< HEAD
const methods = {'Random + type': 3, 'Random + ism': 1, 'Supreme + ism': 5, 'Faith of + Supreme': 5, 'Place + ism': 1, 'Culture + ism': 2, 'Place + ian + type': 6, 'Culture + type': 4};
=======
const methods = {
"Random + type": 3,
"Random + ism": 1,
"Supreme + ism": 5,
"Faith of + Supreme": 5,
"Place + ism": 1,
"Culture + ism": 2,
"Place + ian + type": 6,
"Culture + type": 4
};
>>>>>>> master
const types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Spirits: 1},
Animism: {Spirits: 1, Beliefs: 1},
'Ancestor worship': {Beliefs: 1, Forefathers: 2, Ancestors: 2},
Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
Dualism: {Religion: 3, Faith: 1, Cult: 1},
Monotheism: {Religion: 1, Church: 1},
'Non-theism': {Beliefs: 3, Spirits: 1},
Cult: {Cult: 4, Sect: 4, Worship: 1, Orden: 1, Coterie: 1, Arcanum: 1},
'Dark Cult': {Cult: 2, Sect: 2, Occultism: 1, Idols: 1, Coven: 1, Circle: 1, Blasphemy: 1},
Heresy: {Heresy: 3, Sect: 2, Schism: 1, Dissenters: 1, Circle: 1, Brotherhood: 1, Society: 1, Iconoclasm: 1, Dissent: 1, Apostates: 1}
};
const generate = function () {
TIME && console.time('generateReligions');
const cells = pack.cells,
states = pack.states,
cultures = pack.cultures;
const religions = (pack.religions = []);
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
// add folk religions
pack.cultures.forEach((c) => {
if (!c.i) {
religions.push({i: 0, name: 'No religion'});
return;
}
if (c.removed) {
religions.push({i: c.i, name: 'Extinct religion for ' + c.name, color: getMixedColor(c.color, 0.1, 0), removed: true});
return;
}
const form = rw(forms.Folk);
const name = c.name + ' ' + rw(types[form]);
const deity = form === 'Animism' ? null : getDeityName(c.i);
const color = getMixedColor(c.color, 0.1, 0); // `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type: 'Folk', form, deity, center: c.center, origin: 0});
});
if (religionsInput.value == 0 || pack.cultures.length < 2) {
religions.filter((r) => r.i).forEach((r) => (r.code = abbreviate(r.name)));
return;
}
<<<<<<< HEAD
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
const sorted =
burgs.length > +religionsInput.value ? burgs.sort((a, b) => b.population - a.population).map((b) => b.cell) : cells.i.filter((i) => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
=======
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const sorted =
burgs.length > +religionsInput.value
? burgs.sort((a, b) => b.population - a.population).map(b => b.cell)
: cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
>>>>>>> master
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor((rand(10, 40) / 100) * religionsInput.value);
const count = +religionsInput.value - cultsCount + religions.length;
// generate organized religions
for (let i = 0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length - 1, 5)]; // religion center
const form = rw(forms.Organized);
const state = cells.state[center];
const culture = cells.culture[center];
const deity = form === 'Non-theism' ? null : getDeityName(culture);
let [name, expansion] = getReligionName(form, deity, center);
if (expansion === 'state' && !state) expansion = 'global';
if (expansion === 'culture' && !culture) expansion = 'global';
if (expansion === 'state' && Math.random() > 0.5) center = states[state].center;
if (expansion === 'culture' && Math.random() > 0.5) center = cultures[culture].center;
if (!cells.burg[center] && cells.c[center].some((c) => cells.burg[c])) center = cells.c[center].find((c) => cells.burg[c]);
const x = cells.p[center][0],
y = cells.p[center][1];
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
// add "Old" to name of the folk religion on this culture
const folk = religions.find((r) => r.culture === culture && r.type === 'Folk');
if (folk && expansion === 'culture' && folk.name.slice(0, 3) !== 'Old') folk.name = 'Old ' + folk.name;
const origin = folk ? folk.i : 0;
const expansionism = rand(3, 8);
const color = getMixedColor(religions[origin].color, 0.3, 0); // `url(#hatch${rand(0,5)})`;
religions.push({i: religions.length, name, color, culture, type: 'Organized', form, deity, expansion, expansionism, center, origin});
religionsTree.add([x, y]);
}
// generate cults
for (let i = 0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult);
let center = sorted[biased(0, sorted.length - 1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some((c) => cells.burg[c])) center = cells.c[center].find((c) => cells.burg[c]);
const x = cells.p[center][0],
y = cells.p[center][1];
const s = spacing * gauss(2, 0.3, 1, 3, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center];
const folk = religions.find((r) => r.culture === culture && r.type === 'Folk');
const origin = folk ? folk.i : 0;
const deity = getDeityName(culture);
const name = getCultName(form, center);
const expansionism = gauss(1.1, 0.5, 0, 5);
const color = getMixedColor(cultures[culture].color, 0.5, 0); // "url(#hatch7)";
religions.push({i: religions.length, name, color, culture, type: 'Cult', form, deity, expansion: 'global', expansionism, center, origin});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red");
}
expandReligions();
// generate heresies
religions
.filter((r) => r.type === 'Organized')
.forEach((r) => {
if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3);
for (let i = 0; i < count; i++) {
let center = ra(cells.i.filter((i) => cells.religion[i] === r.i && cells.c[i].some((c) => cells.religion[c] !== r.i)));
if (!center) continue;
if (!cells.burg[center] && cells.c[center].some((c) => cells.burg[c])) center = cells.c[center].find((c) => cells.burg[c]);
const x = cells.p[center][0],
y = cells.p[center][1];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
const culture = cells.culture[center];
const name = getCultName('Heresy', center);
const expansionism = gauss(1.2, 0.5, 0, 5);
const color = getMixedColor(r.color, 0.4, 0.2); // "url(#hatch6)";
<<<<<<< HEAD
religions.push({i: religions.length, name, color, culture, type: 'Heresy', form: r.form, deity: r.deity, expansion: 'global', expansionism, center, origin: r.i});
=======
religions.push({
i: religions.length,
name,
color,
culture,
type: "Heresy",
form: r.form,
deity: r.deity,
expansion: "global",
expansionism,
center,
origin: r.i
});
>>>>>>> master
religionsTree.add([x, y]);
}
});
expandHeresies();
checkCenters();
TIME && console.timeEnd('generateReligions');
};
const add = function (center) {
const cells = pack.cells,
religions = pack.religions;
const r = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center];
const color = getMixedColor(religions[r].color, 0.3, 0);
const type = religions[r].type === 'Organized' ? rw({Organized: 4, Cult: 1, Heresy: 2}) : rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity = type === 'Heresy' ? religions[r].deity : form === 'Non-theism' ? null : getDeityName(culture);
let name, expansion;
if (type === 'Organized') [name, expansion] = getReligionName(form, deity, center);
else {
name = getCultName(form, center);
expansion = 'global';
}
const formName = type === 'Heresy' ? religions[r].form : form;
const code = abbreviate(
name,
religions.map((r) => r.code)
);
religions.push({
i,
name,
color,
culture,
type,
form: formName,
deity,
expansion,
expansionism: 0,
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
origin: r,
code
});
cells.religion[center] = i;
};
// growth algorithm to assign cells to religions
const expandReligions = function () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter((r) => r.type === 'Organized' || r.type === 'Cult')
.forEach((r) => {
cells.religion[r.center] = r.i;
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center], c: r.culture});
cost[r.center] = 1;
});
const neutral = (cells.i.length / 5000) * 200 * gauss(1, 0.3, 0.2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
const popCost = d3.max(cells.pop) / 3; // enougth population to spered religion without penalty
while (queue.length) {
const next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
c = next.c,
s = next.s;
const expansion = religions[r].expansion;
cells.c[n].forEach(function (e) {
if (expansion === 'culture' && c !== cells.culture[e]) return;
if (expansion === 'state' && s !== cells.state[e]) return;
const cultureCost = c !== cells.culture[e] ? 10 : 0;
const stateCost = s !== cells.state[e] ? 10 : 0;
const biomeCost = biomesData.cost[cells.biome[e]];
const populationCost = Math.max(rn(popCost - cells.pop[e]), 0);
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? 500 : 0;
const totalCost = p + (cultureCost + stateCost + biomeCost + populationCost + heightCost + waterCost) / religions[r].expansionism;
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, r, c, s});
}
});
}
};
// growth algorithm to assign cells to heresies
const expandHeresies = function () {
const cells = pack.cells,
religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions
.filter((r) => r.type === 'Heresy')
.forEach((r) => {
const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id
queue.queue({e: r.center, p: 0, r: r.i, b});
cost[r.center] = 1;
});
const neutral = (cells.i.length / 5000) * 500 * neutralInput.value; // limit cost for heresies growth
while (queue.length) {
const next = queue.dequeue(),
n = next.e,
p = next.p,
r = next.r,
b = next.b;
cells.c[n].forEach(function (e) {
const religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? 500 : 0;
const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, 0.1);
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p: totalCost, r});
}
});
}
};
function checkCenters() {
const {cells, religions} = pack;
<<<<<<< HEAD
const codes = religions.map((r) => r.code);
religions
.filter((r) => r.i)
.forEach((r) => {
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter((i) => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => b.pop - a.pop)[0];
});
=======
const codes = religions.map(r => r.code);
religions.forEach(r => {
if (!r.i) return;
r.code = abbreviate(r.name, codes);
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const religCells = cells.i.filter(i => cells.religion[i] === r.i);
if (!religCells.length) return; // extinct religion
r.center = religCells.sort((a, b) => cells.pop[b] - cells.pop[a])[0];
});
>>>>>>> master
}
function updateCultures() {
TIME && console.time('updateCulturesForReligions');
pack.religions = pack.religions.map((religion, index) => {
if (index === 0) {
return religion;
}
return {...religion, culture: pack.cells.culture[religion.center]};
});
TIME && console.timeEnd('updateCulturesForReligions');
}
// get supreme deity name
const getDeityName = function (culture) {
if (culture === undefined) {
ERROR && console.error('Please define a culture');
return;
}
const meaning = generateMeaning();
const cultureName = Names.getCulture(culture, null, null, '', 0.8);
return cultureName + ', The ' + meaning;
};
function generateMeaning() {
const a = ra(approaches); // select generation approach
if (a === 'Number') return ra(base.number);
if (a === 'Being') return ra(base.being);
if (a === 'Adjective') return ra(base.adjective);
if (a === 'Color + Animal') return ra(base.color) + ' ' + ra(base.animal);
if (a === 'Adjective + Animal') return ra(base.adjective) + ' ' + ra(base.animal);
if (a === 'Adjective + Being') return ra(base.adjective) + ' ' + ra(base.being);
if (a === 'Adjective + Genitive') return ra(base.adjective) + ' ' + ra(base.genitive);
if (a === 'Color + Being') return ra(base.color) + ' ' + ra(base.being);
if (a === 'Color + Genitive') return ra(base.color) + ' ' + ra(base.genitive);
if (a === 'Being + of + Genitive') return ra(base.being) + ' of ' + ra(base.genitive);
if (a === 'Being + of the + Genitive') return ra(base.being) + ' of the ' + ra(base.theGenitive);
if (a === 'Animal + of + Genitive') return ra(base.animal) + ' of ' + ra(base.genitive);
if (a === 'Adjective + Being + of + Genitive') return ra(base.adjective) + ' ' + ra(base.being) + ' of ' + ra(base.genitive);
if (a === 'Adjective + Animal + of + Genitive') return ra(base.adjective) + ' ' + ra(base.animal) + ' of ' + ra(base.genitive);
}
function getReligionName(form, deity, center) {
const cells = pack.cells;
const random = function () {
return Names.getCulture(cells.culture[center], null, null, '', 0);
};
const type = function () {
return rw(types[form]);
};
const supreme = function () {
return deity.split(/[ ,]+/)[0];
};
const place = function (adj) {
const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name;
let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name;
};
const culture = function () {
return pack.cultures[cells.culture[center]].name;
};
const m = rw(methods);
if (m === 'Random + type') return [random() + ' ' + type(), 'global'];
if (m === 'Random + ism') return [trimVowels(random()) + 'ism', 'global'];
if (m === 'Supreme + ism' && deity) return [trimVowels(supreme()) + 'ism', 'global'];
if (m === 'Faith of + Supreme' && deity) return [ra(['Faith', 'Way', 'Path', 'Word', 'Witnesses']) + ' of ' + supreme(), 'global'];
if (m === 'Place + ism') return [place() + 'ism', 'state'];
if (m === 'Culture + ism') return [trimVowels(culture()) + 'ism', 'culture'];
if (m === 'Place + ian + type') return [place('adj') + ' ' + type(), 'state'];
if (m === 'Culture + type') return [culture() + ' ' + type(), 'culture'];
return [trimVowels(random()) + 'ism', 'global']; // else
}
function getCultName(form, center) {
const cells = pack.cells;
const type = function () {
return rw(types[form]);
};
const random = function () {
return trimVowels(Names.getCulture(cells.culture[center], null, null, '', 0).split(/[ ,]+/)[0]);
};
const burg = function () {
return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);
};
if (cells.burg[center]) return burg() + 'ian ' + type();
if (Math.random() > 0.5) return random() + 'ian ' + type();
return type() + ' of the ' + generateMeaning();
}
return {generate, add, getDeityName, expandReligions, updateCultures};
})();

View file

@ -1,8 +1,8 @@
"use strict";
'use strict';
window.Rivers = (function () {
const generate = function (allowErosion = true) {
TIME && console.time("generateRivers");
TIME && console.time('generateRivers');
Math.random = aleaPRNG(seed);
const {cells, features} = pack;
@ -28,26 +28,27 @@ window.Rivers = (function () {
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
TIME && console.timeEnd("generateRivers");
TIME && console.timeEnd('generateRivers');
function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30;
const prec = grid.cells.prec;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const area = pack.cells.area;
const land = cells.i.filter((i) => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]]; // add flux from precipitation
cells.fl[i] += (prec[cells.g[i]] * area[i]) / 100; // add flux from precipitation
// create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
const lakes = lakeOutCells[i] ? features.filter((feature) => i === feature.outCell && feature.flux > feature.evaporation) : [];
for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
const lakeCell = cells.c[i].find((c) => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
// allow chain lakes to retain identity
if (cells.r[lakeCell] !== lake.river) {
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
const sameRiver = cells.c[lakeCell].some((c) => cells.r[c] === lake.river);
if (sameRiver) {
cells.r[lakeCell] = lake.river;
@ -78,7 +79,7 @@ window.Rivers = (function () {
// downhill cell (make sure it's not in the source lake)
let min = null;
if (lakeOutCells[i]) {
const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
const filtered = cells.c[i].filter((c) => !lakes.map((lake) => lake.i).includes(cells.f[c]));
min = filtered.sort((a, b) => h[a] - h[b])[0];
} else if (cells.haven[i]) {
min = cells.haven[i];
@ -125,7 +126,7 @@ window.Rivers = (function () {
if (h[toCell] < 20) {
// pour water to the water body
const waterBody = features[cells.f[toCell]];
if (waterBody.type === "lake") {
if (waterBody.type === 'lake') {
if (!waterBody.river || fromFlux > waterBody.enteringFlux) {
waterBody.river = river;
waterBody.enteringFlux = fromFlux;
@ -168,7 +169,7 @@ window.Rivers = (function () {
const widthFactor = !parent || parent === riverId ? 1.2 : 1;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = rn(getApproximateLength(meanderedPoints), 2);
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
@ -180,8 +181,8 @@ window.Rivers = (function () {
if (!cells.conf[i]) continue;
const sortedInflux = cells.c[i]
.filter(c => cells.r[c] && h[c] > h[i])
.map(c => cells.fl[c])
.filter((c) => cells.r[c] && h[c] > h[i])
.map((c) => cells.fl[c])
.sort((a, b) => b - a);
cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
}
@ -193,21 +194,21 @@ window.Rivers = (function () {
const {h, c, t} = pack.cells;
return Array.from(h).map((h, i) => {
if (h < 20 || t[i] < 1) return h;
return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
return h + t[i] / 100 + d3.mean(c[i].map((c) => t[c])) / 10000;
});
};
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function (h) {
const {cells, features} = pack;
const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
const maxIterations = +document.getElementById('resolveDepressionsStepsOutput').value;
const checkLakeMaxIteration = maxIterations * 0.85;
const elevateLakeMaxIteration = maxIterations * 0.75;
const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
const height = (i) => features[cells.f[i]].height || h[i]; // height of lake or specific cell
const lakes = features.filter(f => f.type === "lake");
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
const lakes = features.filter((f) => f.type === 'lake');
const land = cells.i.filter((i) => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
land.sort((a, b) => h[a] - h[b]); // lowest cells go first
const progress = [];
@ -226,12 +227,12 @@ window.Rivers = (function () {
if (iteration < checkLakeMaxIteration) {
for (const l of lakes) {
if (l.closed) continue;
const minHeight = d3.min(l.shoreline.map(s => h[s]));
const minHeight = d3.min(l.shoreline.map((s) => h[s]));
if (minHeight >= 100 || l.height > minHeight) continue;
if (iteration > elevateLakeMaxIteration) {
l.shoreline.forEach(i => (h[i] = cells.h[i]));
l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
l.shoreline.forEach((i) => (h[i] = cells.h[i]));
l.height = d3.min(l.shoreline.map((s) => h[s])) - 1;
l.closed = true;
continue;
}
@ -242,7 +243,7 @@ window.Rivers = (function () {
}
for (const i of land) {
const minHeight = d3.min(cells.c[i].map(c => height(c)));
const minHeight = d3.min(cells.c[i].map((c) => height(c)));
if (minHeight >= 100 || h[i] > minHeight) continue;
depressions++;
@ -318,15 +319,16 @@ window.Rivers = (function () {
};
const getRiverPoints = (riverCells, riverPoints) => {
if (riverPoints) return riverPoints;
const {p} = pack.cells;
return riverCells.map((cell, i) => {
if (riverPoints && riverPoints[i]) return riverPoints[i];
if (cell === -1) return getBorderPoint(riverCells[i - 1]);
return p[cell];
});
};
const getBorderPoint = i => {
const getBorderPoint = (i) => {
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
@ -339,7 +341,7 @@ window.Rivers = (function () {
const MAX_FLUX_WIDTH = 2;
const LENGTH_FACTOR = 200;
const 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 = 1, startingWidth = 0) => {
@ -369,7 +371,7 @@ window.Rivers = (function () {
const right = lineGen(riverPointsRight.reverse());
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
left = left.substring(left.indexOf('C'));
return round(right + left, 1);
};
@ -405,36 +407,39 @@ window.Rivers = (function () {
const getType = function ({i, length, parent}) {
if (smallLength === null) {
const threshold = Math.ceil(pack.rivers.length * 0.15);
smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[threshold];
}
const isSmall = length < smallLength;
const isFork = each(3)(i) && parent && parent !== i;
return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
return rw(riverTypes[isFork ? 'fork' : 'main'][isSmall ? 'small' : 'big']);
};
const getApproximateLength = points => points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
const getApproximateLength = (points) => {
const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
return rn(length, 2);
};
// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
const getWidth = (offset) => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
// remove river and all its tributaries
const remove = function (id) {
const cells = pack.cells;
const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
riversToRemove.forEach(r => rivers.select("#river" + r).remove());
const riversToRemove = pack.rivers.filter((r) => r.i === id || r.parent === id || r.basin === id).map((r) => r.i);
riversToRemove.forEach((r) => rivers.select('#river' + r).remove());
cells.r.forEach((r, i) => {
if (!r || !riversToRemove.includes(r)) return;
cells.r[i] = 0;
cells.fl[i] = grid.cells.prec[cells.g[i]];
cells.conf[i] = 0;
});
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
pack.rivers = pack.rivers.filter((r) => !riversToRemove.includes(r.i));
};
const getBasin = function (r) {
const parent = pack.rivers.find(river => river.i === r)?.parent;
const parent = pack.rivers.find((river) => river.i === r)?.parent;
if (!parent || r === parent) return r;
return getBasin(parent);
};

View file

@ -1,37 +1,37 @@
window.Routes = (function () {
const getRoads = function () {
TIME && console.time("generateMainRoads");
TIME && console.time('generateMainRoads');
const cells = pack.cells;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
const capitals = burgs.filter((b) => b.capital).sort((a, b) => a.population - b.population);
if (capitals.length < 2) return []; // not enough capitals to build main roads
const paths = []; // array to store path segments
for (const b of capitals) {
const connect = capitals.filter(c => c.feature === b.feature && c !== b);
const connect = capitals.filter((c) => c.feature === b.feature && c !== b);
for (const t of connect) {
const [from, exit] = findLandPath(b.cell, t.cell, true);
const segments = restorePath(b.cell, exit, "main", from);
segments.forEach(s => paths.push(s));
const segments = restorePath(b.cell, exit, 'main', from);
segments.forEach((s) => paths.push(s));
}
}
cells.i.forEach(i => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
TIME && console.timeEnd("generateMainRoads");
cells.i.forEach((i) => (cells.s[i] += cells.road[i] / 2)); // add roads to suitability score
TIME && console.timeEnd('generateMainRoads');
return paths;
};
const getTrails = function () {
TIME && console.time("generateTrails");
TIME && console.time('generateTrails');
const cells = pack.cells;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (burgs.length < 2) return []; // not enough burgs to build trails
let paths = []; // array to store path segments
for (const f of pack.features.filter(f => f.land)) {
const isle = burgs.filter(b => b.feature === f.i); // burgs on island
for (const f of pack.features.filter((f) => f.land)) {
const isle = burgs.filter((b) => b.feature === f.i); // burgs on island
if (isle.length < 2) continue;
isle.forEach(function (b, i) {
@ -43,35 +43,35 @@ window.Routes = (function () {
const to = isle[farthest].cell;
if (cells.road[to]) return;
const [from, exit] = findLandPath(b.cell, to, true);
path = restorePath(b.cell, exit, "small", from);
path = restorePath(b.cell, exit, 'small', from);
} else {
// build trail from all other burgs to the closest road on the same island
if (cells.road[b.cell]) return;
const [from, exit] = findLandPath(b.cell, null, true);
if (exit === null) return;
path = restorePath(b.cell, exit, "small", from);
path = restorePath(b.cell, exit, 'small', from);
}
if (path) paths = paths.concat(path);
});
}
TIME && console.timeEnd("generateTrails");
TIME && console.timeEnd('generateTrails');
return paths;
};
const getSearoutes = function () {
TIME && console.time("generateSearoutes");
TIME && console.time('generateSearoutes');
const {cells, burgs, features} = pack;
const allPorts = burgs.filter(b => b.port > 0 && !b.removed);
const allPorts = burgs.filter((b) => b.port > 0 && !b.removed);
if (!allPorts.length) return [];
const bodies = new Set(allPorts.map(b => b.port)); // water features with ports
const bodies = new Set(allPorts.map((b) => b.port)); // water features with ports
let paths = []; // array to store path segments
const connected = []; // store cell id of connected burgs
bodies.forEach(f => {
const ports = allPorts.filter(b => b.port === f); // all ports on the same feature
bodies.forEach((f) => {
const ports = allPorts.filter((b) => b.port === f); // all ports on the same feature
if (!ports.length) return;
if (features[f].border) addOverseaRoute(f, ports[0]);
@ -88,7 +88,7 @@ window.Routes = (function () {
const [from, exit, passable] = findOceanPath(target, source, true);
if (!passable) continue;
const path = restorePath(target, exit, "ocean", from);
const path = restorePath(target, exit, 'ocean', from);
paths = paths.concat(path);
connected[source] = 1;
@ -99,7 +99,7 @@ window.Routes = (function () {
function addOverseaRoute(f, port) {
const {x, y, cell: source} = port;
const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y);
const dist = (p) => Math.abs(p[0] - x) + Math.abs(p[1] - y);
const [x1, y1] = [
[0, y],
[x, 0],
@ -112,39 +112,39 @@ window.Routes = (function () {
const [from, exit, passable] = findOceanPath(target, source, true);
if (passable) {
const path = restorePath(target, exit, "ocean", from);
const path = restorePath(target, exit, 'ocean', from);
paths = paths.concat(path);
last(path).push([x1, y1]);
}
}
}
TIME && console.timeEnd("generateSearoutes");
TIME && console.timeEnd('generateSearoutes');
return paths;
};
const draw = function (main, small, water) {
TIME && console.time("drawRoutes");
TIME && console.time('drawRoutes');
const {cells, burgs} = pack;
const {burg, p} = cells;
const getBurgCoords = b => [burgs[b].x, burgs[b].y];
const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
const getPath = segment => round(lineGen(getPathPoints(segment)), 1);
const getPathsHTML = (paths, type) => paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join("");
const getBurgCoords = (b) => [burgs[b].x, burgs[b].y];
const getPathPoints = (cells) => cells.map((i) => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i]));
const getPath = (segment) => round(lineGen(getPathPoints(segment)), 1);
const getPathsHTML = (paths, type) => paths.map((path, i) => `<path id="${type}${i}" d="${getPath(path)}" />`).join('');
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
roads.html(getPathsHTML(main, "road"));
trails.html(getPathsHTML(small, "trail"));
roads.html(getPathsHTML(main, 'road'));
trails.html(getPathsHTML(small, 'trail'));
lineGen.curve(d3.curveBundle.beta(1));
searoutes.html(getPathsHTML(water, "searoute"));
searoutes.html(getPathsHTML(water, 'searoute'));
TIME && console.timeEnd("drawRoutes");
TIME && console.timeEnd('drawRoutes');
};
const regenerate = function () {
routes.selectAll("path").remove();
routes.selectAll('path').remove();
pack.cells.road = new Uint16Array(pack.cells.i.length);
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
const main = getRoads();
@ -196,9 +196,9 @@ window.Routes = (function () {
let segment = [],
current = end,
prev = end;
const score = type === "main" ? 5 : 1; // to increase road score at cell
const score = type === 'main' ? 5 : 1; // to increase road score at cell
if (type === "ocean" || !cells.road[prev]) segment.push(end);
if (type === 'ocean' || !cells.road[prev]) segment.push(end);
if (!cells.road[prev]) cells.road[prev] = score;
for (let i = 0, limit = 1000; i < limit; i++) {

View file

@ -1,503 +1,132 @@
// Functions to save and load the map
'use strict';
// download map as SVG
async function saveSVG() {
TIME && console.time('saveSVG');
const url = await getMapURL('svg');
const link = document.createElement('a');
link.download = getFileName() + '.svg';
link.href = url;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
TIME && console.timeEnd('saveSVG');
}
// download map as PNG
async function savePNG() {
TIME && console.time('savePNG');
const url = await getMapURL('png');
const link = document.createElement('a');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + '.png';
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
}, 1000);
});
};
TIME && console.timeEnd('savePNG');
}
// download map as JPEG
async function saveJPEG() {
TIME && console.time('saveJPEG');
const url = await getMapURL('png');
const canvas = document.createElement('canvas');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = async function () {
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL('image/jpeg', quality);
const link = document.createElement('a');
link.download = getFileName() + '.jpeg';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
};
TIME && console.timeEnd('saveJPEG');
}
// download map as png tiles
async function saveTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL('tiles', {debug: true});
const zip = new JSZip();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = graphWidth;
canvas.height = graphHeight;
const imgSchema = new Image();
imgSchema.src = urlSchema;
imgSchema.onload = function () {
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => zip.file(`fmg_tile_schema.png`, blob));
};
// download tiles
const url = await getMapURL('tiles');
const tilesX = +document.getElementById('tileColsInput').value;
const tilesY = +document.getElementById('tileRowsInput').value;
const scale = +document.getElementById('tileScaleInput').value;
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
let loaded = 0;
const img = new Image();
img.src = url;
img.onload = function () {
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob((blob) => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
function downloadZip() {
const name = `${getFileName()}.zip`;
zip.generateAsync({type: 'blob'}).then((blob) => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true);
});
}
});
}
// parse map svg to object url
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false} = options;
const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (!debug) clone.select('#debug').remove();
const cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox && type === 'mesh') clone.select('#oceanPattern').remove();
if (globe) clone.select('#scaleBar').remove();
if (noLabels) {
clone.select('#labels #states').remove();
clone.select('#labels #burgLabels').remove();
clone.select('#icons #burgIcons').remove();
}
if (noWater) {
clone.select('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').attr('opacity', 0);
}
if (type !== 'png') {
// reset transform to show the whole map
clone.attr('width', graphWidth).attr('height', graphHeight);
clone.select('#viewbox').attr('transform', null);
}
if (type === 'svg') removeUnusedElements(clone);
if (customization && type === 'mesh') updateMeshCells(clone);
inlineStyle(clone);
// remove unused filters
const filters = cloneEl.querySelectorAll('filter');
for (let i = 0; i < filters.length; i++) {
const id = filters[i].id;
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
if (cloneEl.getAttribute('filter') === 'url(#' + id + ')') continue;
filters[i].remove();
}
// remove unused patterns
const patterns = cloneEl.querySelectorAll('pattern');
for (let i = 0; i < patterns.length; i++) {
const id = patterns[i].id;
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
patterns[i].remove();
}
// remove unused symbols
const symbols = cloneEl.querySelectorAll('symbol');
for (let i = 0; i < symbols.length; i++) {
const id = symbols[i].id;
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
symbols[i].remove();
}
// add displayed emblems
if (layerIsOn('toggleEmblems') && emblems.selectAll('use').size()) {
cloneEl
.getElementById('emblems')
?.querySelectorAll('use')
.forEach((el) => {
const href = el.getAttribute('href') || el.getAttribute('xlink:href');
if (!href) return;
const emblem = document.getElementById(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true));
});
} else {
cloneDefs.querySelector('#defs-emblems')?.remove();
}
// add resources TODO
// replace ocean pattern href to base64
if (PRODUCTION && cloneEl.getElementById('oceanicPattern')) {
const el = cloneEl.getElementById('oceanicPattern');
const url = el.getAttribute('href');
await new Promise((resolve) => {
getBase64(url, (base64) => {
el.setAttribute('href', base64);
resolve();
});
});
}
// add relief icons
if (cloneEl.getElementById('terrain')) {
const uniqueElements = new Set();
const terrainNodes = cloneEl.getElementById('terrain').childNodes;
for (let i = 0; i < terrainNodes.length; i++) {
const href = terrainNodes[i].getAttribute('href') || terrainNodes[i].getAttribute('xlink:href');
uniqueElements.add(href);
}
const defsRelief = svgDefs.getElementById('defs-relief');
for (const terrain of [...uniqueElements]) {
const element = defsRelief.querySelector(terrain);
if (element) cloneDefs.appendChild(element.cloneNode(true));
}
}
// add wind rose
if (cloneEl.getElementById('compass')) {
const rose = svgDefs.getElementById('rose');
if (rose) cloneDefs.appendChild(rose.cloneNode(true));
}
// add port icon
if (cloneEl.getElementById('anchors')) {
const anchor = svgDefs.getElementById('icon-anchor');
if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
}
// add grid pattern
if (cloneEl.getElementById('gridOverlay')?.hasChildNodes()) {
const type = cloneEl.getElementById('gridOverlay').getAttribute('type');
const pattern = svgDefs.getElementById('pattern_' + type);
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
if (!cloneEl.getElementById('hatching').children.length) cloneEl.getElementById('hatching').remove(); // remove unused hatching group
if (!cloneEl.getElementById('fogging-cont')) cloneEl.getElementById('fog').remove(); // remove unused fog
if (!cloneEl.getElementById('regions')) cloneEl.getElementById('statePaths').remove(); // removed unused statePaths
if (!cloneEl.getElementById('labels')) cloneEl.getElementById('textPaths').remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById('armies'))
cloneEl.insertAdjacentHTML(
'afterbegin',
'<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>'
);
// add xlink: for href to support svg1.1
if (type === 'svg') {
cloneEl.querySelectorAll('[href]').forEach((el) => {
const href = el.getAttribute('href');
el.removeAttribute('href');
el.setAttribute('xlink:href', href);
});
}
// load non-standard fonts
const usedFonts = getFontsList(clone);
const webSafe = ['Georgia', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact'];
const fontsToLoad = usedFonts.filter((font) => !webSafe.includes(font));
if (fontsToLoad.length) {
const url = 'https://fonts.googleapis.com/css?family=' + fontsToLoad.join('|');
const fontStyle = await GFontToDataURI(url);
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n'));
}
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
return url;
}
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
function removeUnusedElements(clone) {
if (!terrain.selectAll('use').size()) clone.select('#defs-relief').remove();
if (markers.style('display') === 'none') clone.select('#defs-markers').remove();
for (let empty = 1; empty; ) {
empty = 0;
clone.selectAll('g').each(function () {
if (!this.hasChildNodes() || this.style.display === 'none' || this.classList.contains('hidden')) {
empty++;
this.remove();
}
if (this.hasAttribute('display') && this.style.display === 'inline') this.removeAttribute('display');
});
}
}
function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter((i) => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
clone.select('#heights').attr('filter', 'url(#blur1)');
clone
.select('#heights')
.selectAll('polygon')
.data(data)
.join('polygon')
.attr('points', (d) => getGridPolygon(d))
.attr('id', (d) => 'cell' + d)
.attr('stroke', (d) => getColor(grid.cells.h[d], scheme));
}
// for each g element get inline style
function inlineStyle(clone) {
const emptyG = clone.append('g').node();
const defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll('g, #ruler *, #scaleBar > text').each(function () {
const compStyle = window.getComputedStyle(this);
let style = '';
for (let i = 0; i < compStyle.length; i++) {
const key = compStyle[i];
const value = compStyle.getPropertyValue(key);
// Firefox mask hack
if (key === 'mask-image' && value !== defaultStyles.getPropertyValue(key)) {
style += "mask-image: url('#land');";
continue;
}
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
for (const key in compStyle) {
const value = compStyle.getPropertyValue(key);
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
if (style != '') this.setAttribute('style', style);
});
emptyG.remove();
}
// functions to save project as .map file
// prepare map data for saving
function getMapData() {
TIME && console.time('createMapDataBlob');
TIME && console.time('createMapData');
return new Promise((resolve) => {
const date = new Date();
const dateString = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
const license = 'File can be loaded in azgaar.github.io/Fantasy-Map-Generator';
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join('|');
const settings = [
distanceUnitInput.value,
distanceScaleInput.value,
areaUnit.value,
heightUnit.value,
heightExponentInput.value,
temperatureScale.value,
barSizeInput.value,
barLabel.value,
barBackOpacity.value,
barBackColor.value,
barPosX.value,
barPosY.value,
populationRate,
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
temperatureEquatorOutput.value,
temperaturePoleOutput.value,
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked
].join('|');
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|');
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
const date = new Date();
const dateString = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
const license = 'File can be loaded in azgaar.github.io/Fantasy-Map-Generator';
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join('|');
const settings = [
distanceUnitInput.value,
distanceScaleInput.value,
areaUnit.value,
heightUnit.value,
heightExponentInput.value,
temperatureScale.value,
barSizeInput.value,
barLabel.value,
barBackOpacity.value,
barBackColor.value,
barPosX.value,
barPosY.value,
populationRate,
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
temperatureEquatorOutput.value,
temperaturePoleOutput.value,
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked,
urbanDensity
].join('|');
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|');
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
const fonts = JSON.stringify(getUsedFonts(svg.node()));
// clone svg
const cloneEl = document.getElementById('map').cloneNode(true);
// save svg
const cloneEl = document.getElementById('map').cloneNode(true);
// set transform values to default
cloneEl.setAttribute('width', graphWidth);
cloneEl.setAttribute('height', graphHeight);
cloneEl.querySelector('#viewbox').removeAttribute('transform');
// reset transform values to default
cloneEl.setAttribute('width', graphWidth);
cloneEl.setAttribute('height', graphHeight);
cloneEl.querySelector('#viewbox').removeAttribute('transform');
// always remove rulers
cloneEl.querySelector('#ruler').innerHTML = '';
cloneEl.querySelector('#ruler').innerHTML = ''; // always remove rulers
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features});
const features = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const resources = JSON.stringify(pack.resources);
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const markers = JSON.stringify(pack.markers);
// store name array only if it is not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? '' : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join('/');
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? '' : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join('/');
// round population to save space
const pop = Array.from(pack.cells.pop).map((p) => rn(p, 4));
// round population to save space
const pop = Array.from(pack.cells.pop).map((p) => rn(p, 4));
// data format as below
const data = [
params,
settings,
coords,
biomes,
notesData,
svg_xml,
gridGeneral,
grid.cells.h,
grid.cells.prec,
grid.cells.f,
grid.cells.t,
grid.cells.temp,
features,
cultures,
states,
burgs,
pack.cells.biome,
pack.cells.burg,
pack.cells.conf,
pack.cells.culture,
pack.cells.fl,
pop,
pack.cells.r,
pack.cells.road,
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
pack.cells.crossroad,
religions,
provinces,
namesData,
rivers,
rulersString,
pack.cells.resource,
resources
].join('\r\n');
const blob = new Blob([data], {type: 'text/plain'});
TIME && console.timeEnd('createMapDataBlob');
resolve(blob);
});
// data format as below
const mapData = [
params,
settings,
coords,
biomes,
notesData,
serializedSVG,
gridGeneral,
grid.cells.h,
grid.cells.prec,
grid.cells.f,
grid.cells.t,
grid.cells.temp,
packFeatures,
cultures,
states,
burgs,
pack.cells.biome,
pack.cells.burg,
pack.cells.conf,
pack.cells.culture,
pack.cells.fl,
pop,
pack.cells.r,
pack.cells.road,
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
pack.cells.crossroad,
religions,
provinces,
namesData,
rivers,
rulersString,
fonts,
markers
].join('\r\n');
TIME && console.timeEnd('createMapData');
return mapData;
}
// Download .map file
async function saveMap() {
function dowloadMap() {
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
closeDialogs('#alert');
const blob = await getMapData();
const mapData = getMapData();
const blob = new Blob([mapData], {type: 'text/plain'});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = getFileName() + '.map';
@ -507,127 +136,25 @@ async function saveMap() {
window.URL.revokeObjectURL(URL);
}
function saveGeoJSON_Cells() {
const json = {type: 'FeatureCollection', features: []};
const cells = pack.cells;
const getPopulation = (i) => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = (i) => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
cells.i.forEach((i) => {
const coordinates = getCellCoordinates(cells.v[i]);
const height = getHeight(i);
const biome = cells.biome[i];
const type = pack.features[cells.f[i]].type;
const population = getPopulation(i);
const state = cells.state[i];
const province = cells.province[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const neighbors = cells.c[i];
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
const feature = {type: 'Feature', geometry: {type: 'Polygon', coordinates}, properties};
json.features.push(feature);
});
const name = getFileName('Cells') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Routes() {
const json = {type: 'FeatureCollection', features: []};
routes.selectAll('g > path').each(function () {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, type}};
json.features.push(feature);
});
const name = getFileName('Routes') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Rivers() {
const json = {type: 'FeatureCollection', features: []};
rivers.selectAll('path').each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find((r) => r.i === +id.slice(5));
const name = river ? river.name : '';
const type = river ? river.type : '';
const i = river ? river.i : '';
const basin = river ? river.basin : '';
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, i, basin, name, type, width, increment}};
json.features.push(feature);
});
const name = getFileName('Rivers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Markers() {
const json = {type: 'FeatureCollection', features: []};
markers.selectAll('use').each(function () {
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
const id = this.id;
const type = this.dataset.id.substring(1);
const icon = document.getElementById(type).textContent;
const note = notes.length ? notes.find((note) => note.id === this.id) : null;
const name = note ? note.name : '';
const legend = note ? note.legend : '';
const feature = {type: 'Feature', geometry: {type: 'Point', coordinates}, properties: {id, type, icon, name, legend}};
json.features.push(feature);
});
const name = getFileName('Markers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function getCellCoordinates(vertices) {
const p = pack.vertices.p;
const coordinates = vertices.map((n) => getQGIScoordinates(p[n][0], p[n][1]));
return [coordinates.concat([coordinates[0]])];
}
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getQGIScoordinates(p.x, p.y));
}
return points;
}
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] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x, y]);
}
return points;
}
async function quickSave() {
async function saveToDropbox() {
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
const blob = await getMapData();
closeDialogs('#alert');
const mapData = getMapData();
const filename = getFileName() + '.map';
try {
await Cloud.providers.dropbox.save(filename, mapData);
tip('Map is saved to your Dropbox', true, 'success', 8000);
} catch (msg) {
console.error(msg);
tip('Cannot save .map to your Dropbox', true, 'error', 8000);
}
}
function quickSave() {
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
const mapData = getMapData();
const blob = new Blob([mapData], {type: 'text/plain'});
if (blob) ldb.set('lastMap', blob); // auto-save map
tip('Map is saved to browser memory. Please also save as .map file to secure progress', true, 'success', 2000);
}
@ -642,19 +169,16 @@ const saveReminder = function () {
"Don't forget to save your map on a regular basis!",
'Just a gentle reminder for you to save the map',
"Please don't forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q",
"You'd better to save your progress as .map file",
"Don't want to lose the worldbuiding progress? Save your map right now",
'There is no way to restore the map other than .map file. Please save it regularly'
"Don't want to be reminded about need to save? Press CTRL+Q"
];
const interval = 15 * 60 * 1000; // remind every 15 minutes
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, 'warn', 10000);
}, 1e6);
tip(ra(message), true, 'warn', 2500);
}, interval);
saveReminder.status = 1;
};
saveReminder();
function toggleSaveReminder() {

843
modules/save.js.orig Normal file
View file

@ -0,0 +1,843 @@
<<<<<<< HEAD
// Functions to save and load the map
'use strict';
// download map as SVG
async function saveSVG() {
TIME && console.time('saveSVG');
const url = await getMapURL('svg');
const link = document.createElement('a');
link.download = getFileName() + '.svg';
link.href = url;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
TIME && console.timeEnd('saveSVG');
}
// download map as PNG
async function savePNG() {
TIME && console.time('savePNG');
const url = await getMapURL('png');
const link = document.createElement('a');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + '.png';
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
}, 1000);
});
};
TIME && console.timeEnd('savePNG');
}
// download map as JPEG
async function saveJPEG() {
TIME && console.time('saveJPEG');
const url = await getMapURL('png');
const canvas = document.createElement('canvas');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = async function () {
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL('image/jpeg', quality);
const link = document.createElement('a');
link.download = getFileName() + '.jpeg';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
};
TIME && console.timeEnd('saveJPEG');
}
// download map as png tiles
async function saveTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL('tiles', {debug: true});
const zip = new JSZip();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = graphWidth;
canvas.height = graphHeight;
const imgSchema = new Image();
imgSchema.src = urlSchema;
imgSchema.onload = function () {
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => zip.file(`fmg_tile_schema.png`, blob));
};
// download tiles
const url = await getMapURL('tiles');
const tilesX = +document.getElementById('tileColsInput').value;
const tilesY = +document.getElementById('tileRowsInput').value;
const scale = +document.getElementById('tileScaleInput').value;
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
let loaded = 0;
const img = new Image();
img.src = url;
img.onload = function () {
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob((blob) => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
function downloadZip() {
const name = `${getFileName()}.zip`;
zip.generateAsync({type: 'blob'}).then((blob) => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true);
});
}
});
}
// parse map svg to object url
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false} = options;
const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (!debug) clone.select('#debug').remove();
const cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox && type === 'mesh') clone.select('#oceanPattern').remove();
if (globe) clone.select('#scaleBar').remove();
if (noLabels) {
clone.select('#labels #states').remove();
clone.select('#labels #burgLabels').remove();
clone.select('#icons #burgIcons').remove();
}
if (noWater) {
clone.select('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').attr('opacity', 0);
}
if (type !== 'png') {
// reset transform to show the whole map
clone.attr('width', graphWidth).attr('height', graphHeight);
clone.select('#viewbox').attr('transform', null);
}
if (type === 'svg') removeUnusedElements(clone);
if (customization && type === 'mesh') updateMeshCells(clone);
inlineStyle(clone);
// remove unused filters
const filters = cloneEl.querySelectorAll('filter');
for (let i = 0; i < filters.length; i++) {
const id = filters[i].id;
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
if (cloneEl.getAttribute('filter') === 'url(#' + id + ')') continue;
filters[i].remove();
}
// remove unused patterns
const patterns = cloneEl.querySelectorAll('pattern');
for (let i = 0; i < patterns.length; i++) {
const id = patterns[i].id;
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
patterns[i].remove();
}
// remove unused symbols
const symbols = cloneEl.querySelectorAll('symbol');
for (let i = 0; i < symbols.length; i++) {
const id = symbols[i].id;
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
symbols[i].remove();
}
// add displayed emblems
if (layerIsOn('toggleEmblems') && emblems.selectAll('use').size()) {
cloneEl
.getElementById('emblems')
?.querySelectorAll('use')
.forEach((el) => {
const href = el.getAttribute('href') || el.getAttribute('xlink:href');
if (!href) return;
const emblem = document.getElementById(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true));
});
} else {
cloneDefs.querySelector('#defs-emblems')?.remove();
}
// add resources TODO
// replace ocean pattern href to base64
if (PRODUCTION && cloneEl.getElementById('oceanicPattern')) {
const el = cloneEl.getElementById('oceanicPattern');
const url = el.getAttribute('href');
await new Promise((resolve) => {
getBase64(url, (base64) => {
el.setAttribute('href', base64);
resolve();
});
});
}
// add relief icons
if (cloneEl.getElementById('terrain')) {
const uniqueElements = new Set();
const terrainNodes = cloneEl.getElementById('terrain').childNodes;
for (let i = 0; i < terrainNodes.length; i++) {
const href = terrainNodes[i].getAttribute('href') || terrainNodes[i].getAttribute('xlink:href');
uniqueElements.add(href);
}
const defsRelief = svgDefs.getElementById('defs-relief');
for (const terrain of [...uniqueElements]) {
const element = defsRelief.querySelector(terrain);
if (element) cloneDefs.appendChild(element.cloneNode(true));
}
}
// add wind rose
if (cloneEl.getElementById('compass')) {
const rose = svgDefs.getElementById('rose');
if (rose) cloneDefs.appendChild(rose.cloneNode(true));
}
// add port icon
if (cloneEl.getElementById('anchors')) {
const anchor = svgDefs.getElementById('icon-anchor');
if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
}
// add grid pattern
if (cloneEl.getElementById('gridOverlay')?.hasChildNodes()) {
const type = cloneEl.getElementById('gridOverlay').getAttribute('type');
const pattern = svgDefs.getElementById('pattern_' + type);
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
if (!cloneEl.getElementById('hatching').children.length) cloneEl.getElementById('hatching').remove(); // remove unused hatching group
if (!cloneEl.getElementById('fogging-cont')) cloneEl.getElementById('fog').remove(); // remove unused fog
if (!cloneEl.getElementById('regions')) cloneEl.getElementById('statePaths').remove(); // removed unused statePaths
if (!cloneEl.getElementById('labels')) cloneEl.getElementById('textPaths').remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById('armies'))
cloneEl.insertAdjacentHTML(
'afterbegin',
'<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>'
);
// add xlink: for href to support svg1.1
if (type === 'svg') {
cloneEl.querySelectorAll('[href]').forEach((el) => {
const href = el.getAttribute('href');
el.removeAttribute('href');
el.setAttribute('xlink:href', href);
});
}
// load non-standard fonts
const usedFonts = getFontsList(clone);
const webSafe = ['Georgia', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact'];
const fontsToLoad = usedFonts.filter((font) => !webSafe.includes(font));
if (fontsToLoad.length) {
const url = 'https://fonts.googleapis.com/css?family=' + fontsToLoad.join('|');
const fontStyle = await GFontToDataURI(url);
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n'));
}
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
return url;
}
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
function removeUnusedElements(clone) {
if (!terrain.selectAll('use').size()) clone.select('#defs-relief').remove();
if (markers.style('display') === 'none') clone.select('#defs-markers').remove();
for (let empty = 1; empty; ) {
empty = 0;
clone.selectAll('g').each(function () {
if (!this.hasChildNodes() || this.style.display === 'none' || this.classList.contains('hidden')) {
empty++;
this.remove();
}
if (this.hasAttribute('display') && this.style.display === 'inline') this.removeAttribute('display');
});
}
}
function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter((i) => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
clone.select('#heights').attr('filter', 'url(#blur1)');
clone
.select('#heights')
.selectAll('polygon')
.data(data)
.join('polygon')
.attr('points', (d) => getGridPolygon(d))
.attr('id', (d) => 'cell' + d)
.attr('stroke', (d) => getColor(grid.cells.h[d], scheme));
}
// for each g element get inline style
function inlineStyle(clone) {
const emptyG = clone.append('g').node();
const defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll('g, #ruler *, #scaleBar > text').each(function () {
const compStyle = window.getComputedStyle(this);
let style = '';
for (let i = 0; i < compStyle.length; i++) {
const key = compStyle[i];
const value = compStyle.getPropertyValue(key);
// Firefox mask hack
if (key === 'mask-image' && value !== defaultStyles.getPropertyValue(key)) {
style += "mask-image: url('#land');";
continue;
}
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
for (const key in compStyle) {
const value = compStyle.getPropertyValue(key);
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
if (style != '') this.setAttribute('style', style);
});
emptyG.remove();
}
// prepare map data for saving
function getMapData() {
TIME && console.time('createMapDataBlob');
return new Promise((resolve) => {
const date = new Date();
const dateString = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
const license = 'File can be loaded in azgaar.github.io/Fantasy-Map-Generator';
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join('|');
const settings = [
distanceUnitInput.value,
distanceScaleInput.value,
areaUnit.value,
heightUnit.value,
heightExponentInput.value,
temperatureScale.value,
barSizeInput.value,
barLabel.value,
barBackOpacity.value,
barBackColor.value,
barPosX.value,
barPosY.value,
populationRate,
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
temperatureEquatorOutput.value,
temperaturePoleOutput.value,
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked
].join('|');
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join('|');
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
// clone svg
const cloneEl = document.getElementById('map').cloneNode(true);
// set transform values to default
cloneEl.setAttribute('width', graphWidth);
cloneEl.setAttribute('height', graphHeight);
cloneEl.querySelector('#viewbox').removeAttribute('transform');
// always remove rulers
cloneEl.querySelector('#ruler').innerHTML = '';
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features});
const features = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const resources = JSON.stringify(pack.resources);
// store name array only if it is not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? '' : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join('/');
// round population to save space
const pop = Array.from(pack.cells.pop).map((p) => rn(p, 4));
// data format as below
const data = [
params,
settings,
coords,
biomes,
notesData,
svg_xml,
gridGeneral,
grid.cells.h,
grid.cells.prec,
grid.cells.f,
grid.cells.t,
grid.cells.temp,
features,
cultures,
states,
burgs,
pack.cells.biome,
pack.cells.burg,
pack.cells.conf,
pack.cells.culture,
pack.cells.fl,
pop,
pack.cells.r,
pack.cells.road,
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
pack.cells.crossroad,
religions,
provinces,
namesData,
rivers,
rulersString,
pack.cells.resource,
resources
].join('\r\n');
const blob = new Blob([data], {type: 'text/plain'});
TIME && console.timeEnd('createMapDataBlob');
resolve(blob);
});
}
// Download .map file
async function saveMap() {
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
closeDialogs('#alert');
=======
"use strict";
// functions to save project as .map file
// prepare map data for saving
function getMapData() {
TIME && console.time("createMapData");
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [
distanceUnitInput.value,
distanceScaleInput.value,
areaUnit.value,
heightUnit.value,
heightExponentInput.value,
temperatureScale.value,
barSizeInput.value,
barLabel.value,
barBackOpacity.value,
barBackColor.value,
barPosX.value,
barPosY.value,
populationRate,
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
temperatureEquatorOutput.value,
temperaturePoleOutput.value,
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked,
urbanDensity
].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
const fonts = JSON.stringify(getUsedFonts(svg.node()));
// save svg
const cloneEl = document.getElementById("map").cloneNode(true);
// reset transform values to default
cloneEl.setAttribute("width", graphWidth);
cloneEl.setAttribute("height", graphHeight);
cloneEl.querySelector("#viewbox").removeAttribute("transform");
cloneEl.querySelector("#ruler").innerHTML = ""; // always remove rulers
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const markers = JSON.stringify(pack.markers);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join("/");
// round population to save space
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const mapData = [
params,
settings,
coords,
biomes,
notesData,
serializedSVG,
gridGeneral,
grid.cells.h,
grid.cells.prec,
grid.cells.f,
grid.cells.t,
grid.cells.temp,
packFeatures,
cultures,
states,
burgs,
pack.cells.biome,
pack.cells.burg,
pack.cells.conf,
pack.cells.culture,
pack.cells.fl,
pop,
pack.cells.r,
pack.cells.road,
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
pack.cells.crossroad,
religions,
provinces,
namesData,
rivers,
rulersString,
fonts,
markers
].join("\r\n");
TIME && console.timeEnd("createMapData");
return mapData;
}
// Download .map file
function dowloadMap() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
>>>>>>> master
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = getFileName() + '.map';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.URL.revokeObjectURL(URL);
}
<<<<<<< HEAD
function saveGeoJSON_Cells() {
const json = {type: 'FeatureCollection', features: []};
const cells = pack.cells;
const getPopulation = (i) => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = (i) => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
cells.i.forEach((i) => {
const coordinates = getCellCoordinates(cells.v[i]);
const height = getHeight(i);
const biome = cells.biome[i];
const type = pack.features[cells.f[i]].type;
const population = getPopulation(i);
const state = cells.state[i];
const province = cells.province[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const neighbors = cells.c[i];
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
const feature = {type: 'Feature', geometry: {type: 'Polygon', coordinates}, properties};
json.features.push(feature);
});
const name = getFileName('Cells') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Routes() {
const json = {type: 'FeatureCollection', features: []};
routes.selectAll('g > path').each(function () {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, type}};
json.features.push(feature);
});
const name = getFileName('Routes') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Rivers() {
const json = {type: 'FeatureCollection', features: []};
rivers.selectAll('path').each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find((r) => r.i === +id.slice(5));
const name = river ? river.name : '';
const type = river ? river.type : '';
const i = river ? river.i : '';
const basin = river ? river.basin : '';
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, i, basin, name, type, width, increment}};
json.features.push(feature);
});
const name = getFileName('Rivers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Markers() {
const json = {type: 'FeatureCollection', features: []};
markers.selectAll('use').each(function () {
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
const id = this.id;
const type = this.dataset.id.substring(1);
const icon = document.getElementById(type).textContent;
const note = notes.length ? notes.find((note) => note.id === this.id) : null;
const name = note ? note.name : '';
const legend = note ? note.legend : '';
const feature = {type: 'Feature', geometry: {type: 'Point', coordinates}, properties: {id, type, icon, name, legend}};
json.features.push(feature);
});
const name = getFileName('Markers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function getCellCoordinates(vertices) {
const p = pack.vertices.p;
const coordinates = vertices.map((n) => getQGIScoordinates(p[n][0], p[n][1]));
return [coordinates.concat([coordinates[0]])];
}
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getQGIScoordinates(p.x, p.y));
}
return points;
}
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] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x, y]);
=======
async function saveToDropbox() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
const filename = getFileName() + ".map";
try {
await Cloud.providers.dropbox.save(filename, mapData);
tip("Map is saved to your Dropbox", true, "success", 8000);
} catch (msg) {
console.error(msg);
tip("Cannot save .map to your Dropbox", true, "error", 8000);
>>>>>>> master
}
}
<<<<<<< HEAD
async function quickSave() {
if (customization) return tip('Map cannot be saved when edit mode is active, please exit the mode and retry', false, 'error');
const blob = await getMapData();
if (blob) ldb.set('lastMap', blob); // auto-save map
tip('Map is saved to browser memory. Please also save as .map file to secure progress', true, 'success', 2000);
}
const saveReminder = function () {
if (localStorage.getItem('noReminder')) return;
const message = [
"Please don't forget to save your work as a .map file",
'Please remember to save work as a .map file',
"Saving in .map format will ensure your data won't be lost in case of issues",
'Safety is number one priority. Please save the map',
"Don't forget to save your map on a regular basis!",
'Just a gentle reminder for you to save the map',
"Please don't forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q",
"You'd better to save your progress as .map file",
"Don't want to lose the worldbuiding progress? Save your map right now",
'There is no way to restore the map other than .map file. Please save it regularly'
];
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, 'warn', 10000);
}, 1e6);
=======
function quickSave() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
if (blob) ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
}
const saveReminder = function () {
if (localStorage.getItem("noReminder")) return;
const message = [
"Please don't forget to save your work as a .map file",
"Please remember to save work as a .map file",
"Saving in .map format will ensure your data won't be lost in case of issues",
"Safety is number one priority. Please save the map",
"Don't forget to save your map on a regular basis!",
"Just a gentle reminder for you to save the map",
"Please don't forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q"
];
const interval = 15 * 60 * 1000; // remind every 15 minutes
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, "warn", 2500);
}, interval);
>>>>>>> master
saveReminder.status = 1;
};
saveReminder();
function toggleSaveReminder() {
if (saveReminder.status) {
tip('Save reminder is turned off. Press CTRL+Q again to re-initiate', true, 'warn', 2000);
clearInterval(saveReminder.reminder);
localStorage.setItem('noReminder', true);
saveReminder.status = 0;
} else {
tip('Save reminder is turned on. Press CTRL+Q to turn off', true, 'warn', 2000);
localStorage.removeItem('noReminder');
saveReminder();
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,8 @@
"use strict";
'use strict';
class Battle {
constructor(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
closeDialogs('.stable');
customization = 13; // enter customization to avoid unwanted dialog closing
Battle.prototype.context = this; // store context
@ -14,21 +14,21 @@ class Battle {
this.defenders = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0};
this.addHeaders();
this.addRegiment("attackers", attacker);
this.addRegiment("defenders", defender);
this.addRegiment('attackers', attacker);
this.addRegiment('defenders', defender);
this.place = this.definePlace();
this.defineType();
this.name = this.defineName();
this.randomize();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
this.getInitialMorale();
$("#battleScreen").dialog({
$('#battleScreen').dialog({
title: this.name,
resizable: false,
width: fitContent(),
position: {my: "center", at: "center", of: "#map"},
position: {my: 'center', at: 'center', of: '#map'},
close: () => Battle.prototype.context.cancelResults()
});
@ -36,42 +36,42 @@ class Battle {
modules.Battle = true;
// add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
document.getElementById("battleRun").addEventListener("click", () => Battle.prototype.context.run());
document.getElementById("battleApply").addEventListener("click", () => Battle.prototype.context.applyResults());
document.getElementById("battleCancel").addEventListener("click", () => Battle.prototype.context.cancelResults());
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
document.getElementById('battleType').addEventListener('click', (ev) => this.toggleChange(ev));
document.getElementById('battleType').nextElementSibling.addEventListener('click', (ev) => Battle.prototype.context.changeType(ev));
document.getElementById('battleNameShow').addEventListener('click', () => Battle.prototype.context.showNameSection());
document.getElementById('battleNamePlace').addEventListener('change', (ev) => (Battle.prototype.context.place = ev.target.value));
document.getElementById('battleNameFull').addEventListener('change', (ev) => Battle.prototype.context.changeName(ev));
document.getElementById('battleNameCulture').addEventListener('click', () => Battle.prototype.context.generateName('culture'));
document.getElementById('battleNameRandom').addEventListener('click', () => Battle.prototype.context.generateName('random'));
document.getElementById('battleNameHide').addEventListener('click', this.hideNameSection);
document.getElementById('battleAddRegiment').addEventListener('click', this.addSide);
document.getElementById('battleRoll').addEventListener('click', () => Battle.prototype.context.randomize());
document.getElementById('battleRun').addEventListener('click', () => Battle.prototype.context.run());
document.getElementById('battleApply').addEventListener('click', () => Battle.prototype.context.applyResults());
document.getElementById('battleCancel').addEventListener('click', () => Battle.prototype.context.cancelResults());
document.getElementById('battleWiki').addEventListener('click', () => wiki('Battle-Simulator'));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
document.getElementById('battlePhase_attackers').addEventListener('click', (ev) => this.toggleChange(ev));
document.getElementById('battlePhase_attackers').nextElementSibling.addEventListener('click', (ev) => Battle.prototype.context.changePhase(ev, 'attackers'));
document.getElementById('battlePhase_defenders').addEventListener('click', (ev) => this.toggleChange(ev));
document.getElementById('battlePhase_defenders').nextElementSibling.addEventListener('click', (ev) => Battle.prototype.context.changePhase(ev, 'defenders'));
document.getElementById('battleDie_attackers').addEventListener('click', () => Battle.prototype.context.rollDie('attackers'));
document.getElementById('battleDie_defenders').addEventListener('click', () => Battle.prototype.context.rollDie('defenders'));
}
defineType() {
const attacker = this.attackers.regiments[0];
const defender = this.defenders.regiments[0];
const getType = () => {
const typesA = Object.keys(attacker.u).map(name => options.military.find(u => u.name === name).type);
const typesD = Object.keys(defender.u).map(name => options.military.find(u => u.name === name).type);
const typesA = Object.keys(attacker.u).map((name) => options.military.find((u) => u.name === name).type);
const typesD = Object.keys(defender.u).map((name) => options.military.find((u) => u.name === name).type);
if (attacker.n && defender.n) return "naval"; // attacker and defender are navals
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attackers and defender have only aviation units
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
return "field";
if (attacker.n && defender.n) return 'naval'; // attacker and defender are navals
if (typesA.every((t) => t === 'aviation') && typesD.every((t) => t === 'aviation')) return 'air'; // if attackers and defender have only aviation units
if (attacker.n && !defender.n && typesA.some((t) => t !== 'naval')) return 'landing'; // if attacked is naval with non-naval units and defender is not naval
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return 'siege'; // defender is in walled town
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return 'ambush'; // 20% if defenders are in forest or marshes
return 'field';
};
this.type = getType();
@ -79,25 +79,25 @@ class Battle {
}
setType() {
document.getElementById("battleType").className = "icon-button-" + this.type;
document.getElementById('battleType').className = 'icon-button-' + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers;
const sideSpecific = document.getElementById('battlePhases_' + this.type + '_attackers');
const attackers = sideSpecific ? sideSpecific.content : document.getElementById('battlePhases_' + this.type).content;
const defenders = sideSpecific ? document.getElementById('battlePhases_' + this.type + '_defenders').content : attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
document.getElementById("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
document.getElementById('battlePhase_attackers').nextElementSibling.innerHTML = '';
document.getElementById('battlePhase_defenders').nextElementSibling.innerHTML = '';
document.getElementById('battlePhase_attackers').nextElementSibling.append(attackers.cloneNode(true));
document.getElementById('battlePhase_defenders').nextElementSibling.append(defenders.cloneNode(true));
}
definePlace() {
const cells = pack.cells,
i = this.cell;
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
const getRiver = i => {
const river = pack.rivers.find(r => r.i === i);
return river.name + " " + river.type;
const getRiver = (i) => {
const river = pack.rivers.find((r) => r.i === i);
return river.name + ' ' + river.type;
};
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
@ -105,28 +105,28 @@ class Battle {
}
defineName() {
if (this.type === "field") return "Battle of " + this.place;
if (this.type === "naval") return "Naval Battle of " + this.place;
if (this.type === "siege") return "Siege of " + this.place;
if (this.type === "ambush") return this.place + " Ambush";
if (this.type === "landing") return this.place + " Landing";
if (this.type === "air") return `${this.place} ${P(0.8) ? "Air Battle" : "Dogfight"}`;
if (this.type === 'field') return 'Battle of ' + this.place;
if (this.type === 'naval') return 'Naval Battle of ' + this.place;
if (this.type === 'siege') return 'Siege of ' + this.place;
if (this.type === 'ambush') return this.place + ' Ambush';
if (this.type === 'landing') return this.place + ' Landing';
if (this.type === 'air') return `${this.place} ${P(0.8) ? 'Air Battle' : 'Dogfight'}`;
}
getTypeName() {
if (this.type === "field") return "field battle";
if (this.type === "naval") return "naval battle";
if (this.type === "siege") return "siege";
if (this.type === "ambush") return "ambush";
if (this.type === "landing") return "landing";
if (this.type === "air") return "battle";
if (this.type === 'field') return 'field battle';
if (this.type === 'naval') return 'naval battle';
if (this.type === 'siege') return 'siege';
if (this.type === 'ambush') return 'ambush';
if (this.type === 'landing') return 'landing';
if (this.type === 'air') return 'battle';
}
addHeaders() {
let headers = "<thead><tr><th></th><th></th>";
let headers = '<thead><tr><th></th><th></th>';
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>`;
}
@ -140,7 +140,7 @@ class Battle {
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const color = state.color[0] === '#' ? state.color : '#999';
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
@ -160,28 +160,28 @@ class Battle {
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a || 0}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
const div = side === 'attackers' ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + '</tbody>';
this[side].regiments.push(regiment);
this[side].distances.push(distance);
}
addSide() {
const body = document.getElementById("regimentSelectorBody");
const body = document.getElementById('regimentSelectorBody');
const context = Battle.prototype.context;
const regiments = pack.states
.filter(s => s.military && !s.removed)
.map(s => s.military)
.filter((s) => s.military && !s.removed)
.map((s) => s.military)
.flat();
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
const distance = (reg) => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const isAdded = (reg) => context.defenders.regiments.some((r) => r === reg) || context.attackers.regiments.some((r) => r === reg);
body.innerHTML = regiments
.map(r => {
.map((r) => {
const s = pack.states[r.state],
added = isAdded(r),
dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
dist = added ? '0 ' + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ''} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div>
@ -191,43 +191,43 @@ class Battle {
<div style="width:4em">${dist}</div>
</div>`;
})
.join("");
.join('');
$("#regimentSelectorScreen").dialog({
$('#regimentSelectorScreen').dialog({
resizable: false,
width: fitContent(),
title: "Add regiment to the battle",
position: {my: "left center", at: "right+10 center", of: "#battleScreen"},
title: 'Add regiment to the battle',
position: {my: 'left center', at: 'right+10 center', of: '#battleScreen'},
close: addSideClosed,
buttons: {
"Add to attackers": () => addSideClicked("attackers"),
"Add to defenders": () => addSideClicked("defenders"),
Cancel: () => $("#regimentSelectorScreen").dialog("close")
'Add to attackers': () => addSideClicked('attackers'),
'Add to defenders': () => addSideClicked('defenders'),
Cancel: () => $('#regimentSelectorScreen').dialog('close')
}
});
applySorting(regimentSelectorHeader);
body.addEventListener("click", selectLine);
body.addEventListener('click', selectLine);
function selectLine(ev) {
if (ev.target.className === "inactive") {
tip("Regiment is already in the battle", false, "error");
if (ev.target.className === 'inactive') {
tip('Regiment is already in the battle', false, 'error');
return;
}
ev.target.classList.toggle("selected");
ev.target.classList.toggle('selected');
}
function addSideClicked(side) {
const selected = body.querySelectorAll(".selected");
const selected = body.querySelectorAll('.selected');
if (!selected.length) {
tip("Please select a regiment first", false, "error");
tip('Please select a regiment first', false, 'error');
return;
}
$("#regimentSelectorScreen").dialog("close");
selected.forEach(line => {
$('#regimentSelectorScreen').dialog('close');
selected.forEach((line) => {
const state = pack.states[line.dataset.s];
const regiment = state.military.find(r => r.i == +line.dataset.i);
const regiment = state.military.find((r) => r.i == +line.dataset.i);
Battle.prototype.addRegiment.call(context, side, regiment);
Battle.prototype.calculateStrength.call(context, side);
Battle.prototype.getInitialMorale.call(context);
@ -235,7 +235,7 @@ class Battle {
// move regiment
const defenders = context.defenders.regiments,
attackers = context.attackers.regiments;
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.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
@ -243,34 +243,34 @@ class Battle {
}
function addSideClosed() {
body.innerHTML = "";
body.removeEventListener("click", selectLine);
body.innerHTML = '';
body.removeEventListener('click', selectLine);
}
}
showNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("battleNameSection").style.display = "inline-block";
document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('battleNameSection').style.display = 'inline-block';
document.getElementById("battleNamePlace").value = this.place;
document.getElementById("battleNameFull").value = this.name;
document.getElementById('battleNamePlace').value = this.place;
document.getElementById('battleNameFull').value = this.name;
}
hideNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("battleNameSection").style.display = "none";
document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('battleNameSection').style.display = 'none';
}
changeName(ev) {
this.name = ev.target.value;
$("#battleScreen").dialog({title: this.name});
$('#battleScreen').dialog({title: this.name});
}
generateName(type) {
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
const place = type === 'culture' ? Names.getCulture(pack.cells.culture[this.cell], null, null, '') : Names.getBase(rand(nameBases.length - 1));
document.getElementById('battleNamePlace').value = this.place = place;
document.getElementById('battleNameFull').value = this.name = this.defineName();
$('#battleScreen').dialog({title: this.name});
}
getJoinedForces(regiments) {
@ -324,38 +324,38 @@ class Battle {
const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase;
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
this[side].power = d3.sum(options.military.map((u) => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
document.getElementById('battlePower_' + side).innerHTML = UIvalue;
}
getInitialMorale() {
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
const powerFee = (diff) => minmax(100 - diff ** 1.5 * 10 + 10, 50, 100);
const distanceFee = (dist) => Math.min(d3.mean(dist) / 50, 15);
const powerDiff = this.defenders.power / this.attackers.power;
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
this.defenders.morale = powerFee(1 / powerDiff) - distanceFee(this.defenders.distances);
this.updateMorale("attackers");
this.updateMorale("defenders");
this.updateMorale('attackers');
this.updateMorale('defenders');
}
updateMorale(side) {
const morale = document.getElementById("battleMorale_" + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
const morale = document.getElementById('battleMorale_' + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, '');
morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value;
}
randomize() {
this.rollDie("attackers");
this.rollDie("defenders");
this.rollDie('attackers');
this.rollDie('defenders');
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
}
rollDie(side) {
const el = document.getElementById("battleDie_" + side);
const el = document.getElementById('battleDie_' + side);
const prev = +el.innerHTML;
do {
el.innerHTML = rand(1, 6);
@ -369,131 +369,131 @@ class Battle {
const powerRatio = this.attackers.power / this.defenders.power;
const getFieldBattlePhase = () => {
const prev = [this.attackers.phase || "skirmish", this.defenders.phase || "skirmish"]; // previous phase
const prev = [this.attackers.phase || 'skirmish', this.defenders.phase || 'skirmish']; // previous phase
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
// skirmish phase continuation depends on ranged forces number
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
if (prev[0] === 'skirmish' && prev[1] === 'skirmish') {
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
const total = d3.sum(Object.values(forces)); // total forces
const ranged =
d3.sum(
options.military
.filter(u => u.type === "ranged")
.map(u => u.name)
.map(u => forces[u])
.filter((u) => u.type === 'ranged')
.map((u) => u.name)
.map((u) => forces[u])
) / total; // ranged units
if (P(ranged) || P(0.8 - i / 10)) return ["skirmish", "skirmish"];
if (P(ranged) || P(0.8 - i / 10)) return ['skirmish', 'skirmish'];
}
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getNavalBattlePhase = () => {
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
const prev = [this.attackers.phase || 'shelling', this.defenders.phase || 'shelling']; // previous phase
if (prev[0] === "withdrawal") return ["withdrawal", "chase"];
if (prev[0] === "chase") return ["chase", "withdrawal"];
if (prev[0] === 'withdrawal') return ['withdrawal', 'chase'];
if (prev[0] === 'chase') return ['chase', 'withdrawal'];
// withdrawal phase when power imbalanced
if (!prev[0] === "boarding") {
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ["withdrawal", "chase"];
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ["chase", "withdrawal"];
if (!prev[0] === 'boarding') {
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ['withdrawal', 'chase'];
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ['chase', 'withdrawal'];
}
// boarding phase can start from 2nd iteration
if (prev[0] === "boarding" || P(i / 10 - 0.1)) return ["boarding", "boarding"];
if (prev[0] === 'boarding' || P(i / 10 - 0.1)) return ['boarding', 'boarding'];
return ["shelling", "shelling"]; // default option
return ['shelling', 'shelling']; // default option
};
const getSiegePhase = () => {
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
let phase = ["blockade", "sheltering"]; // default phase
const prev = [this.attackers.phase || 'blockade', this.defenders.phase || 'sheltering']; // previous phase
let phase = ['blockade', 'sheltering']; // default phase
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
if (prev[0] === 'retreat' || prev[0] === 'looting') return prev;
if (P(1 - morale[0] / 30) && powerRatio < 1) return ["retreat", "pursue"]; // attackers retreat chance if moral < 30
if (P(1 - morale[1] / 15)) return ["looting", "surrendering"]; // defenders surrendering chance if moral < 15
if (P(1 - morale[0] / 30) && powerRatio < 1) return ['retreat', 'pursue']; // attackers retreat chance if moral < 30
if (P(1 - morale[1] / 15)) return ['looting', 'surrendering']; // defenders surrendering chance if moral < 15
if (P((powerRatio - 1) / 2)) return ["storming", "defense"]; // start storm
if (P((powerRatio - 1) / 2)) return ['storming', 'defense']; // start storm
if (prev[0] !== "storming") {
const machinery = options.military.filter(u => u.type === "machinery").map(u => u.name); // machinery units
if (prev[0] !== 'storming') {
const machinery = options.military.filter((u) => u.type === 'machinery').map((u) => u.name); // machinery units
const attackers = this.getJoinedForces(this.attackers.regiments);
const machineryA = d3.sum(machinery.map(u => attackers[u]));
if (i && machineryA && P(0.9)) phase[0] = "bombardment";
const machineryA = d3.sum(machinery.map((u) => attackers[u]));
if (i && machineryA && P(0.9)) phase[0] = 'bombardment';
const defenders = this.getJoinedForces(this.defenders.regiments);
const machineryD = d3.sum(machinery.map(u => defenders[u]));
if (machineryD && P(0.9)) phase[1] = "bombardment";
const machineryD = d3.sum(machinery.map((u) => defenders[u]));
if (machineryD && P(0.9)) phase[1] = 'bombardment';
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = "sortie"; // defenders sortie
if (i && prev[1] !== 'sortie' && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = 'sortie'; // defenders sortie
}
return phase;
};
const getAmbushPhase = () => {
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
const prev = [this.attackers.phase || 'shock', this.defenders.phase || 'surprise']; // previous phase
if (prev[1] === "surprise" && P(1 - (powerRatio * i) / 5)) return ["shock", "surprise"];
if (prev[1] === 'surprise' && P(1 - (powerRatio * i) / 5)) return ['shock', 'surprise'];
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getLandingPhase = () => {
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
const prev = [this.attackers.phase || 'landing', this.defenders.phase || 'defense']; // previous phase
if (prev[1] === "waiting") return ["flee", "waiting"];
if (prev[1] === "pursue") return ["flee", P(0.3) ? "pursue" : "waiting"];
if (prev[1] === "retreat") return ["pursue", "retreat"];
if (prev[1] === 'waiting') return ['flee', 'waiting'];
if (prev[1] === 'pursue') return ['flee', P(0.3) ? 'pursue' : 'waiting'];
if (prev[1] === 'retreat') return ['pursue', 'retreat'];
if (prev[0] === "landing") {
const attackers = P(i / 2) ? "melee" : "landing";
const defenders = i ? prev[1] : P(0.5) ? "defense" : "shock";
if (prev[0] === 'landing') {
const attackers = P(i / 2) ? 'melee' : 'landing';
const defenders = i ? prev[1] : P(0.5) ? 'defense' : 'shock';
return [attackers, defenders];
}
if (P(1 - morale[0] / 40)) return ["flee", "pursue"]; // chance if moral < 40
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
if (P(1 - morale[0] / 40)) return ['flee', 'pursue']; // chance if moral < 40
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; // chance if moral < 25
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getAirBattlePhase = () => {
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
const prev = [this.attackers.phase || 'maneuvering', this.defenders.phase || 'maneuvering']; // previous phase
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
if (prev[0] === "maneuvering" && P(1 - i / 10)) return ["maneuvering", "maneuvering"];
if (prev[0] === 'maneuvering' && P(1 - i / 10)) return ['maneuvering', 'maneuvering'];
return ["dogfight", "dogfight"]; // default option
return ['dogfight', 'dogfight']; // default option
};
const phase = (function (type) {
switch (type) {
case "field":
case 'field':
return getFieldBattlePhase();
case "naval":
case 'naval':
return getNavalBattlePhase();
case "siege":
case 'siege':
return getSiegePhase();
case "ambush":
case 'ambush':
return getAmbushPhase();
case "landing":
case 'landing':
return getLandingPhase();
case "air":
case 'air':
return getAirBattlePhase();
default:
getFieldBattlePhase();
@ -503,23 +503,23 @@ class Battle {
this.attackers.phase = phase[0];
this.defenders.phase = phase[1];
const buttonA = document.getElementById("battlePhase_attackers");
buttonA.className = "icon-button-" + this.attackers.phase;
const buttonA = document.getElementById('battlePhase_attackers');
buttonA.className = 'icon-button-' + this.attackers.phase;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip;
const buttonD = document.getElementById("battlePhase_defenders");
buttonD.className = "icon-button-" + this.defenders.phase;
const buttonD = document.getElementById('battlePhase_defenders');
buttonD.className = 'icon-button-' + this.defenders.phase;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip;
}
run() {
// validations
if (!this.attackers.power) {
tip("Attackers army destroyed", false, "warn");
tip('Attackers army destroyed', false, 'warn');
return;
}
if (!this.defenders.power) {
tip("Defenders army destroyed", false, "warn");
tip('Defenders army destroyed', false, 'warn');
return;
}
@ -558,8 +558,8 @@ class Battle {
const casualtiesA = (casualties * defense) / (attack + defense); // attackers casualties, ~5% per iteration
const casualtiesD = (casualties * attack) / (attack + defense); // defenders casualties, ~5% per iteration
this.calculateCasualties("attackers", casualtiesA);
this.calculateCasualties("defenders", casualtiesD);
this.calculateCasualties('attackers', casualtiesA);
this.calculateCasualties('defenders', casualtiesD);
this.attackers.casualties += casualtiesA;
this.defenders.casualties += casualtiesD;
@ -568,14 +568,14 @@ class Battle {
this.defenders.morale = Math.max(this.defenders.morale - casualtiesD * 100 - 1, 0);
// update table values
this.updateTable("attackers");
this.updateTable("defenders");
this.updateTable('attackers');
this.updateTable('defenders');
// prepare for next iteration
this.iteration += 1;
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
}
calculateCasualties(side, casualties) {
@ -591,9 +591,9 @@ class Battle {
updateTable(side) {
for (const r of this[side].regiments) {
const tbody = document.getElementById("battle" + r.state + "-" + r.i);
const battleCasualties = tbody.querySelector(".battleCasualties");
const battleSurvivors = tbody.querySelector(".battleSurvivors");
const tbody = document.getElementById('battle' + r.state + '-' + r.i);
const battleCasualties = tbody.querySelector('.battleCasualties');
const battleSurvivors = tbody.querySelector('.battleSurvivors');
let index = 3; // index to find table element easily
for (const u of options.military) {
@ -615,35 +615,35 @@ class Battle {
const hideSection = function () {
button.style.opacity = 1;
div.style.display = "none";
div.style.display = 'none';
};
if (div.style.display === "block") {
if (div.style.display === 'block') {
hideSection();
return;
}
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].addEventListener('click', hideSection, {once: true});
}
changeType(ev) {
if (ev.target.tagName !== "BUTTON") return;
if (ev.target.tagName !== 'BUTTON') return;
this.type = ev.target.dataset.type;
this.setType();
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
$('#battleScreen').dialog({title: this.name});
}
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 button = document.getElementById("battlePhase_" + side);
button.className = "icon-button-" + phase;
const button = document.getElementById('battlePhase_' + side);
button.className = 'icon-button-' + phase;
button.dataset.tip = ev.target.dataset.tip;
this.calculateStrength(side);
}
@ -654,34 +654,49 @@ class Battle {
const relativeCasualties = this.defenders.casualties / (this.attackers.casualties + this.attackers.casualties);
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
function getBattleStatus(relative, max) {
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
if (max < 0.05) return ["minor skirmishes", "minor skirmishes"];
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
if (relative > 0.7) return ["attackers decisive victory", "defenders disastrous defeat"];
if (relative > 0.6) return ["attackers victory", "defenders defeat"];
if (relative > 0.4) return ["stalemate", "stalemate"];
if (relative > 0.3) return ["attackers defeat", "defenders victory"];
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
return ["stalemate", "stalemate"]; // exception
if (isNaN(relative)) return ['standoff', 'standoff']; // if no casualties at all
if (max < 0.05) return ['minor skirmishes', 'minor skirmishes'];
if (relative > 95) return ['attackers flawless victory', 'disorderly retreat of defenders'];
if (relative > 0.7) return ['attackers decisive victory', 'defenders disastrous defeat'];
if (relative > 0.6) return ['attackers victory', 'defenders defeat'];
if (relative > 0.4) return ['stalemate', 'stalemate'];
if (relative > 0.3) return ['attackers defeat', 'defenders victory'];
if (relative > 0.5) return ['attackers disastrous defeat', 'decisive victory of defenders'];
if (relative >= 0) return ['attackers disorderly retreat', 'flawless victory of defenders'];
return ['stalemate', 'stalemate']; // exception
}
this.attackers.regiments.forEach(r => applyResultForSide(r, "attackers"));
this.defenders.regiments.forEach(r => applyResultForSide(r, "defenders"));
this.attackers.regiments.forEach((r) => applyResultForSide(r, 'attackers'));
this.defenders.regiments.forEach((r) => applyResultForSide(r, 'defenders'));
function applyResultForSide(r, side) {
const id = "regiment" + r.state + "-" + r.i;
const id = 'regiment' + r.state + '-' + r.i;
// add result to regiment note
const note = notes.find(n => n.id === id);
const note = notes.find((n) => n.id === id);
if (note) {
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
const status = side === 'attackers' ? battleStatus[0] : battleStatus[1];
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
const regStatus = losses === 1 ? "is destroyed" : losses > 0.8 ? "is almost completely destroyed" : losses > 0.5 ? "suffered terrible losses" : losses > 0.3 ? "suffered severe losses" : losses > 0.2 ? "suffered heavy losses" : losses > 0.05 ? "suffered significant losses" : losses > 0 ? "suffered unsignificant losses" : "left the battle without loss";
const regStatus =
losses === 1
? 'is destroyed'
: losses > 0.8
? 'is almost completely destroyed'
: losses > 0.5
? 'suffered terrible losses'
: losses > 0.3
? 'suffered severe losses'
: losses > 0.2
? 'suffered heavy losses'
: losses > 0.05
? 'suffered significant losses'
: losses > 0
? 'suffered unsignificant losses'
: 'left the battle without loss';
const casualties = Object.keys(r.casualties)
.map(t => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
.filter(c => c);
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
.map((t) => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
.filter((c) => c);
const casualtiesText = casualties.length ? ' Casualties: ' + list(casualties) + '.' : '';
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
note.legend += legend;
}
@ -691,57 +706,47 @@ class Battle {
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box
}
// append battlefield marker
void (function addMarkerSymbol() {
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0).attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
})();
const i = last(pack.markers)?.i + 1 || 0;
{
// append battlefield marker
const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: '⚔️', type: 'battlefields', dy: 52};
pack.markers.push(marker);
const markerHTML = drawMarker(marker);
document.getElementById('markers').insertAdjacentHTML('beforeend', markerHTML);
}
const getSide = (regs, n) => (regs.length > 1 ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name);
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
const getSide = (regs, n) =>
regs.length > 1 ? `${n ? 'regiments' : 'forces'} of ${list([...new Set(regs.map((r) => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + ' ' + regs[0].name;
const getLosses = (casualties) => Math.min(rn(casualties * 100), 100);
const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(
this.defenders.regiments,
0
)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
const id = getNextId("markerElement");
notes.push({id, name: this.name, legend});
notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000);
tip(`${this.name} is over. ${result}`, true, 'success', 4000);
markers
.append("use")
.attr("id", id)
.attr("xlink:href", "#marker_battlefield")
.attr("data-id", "#marker_battlefield")
.attr("data-x", this.x)
.attr("data-y", this.y)
.attr("x", this.x - 15)
.attr("y", this.y - 30)
.attr("data-size", 1)
.attr("width", 30)
.attr("height", 30);
$("#battleScreen").dialog("destroy");
$('#battleScreen').dialog('destroy');
this.cleanData();
}
cancelResults() {
// move regiments back to initial positions
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
$("#battleScreen").dialog("close");
this.attackers.regiments.concat(this.defenders.regiments).forEach((r) => Military.moveRegiment(r, r.px, r.py));
$('#battleScreen').dialog('close');
this.cleanData();
}
cleanData() {
battleAttackers.innerHTML = battleDefenders.innerHTML = ""; // clean DOM
battleAttackers.innerHTML = battleDefenders.innerHTML = ''; // clean DOM
customization = 0; // exit edit mode
// clean temp data
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => {
this.attackers.regiments.concat(this.defenders.regiments).forEach((r) => {
delete r.px;
delete r.py;
delete r.casualties;

View file

@ -1,55 +1,55 @@
"use strict";
'use strict';
function editBiomes() {
if (customization) return;
closeDialogs("#biomesEditor, .stable");
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
closeDialogs('#biomesEditor, .stable');
if (!layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleStates')) toggleStates();
if (layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleReligions')) toggleReligions();
if (layerIsOn('toggleProvinces')) toggleProvinces();
const body = document.getElementById("biomesBody");
const body = document.getElementById('biomesBody');
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
refreshBiomesEditor();
if (modules.editBiomes) return;
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor",
$('#biomesEditor').dialog({
title: 'Biomes Editor',
resizable: false,
width: fitContent(),
close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}
});
// add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
document.getElementById("biomesEditStyle").addEventListener("click", () => editStyle("biomes"));
document.getElementById("biomesLegend").addEventListener("click", toggleLegend);
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
document.getElementById("biomesManuallyCancel").addEventListener("click", () => exitBiomesCustomizationMode());
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
document.getElementById("biomesAdd").addEventListener("click", addCustomBiome);
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
document.getElementById('biomesEditorRefresh').addEventListener('click', refreshBiomesEditor);
document.getElementById('biomesEditStyle').addEventListener('click', () => editStyle('biomes'));
document.getElementById('biomesLegend').addEventListener('click', toggleLegend);
document.getElementById('biomesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('biomesManually').addEventListener('click', enterBiomesCustomizationMode);
document.getElementById('biomesManuallyApply').addEventListener('click', applyBiomesChange);
document.getElementById('biomesManuallyCancel').addEventListener('click', () => exitBiomesCustomizationMode());
document.getElementById('biomesRestore').addEventListener('click', restoreInitialBiomes);
document.getElementById('biomesAdd').addEventListener('click', addCustomBiome);
document.getElementById('biomesRegenerateReliefIcons').addEventListener('click', regenerateIcons);
document.getElementById('biomesExport').addEventListener('click', downloadBiomesData);
body.addEventListener("click", function (ev) {
body.addEventListener('click', function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("fillRect")) biomeChangeColor(el);
else if (cl.contains("icon-info-circled")) openWiki(el);
else if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
if (cl.contains('fillRect')) biomeChangeColor(el);
else if (cl.contains('icon-info-circled')) openWiki(el);
else if (cl.contains('icon-trash-empty')) removeCustomBiome(el);
if (customization === 6) selectBiomeOnLineClick(el);
});
body.addEventListener("change", function (ev) {
body.addEventListener('change', function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("biomeName")) biomeChangeName(el);
else if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
if (cl.contains('biomeName')) biomeChangeName(el);
else if (cl.contains('biomeHabitability')) biomeChangeHabitability(el);
});
function refreshBiomesEditor() {
@ -76,14 +76,14 @@ function editBiomes() {
}
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const b = biomesData;
let lines = "",
let lines = '',
totalArea = 0,
totalPopulation = 0;
for (const i of b.i) {
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
if (!i || biomesData.name[i] === 'removed') continue; // ignore water and removed biomes
const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate;
const urban = b.urban[i] * populationRate * urbanization;
@ -94,7 +94,9 @@ function editBiomes() {
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
b.color[i]
}" class="fillRect pointer"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 class="biomeHabitability hide" value=${b.habitability[i]}>
@ -105,40 +107,40 @@ function editBiomes() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
</div>`;
}
body.innerHTML = lines;
// update footer
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterBiomes.innerHTML = body.querySelectorAll(':scope > div').length;
biomesFooterCells.innerHTML = pack.cells.h.filter((h) => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation);
biomesFooterArea.dataset.area = totalArea;
biomesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
body.querySelectorAll('div.biomes').forEach((el) => el.addEventListener('mouseenter', (ev) => biomeHighlightOn(ev)));
body.querySelectorAll('div.biomes').forEach((el) => el.addEventListener('mouseleave', (ev) => biomeHighlightOff(ev)));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(biomesHeader);
$("#biomesEditor").dialog({width: fitContent()});
$('#biomesEditor').dialog({width: fitContent()});
}
function biomeHighlightOn(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
biomes
.select("#biome" + biome)
.select('#biome' + biome)
.raise()
.transition(animate)
.attr("stroke-width", 2)
.attr("stroke", "#cd4c11");
.attr('stroke-width', 2)
.attr('stroke', '#cd4c11');
}
function biomeHighlightOff(event) {
@ -146,23 +148,23 @@ function editBiomes() {
const biome = +event.target.dataset.id;
const color = biomesData.color[biome];
biomes
.select("#biome" + biome)
.select('#biome' + biome)
.transition()
.attr("stroke-width", 0.7)
.attr("stroke", color);
.attr('stroke-width', 0.7)
.attr('stroke', color);
}
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const currentFill = el.getAttribute('fill');
const biome = +el.parentNode.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
el.setAttribute('fill', fill);
biomesData.color[biome] = fill;
biomes
.select("#biome" + biome)
.attr("fill", fill)
.attr("stroke", fill);
.select('#biome' + biome)
.attr('fill', fill)
.attr('stroke', fill);
};
openPicker(currentFill, callback);
@ -179,7 +181,7 @@ function editBiomes() {
const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
if (failed) {
el.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-9999", false, "error");
tip('Please provide a valid number in range 0-9999', false, 'error');
return;
}
biomesData.habitability[biome] = +el.value;
@ -190,69 +192,69 @@ function editBiomes() {
function openWiki(el) {
const name = el.parentNode.dataset.name;
if (name === "Custom" || !name) {
tip("Please provide a biome name", false, "error");
if (name === 'Custom' || !name) {
tip('Please provide a biome name', false, 'error');
return;
}
const wiki = "https://en.wikipedia.org/wiki/";
const wiki = 'https://en.wikipedia.org/wiki/';
switch (name) {
case "Hot desert":
openURL(wiki + "Desert_climate#Hot_desert_climates");
case "Cold desert":
openURL(wiki + "Desert_climate#Cold_desert_climates");
case "Savanna":
openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
case "Grassland":
openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
case "Tropical seasonal forest":
openURL(wiki + "Seasonal_tropical_forest");
case "Temperate deciduous forest":
openURL(wiki + "Temperate_deciduous_forest");
case "Tropical rainforest":
openURL(wiki + "Tropical_rainforest");
case "Temperate rainforest":
openURL(wiki + "Temperate_rainforest");
case "Taiga":
openURL(wiki + "Taiga");
case "Tundra":
openURL(wiki + "Tundra");
case "Glacier":
openURL(wiki + "Glacier");
case "Wetland":
openURL(wiki + "Wetland");
case 'Hot desert':
openURL(wiki + 'Desert_climate#Hot_desert_climates');
case 'Cold desert':
openURL(wiki + 'Desert_climate#Cold_desert_climates');
case 'Savanna':
openURL(wiki + 'Tropical_and_subtropical_grasslands,_savannas,_and_shrublands');
case 'Grassland':
openURL(wiki + 'Temperate_grasslands,_savannas,_and_shrublands');
case 'Tropical seasonal forest':
openURL(wiki + 'Seasonal_tropical_forest');
case 'Temperate deciduous forest':
openURL(wiki + 'Temperate_deciduous_forest');
case 'Tropical rainforest':
openURL(wiki + 'Tropical_rainforest');
case 'Temperate rainforest':
openURL(wiki + 'Temperate_rainforest');
case 'Taiga':
openURL(wiki + 'Taiga');
case 'Tundra':
openURL(wiki + 'Tundra');
case 'Glacier':
openURL(wiki + 'Glacier');
case 'Wetland':
openURL(wiki + 'Wetland');
default:
openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
}
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const d = biomesData;
const data = Array.from(d.i)
.filter(i => d.cells[i])
.filter((i) => d.cells[i])
.sort((a, b) => d.area[b] - d.area[a])
.map(i => [i, d.color[i], d.name[i]]);
drawLegend("Biomes", data);
.map((i) => [i, d.color[i], d.name[i]]);
drawLegend('Biomes', data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +biomesFooterCells.innerHTML;
const totalArea = +biomesFooterArea.dataset.area;
const totalPopulation = +biomesFooterPopulation.dataset.population;
body.querySelectorAll(":scope> div").forEach(function (el) {
el.querySelector(".biomeCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%";
body.querySelectorAll(':scope> div').forEach(function (el) {
el.querySelector('.biomeCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100) + '%';
el.querySelector('.biomePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + '%';
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
biomesEditorAddLines();
}
}
@ -261,14 +263,14 @@ function editBiomes() {
const b = biomesData,
i = biomesData.i.length;
if (i > 254) {
tip("Maximum number of biomes reached (255), data cleansing is required", false, "error");
tip('Maximum number of biomes reached (255), data cleansing is required', false, 'error');
return;
}
b.i.push(i);
b.color.push(getRandomColor());
b.habitability.push(50);
b.name.push("Custom");
b.name.push('Custom');
b.iconsDensity.push(0);
b.icons.push([]);
b.cost.push(50);
@ -278,7 +280,7 @@ function editBiomes() {
b.cells.push(0);
b.area.push(0);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
@ -293,84 +295,84 @@ function editBiomes() {
<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
$("#biomesEditor").dialog({width: fitContent()});
body.insertAdjacentHTML('beforeend', line);
biomesFooterBiomes.innerHTML = body.querySelectorAll(':scope > div').length;
$('#biomesEditor').dialog({width: fitContent()});
}
function removeCustomBiome(el) {
const biome = +el.parentNode.dataset.id;
el.parentNode.remove();
biomesData.name[biome] = "removed";
biomesData.name[biome] = 'removed';
biomesFooterBiomes.innerHTML = +biomesFooterBiomes.innerHTML - 1;
}
function regenerateIcons() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
if (!layerIsOn('toggleRelief')) toggleRelief();
}
function downloadBiomesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Biome,Color,Habitability,Cells,Area " + unit + ",Population\n"; // headers
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Biome,Color,Habitability,Cells,Area ' + unit + ',Population\n'; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.habitability + "%,";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += el.dataset.color + ',';
data += el.dataset.habitability + '%,';
data += el.dataset.cells + ',';
data += el.dataset.area + ',';
data += el.dataset.population + '\n';
});
const name = getFileName("Biomes") + ".csv";
const name = getFileName('Biomes') + '.csv';
downloadFile(data, name);
}
function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (!layerIsOn('toggleBiomes')) toggleBiomes();
customization = 6;
biomes.append("g").attr("id", "temp");
biomes.append('g').attr('id', 'temp');
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "none"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "block"));
body.querySelector("div.biomes").classList.add("selected");
document.querySelectorAll('#biomesBottom > button').forEach((el) => (el.style.display = 'none'));
document.querySelectorAll('#biomesBottom > div').forEach((el) => (el.style.display = 'block'));
body.querySelector('div.biomes').classList.add('selected');
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
biomesFooter.style.display = "none";
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
biomesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
biomesFooter.style.display = 'none';
$('#biomesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair").on("click", selectBiomeOnMapClick).call(d3.drag().on("start", dragBiomeBrush)).on("touchmove mousemove", moveBiomeBrush);
tip('Click on biome to select, drag the circle to change biome', true);
viewbox.style('cursor', 'crosshair').on('click', selectBiomeOnMapClick).call(d3.drag().on('start', dragBiomeBrush)).on('touchmove mousemove', moveBiomeBrush);
}
function selectBiomeOnLineClick(line) {
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
line.classList.add("selected");
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
line.classList.add('selected');
}
function selectBiomeOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) {
tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error");
tip('You cannot reassign water via biomes. Please edit the Heightmap to change water', false, 'error');
return;
}
const assigned = biomes.select("#temp").select("polygon[data-cell='" + i + "']");
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
const assigned = biomes.select('#temp').select("polygon[data-cell='" + i + "']");
const biome = assigned.size() ? +assigned.attr('data-biome') : pack.cells.biome[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='" + biome + "']").classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
body.querySelector("div[data-id='" + biome + "']").classList.add('selected');
}
function dragBiomeBrush() {
const r = +biomesManuallyBrush.value;
d3.event.on("drag", () => {
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -383,20 +385,20 @@ function editBiomes() {
// change region within selection
function changeBiomeForSelection(selection) {
const temp = biomes.select("#temp");
const selected = body.querySelector("div.selected");
const temp = biomes.select('#temp');
const selected = body.querySelector('div.selected');
const biomeNew = selected.dataset.id;
const color = biomesData.color[biomeNew];
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
const biomeOld = exists.size() ? +exists.attr('data-biome') : pack.cells.biome[i];
if (biomeNew === biomeOld) return;
// change of append new element
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
if (exists.size()) exists.attr('data-biome', biomeNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-biome', biomeNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
});
}
@ -408,7 +410,7 @@ function editBiomes() {
}
function applyBiomesChange() {
const changed = biomes.select("#temp").selectAll("polygon");
const changed = biomes.select('#temp').selectAll('polygon');
changed.each(function () {
const i = +this.dataset.cell;
const b = +this.dataset.biome;
@ -424,21 +426,21 @@ function editBiomes() {
function exitBiomesCustomizationMode(close) {
customization = 0;
biomes.select("#temp").remove();
biomes.select('#temp').remove();
removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "none"));
document.querySelectorAll('#biomesBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.querySelectorAll('#biomesBottom > div').forEach((el) => (el.style.display = 'none'));
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
biomesFooter.style.display = "block";
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
biomesEditor.querySelectorAll('.hide').forEach((el) => el.classList.remove('hidden'));
biomesFooter.style.display = 'block';
if (!close) $('#biomesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
restoreDefaultEvents();
clearMainTip();
const selected = document.querySelector("#biomesBody > div.selected");
if (selected) selected.classList.remove("selected");
const selected = document.querySelector('#biomesBody > div.selected');
if (selected) selected.classList.remove('selected');
}
function restoreInitialBiomes() {
@ -450,6 +452,6 @@ function editBiomes() {
}
function closeBiomesEditor() {
exitBiomesCustomizationMode("close");
exitBiomesCustomizationMode('close');
}
}

View file

@ -10,15 +10,14 @@ function editBurg(id) {
burgLabels.selectAll('text').call(d3.drag().on('start', dragBurgLabel)).classed('draggable', true);
updateBurgValues();
const my = id || d3.event.target.tagName === 'text' ? 'center bottom-40' : 'center top+40';
const at = id ? 'center' : d3.event.target.tagName === 'text' ? 'top' : 'bottom';
const of = id ? 'svg' : d3.event.target;
const my = id || d3.event.target.tagName === "text" ? "center bottom-20" : "center top+20";
const at = id ? "center" : d3.event.target.tagName === "text" ? "top" : "bottom";
const of = id ? "svg" : d3.event.target;
$('#burgEditor').dialog({
title: 'Edit Burg',
resizable: false,
close: closeBurgEditor,
position: {my, at, of, collision: 'fit'}
position: {my, at, of, collision: "fit"}
});
if (modules.editBurg) return;
@ -39,6 +38,8 @@ function editBurg(id) {
document.getElementById('burgNameReCulture').addEventListener('click', generateNameCulture);
document.getElementById('burgPopulation').addEventListener('change', changePopulation);
burgBody.querySelectorAll('.burgFeature').forEach((el) => el.addEventListener('click', toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById('burgStyleShow').addEventListener('click', showStyleSection);
document.getElementById('burgStyleHide').addEventListener('click', hideStyleSection);
@ -48,6 +49,8 @@ function editBurg(id) {
document.getElementById('burgSeeInMFCG').addEventListener('click', openInMFCG);
document.getElementById('burgEditEmblem').addEventListener('click', openEmblemEdit);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById('burgRelocate').addEventListener('click', toggleRelocateBurg);
document.getElementById('burglLegend').addEventListener('click', editBurgLegend);
document.getElementById('burgLock').addEventListener('click', toggleBurgLockButton);
@ -68,7 +71,14 @@ function editBurg(id) {
document.getElementById('burgState').innerHTML = stateName;
document.getElementById('burgProvince').innerHTML = provinceName;
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
document.getElementById("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
document.getElementById("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
// update list and select culture
const cultureSelect = document.getElementById('burgCulture');
@ -119,6 +129,14 @@ function editBurg(id) {
const coaID = 'burgCOA' + id;
COArenderer.trigger(coaID, b.coa);
document.getElementById('burgEmblem').setAttribute('href', '#' + coaID);
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
}
function getProduction(pool) {
@ -411,11 +429,7 @@ function editBurg(id) {
}
}
function showBurgELockTip() {
const id = +elSelected.attr('data-id');
showBurgLockTip(id);
}
function showStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgStyleSection').style.display = 'inline-block';
@ -443,57 +457,62 @@ function editBurg(id) {
function openInMFCG(event) {
const id = elSelected.attr('data-id');
const burg = pack.burgs[id];
const defSeed = +(seed + id.padStart(4, 0));
if (isCtrlClick(event)) {
prompt(
`Please provide a Medieval Fantasy City Generator seed.
Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
{default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
(v) => {
burg.MFCG = v;
openMFCG(v);
}
);
} else openMFCG();
}
function getBurgSeed(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {
openURL(burg.MFCGlink);
return;
}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
const population = rn(burg.population * populationRate * urbanization);
function getMFCGlink(burg) {
const {cells} = pack;
const {name, population, cell} = burg;
const burgSeed = getBurgSeed(burg);
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
const people = rn(population * populationRate * urbanization);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : '';
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return '&sea=' + norm;
}
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea=" + norm;
}
const site = 'http://fantasycities.watabou.ru/?random=0&continuous=0';
const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
}
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
return url;
}
function changeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
}
function randomizeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function openEmblemEdit() {
@ -502,6 +521,12 @@ function editBurg(id) {
editEmblem('burg', 'burgCOA' + id, burg);
}
function toggleMFCGMap() {
options.showMFCGMap = !options.showMFCGMap;
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
}
function toggleRelocateBurg() {
const toggler = document.getElementById('toggleCells');
document.getElementById('burgRelocate').classList.toggle('pressed');

View file

@ -0,0 +1,739 @@
'use strict';
function editBurg(id) {
if (customization) return;
closeDialogs('.stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = id || d3.event.target.dataset.id;
elSelected = burgLabels.select("[data-id='" + burg + "']");
burgLabels.selectAll('text').call(d3.drag().on('start', dragBurgLabel)).classed('draggable', true);
updateBurgValues();
<<<<<<< HEAD
const my = id || d3.event.target.tagName === 'text' ? 'center bottom-40' : 'center top+40';
const at = id ? 'center' : d3.event.target.tagName === 'text' ? 'top' : 'bottom';
const of = id ? 'svg' : d3.event.target;
$('#burgEditor').dialog({
title: 'Edit Burg',
resizable: false,
close: closeBurgEditor,
position: {my, at, of, collision: 'fit'}
=======
$("#burgEditor").dialog({
title: "Edit Burg",
resizable: false,
close: closeBurgEditor,
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
>>>>>>> master
});
if (modules.editBurg) return;
modules.editBurg = true;
// add listeners
<<<<<<< HEAD
document.getElementById('burgGroupShow').addEventListener('click', showGroupSection);
document.getElementById('burgGroupHide').addEventListener('click', hideGroupSection);
document.getElementById('burgSelectGroup').addEventListener('change', changeGroup);
document.getElementById('burgInputGroup').addEventListener('change', createNewGroup);
document.getElementById('burgAddGroup').addEventListener('click', toggleNewGroupInput);
document.getElementById('burgRemoveGroup').addEventListener('click', removeBurgsGroup);
document.getElementById('burgName').addEventListener('input', changeName);
document.getElementById('burgNameReRandom').addEventListener('click', generateNameRandom);
document.getElementById('burgType').addEventListener('input', changeType);
document.getElementById('burgCulture').addEventListener('input', changeCulture);
document.getElementById('burgNameReCulture').addEventListener('click', generateNameCulture);
document.getElementById('burgPopulation').addEventListener('change', changePopulation);
burgBody.querySelectorAll('.burgFeature').forEach((el) => el.addEventListener('click', toggleFeature));
document.getElementById('burgStyleShow').addEventListener('click', showStyleSection);
document.getElementById('burgStyleHide').addEventListener('click', hideStyleSection);
document.getElementById('burgEditLabelStyle').addEventListener('click', editGroupLabelStyle);
document.getElementById('burgEditIconStyle').addEventListener('click', editGroupIconStyle);
document.getElementById('burgEditAnchorStyle').addEventListener('click', editGroupAnchorStyle);
document.getElementById('burgSeeInMFCG').addEventListener('click', openInMFCG);
document.getElementById('burgEditEmblem').addEventListener('click', openEmblemEdit);
document.getElementById('burgRelocate').addEventListener('click', toggleRelocateBurg);
document.getElementById('burglLegend').addEventListener('click', editBurgLegend);
document.getElementById('burgLock').addEventListener('click', toggleBurgLockButton);
document.getElementById('burgLock').addEventListener('mouseover', showBurgELockTip);
document.getElementById('burgRemove').addEventListener('click', removeSelectedBurg);
=======
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
document.getElementById("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgType").addEventListener("input", changeType);
document.getElementById("burgCulture").addEventListener("input", changeCulture);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
>>>>>>> master
function updateBurgValues() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
document.getElementById('burgName').value = b.name;
document.getElementById('burgType').value = b.type || 'Generic';
document.getElementById('burgPopulation').value = rn(b.population * populationRate * urbanization);
const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
const province = pack.cells.province[b.cell];
const provinceName = province ? pack.provinces[province].fullName : '';
document.getElementById('burgState').innerHTML = stateName;
document.getElementById('burgProvince').innerHTML = provinceName;
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
// update list and select culture
const cultureSelect = document.getElementById('burgCulture');
cultureSelect.options.length = 0;
const cultures = pack.cultures.filter((c) => !c.removed);
cultures.forEach((c) => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
document.getElementById('burgTemperature').innerHTML = convertTemperature(temperature);
document.getElementById('burgTemperatureLike').innerHTML = getTemperatureLikeness(temperature);
document.getElementById('burgElevation').innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features
if (b.capital) document.getElementById('burgCapital').classList.remove('inactive');
else document.getElementById('burgCapital').classList.add('inactive');
if (b.port) document.getElementById('burgPort').classList.remove('inactive');
else document.getElementById('burgPort').classList.add('inactive');
if (b.citadel) document.getElementById('burgCitadel').classList.remove('inactive');
else document.getElementById('burgCitadel').classList.add('inactive');
if (b.walls) document.getElementById('burgWalls').classList.remove('inactive');
else document.getElementById('burgWalls').classList.add('inactive');
if (b.plaza) document.getElementById('burgPlaza').classList.remove('inactive');
else document.getElementById('burgPlaza').classList.add('inactive');
if (b.temple) document.getElementById('burgTemple').classList.remove('inactive');
else document.getElementById('burgTemple').classList.add('inactive');
if (b.shanty) document.getElementById('burgShanty').classList.remove('inactive');
else document.getElementById('burgShanty').classList.add('inactive');
// economics block
document.getElementById('burgProduction').innerHTML = getProduction(b.produced);
const deals = pack.trade.deals;
document.getElementById('burgExport').innerHTML = getExport(deals.filter((deal) => deal.exporter === b.i));
document.getElementById('burgImport').innerHTML = '';
//toggle lock
updateBurgLockIcon();
// select group
const group = elSelected.node().parentNode.id;
const select = document.getElementById('burgSelectGroup');
select.options.length = 0; // remove all options
burgLabels.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
// set emlem image
const coaID = 'burgCOA' + id;
COArenderer.trigger(coaID, b.coa);
<<<<<<< HEAD
document.getElementById('burgEmblem').setAttribute('href', '#' + coaID);
=======
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
>>>>>>> master
}
function getProduction(pool) {
let html = '';
for (const resourceId in pool) {
const {name, unit, icon} = Resources.get(+resourceId);
const production = pool[resourceId];
const unitName = production > 1 ? unit + 's' : unit;
html += `<span data-tip="${name}: ${production} ${unitName}">
<svg class="resIcon"><use href="#${icon}"></svg>
<span style="margin: 0 0.2em 0 -0.2em">${production}</span>
</span>`;
}
return html;
}
function getExport(dealsArray) {
if (!dealsArray.length) return 'no';
const totalIncome = rn(d3.sum(dealsArray.map((deal) => deal.burgIncome)));
const exported = dealsArray.map((deal) => {
const {resourceId, quantity, burgIncome} = deal;
const {name, unit, icon} = Resources.get(resourceId);
const unitName = quantity > 1 ? unit + 's' : unit;
return `<span data-tip="${name}: ${quantity} ${unitName}. Income: ${rn(burgIncome)}">
<svg class="resIcon"><use href="#${icon}"></svg>
<span style="margin: 0 0.2em 0 -0.2em">${quantity}</span>
</span>`;
});
return `${totalIncome}: ${exported.join('')}`;
}
// [-1; 31] °C, source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
function getTemperatureLikeness(temperature) {
if (temperature < -15) return 'nowhere in the real-world';
if (temperature < -5) return 'in Yakutsk';
if (temperature > 31) return 'nowhere in the real-world';
const cities = [
'Snag (Yukon)',
'Yellowknife (Canada)',
'Okhotsk (Russia)',
'Fairbanks (Alaska)',
'Nuuk (Greenland)',
'Murmansk', // -5 - 0
'Arkhangelsk',
'Anchorage',
'Tromsø',
'Reykjavik',
'Riga',
'Stockholm',
'Halifax',
'Prague',
'Copenhagen',
'London', // 1 - 10
'Antwerp',
'Paris',
'Milan',
'Batumi',
'Rome',
'Dubrovnik',
'Lisbon',
'Barcelona',
'Marrakesh',
'Alexandria', // 11 - 20
'Tegucigalpa',
'Guangzhou',
'Rio de Janeiro',
'Dakar',
'Miami',
'Jakarta',
'Mogadishu',
'Bangkok',
'Aden',
'Khartoum'
]; // 21 - 30
if (temperature > 30) return 'Mecca';
return cities[temperature + 5] || null;
}
function dragBurgLabel() {
const tr = parseTransform(this.getAttribute('transform'));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on('drag', function () {
const x = d3.event.x,
y = d3.event.y;
this.setAttribute('transform', `translate(${dx + x},${dy + y})`);
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, 'warning');
});
}
function showGroupSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgGroupSection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('burgGroupSection').style.display = 'none';
document.getElementById('burgInputGroup').style.display = 'none';
document.getElementById('burgInputGroup').value = '';
document.getElementById('burgSelectGroup').style.display = 'inline-block';
}
function changeGroup() {
const id = +elSelected.attr('data-id');
moveBurgToGroup(id, this.value);
}
function toggleNewGroupInput() {
if (burgInputGroup.style.display === 'none') {
burgInputGroup.style.display = 'inline-block';
burgInputGroup.focus();
burgSelectGroup.style.display = 'none';
} else {
burgInputGroup.style.display = 'none';
burgSelectGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {
tip('Please provide a valid group name', false, 'error');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip('Group name should start with a letter', false, 'error');
return;
}
const id = +elSelected.attr('data-id');
const oldGroup = elSelected.node().parentNode.id;
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {
ERROR && console.error('Cannot find label or icon elements');
return;
}
const labelG = document.querySelector('#burgLabels > #' + oldGroup);
const iconG = document.querySelector('#burgIcons > #' + oldGroup);
const anchorG = document.querySelector('#anchors > #' + oldGroup);
// just rename if only 1 element left
const count = elSelected.node().parentNode.childElementCount;
if (oldGroup !== 'cities' && oldGroup !== 'towns' && count === 1) {
document.getElementById('burgSelectGroup').selectedOptions[0].remove();
document.getElementById('burgSelectGroup').options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById('burgInputGroup').value = '';
labelG.id = group;
iconG.id = group;
if (anchor) anchorG.id = group;
return;
}
// create new groups
document.getElementById('burgSelectGroup').options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById('burgInputGroup').value = '';
const newLabelG = document.querySelector('#burgLabels').appendChild(labelG.cloneNode(false));
newLabelG.id = group;
const newIconG = document.querySelector('#burgIcons').appendChild(iconG.cloneNode(false));
newIconG.id = group;
if (anchor) {
const newAnchorG = document.querySelector('#anchors').appendChild(anchorG.cloneNode(false));
newAnchorG.id = group;
}
moveBurgToGroup(id, group);
}
function removeBurgsGroup() {
const group = elSelected.node().parentNode;
const basic = group.id === 'cities' || group.id === 'towns';
const burgsInGroup = [];
for (let i = 0; i < group.children.length; i++) {
burgsInGroup.push(+group.children[i].dataset.id);
}
const burgsToRemove = burgsInGroup.filter((b) => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length;
const message = `Are you sure you want to remove
${basic || capital ? 'all unlocked elements in the 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}`;
confirmationDialog({title: 'Remove burg group', message, confirm: 'Remove', onConfirm: removeGroup});
function removeGroup() {
$(this).dialog('close');
$('#burgEditor').dialog('close');
hideGroupSection();
burgsToRemove.forEach((b) => removeBurg(b));
if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector('#burgLabels > #' + group.id);
const iconG = document.querySelector('#burgIcons > #' + group.id);
const anchorG = document.querySelector('#anchors > #' + group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
}
}
function changeName() {
const id = +elSelected.attr('data-id');
pack.burgs[id].name = burgName.value;
elSelected.text(burgName.value);
}
function generateNameRandom() {
const base = rand(nameBases.length - 1);
burgName.value = Names.getBase(base);
changeName();
}
function changeType() {
const id = +elSelected.attr('data-id');
pack.burgs[id].type = this.value;
}
function changeCulture() {
const id = +elSelected.attr('data-id');
pack.burgs[id].culture = +this.value;
}
function generateNameCulture() {
const id = +elSelected.attr('data-id');
const culture = pack.burgs[id].culture;
burgName.value = Names.getCulture(culture);
changeName();
}
function changePopulation() {
const id = +elSelected.attr('data-id');
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
}
function toggleFeature() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains('inactive');
if (feature === 'port') togglePort(id);
else if (feature === 'capital') toggleCapital(id);
else b[feature] = +turnOn;
if (b[feature]) this.classList.remove('inactive');
else if (!b[feature]) this.classList.add('inactive');
if (b.port) document.getElementById('burgEditAnchorStyle').style.display = 'inline-block';
else document.getElementById('burgEditAnchorStyle').style.display = 'none';
}
function toggleBurgLockButton() {
const id = +elSelected.attr('data-id');
toggleBurgLock(id);
updateBurgLockIcon();
}
function updateBurgLockIcon() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
if (b.lock) {
document.getElementById('burgLock').classList.remove('icon-lock-open');
document.getElementById('burgLock').classList.add('icon-lock');
} else {
document.getElementById('burgLock').classList.remove('icon-lock');
document.getElementById('burgLock').classList.add('icon-lock-open');
}
}
<<<<<<< HEAD
function showBurgELockTip() {
const id = +elSelected.attr('data-id');
showBurgLockTip(id);
}
=======
>>>>>>> master
function showStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgStyleSection').style.display = 'inline-block';
}
function hideStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('burgStyleSection').style.display = 'none';
}
function editGroupLabelStyle() {
const g = elSelected.node().parentNode.id;
editStyle('labels', g);
}
function editGroupIconStyle() {
const g = elSelected.node().parentNode.id;
editStyle('burgIcons', g);
}
function editGroupAnchorStyle() {
const g = elSelected.node().parentNode.id;
editStyle('anchors', g);
}
<<<<<<< HEAD
function openInMFCG(event) {
const id = elSelected.attr('data-id');
const burg = pack.burgs[id];
const defSeed = +(seed + id.padStart(4, 0));
if (isCtrlClick(event)) {
prompt(
`Please provide a Medieval Fantasy City Generator seed.
Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
{default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
(v) => {
burg.MFCG = v;
openMFCG(v);
}
);
} else openMFCG();
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {
openURL(burg.MFCGlink);
return;
}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
const population = rn(burg.population * populationRate * urbanization);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : '';
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return '&sea=' + norm;
}
const site = 'http://fantasycities.watabou.ru/?random=0&continuous=0';
const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
=======
function updateMFCGFrame(burg) {
const mfcgURL = getMFCGlink(burg);
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
}
function getBurgSeed(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function getMFCGlink(burg) {
const {cells} = pack;
const {name, population, cell} = burg;
const burgSeed = getBurgSeed(burg);
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
const people = rn(population * populationRate * urbanization);
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea=" + norm;
>>>>>>> master
}
const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0";
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
return url;
}
function changeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
}
function randomizeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function openEmblemEdit() {
const id = +elSelected.attr('data-id'),
burg = pack.burgs[id];
editEmblem('burg', 'burgCOA' + id, burg);
}
function toggleMFCGMap() {
options.showMFCGMap = !options.showMFCGMap;
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
}
function toggleRelocateBurg() {
const toggler = document.getElementById('toggleCells');
document.getElementById('burgRelocate').classList.toggle('pressed');
if (document.getElementById('burgRelocate').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', relocateBurgOnClick);
tip('Click on map to relocate burg. Hold Shift for continuous move', true);
if (!layerIsOn('toggleCells')) {
toggleCells();
toggler.dataset.forced = true;
}
} else {
clearMainTip();
viewbox.on('click', clicked).style('cursor', 'default');
if (layerIsOn('toggleCells') && toggler.dataset.forced) {
toggleCells();
toggler.dataset.forced = false;
}
}
}
function relocateBurgOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const id = +elSelected.attr('data-id');
const burg = pack.burgs[id];
if (cells.h[cell] < 20) {
tip('Cannot place burg into the water! Select a land cell', false, 'error');
return;
}
if (cells.burg[cell] && cells.burg[cell] !== id) {
tip('There is already a burg in this cell. Please select a free cell', false, 'error');
return;
}
const newState = cells.state[cell];
const oldState = burg.state;
if (newState !== oldState && burg.capital) {
tip('Capital cannot be relocated into another state!', false, 'error');
return;
}
// change UI
const x = rn(point[0], 2),
y = rn(point[1], 2);
burgIcons
.select("[data-id='" + id + "']")
.attr('transform', null)
.attr('cx', x)
.attr('cy', y);
burgLabels
.select("text[data-id='" + id + "']")
.attr('transform', null)
.attr('x', x)
.attr('y', y);
const anchor = anchors.select("use[data-id='" + id + "']");
if (anchor.size()) {
const size = anchor.attr('width');
const xa = rn(x - size * 0.47, 2);
const ya = rn(y - size * 0.47, 2);
anchor.attr('transform', null).attr('x', xa).attr('y', ya);
}
// change data
cells.burg[burg.cell] = 0;
cells.burg[cell] = id;
burg.cell = cell;
burg.state = newState;
burg.x = x;
burg.y = y;
if (burg.capital) pack.states[newState].center = burg.cell;
if (d3.event.shiftKey === false) toggleRelocateBurg();
}
function editBurgLegend() {
const id = elSelected.attr('data-id');
const name = elSelected.text();
editNotes('burg' + id, name);
}
function removeSelectedBurg() {
const id = +elSelected.attr('data-id');
if (pack.burgs[id].capital) {
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
You can change the capital using Burgs Editor (shift + T)`;
$('#alert').dialog({
resizable: false,
title: 'Remove burg',
buttons: {
Ok: function () {
$(this).dialog('close');
}
}
});
} else {
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(id);
$('#burgEditor').dialog('close');
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
}
function closeBurgEditor() {
document.getElementById('burgRelocate').classList.remove('pressed');
burgLabels.selectAll('text').call(d3.drag().on('drag', null)).classed('draggable', false);
unselect();
}
}

View file

@ -79,17 +79,20 @@ function overviewBurgs() {
const province = prov ? pack.provinces[prov].name : '';
const culture = pack.cultures[b.culture].name;
lines += `<div class="states" data-id=${b.i} data-name="${b.name}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Edit burg" class="icon-pencil"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
lines += `<div class="states" data-id=${b.i} data-name="${
b.name
}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<input data-tip="Burg province" class="burgState" value="${province}" disabled>
<input data-tip="Burg state" class="burgState" value="${state}" disabled>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(b.culture)}</select>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(
b.culture
)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
}"></span>
</div>
<span data-tip="Zoom to burg" class="icon-dot-circled pointer"></span>
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
@ -202,11 +205,6 @@ function overviewBurgs() {
}
}
function showBurgOLockTip() {
const burg = +this.parentNode.dataset.id;
showBurgLockTip(burg);
}
function openBurgEditor() {
const burg = +this.parentNode.dataset.id;
editBurg(burg);
@ -281,6 +279,7 @@ function overviewBurgs() {
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const burgs = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => {
@ -292,6 +291,7 @@ function overviewBurgs() {
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error");
const root = d3
.stratify()
@ -401,6 +401,12 @@ function overviewBurgs() {
const base = this.value === 'states' ? getStatesData() : this.value === 'cultures' ? getCulturesData() : this.value === 'parent' ? getParentData() : getProvincesData();
burgs.forEach((b) => (b.id = b.i + base.length - 1));
? getStatesData()
: this.value === "cultures"
? getCulturesData()
: this.value === "parent"
? getParentData()
: getProvincesData();
const data = base.concat(burgs);
@ -435,6 +441,8 @@ function overviewBurgs() {
function downloadBurgsData() {
let data = 'Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (' + heightUnit.value + '),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n'; // headers
const valid = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
heightUnit.value +
"),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
valid.forEach((b) => {
data += b.i + ',';

View file

@ -0,0 +1,603 @@
'use strict';
function overviewBurgs() {
if (customization) return;
closeDialogs('#burgsOverview, .stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const body = document.getElementById('burgsBody');
updateFilter();
burgsOverviewAddLines();
$('#burgsOverview').dialog();
if (modules.overviewBurgs) return;
modules.overviewBurgs = true;
$('#burgsOverview').dialog({
title: 'Burgs Overview',
resizable: false,
width: fitContent(),
close: exitAddBurgMode,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('burgsOverviewRefresh').addEventListener('click', refreshBurgsEditor);
document.getElementById('burgsChart').addEventListener('click', showBurgsChart);
document.getElementById('burgsFilterState').addEventListener('change', burgsOverviewAddLines);
document.getElementById('burgsFilterCulture').addEventListener('change', burgsOverviewAddLines);
document.getElementById('regenerateBurgNames').addEventListener('click', regenerateNames);
document.getElementById('addNewBurg').addEventListener('click', enterAddBurgMode);
document.getElementById('burgsExport').addEventListener('click', downloadBurgsData);
document.getElementById('burgNamesImport').addEventListener('click', renameBurgsInBulk);
document.getElementById('burgsListToLoad').addEventListener('change', function () {
uploadFile(this, importBurgNames);
});
document.getElementById('burgsRemoveAll').addEventListener('click', triggerAllBurgsRemove);
function refreshBurgsEditor() {
updateFilter();
burgsOverviewAddLines();
}
function updateFilter() {
const stateFilter = document.getElementById('burgsFilterState');
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
stateFilter.options.add(new Option(pack.states[0].name, 0, false, !selectedState));
const statesSorted = pack.states.filter((s) => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach((s) => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const cultureFilter = document.getElementById('burgsFilterCulture');
const selectedCulture = cultureFilter.value || -1;
cultureFilter.options.length = 0; // remove all options
cultureFilter.options.add(new Option(`all`, -1, false, selectedCulture == -1));
cultureFilter.options.add(new Option(pack.cultures[0].name, 0, false, !selectedCulture));
const culturesSorted = pack.cultures.filter((c) => c.i && !c.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
culturesSorted.forEach((c) => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
}
// add line for each burg
function burgsOverviewAddLines() {
const selectedState = +document.getElementById('burgsFilterState').value;
const selectedCulture = +document.getElementById('burgsFilterCulture').value;
let filtered = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter((b) => b.state === selectedState); // filtered by state
if (selectedCulture != -1) filtered = filtered.filter((b) => b.culture === selectedCulture); // filtered by culture
body.innerHTML = '';
let lines = '',
totalPopulation = 0;
for (const b of filtered) {
const population = b.population * populationRate * urbanization;
totalPopulation += population;
const type = b.capital && b.port ? 'a-capital-port' : b.capital ? 'c-capital' : b.port ? 'p-port' : 'z-burg';
const state = pack.states[b.state].name;
const prov = pack.cells.province[b.cell];
const province = prov ? pack.provinces[prov].name : '';
const culture = pack.cultures[b.culture].name;
<<<<<<< HEAD
lines += `<div class="states" data-id=${b.i} data-name="${b.name}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Edit burg" class="icon-pencil"></span>
=======
lines += `<div class="states" data-id=${b.i} data-name="${
b.name
}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
>>>>>>> master
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
<input data-tip="Burg province" class="burgState" value="${province}" disabled>
<input data-tip="Burg state" class="burgState" value="${state}" disabled>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(
b.culture
)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<<<<<<< HEAD
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
</div>
<span data-tip="Zoom to burg" class="icon-dot-circled pointer"></span>
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
=======
<span data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}" class="icon-star-empty${
b.capital ? "" : " inactive pointer"
}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? "" : " inactive"}" style="font-size:.9em"></span>
</div>
<span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${b.lock ? "icon-lock" : "icon-lock-open inactive"}" onmouseover="showElementLockTip(event)"></span>
>>>>>>> master
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners
<<<<<<< HEAD
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => burgHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => burgHighlightOff(ev)));
body.querySelectorAll('div > input.burgName').forEach((el) => el.addEventListener('input', changeBurgName));
body.querySelectorAll('div > span.icon-dot-circled').forEach((el) => el.addEventListener('click', zoomIntoBurg));
body.querySelectorAll('div > select.stateCulture').forEach((el) => el.addEventListener('change', changeBurgCulture));
body.querySelectorAll('div > input.burgPopulation').forEach((el) => el.addEventListener('change', changeBurgPopulation));
body.querySelectorAll('div > span.icon-star-empty').forEach((el) => el.addEventListener('click', toggleCapitalStatus));
body.querySelectorAll('div > span.icon-anchor').forEach((el) => el.addEventListener('click', togglePortStatus));
body.querySelectorAll('div > span.locks').forEach((el) => el.addEventListener('click', toggleBurgLockStatus));
body.querySelectorAll('div > span.locks').forEach((el) => el.addEventListener('mouseover', showBurgOLockTip));
body.querySelectorAll('div > span.icon-pencil').forEach((el) => el.addEventListener('click', openBurgEditor));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', triggerBurgRemove));
=======
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev)));
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
>>>>>>> master
applySorting(burgsHeader);
}
function getCultureOptions(culture) {
let options = '';
pack.cultures.filter((c) => !c.removed).forEach((c) => (options += `<option ${c.i === culture ? 'selected' : ''} value="${c.i}">${c.name}</option>`));
return options;
}
function burgHighlightOn(event) {
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed('drag', true);
}
function burgHighlightOff() {
burgLabels.selectAll('text.drag').classed('drag', false);
}
function changeBurgName() {
if (this.value == '') tip('Please provide a name', false, 'error');
const burg = +this.parentNode.dataset.id;
pack.burgs[burg].name = this.value;
this.parentNode.dataset.name = this.value;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
if (label) label.innerHTML = this.value;
}
function zoomIntoBurg() {
const burg = +this.parentNode.dataset.id;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
const x = +label.getAttribute('x'),
y = +label.getAttribute('y');
zoomTo(x, y, 8, 2000);
}
function changeBurgCulture() {
const burg = +this.parentNode.dataset.id;
const v = +this.value;
pack.burgs[burg].culture = v;
this.parentNode.dataset.culture = pack.cultures[v].name;
}
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == '' || isNaN(+this.value)) {
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
this.value = si(pack.burgs[burg].population * populationRate * urbanization);
return;
}
pack.burgs[burg].population = this.value / populationRate / urbanization;
this.parentNode.dataset.population = this.value;
this.value = si(this.value);
const population = [];
body.querySelectorAll(':scope > div').forEach((el) => population.push(+getInteger(el.dataset.population)));
burgsFooterPopulation.innerHTML = si(d3.mean(population));
}
function toggleCapitalStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
toggleCapital(burg);
burgsOverviewAddLines();
}
function togglePortStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
togglePort(burg);
if (this.classList.contains('inactive')) this.classList.remove('inactive');
else this.classList.add('inactive');
}
function toggleBurgLockStatus() {
const burg = +this.parentNode.dataset.id;
toggleBurgLock(burg);
if (this.classList.contains('icon-lock')) {
this.classList.remove('icon-lock');
this.classList.add('icon-lock-open');
this.classList.add('inactive');
} else {
this.classList.remove('icon-lock-open');
this.classList.add('icon-lock');
this.classList.remove('inactive');
}
}
function openBurgEditor() {
const burg = +this.parentNode.dataset.id;
editBurg(burg);
}
function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id;
if (pack.burgs[burg].capital) {
tip('You cannot remove the capital. Please change the capital first', false, 'error');
return;
}
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(burg);
burgsOverviewAddLines();
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
function regenerateNames() {
body.querySelectorAll(':scope > div').forEach(function (el) {
const burg = +el.dataset.id;
//if (pack.burgs[burg].lock) return;
const culture = pack.burgs[burg].culture;
const name = Names.getCulture(culture);
if (!pack.burgs[burg].lock) {
el.querySelector('.burgName').value = name;
pack.burgs[burg].name = el.dataset.name = name;
burgLabels.select("[data-id='" + burg + "']").text(name);
}
});
}
function enterAddBurgMode() {
if (this.classList.contains('pressed')) {
exitAddBurgMode();
return;
}
customization = 3;
this.classList.add('pressed');
tip('Click on the map to create a new burg. Hold Shift to add multiple', true, 'warn');
viewbox.style('cursor', 'crosshair').on('click', addBurgOnClick);
}
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
if (pack.cells.h[cell] < 20) return tip('You cannot place state into the water. Please click on a land cell', false, 'error');
if (pack.cells.burg[cell]) return tip('There is already a burg in this cell. Please select a free cell', false, 'error');
addBurg(point); // add new burg
if (d3.event.shiftKey === false) {
exitAddBurgMode();
burgsOverviewAddLines();
}
}
function exitAddBurgMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
if (addBurgTool.classList.contains('pressed')) addBurgTool.classList.remove('pressed');
if (addNewBurg.classList.contains('pressed')) addNewBurg.classList.remove('pressed');
}
function showBurgsChart() {
// build hierarchy tree
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const burgs = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => {
const id = b.i + states.length - 1;
const population = b.population;
const capital = b.capital;
const province = pack.cells.province[b.cell];
const parent = province ? province + states.length - 1 : b.state;
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error");
const root = d3
.stratify()
.parentId((d) => d.state)(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value,
height = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: -10, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
const treeLayout = d3.pack().size([w, h]).padding(3);
// prepare svg
alertMessage.innerHTML = `<select id="burgsTreeType" style="display:block; margin-left:13px; font-size:11px">
<option value="states" selected>Group by state</option>
<option value="cultures">Group by culture</option>
<option value="parent">Group by province and state</option>
<option value="provinces">Group by province</option></select>`;
alertMessage.innerHTML += `<div id='burgsInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3
.select('#alertMessage')
.insert('svg', '#burgsInfo')
.attr('id', 'burgsTree')
.attr('width', width)
.attr('height', height - 10)
.attr('stroke-width', 2);
const graph = svg.append('g').attr('transform', `translate(-50, -10)`);
document.getElementById('burgsTreeType').addEventListener('change', updateChart);
treeLayout(root);
const node = graph
.selectAll('circle')
.data(root.leaves())
.join('circle')
.attr('data-id', (d) => d.data.i)
.attr('r', (d) => d.r)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.on('mouseenter', (d) => showInfo(event, d))
.on('mouseleave', (d) => hideInfo(event, d))
.on('click', (d) => zoomTo(d.data.x, d.data.y, 8, 2000));
function showInfo(ev, d) {
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
const name = d.data.name;
const parent = d.parent.data.name;
const population = si(d.value * populationRate * urbanization);
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
burgHighlightOn(ev);
tip('Click to zoom into view');
}
function hideInfo(ev) {
burgHighlightOff(ev);
if (!document.getElementById('burgsInfo')) return;
burgsInfo.innerHTML = '&#8205;';
d3.select(ev.target).transition().attr('stroke', null);
tip('');
}
function updateChart() {
const getStatesData = () =>
pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const getCulturesData = () =>
pack.cultures.map((c) => {
const color = c.color ? c.color : '#ccc';
return {id: c.i, culture: c.i ? 0 : null, color, name: c.name};
});
const getParentData = () => {
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, parent: s.i ? 0 : null, color, name};
});
const provinces = pack.provinces
.filter((p) => p.i && !p.removed)
.map((p) => {
return {id: p.i + states.length - 1, parent: p.state, color: p.color, name: p.fullName};
});
return states.concat(provinces);
};
const getProvincesData = () =>
pack.provinces.map((p) => {
const color = p.color ? p.color : '#ccc';
const name = p.fullName ? p.fullName : p.name;
return {id: p.i ? p.i : 0, province: p.i ? 0 : null, color, name};
});
const value = (d) => {
if (this.value === 'states') return d.state;
if (this.value === 'cultures') return d.culture;
if (this.value === 'parent') return d.parent;
if (this.value === 'provinces') return d.province;
};
<<<<<<< HEAD
const base = this.value === 'states' ? getStatesData() : this.value === 'cultures' ? getCulturesData() : this.value === 'parent' ? getParentData() : getProvincesData();
burgs.forEach((b) => (b.id = b.i + base.length - 1));
=======
const base =
this.value === "states"
? getStatesData()
: this.value === "cultures"
? getCulturesData()
: this.value === "parent"
? getParentData()
: getProvincesData();
burgs.forEach(b => (b.id = b.i + base.length - 1));
>>>>>>> master
const data = base.concat(burgs);
const root = d3
.stratify()
.parentId((d) => value(d))(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
node
.data(treeLayout(root).leaves())
.transition()
.duration(2000)
.attr('data-id', (d) => d.data.i)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.r);
}
$('#alert').dialog({
title: 'Burgs bubble chart',
width: fitContent(),
position: {my: 'left bottom', at: 'left+10 bottom-10', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
}
function downloadBurgsData() {
<<<<<<< HEAD
let data = 'Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (' + heightUnit.value + '),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n'; // headers
const valid = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
=======
let data =
"Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Longitude,Latitude,Elevation (" +
heightUnit.value +
"),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
>>>>>>> master
valid.forEach((b) => {
data += b.i + ',';
data += b.name + ',';
const province = pack.cells.province[b.cell];
<<<<<<< HEAD
data += province ? pack.provinces[province].fullName + ',' : ',';
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
data += pack.cultures[b.culture].name + ',';
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
data += rn(b.population * populationRate * urbanization) + ',';
=======
data += province ? pack.provinces[province].name + "," : ",";
data += province ? pack.provinces[province].fullName + "," : ",";
data += pack.states[b.state].name + ",";
data += pack.states[b.state].fullName + ",";
data += pack.cultures[b.culture].name + ",";
data += pack.religions[pack.cells.religion[b.cell]].name + ",";
data += rn(b.population * populationRate * urbanization) + ",";
>>>>>>> master
// add geography data
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
data += mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT + ','; // this is inverted in QGIS otherwise
data += parseInt(getHeight(pack.cells.h[b.cell])) + ',';
// add status data
data += b.capital ? 'capital,' : ',';
data += b.port ? 'port,' : ',';
data += b.citadel ? 'citadel,' : ',';
data += b.walls ? 'walls,' : ',';
data += b.plaza ? 'plaza,' : ',';
data += b.temple ? 'temple,' : ',';
data += b.shanty ? 'shanty town\n' : '\n';
});
const name = getFileName('Burgs') + '.csv';
downloadFile(data, name);
}
function renameBurgsInBulk() {
const message = `Download burgs list as a text file, make changes and re-upload the file.
If you do not want to change the name, just leave it as is`;
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Download: function () {
const data = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => b.name)
.join('\r\n');
const name = getFileName('Burg names') + '.txt';
downloadFile(data, name);
},
Upload: () => burgsListToLoad.click(),
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function importBurgNames(dataLoaded) {
if (!dataLoaded) return tip('Cannot load the file, please check the format', false, 'error');
const data = dataLoaded.split('\r\n');
if (!data.length) return tip('Cannot parse the list, please check the file format', false, 'error');
let change = [],
message = `Burgs will be renamed as below. Please confirm`;
message += `<table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
for (let i = 0; i < data.length && i <= burgs.length; i++) {
const v = data[i];
if (!v || !burgs[i] || v == burgs[i].name) continue;
change.push({id: burgs[i].i, name: v});
message += `<tr><td style="width:20%">${burgs[i].i}</td><td style="width:40%">${burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table>`;
if (!change.length) message = 'No changes found in the file. Please change some names to get a result';
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Cancel: function () {
$(this).dialog('close');
},
Confirm: function () {
for (let i = 0; i < change.length; i++) {
const id = change[i].id;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
$(this).dialog('close');
burgsOverviewAddLines();
}
}
});
}
function triggerAllBurgsRemove() {
const message = 'Are you sure you want to remove all unlocked burgs except for capitals?<br><i>To remove a capital you have to remove the state first</i>';
confirmationDialog({title: 'Remove all burgs', message, confirm: 'Remove', onConfirm: removeAllBurgs});
}
function removeAllBurgs() {
pack.burgs.filter((b) => b.i && !(b.capital || b.lock)).forEach((b) => removeBurg(b.i));
burgsOverviewAddLines();
}
}

View file

@ -63,8 +63,8 @@ function editCultures() {
function culturesEditorAddLines() {
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '',
totalArea = 0,
totalPopulation = 0;
let totalArea = 0;
let totalPopulation = 0;
const emblemShapeGroup = document.getElementById('emblemShape').selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === 'Diversiform';
@ -84,7 +84,8 @@ function editCultures() {
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<input data-tip="Neutral culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span class="icon-cw placeholder"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
@ -96,17 +97,22 @@ function editCultures() {
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
</div>`;
continue;
}
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
c.color
}" class="fillRect pointer"></svg>
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
</svg>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
@ -120,7 +126,11 @@ function editCultures() {
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
@ -141,6 +151,7 @@ function editCultures() {
body.querySelectorAll('rect.fillRect').forEach((el) => el.addEventListener('click', cultureChangeColor));
body.querySelectorAll('div > input.cultureName').forEach((el) => el.addEventListener('input', cultureChangeName));
body.querySelectorAll('div > input.statePower').forEach((el) => el.addEventListener('input', cultureChangeExpansionism));
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll('div > select.cultureType').forEach((el) => el.addEventListener('change', cultureChangeType));
body.querySelectorAll('div > select.cultureBase').forEach((el) => el.addEventListener('change', cultureChangeBase));
body.querySelectorAll('div > select.cultureShape').forEach((el) => el.addEventListener('change', cultureChangeShape));
@ -262,6 +273,13 @@ function editCultures() {
);
}
function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture);
this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name;
}
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
@ -534,6 +552,9 @@ function editCultures() {
const graph = svg.append('g').attr('transform', `translate(10, -45)`);
const links = graph.append('g').attr('fill', 'none').attr('stroke', '#aaaaaa');
const nodes = graph.append('g');
.attr("width", width)
.attr("height", height)
.style("text-anchor", "middle");
renderTree();
function renderTree() {
@ -683,6 +704,10 @@ function editCultures() {
tip('Click on culture to select, drag the circle to change culture', true);
viewbox.style('cursor', 'crosshair').on('click', selectCultureOnMapClick).call(d3.drag().on('start', dragCultureBrush)).on('touchmove mousemove', moveCultureBrush);
.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
body.querySelector('div').classList.add('selected');
}
@ -733,7 +758,14 @@ function editCultures() {
// change of append new element
if (exists.size()) exists.attr('data-culture', cultureNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-culture', cultureNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
else
temp
.append("polygon")
.attr("data-cell", i)
.attr("data-culture", cultureNew)
.attr("points", getPackPolygon(i))
.attr("fill", color)
.attr("stroke", color);
});
}

View file

@ -0,0 +1,965 @@
'use strict';
function editCultures() {
if (customization) return;
closeDialogs('#culturesEditor, .stable');
if (!layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleStates')) toggleStates();
if (layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleReligions')) toggleReligions();
if (layerIsOn('toggleProvinces')) toggleProvinces();
const body = document.getElementById('culturesBody');
drawCultureCenters();
refreshCulturesEditor();
if (modules.editCultures) return;
modules.editCultures = true;
$('#culturesEditor').dialog({
title: 'Cultures Editor',
resizable: false,
width: fitContent(),
close: closeCulturesEditor,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}
});
body.focus();
// add listeners
document.getElementById('culturesEditorRefresh').addEventListener('click', refreshCulturesEditor);
document.getElementById('culturesEditStyle').addEventListener('click', () => editStyle('cults'));
document.getElementById('culturesLegend').addEventListener('click', toggleLegend);
document.getElementById('culturesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('culturesHeirarchy').addEventListener('click', showHierarchy);
document.getElementById('culturesRecalculate').addEventListener('click', () => recalculateCultures(true));
document.getElementById('culturesManually').addEventListener('click', enterCultureManualAssignent);
document.getElementById('culturesManuallyApply').addEventListener('click', applyCultureManualAssignent);
document.getElementById('culturesManuallyCancel').addEventListener('click', () => exitCulturesManualAssignment());
document.getElementById('culturesEditNamesBase').addEventListener('click', editNamesbase);
document.getElementById('culturesAdd').addEventListener('click', enterAddCulturesMode);
document.getElementById('culturesExport').addEventListener('click', downloadCulturesData);
function refreshCulturesEditor() {
culturesCollectStatistics();
culturesEditorAddLines();
drawCultureCenters();
}
function culturesCollectStatistics() {
const cells = pack.cells,
cultures = pack.cultures;
cultures.forEach((c) => (c.cells = c.area = c.rural = c.urban = 0));
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const c = cells.culture[i];
cultures[c].cells += 1;
cultures[c].area += cells.area[i];
cultures[c].rural += cells.pop[i];
if (cells.burg[i]) cultures[c].urban += pack.burgs[cells.burg[i]].population;
}
}
// add line for each culture
function culturesEditorAddLines() {
<<<<<<< HEAD
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '',
totalArea = 0,
totalPopulation = 0;
=======
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
let totalArea = 0;
let totalPopulation = 0;
>>>>>>> master
const emblemShapeGroup = document.getElementById('emblemShape').selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === 'Diversiform';
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * distanceScaleInput.value ** 2;
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`;
totalArea += area;
totalPopulation += population;
if (!c.i) {
// Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Neutral culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span class="icon-cw placeholder"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
<input class="statePower placeholder hide" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
<<<<<<< HEAD
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
=======
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
>>>>>>> master
</div>`;
continue;
}
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<<<<<<< HEAD
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
c.color
}" class="fillRect pointer"></svg>
=======
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
</svg>
>>>>>>> master
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
<input data-tip="Culture expansionism. Defines competitive size. Click to change, then click Recalculate to apply change" class="statePower hide" type="number" min=0 max=99 step=.1 value=${
c.expansionism
}>
<select data-tip="Culture type. Defines growth model. Click to change" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
<<<<<<< HEAD
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
=======
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
>>>>>>> master
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
culturesFooterCultures.innerHTML = pack.cultures.filter((c) => c.i && !c.removed).length;
culturesFooterCells.innerHTML = pack.cells.h.filter((h) => h >= 20).length;
culturesFooterArea.innerHTML = si(totalArea) + unit;
culturesFooterPopulation.innerHTML = si(totalPopulation);
culturesFooterArea.dataset.area = totalArea;
culturesFooterPopulation.dataset.population = totalPopulation;
// add listeners
<<<<<<< HEAD
body.querySelectorAll('div.cultures').forEach((el) => el.addEventListener('mouseenter', (ev) => cultureHighlightOn(ev)));
body.querySelectorAll('div.cultures').forEach((el) => el.addEventListener('mouseleave', (ev) => cultureHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('click', selectCultureOnLineClick));
body.querySelectorAll('rect.fillRect').forEach((el) => el.addEventListener('click', cultureChangeColor));
body.querySelectorAll('div > input.cultureName').forEach((el) => el.addEventListener('input', cultureChangeName));
body.querySelectorAll('div > input.statePower').forEach((el) => el.addEventListener('input', cultureChangeExpansionism));
body.querySelectorAll('div > select.cultureType').forEach((el) => el.addEventListener('change', cultureChangeType));
body.querySelectorAll('div > select.cultureBase').forEach((el) => el.addEventListener('change', cultureChangeBase));
body.querySelectorAll('div > select.cultureShape').forEach((el) => el.addEventListener('change', cultureChangeShape));
body.querySelectorAll('div > div.culturePopulation').forEach((el) => el.addEventListener('click', changePopulation));
body.querySelectorAll('div > span.icon-arrows-cw').forEach((el) => el.addEventListener('click', cultureRegenerateBurgs));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', cultureRemove));
culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? 'inline-block' : 'none';
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
=======
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("change", cultureChangeBase));
body.querySelectorAll("div > select.cultureShape").forEach(el => el.addEventListener("change", cultureChangeShape));
body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? "inline-block" : "none";
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
>>>>>>> master
togglePercentageMode();
}
applySorting(culturesHeader);
$('#culturesEditor').dialog({width: fitContent()});
}
function getTypeOptions(type) {
let options = '';
const types = ['Generic', 'River', 'Lake', 'Naval', 'Nomadic', 'Hunting', 'Highland'];
types.forEach((t) => (options += `<option ${type === t ? 'selected' : ''} value="${t}">${t}</option>`));
return options;
}
function getBaseOptions(base) {
let options = '';
nameBases.forEach((n, i) => (options += `<option ${base === i ? 'selected' : ''} value="${i}">${n.name}</option>`));
return options;
}
function getShapeOptions(selected) {
const shapes = Object.keys(COA.shields.types)
.map((type) => Object.keys(COA.shields[type]))
.flat();
return shapes.map((shape) => `<option ${shape === selected ? 'selected' : ''} value="${shape}">${capitalize(shape)}</option>`);
}
function cultureHighlightOn(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById('cultureInfo');
if (info) {
d3.select('#hierarchy')
.select("g[data-id='" + culture + "'] > path")
.classed('selected', 1);
const c = pack.cultures[culture];
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rural + urban > 0 ? si(rn(rural + urban)) + ' people' : 'Extinct';
info.innerHTML = `${c.name} culture. ${c.type}. ${population}`;
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
}
if (!layerIsOn('toggleCultures')) return;
if (customization) return;
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
cults
.select('#culture' + culture)
.raise()
.transition(animate)
.attr('stroke-width', 2.5)
.attr('stroke', '#d0240f');
debug
.select('#cultureCenter' + culture)
.raise()
.transition(animate)
.attr('r', 8)
.attr('stroke', '#d0240f');
}
function cultureHighlightOff(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById('cultureInfo');
if (info) {
d3.select('#hierarchy')
.select("g[data-id='" + culture + "'] > path")
.classed('selected', 0);
info.innerHTML = '&#8205;';
tip('');
}
if (!layerIsOn('toggleCultures')) return;
cults
.select('#culture' + culture)
.transition()
.attr('stroke-width', null)
.attr('stroke', null);
debug
.select('#cultureCenter' + culture)
.transition()
.attr('r', 6)
.attr('stroke', null);
}
function cultureChangeColor() {
const el = this;
const currentFill = el.getAttribute('fill');
const culture = +el.parentNode.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute('fill', fill);
pack.cultures[culture].color = fill;
cults
.select('#culture' + culture)
.attr('fill', fill)
.attr('stroke', fill);
debug.select('#cultureCenter' + culture).attr('fill', fill);
};
openPicker(currentFill, callback);
}
function cultureChangeName() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.cultures[culture].name = this.value;
pack.cultures[culture].code = abbreviate(
this.value,
pack.cultures.map((c) => c.code)
);
}
function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture);
this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name;
}
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.cultures[culture].expansionism = +this.value;
recalculateCultures();
}
function cultureChangeType() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.cultures[culture].type = this.value;
recalculateCultures();
}
function cultureChangeBase() {
const culture = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.cultures[culture].base = v;
}
function cultureChangeShape() {
const culture = +this.parentNode.dataset.id;
const shape = this.value;
this.parentNode.dataset.emblems = pack.cultures[culture].shield = shape;
const rerenderCOA = (id, coa) => {
const coaEl = document.getElementById(id);
if (!coaEl) return; // not rendered
coaEl.remove();
COArenderer.trigger(id, coa);
};
pack.states.forEach((state) => {
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === 'custom') return;
if (shape === state.coa.shield) return;
state.coa.shield = shape;
rerenderCOA('stateCOA' + state.i, state.coa);
});
pack.provinces.forEach((province) => {
if (pack.cells.culture[province.center] !== culture || !province.i || province.removed || !province.coa || province.coa === 'custom') return;
if (shape === province.coa.shield) return;
province.coa.shield = shape;
rerenderCOA('provinceCOA' + province.i, province.coa);
});
pack.burgs.forEach((burg) => {
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === 'custom') return;
if (shape === burg.coa.shield) return;
burg.coa.shield = shape;
rerenderCOA('burgCOA' + burg.i, burg.coa);
});
}
function changePopulation() {
const culture = +this.parentNode.dataset.id;
const c = pack.cultures[culture];
if (!c.cells) {
tip('Culture does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(c.rural * populationRate);
const urban = rn(c.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
const burgs = pack.burgs.filter((b) => !b.removed && b.culture === culture);
alertMessage.innerHTML = `
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? '' : 'disabled'}>
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
if (isNaN(totalNew)) return;
totalPop.innerHTML = l(totalNew);
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
};
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$('#alert').dialog({
resizable: false,
title: 'Change culture population',
width: '24em',
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
const pop = rn(points / cells.length);
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach((b) => (b.population = population));
}
refreshCulturesEditor();
}
}
function cultureRegenerateBurgs() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter((b) => b.culture === culture && !b.lock);
cBurgs.forEach((b) => {
b.name = Names.getCulture(culture);
labels.select("[data-id='" + b.i + "']").text(b.name);
});
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, 'success');
}
function cultureRemove() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
alertMessage.innerHTML = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
$('#alert').dialog({
resizable: false,
title: 'Remove culture',
buttons: {
Remove: function () {
cults.select('#culture' + culture).remove();
debug.select('#cultureCenter' + culture).remove();
pack.burgs.filter((b) => b.culture == culture).forEach((b) => (b.culture = 0));
pack.states.forEach((s, i) => {
if (s.culture === culture) s.culture = 0;
});
pack.cells.culture.forEach((c, i) => {
if (c === culture) pack.cells.culture[i] = 0;
});
pack.cultures[culture].removed = true;
const origin = pack.cultures[culture].origin;
pack.cultures.forEach((c) => {
if (c.origin === culture) c.origin = origin;
});
refreshCulturesEditor();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select('#cultureCenters').remove();
const cultureCenters = debug.append('g').attr('id', 'cultureCenters').attr('stroke-width', 2).attr('stroke', '#444444').style('cursor', 'move');
const data = pack.cultures.filter((c) => c.i && !c.removed);
cultureCenters
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('id', (d) => 'cultureCenter' + d.i)
.attr('data-id', (d) => d.i)
.attr('r', 6)
.attr('fill', (d) => d.color)
.attr('cx', (d) => pack.cells.p[d.center][0])
.attr('cy', (d) => pack.cells.p[d.center][1])
.on('mouseenter', (d) => {
tip(tooltip, true);
body.querySelector(`div[data-id='${d.i}']`).classList.add('selected');
cultureHighlightOn(event);
})
.on('mouseleave', (d) => {
tip('', true);
body.querySelector(`div[data-id='${d.i}']`).classList.remove('selected');
cultureHighlightOff(event);
})
.call(d3.drag().on('start', cultureCenterDrag));
}
function cultureCenterDrag() {
const el = d3.select(this);
const c = +this.id.slice(13);
d3.event.on('drag', () => {
el.attr('cx', d3.event.x).attr('cy', d3.event.y);
const cell = findCell(d3.event.x, d3.event.y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.cultures[c].center = cell;
recalculateCultures();
});
}
function toggleLegend() {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const data = pack.cultures
.filter((c) => c.i && !c.removed && c.cells)
.sort((a, b) => b.area - a.area)
.map((c) => [c.i, c.color, c.name]);
drawLegend('Cultures', data);
}
function togglePercentageMode() {
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +culturesFooterCells.innerHTML;
const totalArea = +culturesFooterArea.dataset.area;
const totalPopulation = +culturesFooterPopulation.dataset.population;
body.querySelectorAll(':scope > div').forEach(function (el) {
el.querySelector('.stateCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100) + '%';
el.querySelector('.culturePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + '%';
});
} else {
body.dataset.type = 'absolute';
culturesEditorAddLines();
}
}
function showHierarchy() {
// build hierarchy tree
pack.cultures[0].origin = null;
const cultures = pack.cultures.filter((c) => !c.removed);
if (cultures.length < 3) {
tip('Not enough cultures to show hierarchy', false, 'error');
return;
}
const root = d3
.stratify()
.id((d) => d.i)
.parentId((d) => d.origin)(cultures);
const treeWidth = root.leaves().length;
const treeHeight = root.height;
const width = treeWidth * 40,
height = treeHeight * 60;
const margin = {top: 10, right: 10, bottom: -5, left: 10};
const w = width - margin.left - margin.right;
const h = height + 30 - margin.top - margin.bottom;
const treeLayout = d3.tree().size([w, h]);
// prepare svg
alertMessage.innerHTML = "<div id='cultureInfo' class='chartInfo'>&#8205;</div>";
<<<<<<< HEAD
const svg = d3.select('#alertMessage').insert('svg', '#cultureInfo').attr('id', 'hierarchy').attr('width', width).attr('height', height).style('text-anchor', 'middle');
const graph = svg.append('g').attr('transform', `translate(10, -45)`);
const links = graph.append('g').attr('fill', 'none').attr('stroke', '#aaaaaa');
const nodes = graph.append('g');
=======
const svg = d3
.select("#alertMessage")
.insert("svg", "#cultureInfo")
.attr("id", "hierarchy")
.attr("width", width)
.attr("height", height)
.style("text-anchor", "middle");
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
const nodes = graph.append("g");
>>>>>>> master
renderTree();
function renderTree() {
treeLayout(root);
links
.selectAll('path')
.data(root.links())
.enter()
<<<<<<< HEAD
.append('path')
.attr('d', (d) => {
return (
'M' +
d.source.x +
',' +
d.source.y +
'C' +
d.source.x +
',' +
(d.source.y * 3 + d.target.y) / 4 +
' ' +
d.target.x +
',' +
(d.source.y * 2 + d.target.y) / 3 +
' ' +
d.target.x +
',' +
=======
.append("path")
.attr("d", d => {
return (
"M" +
d.source.x +
"," +
d.source.y +
"C" +
d.source.x +
"," +
(d.source.y * 3 + d.target.y) / 4 +
" " +
d.target.x +
"," +
(d.source.y * 2 + d.target.y) / 3 +
" " +
d.target.x +
"," +
>>>>>>> master
d.target.y
);
});
const node = nodes
.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr('data-id', (d) => d.data.i)
.attr('stroke', '#333333')
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
.on('mouseenter', () => cultureHighlightOn(event))
.on('mouseleave', () => cultureHighlightOff(event))
.call(d3.drag().on('start', (d) => dragToReorigin(d)));
node
.append('path')
.attr('d', (d) => {
if (!d.data.i) return 'M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0';
// small circle
else if (d.data.type === 'Generic') return 'M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0';
// circle
else if (d.data.type === 'River') return 'M0,-14L14,0L0,14L-14,0Z';
// diamond
else if (d.data.type === 'Lake') return 'M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z';
// hexagon
else if (d.data.type === 'Naval') return 'M-11,-11h22v22h-22Z'; // square
if (d.data.type === 'Highland') return 'M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z'; // concave square
if (d.data.type === 'Nomadic') return 'M-4.97,-12.01 l9.95,0 l7.04,7.04 l0,9.95 l-7.04,7.04 l-9.95,0 l-7.04,-7.04 l0,-9.95Z'; // octagon
if (d.data.type === 'Hunting') return 'M0,-14l14,11l-6,14h-16l-6,-14Z'; // pentagon
return 'M-11,-11h22v22h-22Z'; // square
})
.attr('fill', (d) => (d.data.i ? d.data.color : '#ffffff'))
.attr('stroke-dasharray', (d) => (d.data.cells ? 'null' : '1'));
node
.append('text')
.attr('dy', '.35em')
.text((d) => (d.data.i ? d.data.code : ''));
}
$('#alert').dialog({
title: 'Cultures tree',
width: fitContent(),
resizable: false,
position: {my: 'left center', at: 'left+10 center', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
function dragToReorigin(d) {
if (isCtrlClick(d3.event.sourceEvent)) {
changeCode(d);
return;
}
const originLine = graph.append('path').attr('class', 'dragLine').attr('d', `M${d.x},${d.y}L${d.x},${d.y}`);
d3.event.on('drag', () => {
originLine.attr('d', `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`);
});
d3.event.on('end', () => {
originLine.remove();
const selected = graph.select('path.selected');
if (!selected.size()) return;
const culture = d.data.i;
const oldOrigin = d.data.origin;
let newOrigin = selected.datum().data.i;
if (newOrigin == oldOrigin) return; // already a child of the selected node
if (newOrigin == culture) newOrigin = 0; // move to top
if (newOrigin && d.descendants().some((node) => node.id == newOrigin)) return; // cannot be a child of its own child
pack.cultures[culture].origin = d.data.origin = newOrigin; // change data
showHierarchy(); // update hierarchy
});
}
function changeCode(d) {
prompt(`Please provide an abbreviation for culture: ${d.data.name}`, {default: d.data.code}, (v) => {
pack.cultures[d.data.i].code = v;
nodes
.select("g[data-id='" + d.data.i + "']")
.select('text')
.text(v);
});
}
}
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i;
});
Cultures.expand();
drawCultures();
pack.burgs.forEach((b) => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor();
document.querySelector('input.statePower').focus(); // to not trigger hotkeys
}
function enterCultureManualAssignent() {
if (!layerIsOn('toggleCultures')) toggleCultures();
customization = 4;
cults.append('g').attr('id', 'temp');
document.querySelectorAll('#culturesBottom > *').forEach((el) => (el.style.display = 'none'));
document.getElementById('culturesManuallyButtons').style.display = 'inline-block';
debug.select('#cultureCenters').style('display', 'none');
culturesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
culturesHeader.querySelector("div[data-sortby='type']").style.left = '8.8em';
culturesHeader.querySelector("div[data-sortby='base']").style.left = '13.6em';
culturesFooter.style.display = 'none';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
$('#culturesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
<<<<<<< HEAD
tip('Click on culture to select, drag the circle to change culture', true);
viewbox.style('cursor', 'crosshair').on('click', selectCultureOnMapClick).call(d3.drag().on('start', dragCultureBrush)).on('touchmove mousemove', moveCultureBrush);
=======
tip("Click on culture to select, drag the circle to change culture", true);
viewbox
.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
>>>>>>> master
body.querySelector('div').classList.add('selected');
}
function selectCultureOnLineClick(i) {
if (customization !== 4) return;
body.querySelector('div.selected').classList.remove('selected');
this.classList.add('selected');
}
function selectCultureOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = cults.select('#temp').select("polygon[data-cell='" + i + "']");
const culture = assigned.size() ? +assigned.attr('data-culture') : pack.cells.culture[i];
body.querySelector('div.selected').classList.remove('selected');
body.querySelector("div[data-id='" + culture + "']").classList.add('selected');
}
function dragCultureBrush() {
const r = +culturesManuallyBrush.value;
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeCultureForSelection(selection);
});
}
function changeCultureForSelection(selection) {
const temp = cults.select('#temp');
const selected = body.querySelector('div.selected');
const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color || '#ffffff';
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const cultureOld = exists.size() ? +exists.attr('data-culture') : pack.cells.culture[i];
if (cultureNew === cultureOld) return;
// change of append new element
<<<<<<< HEAD
if (exists.size()) exists.attr('data-culture', cultureNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-culture', cultureNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
=======
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
else
temp
.append("polygon")
.attr("data-cell", i)
.attr("data-culture", cultureNew)
.attr("points", getPackPolygon(i))
.attr("fill", color)
.attr("stroke", color);
>>>>>>> master
});
}
function moveCultureBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +culturesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyCultureManualAssignent() {
const changed = cults.select('#temp').selectAll('polygon');
changed.each(function () {
const i = +this.dataset.cell;
const c = +this.dataset.culture;
pack.cells.culture[i] = c;
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
});
if (changed.size()) {
drawCultures();
refreshCulturesEditor();
}
exitCulturesManualAssignment();
}
function exitCulturesManualAssignment(close) {
customization = 0;
cults.select('#temp').remove();
removeCircle();
document.querySelectorAll('#culturesBottom > *').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('culturesManuallyButtons').style.display = 'none';
culturesEditor.querySelectorAll('.hide').forEach((el) => el.classList.remove('hidden'));
culturesHeader.querySelector("div[data-sortby='type']").style.left = '18.6em';
culturesHeader.querySelector("div[data-sortby='base']").style.left = '35.8em';
culturesFooter.style.display = 'block';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (!close) $('#culturesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
debug.select('#cultureCenters').style('display', null);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
}
function enterAddCulturesMode() {
if (this.classList.contains('pressed')) {
exitAddCultureMode();
return;
}
customization = 9;
this.classList.add('pressed');
tip('Click on the map to add a new culture', true);
viewbox.style('cursor', 'crosshair').on('click', addCulture);
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
}
function exitAddCultureMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (culturesAdd.classList.contains('pressed')) culturesAdd.classList.remove('pressed');
}
function addCulture() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {
tip('You cannot place culture center into the water. Please click on a land cell', false, 'error');
return;
}
const occupied = pack.cultures.some((c) => !c.removed && c.center === center);
if (occupied) {
tip('This cell is already a culture center. Please select a different cell', false, 'error');
return;
}
if (d3.event.shiftKey === false) exitAddCultureMode();
Cultures.add(center);
drawCultureCenters();
culturesEditorAddLines();
}
function downloadCulturesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Culture,Color,Cells,Expansionism,Type,Area ' + unit + ',Population,Namesbase,Emblems Shape\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += el.dataset.color + ',';
data += el.dataset.cells + ',';
data += el.dataset.expansionism + ',';
data += el.dataset.type + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
const base = +el.dataset.base;
data += nameBases[base].name + ',';
data += el.dataset.emblems + '\n';
});
const name = getFileName('Cultures') + '.csv';
downloadFile(data, name);
}
function closeCulturesEditor() {
debug.select('#cultureCenters').remove();
exitCulturesManualAssignment('close');
exitAddCultureMode();
}
}

View file

@ -1,45 +1,57 @@
"use strict";
'use strict';
function editDiplomacy() {
if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
tip("There should be at least 2 states to edit the diplomacy", false, "error");
if (pack.states.filter((s) => s.i && !s.removed).length < 2) {
tip('There should be at least 2 states to edit the diplomacy', false, 'error');
return;
}
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
closeDialogs('#diplomacyEditor, .stable');
if (!layerIsOn('toggleStates')) toggleStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
if (layerIsOn('toggleProvinces')) toggleProvinces();
if (layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleReligions')) toggleReligions();
const body = document.getElementById("diplomacyBodySection");
const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ",
" is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "];
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
const body = document.getElementById('diplomacyBodySection');
const statuses = ['Ally', 'Friendly', 'Neutral', 'Suspicion', 'Enemy', 'Unknown', 'Rival', 'Vassal', 'Suzerain'];
const description = [
' is an ally of ',
' is friendly to ',
' is neutral to ',
' is suspicious of ',
' is at war with ',
' does not know about ',
' is a rival of ',
' is a vassal of ',
' is suzerain to '
];
const colors = ['#00b300', '#d4f8aa', '#edeee8', '#eeafaa', '#e64b40', '#a9a9a9', '#ad5a1f', '#87CEFA', '#00008B'];
refreshDiplomacyEditor();
tip("Click on a state to see its diplomatic relations", false, "warning");
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
tip('Click on a state to see its diplomatic relations', false, 'warning');
viewbox.style('cursor', 'crosshair').on('click', selectStateOnMapClick);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#diplomacyEditor').dialog({
title: 'Diplomacy Editor',
resizable: false,
width: fitContent(),
close: closeDiplomacyEditor,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
document.getElementById("diplomacySelect").addEventListener("mouseup", diplomacyChangeRelations);
document.getElementById('diplomacyEditorRefresh').addEventListener('click', refreshDiplomacyEditor);
document.getElementById('diplomacyEditStyle').addEventListener('click', () => editStyle('regions'));
document.getElementById('diplomacyRegenerate').addEventListener('click', regenerateRelations);
document.getElementById('diplomacyMatrix').addEventListener('click', showRelationsMatrix);
document.getElementById('diplomacyHistory').addEventListener('click', showRelationsHistory);
document.getElementById('diplomacyExport').addEventListener('click', downloadDiplomacyData);
document.getElementById('diplomacySelect').addEventListener('mouseup', diplomacyChangeRelations);
function refreshDiplomacyEditor() {
diplomacyEditorAddLines();
@ -49,12 +61,12 @@ function editDiplomacy() {
// add line for each state
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const selectedLine = body.querySelector('div.Self');
const sel = selectedLine ? +selectedLine.dataset.id : states.find((s) => s.i && !s.removed).i;
const selName = states[sel].fullName;
diplomacySelect.style.display = "none";
diplomacySelect.style.display = 'none';
COArenderer.trigger("stateCOA"+sel, states[sel].coa);
COArenderer.trigger('stateCOA' + sel, states[sel].coa);
let lines = `<div class="states Self" data-id=${sel} data-tip="List below shows relations to ${selName}">
<div style="width: max-content">${selName}</div>
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${sel}"></use></svg>
@ -68,7 +80,7 @@ function editDiplomacy() {
const tip = s.fullName + description[index] + selName;
const tipSelect = `${tip}. Click to see relations to ${s.name}`;
const tipChange = `${tip}. Click to change relations to ${selName}`;
COArenderer.trigger("stateCOA"+s.i, s.coa);
COArenderer.trigger('stateCOA' + s.i, s.coa);
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
@ -82,57 +94,61 @@ function editDiplomacy() {
body.innerHTML = lines;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => stateHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => stateHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('click', selectStateOnLineClick));
body.querySelectorAll('.changeRelations').forEach((el) => el.addEventListener('click', toggleDiplomacySelect));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
$('#diplomacyEditor').dialog();
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
if (!layerIsOn('toggleStates')) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const d = regions.select("#state"+state).attr("d");
const d = regions.select('#state' + state).attr('d');
const path = debug.append("path").attr("class", "highlight").attr("d", d)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)");
const path = debug.append('path').attr('class', 'highlight').attr('d', d).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 1).attr('opacity', 1).attr('filter', 'url(#blur1)');
const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString('0,' + l, l + ',' + l);
path
.transition()
.duration(dur)
.attrTween('stroke-dasharray', function () {
return (t) => i(t);
});
}
function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function() {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
debug.selectAll('.highlight').each(function () {
d3.select(this).transition().duration(1000).attr('opacity', 0).remove();
});
}
function showStateRelations() {
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find(s => s.i && !s.removed).i;
const selectedLine = body.querySelector('div.Self');
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find((s) => s.i && !s.removed).i;
if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn('toggleStates')) toggleStates();
statesBody.selectAll("path").each(function() {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
statesBody.selectAll('path').each(function () {
if (this.id.slice(0, 9) === 'state-gap') return; // exclude state gap element
const id = +this.id.slice(5); // state id
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
this.setAttribute("fill", clr);
statesBody.select("#state-gap"+id).attr("stroke", clr);
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
const clr = index !== -1 ? colors[index] : '#4682b4'; // Self (bluish)
this.setAttribute('fill', clr);
statesBody.select('#state-gap' + id).attr('stroke', clr);
statesHalo.select('#state-border' + id).attr('stroke', d3.color(clr).darker().hex());
});
}
function selectStateOnLineClick() {
if (this.classList.contains("Self")) return;
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self");
if (this.classList.contains('Self')) return;
body.querySelector('div.Self').classList.remove('Self');
this.classList.add('Self');
refreshDiplomacyEditor();
}
@ -141,35 +157,39 @@ function editDiplomacy() {
const i = findCell(point[0], point[1]);
const state = pack.cells.state[i];
if (!state) return;
const selectedLine = body.querySelector("div.Self");
const selectedLine = body.querySelector('div.Self');
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
selectedLine.classList.remove('Self');
body.querySelector("div[data-id='" + state + "']").classList.add('Self');
refreshDiplomacyEditor();
}
function toggleDiplomacySelect(event) {
event.stopPropagation();
const select = document.getElementById("diplomacySelect");
const show = select.style.display === "none";
if (!show) {select.style.display = "none"; return;}
select.style.display = "block";
const input = event.target.closest("div").querySelector("input");
select.style.left = input.getBoundingClientRect().left + "px";
select.style.top = input.getBoundingClientRect().bottom + "px";
body.dataset.state = event.target.closest("div.states").dataset.id;
const select = document.getElementById('diplomacySelect');
const show = select.style.display === 'none';
if (!show) {
select.style.display = 'none';
return;
}
select.style.display = 'block';
const input = event.target.closest('div').querySelector('input');
select.style.left = input.getBoundingClientRect().left + 'px';
select.style.top = input.getBoundingClientRect().bottom + 'px';
body.dataset.state = event.target.closest('div.states').dataset.id;
}
function diplomacyChangeRelations(event) {
event.stopPropagation();
diplomacySelect.style.display = "none";
diplomacySelect.style.display = 'none';
const subject = +body.dataset.state;
const rel = event.target.innerHTML;
const states = pack.states, chronicle = states[0].diplomacy;
const selectedLine = body.querySelector("div.Self");
const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const states = pack.states,
chronicle = states[0].diplomacy;
const selectedLine = body.querySelector('div.Self');
const object = selectedLine ? +selectedLine.dataset.id : states.find((s) => s.i && !s.removed).i;
if (!object) return;
const objectName = states[object].name; // object of relations change
const subjectName = states[subject].name; // subject of relations change - actor
@ -177,7 +197,7 @@ function editDiplomacy() {
const oldRel = states[subject].diplomacy[object];
if (rel === oldRel) return;
states[subject].diplomacy[object] = rel;
states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
states[object].diplomacy[subject] = rel === 'Vassal' ? 'Suzerain' : rel === 'Suzerain' ? 'Vassal' : rel;
// update relation history
const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${rel.toLowerCase()}`];
@ -189,22 +209,18 @@ function editDiplomacy() {
const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`];
const peace = () => {
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
const changed = rel === "Ally" ? ally()
: rel === "Vassal" ? vassal()
: rel === "Suzerain" ? suzerain()
: rel === "Unknown" ? unknown()
: change();
const changed = rel === 'Ally' ? ally() : rel === 'Vassal' ? vassal() : rel === 'Suzerain' ? suzerain() : rel === 'Unknown' ? unknown() : change();
return [`War termination`, treaty, changed[1]];
}
};
if (oldRel === "Enemy") chronicle.push(peace()); else
if (rel === "Enemy") chronicle.push(war()); else
if (rel === "Vassal") chronicle.push(vassal()); else
if (rel === "Suzerain") chronicle.push(suzerain()); else
if (rel === "Ally") chronicle.push(ally()); else
if (rel === "Unknown") chronicle.push(unknown()); else
if (rel === "Rival") chronicle.push(rival()); else
chronicle.push(change());
if (oldRel === 'Enemy') chronicle.push(peace());
else if (rel === 'Enemy') chronicle.push(war());
else if (rel === 'Vassal') chronicle.push(vassal());
else if (rel === 'Suzerain') chronicle.push(suzerain());
else if (rel === 'Ally') chronicle.push(ally());
else if (rel === 'Unknown') chronicle.push(unknown());
else if (rel === 'Rival') chronicle.push(rival());
else chronicle.push(change());
refreshDiplomacyEditor();
}
@ -216,79 +232,95 @@ function editDiplomacy() {
function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
if (!chronicle.length) {
tip('Relations history is blank', false, 'error');
return;
}
let message = `<div autocorrect="off" spellcheck="false">`;
chronicle.forEach((e, d) => {
message += `<div>`;
e.forEach((l, i) => message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
e.forEach((l, i) => (message += `<div contenteditable="true" data-id="${d}-${i}"${i ? '' : " style='font-weight:bold'"}>${l}</div>`));
message += `&#8205;</div>`;
});
alertMessage.innerHTML = message + `</div><i id="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</i>`;
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
alertMessage.querySelectorAll("div[contenteditable='true']").forEach((el) => el.addEventListener('input', changeReliationsHistory));
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
$('#alert').dialog({
title: 'Relations history',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Save: function() {
const data = this.querySelector("div").innerText.split("\n").join("\r\n");
const name = getFileName("Relations history") + ".txt";
Save: function () {
const data = this.querySelector('div').innerText.split('\n').join('\r\n');
const name = getFileName('Relations history') + '.txt';
downloadFile(data, name);
},
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");}
Clear: function () {
pack.states[0].diplomacy = [];
$(this).dialog('close');
},
Close: function () {
$(this).dialog('close');
}
}
});
}
function changeReliationsHistory() {
const i = this.dataset.id.split("-");
const i = this.dataset.id.split('-');
const group = pack.states[0].diplomacy[i[0]];
if (this.innerHTML === "") {
if (this.innerHTML === '') {
group.splice(i[1], 1);
this.remove();
} else group[i[1]] = this.innerHTML;
}
function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
const states = pack.states.filter((s) => s.i && !s.removed);
const valid = states.map((s) => s.i);
let message = `<table class="matrix-table"><tr><th data-tip='&#8205;'></th>`;
message += states.map(s => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join("") + `</tr>`; // headers
states.forEach(s => {
message += `<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` + s.diplomacy
.filter((v, i) => valid.includes(i)).map((r, i) => {
const desc = description[statuses.indexOf(r)];
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;';
return `<td data-tip='${tip}' class='${r}'>${r}</td>`
}).join("") + "</tr>";
message += states.map((s) => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join('') + `</tr>`; // headers
states.forEach((s) => {
message +=
`<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` +
s.diplomacy
.filter((v, i) => valid.includes(i))
.map((r, i) => {
const desc = description[statuses.indexOf(r)];
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;';
return `<td data-tip='${tip}' class='${r}'>${r}</td>`;
})
.join('') +
'</tr>';
});
message += `</table>`;
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
$('#alert').dialog({title: 'Relations matrix', width: fitContent(), position: {my: 'center', at: 'center', of: 'svg'}, buttons: {}});
}
function downloadDiplomacyData() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
const states = pack.states.filter((s) => s.i && !s.removed);
const valid = states.map((s) => s.i);
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
states.forEach(s => {
let data = ',' + states.map((s) => s.name).join(',') + '\n'; // headers
states.forEach((s) => {
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
data += s.name + "," + rels.join(",") + "\n";
data += s.name + ',' + rels.join(',') + '\n';
});
const name = getFileName("Relations") + ".csv";
const name = getFileName('Relations') + '.csv';
downloadFile(data, name);
}
function closeDiplomacyEditor() {
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
debug.selectAll(".highlight").remove();
const selected = body.querySelector('div.Self');
if (selected) selected.classList.remove('Self');
if (layerIsOn('toggleStates')) drawStates();
else toggleStates();
debug.selectAll('.highlight').remove();
}
}

View file

@ -1,6 +1,7 @@
// module stub to store common functions for ui editors
'use strict';
modules.editors = true;
restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events
@ -264,15 +265,8 @@ function toggleBurgLock(burg) {
b.lock = b.lock ? 0 : 1;
}
function showBurgLockTip(burg) {
const b = pack.burgs[burg];
if (b.lock) {
tip('Click to unlock burg and allow it to be change by regeneration tools');
} else {
tip('Click to lock burg and prevent changes by regeneration tools');
}
}
// draw legend box
function drawLegend(name, data) {
legend.selectAll('*').remove(); // fully redraw every time
@ -385,6 +379,14 @@ function createPicker() {
const contaiter = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
contaiter.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('opacity', 0.2).on('mousemove', cl).on('click', closePicker);
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", "100%")
.attr("height", "100%")
.attr("opacity", 0.2)
.on("mousemove", cl)
.on("click", closePicker);
const picker = contaiter
.append('g')
.attr('id', 'picker')
@ -489,6 +491,17 @@ function createPicker() {
picker.insert('text', ':first-child').attr('x', 12).attr('y', -10).attr('id', 'pickerLabel').text('Color Picker').on('mousemove', pos);
picker.insert('rect', ':first-child').attr('x', 0).attr('y', -30).attr('width', width).attr('height', 30).attr('id', 'pickerHeader').on('mousemove', pos);
picker.attr('transform', `translate(${(svgWidth - width) / 2},${(svgHeight - height) / 2})`);
.attr("fill", "#ffffff")
.attr("stroke", "#5d4651")
.on("mousemove", pos);
.insert("rect", ":first-child")
.attr("x", 288)
.attr("y", -21)
.attr("id", "pickerCloseRect")
.attr("width", 14)
.attr("height", 14)
.on("mousemove", cl)
.on("click", closePicker);
}
function updateSelectedRect(fill) {
@ -693,23 +706,32 @@ function uploadFile(el, callback) {
fileReader.onload = (loaded) => callback(loaded.target.result);
}
function highlightElement(element) {
function getBBox(element) {
if (debug.select('.highlighted').size()) return; // allow only 1 highlight element simultaniosly
const box = element.getBBox();
const y = +element.getAttribute("y");
const transform = element.getAttribute('transform') || null;
const height = +element.getAttribute("height");
return {x, y, width, height};
}
function highlightElement(element, zoom) {
if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaneously
const box = element.tagName === "svg" ? getBBox(element) : element.getBBox();
const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
const exit = d3.transition().duration(500).ease(d3.easeLinear);
const highlight = debug.append('rect').attr('x', box.x).attr('y', box.y).attr('width', box.width).attr('height', box.height).attr('transform', transform);
highlight.classed("highlighted", 1).attr("transform", transform);
highlight.classed('highlighted', 1).transition(enter).style('outline-offset', '0px').transition(exit).style('outline-color', 'transparent').delay(1000).remove();
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
if (tr[0]) x += tr[0];
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
zoomTo(x, y, scale > 2 ? scale : 3, 1600);
if (zoom) {
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
if (tr[0]) x += tr[0];
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
zoomTo(x, y, scale > 2 ? scale : zoom, 1600);
}
}
function selectIcon(initial, callback) {
@ -945,6 +967,37 @@ function selectIcon(initial, callback) {
});
}
function confirmationDialog(options) {
const {
title = "Confirm action",
message = "Are you sure you want to continue? <br>The action cannot be reverted",
cancel = "Cancel",
confirm = "Continue",
onCancel,
onConfirm
} = options;
const buttons = {
[confirm]: function () {
if (onConfirm) onConfirm();
$(this).dialog("close");
},
[cancel]: function () {
if (onCancel) onCancel();
$(this).dialog("close");
}
};
document.getElementById("alertMessage").innerHTML = message;
$("#alert").dialog({resizable: false, title, buttons});
}
// add and register event listeners to clean up on editor closure
function listen(element, event, handler) {
element.addEventListener(event, handler);
return () => element.removeEventListener(event, handler);
}
// Calls the refresh for all currently open editors
function refreshAllEditors() {
TIME && console.time('refreshAllEditors');

1098
modules/ui/editors.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
"use strict";
'use strict';
function showEPForRoute(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
@ -17,10 +17,10 @@ function showEPForRoute(node) {
function showEPForRiver(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
@ -30,16 +30,16 @@ function showEPForRiver(node) {
function showElevationProfile(data, routeLen, isRiver) {
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
document.getElementById("epScaleRange").addEventListener("change", draw);
document.getElementById("epCurve").addEventListener("change", draw);
document.getElementById("epSave").addEventListener("click", downloadCSV);
document.getElementById('epScaleRange').addEventListener('change', draw);
document.getElementById('epCurve').addEventListener('change', draw);
document.getElementById('epSave').addEventListener('click', downloadCSV);
$("#elevationProfile").dialog({
title: "Elevation profile",
$('#elevationProfile').dialog({
title: 'Elevation profile',
resizable: false,
width: window.width,
close: closeElevationProfile,
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
position: {my: 'left top', at: 'left+20 bottom-500', of: window, collision: 'fit'}
});
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
@ -67,7 +67,7 @@ function showElevationProfile(data, routeLen, isRiver) {
let h = pack.cells.h[cell];
if (h < 20) {
const f = pack.features[pack.cells.f[cell]];
if (f.type === "lake") h = f.height;
if (f.type === 'lake') h = f.height;
else h = 20;
}
@ -94,7 +94,7 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.burg[i] = b;
chartData.cell[i] = cell;
let sh = getHeight(h);
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(" ")));
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
chartData.mih = Math.min(chartData.mih, h);
chartData.mah = Math.max(chartData.mah, h);
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
@ -109,7 +109,7 @@ function showElevationProfile(data, routeLen, isRiver) {
draw();
function downloadCSV() {
let data = "Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
let data = 'Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n'; // headers
for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k];
@ -122,34 +122,34 @@ function showElevationProfile(data, routeLen, isRiver) {
let pop = pack.cells.pop[cell];
let h = pack.cells.h[cell];
data += k + 1 + ",";
data += chartData.points[k][0] + ",";
data += chartData.points[k][1] + ",";
data += cell + ",";
data += getHeight(h) + ",";
data += h + ",";
data += rn(pop * populationRate) + ",";
data += k + 1 + ',';
data += chartData.points[k][0] + ',';
data += chartData.points[k][1] + ',';
data += cell + ',';
data += getHeight(h) + ',';
data += h + ',';
data += rn(pop * populationRate) + ',';
if (burg) {
data += pack.burgs[burg].name + ",";
data += pack.burgs[burg].population * populationRate * urbanization + ",";
data += pack.burgs[burg].name + ',';
data += pack.burgs[burg].population * populationRate * urbanization + ',';
} else {
data += ",0,";
data += ',0,';
}
data += biomesData.name[biome] + ",";
data += biomesData.color[biome] + ",";
data += pack.cultures[culture].name + ",";
data += pack.cultures[culture].color + ",";
data += pack.religions[religion].name + ",";
data += pack.religions[religion].color + ",";
data += pack.provinces[province].name + ",";
data += pack.provinces[province].color + ",";
data += pack.states[state].name + ",";
data += pack.states[state].color + ",";
data += biomesData.name[biome] + ',';
data += biomesData.color[biome] + ',';
data += pack.cultures[culture].name + ',';
data += pack.cultures[culture].color + ',';
data += pack.religions[religion].name + ',';
data += pack.religions[religion].color + ',';
data += pack.provinces[province].name + ',';
data += pack.provinces[province].color + ',';
data += pack.states[state].name + ',';
data += pack.states[state].color + ',';
data = data + "\n";
data = data + '\n';
}
const name = getFileName("elevation profile") + ".csv";
const name = getFileName('elevation profile') + '.csv';
downloadFile(data, name);
}
@ -169,37 +169,48 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
}
document.getElementById("elevationGraph").innerHTML = "";
document.getElementById('elevationGraph').innerHTML = '';
const chart = d3
.select("#elevationGraph")
.append("svg")
.attr("width", chartWidth + 120)
.attr("height", chartHeight + yOffset + biomesHeight)
.attr("id", "elevationSVG")
.attr("class", "epbackground");
.select('#elevationGraph')
.append('svg')
.attr('width', chartWidth + 120)
.attr('height', chartHeight + yOffset + biomesHeight)
.attr('id', 'elevationSVG')
.attr('class', 'epbackground');
// arrow-head definition
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
chart
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('orient', 'auto')
.attr('markerWidth', '2')
.attr('markerHeight', '4')
.attr('refX', '0.1')
.attr('refY', '2')
.append('path')
.attr('d', 'M0,0 V4 L2,2 Z')
.attr('fill', 'darkgray');
let colors = getColorScheme();
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
const landdef = chart.select('defs').append('linearGradient').attr('id', 'landdef').attr('x1', '0%').attr('y1', '0%').attr('x2', '0%').attr('y2', '100%');
if (chartData.mah == chartData.mih) {
landdef
.append("stop")
.attr("offset", "0%")
.attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:' + getColor(chartData.mih, colors) + ';stop-opacity:1');
landdef
.append("stop")
.attr("offset", "100%")
.attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:' + getColor(chartData.mah, colors) + ';stop-opacity:1');
} else {
for (let k = chartData.mah; k >= chartData.mih; k--) {
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
landdef
.append("stop")
.attr("offset", perc * 100 + "%")
.attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', perc * 100 + '%')
.attr('style', 'stop-color:' + getColor(k, colors) + ';stop-opacity:1');
}
}
@ -231,14 +242,14 @@ function showElevationProfile(data, routeLen, isRiver) {
let extra = chartData.points.slice();
let path = curve(extra);
// this completes the right-hand side and bottom of our land "polygon"
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length - 1][1]);
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += "Z";
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)");
path += ' L' + parseInt(xscale(extra.length) + +xOffset) + ',' + parseInt(extra[extra.length - 1][1]);
path += ' L' + parseInt(xscale(extra.length) + +xOffset) + ',' + parseInt(yscale(0) + +yOffset);
path += ' L' + parseInt(xscale(0) + +xOffset) + ',' + parseInt(yscale(0) + +yOffset);
path += 'Z';
chart.append('g').attr('id', 'epland').append('path').attr('d', path).attr('stroke', 'purple').attr('stroke-width', '0').attr('fill', 'url(#landdef)');
// biome / heights
let g = chart.append("g").attr("id", "epbiomes");
let g = chart.append('g').attr('id', 'epbiomes');
const hu = heightUnit.value;
for (let k = 0; k < chartData.points.length; k++) {
const x = chartData.points[k][0];
@ -257,65 +268,82 @@ function showElevationProfile(data, routeLen, isRiver) {
const populationDesc = rn(pop * populationRate);
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
const dataTip = biomesData.name[chartData.biome[k]] + provinceDesc + ", " + pack.states[state].name + ", " + pack.religions[religion].name + ", " + pack.cultures[culture].name + " (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
const provinceDesc = province ? ', ' + pack.provinces[province].name : '';
const dataTip =
biomesData.name[chartData.biome[k]] +
provinceDesc +
', ' +
pack.states[state].name +
', ' +
pack.religions[religion].name +
', ' +
pack.cultures[culture].name +
' (height: ' +
chartData.height[k] +
' ' +
hu +
', population ' +
populationDesc +
', cell ' +
chartData.cell[k] +
')';
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
g.append('rect').attr('stroke', c).attr('fill', c).attr('x', x).attr('y', y).attr('width', xscale(1)).attr('height', 15).attr('data-tip', dataTip);
}
const xAxis = d3
.axisBottom(xscale)
.ticks(10)
.tickFormat(function (d) {
return rn((d / chartData.points.length) * routeLen) + " " + distanceUnitInput.value;
return rn((d / chartData.points.length) * routeLen) + ' ' + distanceUnitInput.value;
});
const yAxis = d3
.axisLeft(yscale)
.ticks(5)
.tickFormat(function (d) {
return d + " " + hu;
return d + ' ' + hu;
});
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat('');
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat('');
chart
.append("g")
.attr("id", "epxaxis")
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
.append('g')
.attr('id', 'epxaxis')
.attr('transform', 'translate(' + xOffset + ',' + parseInt(chartHeight + +yOffset + 20) + ')')
.call(xAxis)
.selectAll("text")
.style("text-anchor", "center")
.attr("transform", function (d) {
return "rotate(0)"; // used to rotate labels, - anti-clockwise, + clockwise
.selectAll('text')
.style('text-anchor', 'center')
.attr('transform', function (d) {
return 'rotate(0)'; // used to rotate labels, - anti-clockwise, + clockwise
});
chart
.append("g")
.attr("id", "epyaxis")
.attr("transform", "translate(" + parseInt(+xOffset - 10) + "," + parseInt(+yOffset) + ")")
.append('g')
.attr('id', 'epyaxis')
.attr('transform', 'translate(' + parseInt(+xOffset - 10) + ',' + parseInt(+yOffset) + ')')
.call(yAxis);
// add the X gridlines
chart
.append("g")
.attr("id", "epxgrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset) + ")")
.append('g')
.attr('id', 'epxgrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + parseInt(chartHeight + +yOffset) + ')')
.call(xGrid);
// add the Y gridlines
chart
.append("g")
.attr("id", "epygrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")")
.append('g')
.attr('id', 'epygrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + yOffset + ')')
.call(yGrid);
// draw city labels - try to avoid putting labels over one another
g = chart.append("g").attr("id", "epburglabels");
g = chart.append('g').attr('id', 'epburglabels');
let y1 = 0;
const add = 15;
@ -331,31 +359,31 @@ function showElevationProfile(data, routeLen, isRiver) {
if (y1 >= yOffset) y1 = add;
// burg name
g.append("text")
.attr("id", "ep" + b)
.attr("class", "epburglabel")
.attr("x", x1)
.attr("y", y1)
.attr("text-anchor", "middle");
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
g.append('text')
.attr('id', 'ep' + b)
.attr('class', 'epburglabel')
.attr('x', x1)
.attr('y', y1)
.attr('text-anchor', 'middle');
document.getElementById('ep' + b).innerHTML = pack.burgs[b].name;
// arrow from burg name to graph line
g.append("path")
.attr("id", "eparrow" + b)
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString())
.attr("stroke", "darkgray")
.attr("fill", "lightgray")
.attr("stroke-width", "1")
.attr("marker-end", "url(#arrowhead)");
g.append('path')
.attr('id', 'eparrow' + b)
.attr('d', 'M' + x1.toString() + ',' + (y1 + 3).toString() + 'L' + x1.toString() + ',' + parseInt(chartData.points[k][1] - 3).toString())
.attr('stroke', 'darkgray')
.attr('fill', 'lightgray')
.attr('stroke-width', '1')
.attr('marker-end', 'url(#arrowhead)');
}
}
}
function closeElevationProfile() {
document.getElementById("epScaleRange").removeEventListener("change", draw);
document.getElementById("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = "";
document.getElementById('epScaleRange').removeEventListener('change', draw);
document.getElementById('epCurve').removeEventListener('change', draw);
document.getElementById('epSave').removeEventListener('click', downloadCSV);
document.getElementById('elevationGraph').innerHTML = '';
modules.elevation = false;
}
}

View file

@ -1,8 +1,8 @@
// Module to store general UI functions
'use strict';
// Module to store general UI functions
// fit full-screen map if window is resized
$(window).resize(function (e) {
window.addEventListener("resize", function (e) {
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
@ -10,6 +10,7 @@ $(window).resize(function (e) {
});
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
}
// Tooltips
const tooltip = document.getElementById('tooltip');
@ -19,12 +20,6 @@ document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
@ -32,11 +27,15 @@ function tip(tip = 'Tip is undefined', main, type, time) {
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
if (main) tooltip.dataset.main = tip; // set main tip
if (main) {
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
tooltip.dataset.color = tooltip.style.background;
}
if (time) setTimeout(() => clearMainTip(), time);
}
function showMainTip() {
tooltip.style.background = tooltip.dataset.color;
tooltip.innerHTML = tooltip.dataset.main;
}
@ -55,6 +54,15 @@ function showDataTip(e) {
tip(dataTip);
}
function showElementLockTip(event) {
const locked = event?.target?.classList?.contains("icon-lock");
if (locked) {
tip("Click to unlock the element and allow it to be changed by regeneration tools");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
}
}
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
@ -79,7 +87,7 @@ function showNotes(e, i) {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
} else if (!options.pinNotes) {
} else if (!options.pinNotes && !markerEditor.offsetParent) {
document.getElementById('notes').style.display = 'none';
document.getElementById('notesHeader').innerHTML = '';
document.getElementById('notesBody').innerHTML = '';
@ -101,6 +109,7 @@ function showMapTooltip(point, e, i, g) {
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
@ -497,229 +506,7 @@ function showInfo() {
});
}
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener('keydown', (event) => {
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keyup', (event) => {
if (!window.closeDialogs) return; // not all modules are loaded
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
const active = document.activeElement.tagName;
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
event.stopPropagation();
const key = event.keyCode;
const ctrl = event.ctrlKey || event.metaKey || key === 17;
const shift = event.shiftKey || key === 16;
const alt = event.altKey || key === 18;
if (key === 112) showInfo();
// "F1" to show info
else if (key === 113) regeneratePrompt();
// "F2" for new map
else if (key === 113) regeneratePrompt();
// "F2" for a new map
else if (key === 117) quickSave();
// "F6" for quick save
else if (key === 120) quickLoad();
// "F9" for quick load
else if (key === 9) toggleOptions(event);
// Tab to toggle options
else if (key === 27) {
closeDialogs();
hideOptions();
} // Escape to close all dialogs
else if (key === 46) removeElementOnKey();
// "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions();
// "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder();
// Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap();
// Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click();
// Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click();
// Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap();
// Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes();
// Shift + "B" to edit Biomes
else if (shift && key === 83) editStates();
// Shift + "S" to edit States
else if (shift && key === 80) editProvinces();
// Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy();
// Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures();
// Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase();
// Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones();
// Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions();
// Shift + "R" to edit Religions
else if (shift && key === 81) editResources();
// Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor();
// Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits();
// Shift + "W" to edit Units
else if (shift && key === 79) editNotes();
// Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs();
// Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers();
// Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary();
// Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails();
// Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg();
// Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel();
// Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver();
// Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute();
// Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker();
// Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs);
// Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states);
// Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures);
// Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions);
// Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features);
// Alt + "F" to log features data
else if (key === 88) toggleTexture();
// "X" to toggle Texture layer
else if (key === 72) toggleHeight();
// "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes();
// "B" to toggle Biomes layer
else if (key === 69) toggleCells();
// "E" to toggle Cells layer
else if (key === 71) toggleGrid();
// "G" to toggle Grid layer
else if (key === 79) toggleCoordinates();
// "O" to toggle Coordinates layer
else if (key === 87) toggleCompass();
// "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers();
// "V" to toggle Rivers layer
else if (key === 70) toggleRelief();
// "F" to toggle Relief icons layer
else if (key === 67) toggleCultures();
// "C" to toggle Cultures layer
else if (key === 83) toggleStates();
// "S" to toggle States layer
else if (key === 80) toggleProvinces();
// "P" to toggle Provinces layer
else if (key === 90) toggleZones();
// "Z" to toggle Zones
else if (key === 68) toggleBorders();
// "D" to toggle Borders layer
else if (key === 82) toggleReligions();
// "R" to toggle Religions layer
else if (key === 85) toggleRoutes();
// "U" to toggle Routes layer
else if (key === 84) toggleTemp();
// "T" to toggle Temperature layer
else if (key === 78) togglePopulation();
// "N" to toggle Population layer
else if (key === 74) toggleIce();
// "J" to toggle Ice layer
else if (key === 65) togglePrec();
// "A" to toggle Precipitation layer
else if (key === 81) toggleResources();
// "Q" to toggle Resources layer
else if (key === 89) toggleEmblems();
// "Y" to toggle Emblems layer
else if (key === 76) toggleLabels();
// "L" to toggle Labels layer
else if (key === 73) toggleIcons();
// "I" to toggle Icons layer
else if (key === 77) toggleMilitary();
// "M" to toggle Military layer
else if (key === 75) toggleMarkers();
// "K" to toggle Markers layer
else if (key === 187) toggleRulers();
// Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar();
// Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0);
// Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0);
// Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10);
// Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10);
// Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key);
// Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000);
// 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
// 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
// 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
// 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
// 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
// 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
// 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
// 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
// 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
// 9 to zoom to 9
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$('.dialog:visible .fastDelete').click();
$("button:visible:contains('Remove')").click();
}

810
modules/ui/general.js.orig Normal file
View file

@ -0,0 +1,810 @@
<<<<<<< HEAD
// Module to store general UI functions
'use strict';
// fit full-screen map if window is resized
$(window).resize(function (e) {
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
=======
"use strict";
// Module to store general UI functions
// fit full-screen map if window is resized
window.addEventListener("resize", function (e) {
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
>>>>>>> master
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
changeMapSize();
});
<<<<<<< HEAD
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
=======
if (location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
window.onbeforeunload = () => "Are you sure you want to navigate away?";
}
>>>>>>> master
// Tooltips
const tooltip = document.getElementById('tooltip');
// show tip for non-svg elemets with data-tip
document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
<<<<<<< HEAD
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
=======
function tip(tip = "Tip is undefined", main, type, time) {
>>>>>>> master
tooltip.innerHTML = tip;
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
if (type === 'error') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)';
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
<<<<<<< HEAD
if (main) tooltip.dataset.main = tip; // set main tip
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
=======
if (main) {
tooltip.dataset.main = tip;
tooltip.dataset.color = tooltip.style.background;
}
if (time) setTimeout(() => clearMainTip(), time);
>>>>>>> master
}
function showMainTip() {
tooltip.style.background = tooltip.dataset.color;
tooltip.innerHTML = tooltip.dataset.main;
}
function clearMainTip() {
<<<<<<< HEAD
tooltip.dataset.main = '';
tooltip.innerHTML = '';
=======
tooltip.dataset.color = "";
tooltip.dataset.main = "";
tooltip.innerHTML = "";
>>>>>>> master
}
// show tip at the bottom of the screen, consider possible translation
function showDataTip(e) {
if (!e.target) return;
let dataTip = e.target.dataset.tip;
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip;
if (!dataTip) return;
//const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
tip(dataTip);
}
function showElementLockTip(event) {
const locked = event?.target?.classList?.contains("icon-lock");
if (locked) {
tip("Click to unlock the element and allow it to be changed by regeneration tools");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
}
}
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack cell id
if (i === undefined) return;
showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g);
if (cellInfo.offsetParent) updateCellInfo(point, i, g);
}
// show note box on hover (if any)
function showNotes(e, i) {
if (notesEditor.offsetParent) return;
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
<<<<<<< HEAD
if (e.target.parentNode.parentNode.id === 'burgLabels') id = 'burg' + e.target.dataset.id;
else if (e.target.parentNode.parentNode.id === 'burgIcons') id = 'burg' + e.target.dataset.id;
const note = notes.find((note) => note.id === id);
if (note !== undefined && note.legend !== '') {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
} else if (!options.pinNotes) {
document.getElementById('notes').style.display = 'none';
document.getElementById('notesHeader').innerHTML = '';
document.getElementById('notesBody').innerHTML = '';
=======
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id;
else if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
} else if (!options.pinNotes && !markerEditor.offsetParent) {
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
>>>>>>> master
}
}
// show viewbox tooltip if main tooltip is blank
function showMapTooltip(point, e, i, g) {
tip(''); // clear tip
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
if (!path[path.length - 8]) return;
const group = path[path.length - 7].id;
const subgroup = path[path.length - 8].id;
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === 'armies') return tip(e.target.parentNode.dataset.name + '. Click to edit');
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
<<<<<<< HEAD
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
=======
const [g, type] =
parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
>>>>>>> master
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
d3.select(e.target).raise();
d3.select(parent).raise();
const name = g[i].fullName || g[i].name;
tip(`${name} ${type} emblem. Click to edit. Hold Shift to show associated area or place`);
return;
}
if (group === 'goods') {
const id = +e.target.dataset.i;
const resource = pack.resources.find((resource) => resource.i === id);
tip('Resource: ' + resource.name);
return;
}
if (group === 'rivers') {
const river = +e.target.id.slice(5);
const r = pack.rivers.find((r) => r.i === river);
const name = r ? r.name + ' ' + r.type : '';
tip(name + '. Click to edit');
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
if (group === 'routes') return tip('Click to edit the Route');
if (group === 'terrain') return tip('Click to edit the Relief Icon');
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate * urbanization);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === 'labels') return tip('Click to edit the Label');
<<<<<<< HEAD
if (group === 'markers') return tip('Click to edit the Marker');
=======
if (group === "markers") return tip("Click to edit the Marker and pin the marker note");
>>>>>>> master
if (group === 'ruler') {
const tag = e.target.tagName;
const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') return tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
if (tag === 'circle' && className === 'control') return tip('Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point');
if (tag === 'circle') return tip('Drag to adjust the measurer');
if (tag === 'polyline') return tip('Click on drag to add a control point');
if (tag === 'path') return tip('Drag to move the measurer');
if (tag === 'text') return tip('Drag to move, click to remove the measurer');
}
if (subgroup === 'burgIcons') return tip('Click to edit the Burg');
if (subgroup === 'burgLabels') return tip('Click to edit the Burg');
if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
const fullName = subgroup === 'freshwater' ? name : name + ' ' + subgroup;
tip(`${fullName} lake. Click to edit`);
return;
}
if (group === 'coastline') return tip('Click to edit the coastline');
if (group === 'zones') {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === 'ice') return tip('Click to edit the Ice');
// covering elements
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));
else if (layerIsOn('togglePopulation')) tip(getPopulationTip(i));
else if (layerIsOn('toggleTemp')) tip('Temperature: ' + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn('toggleBiomes') && pack.cells.biome[i]) {
const biome = pack.cells.biome[i];
tip('Biome: ' + biomesData.name[biome]);
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
} else if (layerIsOn('toggleReligions') && pack.cells.religion[i]) {
const religion = pack.cells.religion[i];
const r = pack.religions[religion];
const type = r.type === 'Cult' || r.type == 'Heresy' ? r.type : r.type + ' religion';
tip(type + ': ' + r.name);
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
} else if (pack.cells.state[i] && (layerIsOn('toggleProvinces') || layerIsOn('toggleStates'))) {
const state = pack.cells.state[i];
const stateName = pack.states[state].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ', ' : '';
tip(prov + stateName);
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
} else if (layerIsOn('toggleCultures') && pack.cells.culture[i]) {
const culture = pack.cells.culture[i];
tip('Culture: ' + pack.cultures[culture].name);
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
} else if (layerIsOn('toggleHeight')) tip('Height: ' + getFriendlyHeight(point));
}
function highlightEditorLine(editor, id, timeout = 15000) {
Array.from(editor.getElementsByClassName('states hovered')).forEach((el) => el.classList.remove('hovered')); // clear all hovered
const hovered = Array.from(editor.querySelectorAll('div')).find((el) => el.dataset.id == id);
if (hovered) hovered.classList.add('hovered'); // add hovered class
if (timeout)
setTimeout(() => {
hovered && hovered.classList.remove('hovered');
}, timeout);
}
// get cell info on mouse move
function updateCellInfo(point, i, g) {
const cells = pack.cells;
const x = (infoX.innerHTML = rn(point[0]));
const y = (infoY.innerHTML = rn(point[1]));
const f = cells.f[i];
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, 'lat');
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, 'lon');
infoCell.innerHTML = i;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : 'n/a';
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : 'n/a';
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : 'no';
infoState.innerHTML = cells.h[i] >= 20 ? (cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : 'neutral lands (0)') : 'no';
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : 'no';
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : 'no';
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : 'no';
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + ' (' + cells.burg[i] + ')' : 'no';
infoFeature.innerHTML = f ? pack.features[f].group + ' (' + f + ')' : 'n/a';
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
// convert coordinate to DMS format
function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord));
const minutesNotTruncated = (Math.abs(coord) - degrees) * 60;
const minutes = Math.floor(minutesNotTruncated);
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
const cardinal = c === 'lat' ? (coord >= 0 ? 'N' : 'S') : coord >= 0 ? 'E' : 'W';
return degrees + '° ' + minutes + ' ' + seconds + '″ ' + cardinal;
}
// get surface elevation
function getElevation(f, h) {
if (f.land) return getHeight(h) + ' (' + h + ')'; // land: usual height
if (f.border) return '0 ' + heightUnit.value; // ocean: 0
if (f.type === 'lake') return getHeight(f.height) + ' (' + f.height + ')'; // lake: defined on river generation
}
// get water depth
function getDepth(f, h, p) {
if (f.land) return '0 ' + heightUnit.value; // land: 0
// lake: difference between surface and bottom
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
if (f.type === 'lake') {
const depth = gridH === 19 ? f.height / 2 : gridH;
return getHeight(depth, 'abs');
}
return getHeight(gridH, 'abs'); // ocean: grid height
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) {
const packH = pack.cells.h[findCell(p[0], p[1])];
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
const h = packH < 20 ? gridH : packH;
return getHeight(h);
}
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === 'm') unitRatio = 1;
// if meter
else if (unit === 'f') unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = ((h - 20) / h) * 50;
if (abs) height = Math.abs(height);
return rn(height * unitRatio) + ' ' + unit;
}
// get user-friendly (real-world) precipitation value from map data
function getFriendlyPrecipitation(i) {
const prec = grid.cells.prec[pack.cells.g[i]];
return prec * 100 + ' mm';
}
function getRiverInfo(id) {
const r = pack.rivers.find((r) => r.i == id);
return r ? `${r.name} ${r.type} (${id})` : 'n/a';
}
function getCellPopulation(i) {
const rural = pack.cells.pop[i] * populationRate;
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate * urbanization : 0;
return [rural, urban];
}
// get user-friendly (real-world) population value from map data
function getFriendlyPopulation(i) {
const [rural, urban] = getCellPopulation(i);
return `${si(rural + urban)} (${si(rural)} rural, urban ${si(urban)})`;
}
function getPopulationTip(i) {
const [rural, urban] = getCellPopulation(i);
return `Cell population: ${si(rural + urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`;
}
function highlightEmblemElement(type, el) {
const i = el.i,
cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
if (type === 'burg') {
const {x, y} = el;
debug
<<<<<<< HEAD
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', 'none')
.attr('stroke', '#d0240f')
.attr('stroke-width', 1)
.attr('opacity', 1)
.transition(animation)
.attr('r', 20)
.attr('opacity', 0.1)
.attr('stroke-width', 0)
=======
.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 0)
.attr("fill", "none")
.attr("stroke", "#d0240f")
.attr("stroke-width", 1)
.attr("opacity", 1)
.transition(animation)
.attr("r", 20)
.attr("opacity", 0.1)
.attr("stroke-width", 0)
>>>>>>> master
.remove();
return;
}
const [x, y] = el.pole || pack.cells.p[el.center];
const obj = type === 'state' ? cells.state : cells.province;
const borderCells = cells.i.filter((id) => obj[id] === i && cells.c[id].some((n) => obj[n] !== i));
const data = Array.from(borderCells)
.filter((c, i) => !(i % 2))
.map((i) => cells.p[i])
.map((i) => [i[0], i[1], Math.hypot(i[0] - x, i[1] - y)]);
debug
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', x)
.attr('y1', y)
.attr('x2', (d) => d[0])
.attr('y2', (d) => d[1])
.attr('stroke', '#d0240f')
.attr('stroke-width', 0.5)
.attr('opacity', 0.2)
.attr('stroke-dashoffset', (d) => d[2])
.attr('stroke-dasharray', (d) => d[2])
.transition(animation)
.attr('stroke-dashoffset', 0)
.attr('opacity', 1)
.transition(animation)
.delay(1000)
.attr('stroke-dashoffset', (d) => d[2])
.attr('opacity', 0)
.remove();
}
// assign lock behavior
document.querySelectorAll('[data-locked]').forEach(function (e) {
e.addEventListener('mouseover', function (event) {
if (this.className === 'icon-lock') tip('Click to unlock the option and allow it to be randomized on new map generation');
else tip('Click to lock the option and always use the current value on new map generation');
event.stopPropagation();
});
e.addEventListener('click', function () {
const id = this.id.slice(5);
if (this.className === 'icon-lock') unlock(id);
else lock(id);
});
});
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='" + id + "']");
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 1;
el.className = 'icon-lock';
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 0;
el.className = 'icon-lock-open';
}
// check if option is locked
function locked(id) {
const lockEl = document.getElementById('lock_' + id);
return lockEl.dataset.locked == 1;
}
// check if option is stored in localStorage
function stored(option) {
return localStorage.getItem(option);
}
// assign skeaker behaviour
Array.from(document.getElementsByClassName('speaker')).forEach((el) => {
const input = el.previousElementSibling;
el.addEventListener('click', () => speak(input.value));
});
function speak(text) {
const speaker = new SpeechSynthesisUtterance(text);
const voices = speechSynthesis.getVoices();
if (voices.length) {
const voiceId = +document.getElementById('speakerVoice').value;
speaker.voice = voices[voiceId];
}
speechSynthesis.speak(speaker);
}
// apply drop-down menu option. If the value is not in options, add it
function applyOption(select, id, name = id) {
const custom = !Array.from(select.options).some((o) => o.value == id);
if (custom) select.options.add(new Option(name, id));
select.value = id;
}
// show info about the generator in a popup
function showInfo() {
const Discord = link('https://discordapp.com/invite/X7E84HU', 'Discord');
const Reddit = link('https://www.reddit.com/r/FantasyMapGenerator', 'Reddit');
const Patreon = link('https://www.patreon.com/azgaar', 'Patreon');
const Trello = link('https://trello.com/b/7x832DG4/fantasy-map-generator', 'Trello');
const Armoria = link('https://azgaar.github.io/Armoria', 'Armoria');
const QuickStart = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial', 'Quick start tutorial');
const QAA = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A', 'Q&A page');
alertMessage.innerHTML = `
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them.
<p>The development is supported by community, you can donate on ${Patreon}.
You can also help creating overviews, tutorials and spreding the word about the Generator.</p>
<p>The best way to get help is to contact the community on ${Discord} and ${Reddit}.
Before asking questions, please check out the ${QuickStart} and the ${QAA}.</p>
<p>Track the development process on ${Trello}.</p>
<p>Check out our new project: ${Armoria}, heraldry generator and editor.</p>
<b>Links:</b>
<ul style="columns:2">
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator', 'GitHub repository')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE', 'License')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'Changelog')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys', 'Hotkeys')}</li>
</ul>`;
$('#alert').dialog({
resizable: false,
title: document.title,
width: '28em',
buttons: {
OK: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
}
<<<<<<< HEAD
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener('keydown', (event) => {
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keyup', (event) => {
if (!window.closeDialogs) return; // not all modules are loaded
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
const active = document.activeElement.tagName;
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
event.stopPropagation();
const key = event.keyCode;
const ctrl = event.ctrlKey || event.metaKey || key === 17;
const shift = event.shiftKey || key === 16;
const alt = event.altKey || key === 18;
if (key === 112) showInfo();
// "F1" to show info
else if (key === 113) regeneratePrompt();
// "F2" for new map
else if (key === 113) regeneratePrompt();
// "F2" for a new map
else if (key === 117) quickSave();
// "F6" for quick save
else if (key === 120) quickLoad();
// "F9" for quick load
else if (key === 9) toggleOptions(event);
// Tab to toggle options
else if (key === 27) {
closeDialogs();
hideOptions();
} // Escape to close all dialogs
else if (key === 46) removeElementOnKey();
// "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions();
// "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder();
// Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap();
// Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click();
// Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click();
// Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap();
// Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes();
// Shift + "B" to edit Biomes
else if (shift && key === 83) editStates();
// Shift + "S" to edit States
else if (shift && key === 80) editProvinces();
// Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy();
// Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures();
// Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase();
// Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones();
// Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions();
// Shift + "R" to edit Religions
else if (shift && key === 81) editResources();
// Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor();
// Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits();
// Shift + "W" to edit Units
else if (shift && key === 79) editNotes();
// Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs();
// Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers();
// Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary();
// Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails();
// Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg();
// Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel();
// Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver();
// Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute();
// Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker();
// Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs);
// Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states);
// Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures);
// Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions);
// Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features);
// Alt + "F" to log features data
else if (key === 88) toggleTexture();
// "X" to toggle Texture layer
else if (key === 72) toggleHeight();
// "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes();
// "B" to toggle Biomes layer
else if (key === 69) toggleCells();
// "E" to toggle Cells layer
else if (key === 71) toggleGrid();
// "G" to toggle Grid layer
else if (key === 79) toggleCoordinates();
// "O" to toggle Coordinates layer
else if (key === 87) toggleCompass();
// "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers();
// "V" to toggle Rivers layer
else if (key === 70) toggleRelief();
// "F" to toggle Relief icons layer
else if (key === 67) toggleCultures();
// "C" to toggle Cultures layer
else if (key === 83) toggleStates();
// "S" to toggle States layer
else if (key === 80) toggleProvinces();
// "P" to toggle Provinces layer
else if (key === 90) toggleZones();
// "Z" to toggle Zones
else if (key === 68) toggleBorders();
// "D" to toggle Borders layer
else if (key === 82) toggleReligions();
// "R" to toggle Religions layer
else if (key === 85) toggleRoutes();
// "U" to toggle Routes layer
else if (key === 84) toggleTemp();
// "T" to toggle Temperature layer
else if (key === 78) togglePopulation();
// "N" to toggle Population layer
else if (key === 74) toggleIce();
// "J" to toggle Ice layer
else if (key === 65) togglePrec();
// "A" to toggle Precipitation layer
else if (key === 81) toggleResources();
// "Q" to toggle Resources layer
else if (key === 89) toggleEmblems();
// "Y" to toggle Emblems layer
else if (key === 76) toggleLabels();
// "L" to toggle Labels layer
else if (key === 73) toggleIcons();
// "I" to toggle Icons layer
else if (key === 77) toggleMilitary();
// "M" to toggle Military layer
else if (key === 75) toggleMarkers();
// "K" to toggle Markers layer
else if (key === 187) toggleRulers();
// Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar();
// Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0);
// Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0);
// Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10);
// Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10);
// Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key);
// Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000);
// 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
// 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
// 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
// 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
// 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
// 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
// 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
// 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
// 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
// 9 to zoom to 9
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$('.dialog:visible .fastDelete').click();
$("button:visible:contains('Remove')").click();
}
=======
>>>>>>> master

View file

@ -7,8 +7,8 @@ function editHeightmap() {
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Check out ${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization', 'wiki')} for guidance.</p>`;
<p>Please <span class="pseudoLink" onclick=dowloadMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p style="margin-bottom: 0">Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
$('#alert').dialog({
resizable: false,
@ -222,7 +222,7 @@ function editHeightmap() {
Lakes.generateName();
Military.generate();
addMarkers();
Markers.generate();
addZones();
TIME && console.timeEnd('regenerateErasedData');
INFO && console.groupEnd('Edit Heightmap');
@ -334,10 +334,10 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
const land = pack.cells.h[i] >= 20;
const isLand = pack.cells.h[i] >= 20;
// check biome
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
pack.cells.biome[i] = isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]);
// rivers data
if (!erosionAllowed) {
@ -346,7 +346,7 @@ function editHeightmap() {
pack.cells.fl[i] = fl[g];
}
if (!land) continue;
if (!isLand) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
@ -614,7 +614,7 @@ function editHeightmap() {
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {
return Math.max(Math.min(v, 100), land ? 20 : 0);
return minmax(v, land ? 20 : 0, 100);
}
const h = grid.cells.h;
@ -626,6 +626,8 @@ function editHeightmap() {
else if (brush === 'brushAlign') s.forEach((i) => (h[i] = lim(h[start])));
else if (brush === 'brushSmooth') s.forEach((i) => (h[i] = rn((d3.mean(grid.cells.c[i].filter((i) => (land ? h[i] >= 20 : 1)).map((c) => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1)));
else if (brush === 'brushDisrupt') s.forEach((i) => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power)));
i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1))
);
mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step
@ -775,6 +777,7 @@ function editHeightmap() {
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4 || '15-85'}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3 || '40-50'}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count || '1-2'}></span>`;
}></span>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === 'Hill' || type === 'Pit' || type === 'Range' || type === 'Trough') return blob;
@ -792,6 +795,8 @@ function editHeightmap() {
} min=0 max=10 step=.1></span></div>`;
if (type === 'Smooth')
return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count || 2}></span></div>`;
count || 2
}></span></div>`;
}
function setRange(event) {
@ -853,31 +858,27 @@ function editHeightmap() {
const steps = body.querySelectorAll('#templateBody > div');
if (!steps.length) return;
const {addHill, addPit, addRange, addTrough, addStrait, modify, smooth} = HeightmapGenerator;
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.dataset.type;
for (const step of steps) {
if (step.style.opacity === "0.5") continue;
const type = step.dataset.type;
const elCount = s.querySelector('.templateCount') || '';
const elHeight = s.querySelector('.templateHeight') || '';
const count = step.querySelector(".templateCount")?.value || "";
const height = step.querySelector(".templateHeight")?.value || "";
const dist = step.querySelector(".templateDist")?.value || null;
const x = step.querySelector(".templateX")?.value || null;
const y = step.querySelector(".templateY")?.value || null;
const elDist = s.querySelector('.templateDist');
const dist = elDist ? elDist.value : null;
const templateX = s.querySelector('.templateX');
const x = templateX ? templateX.value : null;
const templateY = s.querySelector('.templateY');
const y = templateY ? templateY.value : null;
if (type === 'Hill') HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y);
else if (type === 'Pit') HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y);
else if (type === 'Range') HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y);
else if (type === 'Trough') HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y);
else if (type === 'Strait') HeightmapGenerator.addStrait(elCount.value, dist);
else if (type === 'Add') HeightmapGenerator.modify(dist, +elCount.value, 1);
else if (type === 'Multiply') HeightmapGenerator.modify(dist, 0, +elCount.value);
else if (type === 'Smooth') HeightmapGenerator.smooth(+elCount.value);
if (type === "Hill") addHill(count, height, x, y);
else if (type === "Pit") addPit(count, height, x, y);
else if (type === "Range") addRange(count, height, x, y);
else if (type === "Trough") addTrough(count, height, x, y);
else if (type === "Strait") addStrait(count, dist);
else if (type === "Add") modify(dist, +count, 1);
else if (type === "Multiply") modify(dist, 0, +count);
else if (type === "Smooth") smooth(+count);
updateHistory('noStat'); // update history every step
}
@ -896,17 +897,13 @@ function editHeightmap() {
let data = '';
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.getAttribute('data-type');
if (s.style.opacity === "0.5") continue;
const elCount = s.querySelector('.templateCount');
const count = elCount ? elCount.value : '0';
const elHeight = s.querySelector('.templateHeight');
const elDist = s.querySelector('.templateDist');
const arg3 = elHeight ? elHeight.value : elDist ? elDist.value : '0';
const templateX = s.querySelector('.templateX');
const x = templateX ? templateX.value : '0';
const templateY = s.querySelector('.templateY');
const y = templateY ? templateY.value : '0';
const count = s.querySelector(".templateCount")?.value || "0";
const arg3 = s.querySelector(".templateHeight")?.value || s.querySelector(".templateDist")?.value || "0";
const x = s.querySelector(".templateX")?.value || "0";
const y = s.querySelector(".templateY")?.value || "0";
data += `${type} ${count} ${arg3} ${x} ${y}\r\n`;
}
@ -1194,10 +1191,14 @@ function editHeightmap() {
}
function setConvertColorsNumber() {
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`, {default: +convertColors.value, step: 1, min: 3, max: 255}, (number) => {
convertColors.value = number;
heightsFromImage(number);
});
prompt(
`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`,
{default: +convertColors.value, step: 1, min: 3, max: 255},
number => {
convertColors.value = number;
heightsFromImage(number);
}
);
}
function setOverlayOpacity(v) {

File diff suppressed because it is too large Load diff

153
modules/ui/hotkeys.js Normal file
View file

@ -0,0 +1,153 @@
'use strict';
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keydown', handleKeydown);
document.addEventListener('keyup', handleKeyup);
function handleKeydown(event) {
const {code, ctrlKey, altKey} = event;
if (altKey && !ctrlKey) event.preventDefault(); // disallow alt key combinations
if (ctrlKey && ['KeyS', 'KeyC'].includes(code)) event.preventDefault(); // disallow CTRL + S and CTRL + C
if (['F1', 'F2', 'F6', 'F9', 'Tab'].includes(code)) event.preventDefault(); // disallow default Fn and Tab
}
function handleKeyup(event) {
if (!modules.editors) return; // if editors are not loaded, do nothing
const {tagName, contentEditable} = document.activeElement;
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(tagName)) return; // don't trigger if user inputs text
if (tagName === 'DIV' && contentEditable === 'true') return; // don't trigger if user inputs a text
if (document.getSelection().toString()) return; // don't trigger if user selects text
event.stopPropagation();
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
const ctrl = ctrlKey || metaKey || key === 'Control';
const shift = shiftKey || key === 'Shift';
const alt = altKey || key === 'Alt';
if (code === 'F1') showInfo();
else if (code === 'F2') regeneratePrompt('hotkey');
else if (code === 'F6') quickSave();
else if (code === 'F9') quickLoad();
else if (code === 'Tab') toggleOptions(event);
else if (code === 'Escape') closeAllDialogs();
else if (code === 'Delete') removeElementOnKey();
else if (code === 'KeyO' && document.getElementById('canvas3d')) toggle3dOptions();
else if (ctrl && code === 'KeyQ') toggleSaveReminder();
else if (ctrl && code === 'KeyS') dowloadMap();
else if (ctrl && code === 'KeyC') saveToDropbox();
else if (ctrl && code === 'KeyZ' && undo.offsetParent) undo.click();
else if (ctrl && code === 'KeyY' && redo.offsetParent) redo.click();
else if (shift && code === 'KeyH') editHeightmap();
else if (shift && code === 'KeyB') editBiomes();
else if (shift && code === 'KeyS') editStates();
else if (shift && code === 'KeyP') editProvinces();
else if (shift && code === 'KeyD') editDiplomacy();
else if (shift && code === 'KeyC') editCultures();
else if (shift && code === 'KeyN') editNamesbase();
else if (shift && code === 'KeyZ') editZones();
else if (shift && code === 'KeyR') editReligions();
else if (shift && code === 'KeyY') openEmblemEditor();
else if (shift && code === 'KeyQ') editUnits();
else if (shift && code === 'KeyO') editNotes();
else if (shift && code === 'KeyT') overviewBurgs();
else if (shift && code === 'KeyV') overviewRivers();
else if (shift && code === 'KeyM') overviewMilitary();
else if (shift && code === 'KeyK') overviewMarkers();
else if (shift && code === 'KeyE') viewCellDetails();
else if (key === '!') toggleAddBurg();
else if (key === '@') toggleAddLabel();
else if (key === '#') toggleAddRiver();
else if (key === '$') toggleAddRoute();
else if (key === '%') toggleAddMarker();
else if (alt && code === 'KeyB') console.table(pack.burgs);
else if (alt && code === 'KeyS') console.table(pack.states);
else if (alt && code === 'KeyC') console.table(pack.cultures);
else if (alt && code === 'KeyR') console.table(pack.religions);
else if (alt && code === 'KeyF') console.table(pack.features);
else if (code === 'KeyX') toggleTexture();
else if (code === 'KeyH') toggleHeight();
else if (code === 'KeyB') toggleBiomes();
else if (code === 'KeyE') toggleCells();
else if (code === 'KeyG') toggleGrid();
else if (code === 'KeyO') toggleCoordinates();
else if (code === 'KeyW') toggleCompass();
else if (code === 'KeyV') toggleRivers();
else if (code === 'KeyF') toggleRelief();
else if (code === 'KeyC') toggleCultures();
else if (code === 'KeyS') toggleStates();
else if (code === 'KeyP') toggleProvinces();
else if (code === 'KeyZ') toggleZones();
else if (code === 'KeyD') toggleBorders();
else if (code === 'KeyR') toggleReligions();
else if (code === 'KeyU') toggleRoutes();
else if (code === 'KeyT') toggleTemp();
else if (code === 'KeyN') togglePopulation();
else if (code === 'KeyJ') toggleIce();
else if (code === 'KeyA') togglePrec();
else if (code === 'KeyY') toggleEmblems();
else if (code === 'KeyL') toggleLabels();
else if (code === 'KeyI') toggleIcons();
else if (code === 'KeyM') toggleMilitary();
else if (code === 'KeyK') toggleMarkers();
else if (code === 'Equal') toggleRulers();
else if (code === 'Slash') toggleScaleBar();
else if (code === 'ArrowLeft') zoom.translateBy(svg, 10, 0);
else if (code === 'ArrowRight') zoom.translateBy(svg, -10, 0);
else if (code === 'ArrowUp') zoom.translateBy(svg, 0, 10);
else if (code === 'ArrowDown') zoom.translateBy(svg, 0, -10);
else if (key === '+' || key === '-') pressNumpadSign(key);
else if (key === '0') resetZoom(1000);
else if (key === '1') zoom.scaleTo(svg, 1);
else if (key === '2') zoom.scaleTo(svg, 2);
else if (key === '3') zoom.scaleTo(svg, 3);
else if (key === '4') zoom.scaleTo(svg, 4);
else if (key === '5') zoom.scaleTo(svg, 5);
else if (key === '6') zoom.scaleTo(svg, 6);
else if (key === '7') zoom.scaleTo(svg, 7);
else if (key === '8') zoom.scaleTo(svg, 8);
else if (key === '9') zoom.scaleTo(svg, 9);
else if (ctrl) toggleMode();
}
function pressNumpadSign(key) {
const change = key === '+' ? 1 : -1;
let brush = null;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = minmax(+brush.value + change, +brush.min, +brush.max);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === '+' ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no brush elements displayed, zoom map
}
function toggleMode() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
function removeElementOnKey() {
const fastDelete = Array.from(document.querySelectorAll("[role='dialog'] .fastDelete")).find((dialog) => dialog.style.display !== 'none');
if (fastDelete) fastDelete.click();
const visibleDialogs = Array.from(document.querySelectorAll("[role='dialog']")).filter((dialog) => dialog.style.display !== 'none');
if (!visibleDialogs.length) return;
visibleDialogs.forEach((dialog) => dialog.querySelectorAll('button').forEach((button) => button.textContent === 'Remove' && button.click()));
}
function closeAllDialogs() {
closeDialogs();
hideOptions();
}

View file

@ -51,8 +51,8 @@ function editLabel() {
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.parentNode.id === elSelected.attr('id')) tip('Drag to shift the label');
else if (d3.event.target.parentNode.id === 'controlPoints') {
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label");
else if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === 'circle') tip('Drag to move, click to delete the control point');
if (d3.event.target.tagName === 'path') tip('Click to add a control point');
}
@ -253,7 +253,13 @@ function editLabel() {
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?<br><br>Labels to be removed: ${count}`;
const onConfirm = () => {
$('#labelEditor').dialog('close');
hideGroupSection();
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
labels
.select('#' + group)
.selectAll('text')
@ -357,10 +363,13 @@ function editLabel() {
const message = 'Are you sure you want to remove the label? <br>This action cannot be reverted';
const onConfirm = () => {
defs.select('#textPath_' + elSelected.attr('id')).remove();
title: "Remove label",
elSelected.remove();
$('#labelEditor').dialog('close');
};
confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm});
$(this).dialog("close");
}
}
function closeLabelEditor() {

View file

@ -0,0 +1,549 @@
'use strict';
function editLabel() {
if (customization) return;
closeDialogs();
if (!layerIsOn('toggleLabels')) toggleLabels();
const tspan = d3.event.target;
const textPath = tspan.parentNode;
const text = textPath.parentNode;
<<<<<<< HEAD
elSelected = d3.select(text).call(d3.drag().on('start', dragLabel)).classed('draggable', true);
viewbox.on('touchmove mousemove', showEditorTips);
$('#labelEditor').dialog({
title: 'Edit Label',
resizable: false,
width: fitContent(),
position: {my: 'center top+10', at: 'bottom', of: text, collision: 'fit'},
=======
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label",
resizable: false,
width: fitContent(),
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
>>>>>>> master
close: closeLabelEditor
});
drawControlPointsAndLine();
selectLabelGroup(text);
updateValues(textPath);
if (modules.editLabel) return;
modules.editLabel = true;
// add listeners
document.getElementById('labelGroupShow').addEventListener('click', showGroupSection);
document.getElementById('labelGroupHide').addEventListener('click', hideGroupSection);
document.getElementById('labelGroupSelect').addEventListener('click', changeGroup);
document.getElementById('labelGroupInput').addEventListener('change', createNewGroup);
document.getElementById('labelGroupNew').addEventListener('click', toggleNewGroupInput);
document.getElementById('labelGroupRemove').addEventListener('click', removeLabelsGroup);
document.getElementById('labelTextShow').addEventListener('click', showTextSection);
document.getElementById('labelTextHide').addEventListener('click', hideTextSection);
document.getElementById('labelText').addEventListener('input', changeText);
document.getElementById('labelTextRandom').addEventListener('click', generateRandomName);
document.getElementById('labelEditStyle').addEventListener('click', editGroupStyle);
document.getElementById('labelSizeShow').addEventListener('click', showSizeSection);
document.getElementById('labelSizeHide').addEventListener('click', hideSizeSection);
document.getElementById('labelStartOffset').addEventListener('input', changeStartOffset);
document.getElementById('labelRelativeSize').addEventListener('input', changeRelativeSize);
document.getElementById('labelAlign').addEventListener('click', editLabelAlign);
document.getElementById('labelLegend').addEventListener('click', editLabelLegend);
document.getElementById('labelRemoveSingle').addEventListener('click', removeLabel);
function showEditorTips() {
showMainTip();
<<<<<<< HEAD
if (d3.event.target.parentNode.parentNode.id === elSelected.attr('id')) tip('Drag to shift the label');
else if (d3.event.target.parentNode.id === 'controlPoints') {
if (d3.event.target.tagName === 'circle') tip('Drag to move, click to delete the control point');
if (d3.event.target.tagName === 'path') tip('Click to add a control point');
=======
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label");
else if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
>>>>>>> master
}
}
function selectLabelGroup(text) {
const group = text.parentNode.id;
const select = document.getElementById('labelGroupSelect');
select.options.length = 0; // remove all options
<<<<<<< HEAD
labels.selectAll(':scope > g').each(function () {
if (this.id === 'burgLabels') return;
=======
labels.selectAll(":scope > g").each(function () {
if (this.id === "burgLabels") return;
>>>>>>> master
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function updateValues(textPath) {
document.getElementById('labelText').value = [...textPath.querySelectorAll('tspan')].map((tspan) => tspan.textContent).join('|');
document.getElementById('labelStartOffset').value = parseFloat(textPath.getAttribute('startOffset'));
document.getElementById('labelRelativeSize').value = parseFloat(textPath.getAttribute('font-size'));
}
function drawControlPointsAndLine() {
debug.select('#controlPoints').remove();
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
const path = document.getElementById('textPath_' + elSelected.attr('id'));
debug.select('#controlPoints').append('path').attr('d', path.getAttribute('d')).on('click', addInterimControlPoint);
const l = path.getTotalLength();
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
for (let i = 0; i <= l; i += increment) {
addControlPoint(path.getPointAtLength(i));
}
}
function addControlPoint(point) {
<<<<<<< HEAD
debug
.select('#controlPoints')
.append('circle')
.attr('cx', point.x)
.attr('cy', point.y)
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.call(d3.drag().on('drag', dragControlPoint))
.on('click', clickControlPoint);
=======
debug.select("#controlPoints").append("circle").attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", 0.8).call(d3.drag().on("drag", dragControlPoint)).on("click", clickControlPoint);
>>>>>>> master
}
function dragControlPoint() {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawLabelPath();
}
function redrawLabelPath() {
const path = document.getElementById('textPath_' + elSelected.attr('id'));
lineGen.curve(d3.curveBundle.beta(1));
const points = [];
debug
<<<<<<< HEAD
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([this.getAttribute('cx'), this.getAttribute('cy')]);
=======
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
>>>>>>> master
});
const d = round(lineGen(points));
path.setAttribute('d', d);
debug.select('#controlPoints > path').attr('d', d);
}
function clickControlPoint() {
this.remove();
redrawLabelPath();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug
<<<<<<< HEAD
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const x = +this.getAttribute('cx');
const y = +this.getAttribute('cy');
=======
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
>>>>>>> master
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a - b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest + 1;
else index = next + 1;
}
<<<<<<< HEAD
const before = ':nth-child(' + (index + 2) + ')';
debug
.select('#controlPoints')
.insert('circle', before)
.attr('cx', point[0])
.attr('cy', point[1])
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.call(d3.drag().on('drag', dragControlPoint))
.on('click', clickControlPoint);
=======
const before = ":nth-child(" + (index + 2) + ")";
debug.select("#controlPoints").insert("circle", before).attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", 0.8).call(d3.drag().on("drag", dragControlPoint)).on("click", clickControlPoint);
>>>>>>> master
redrawLabelPath();
}
function dragLabel() {
<<<<<<< HEAD
const tr = parseTransform(elSelected.attr('transform'));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on('drag', function () {
const x = d3.event.x,
y = d3.event.y;
const transform = `translate(${dx + x},${dy + y})`;
elSelected.attr('transform', transform);
debug.select('#controlPoints').attr('transform', transform);
=======
const tr = parseTransform(elSelected.attr("transform"));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on("drag", function () {
const x = d3.event.x,
y = d3.event.y;
const transform = `translate(${dx + x},${dy + y})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
>>>>>>> master
});
}
function showGroupSection() {
<<<<<<< HEAD
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelGroupSection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelGroupSection').style.display = 'none';
document.getElementById('labelGroupInput').style.display = 'none';
document.getElementById('labelGroupInput').value = '';
document.getElementById('labelGroupSelect').style.display = 'inline-block';
=======
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = "";
document.getElementById("labelGroupSelect").style.display = "inline-block";
>>>>>>> master
}
function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
if (labelGroupInput.style.display === 'none') {
labelGroupInput.style.display = 'inline-block';
labelGroupInput.focus();
labelGroupSelect.style.display = 'none';
} else {
<<<<<<< HEAD
labelGroupInput.style.display = 'none';
labelGroupSelect.style.display = 'inline-block';
=======
labelGroupInput.style.display = "none";
labelGroupSelect.style.display = "inline-block";
>>>>>>> master
}
}
function createNewGroup() {
if (!this.value) {
<<<<<<< HEAD
tip('Please provide a valid group name');
=======
tip("Please provide a valid group name");
>>>>>>> master
return;
}
const group = this.value
.toLowerCase()
<<<<<<< HEAD
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
=======
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
>>>>>>> master
if (document.getElementById(group)) {
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip('Group name should start with a letter', false, 'error');
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
if (oldGroup !== 'states' && oldGroup !== 'addedLabels' && oldGroup.childElementCount === 1) {
document.getElementById('labelGroupSelect').selectedOptions[0].remove();
document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById('labelGroupInput').value = '';
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById('labels').appendChild(newGroup);
newGroup.id = group;
document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById('labelGroupInput').value = '';
}
function removeLabelsGroup() {
const group = elSelected.node().parentNode.id;
const basic = group === 'states' || group === 'addedLabels';
const count = elSelected.node().parentNode.childElementCount;
<<<<<<< HEAD
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?<br><br>Labels to be removed: ${count}`;
const onConfirm = () => {
$('#labelEditor').dialog('close');
hideGroupSection();
labels
.select('#' + group)
.selectAll('text')
.each(function () {
document.getElementById('textPath_' + this.id).remove();
this.remove();
});
if (!basic) labels.select('#' + group).remove();
};
confirmationDialog({title: 'Remove label group', message, confirm: 'Remove', onConfirm});
}
function showTextSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelTextSection').style.display = 'inline-block';
}
function hideTextSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelTextSection').style.display = 'none';
}
function changeText() {
const input = document.getElementById('labelText').value;
const el = elSelected.select('textPath').node();
const example = d3.select(elSelected.node().parentNode).append('text').attr('x', 0).attr('x', 0).attr('font-size', el.getAttribute('font-size')).node();
=======
alertMessage.innerHTML = `Are you sure you want to remove
${basic ? "all elements in the group" : "the entire label group"}?
<br><br>Labels to be removed: ${count}`;
$("#alert").dialog({
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
labels
.select("#" + group)
.selectAll("text")
.each(function () {
document.getElementById("textPath_" + this.id).remove();
this.remove();
});
if (!basic) labels.select("#" + group).remove();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none";
}
function changeText() {
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node();
>>>>>>> master
const lines = input.split('|');
const top = (lines.length - 1) / -2; // y offset
const inner = lines
.map((l, d) => {
example.innerHTML = l;
const left = example.getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
})
<<<<<<< HEAD
.join('');
=======
.join("");
>>>>>>> master
el.innerHTML = inner;
example.remove();
<<<<<<< HEAD
if (elSelected.attr('id').slice(0, 10) === 'stateLabel') tip('Use States Editor to change an actual state name, not just a label', false, 'warning');
}
function generateRandomName() {
let name = '';
if (elSelected.attr('id').slice(0, 10) === 'stateLabel') {
const id = +elSelected.attr('id').slice(10);
=======
if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {
let name = "";
if (elSelected.attr("id").slice(0, 10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
>>>>>>> master
const culture = pack.states[id].culture;
name = Names.getState(Names.getCulture(culture, 4, 7, ''), culture);
} else {
const box = elSelected.node().getBBox();
const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2);
const culture = pack.cells.culture[cell];
name = Names.getCulture(culture);
}
document.getElementById('labelText').value = name;
changeText();
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle('labels', g);
}
function showSizeSection() {
<<<<<<< HEAD
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelSizeSection').style.display = 'inline-block';
}
function hideSizeSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelSizeSection').style.display = 'none';
=======
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelSizeSection").style.display = "none";
>>>>>>> master
}
function changeStartOffset() {
elSelected.select('textPath').attr('startOffset', this.value + '%');
tip('Label offset: ' + this.value + '%');
}
function changeRelativeSize() {
elSelected.select('textPath').attr('font-size', this.value + '%');
tip('Label relative size: ' + this.value + '%');
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
<<<<<<< HEAD
const path = defs.select('#textPath_' + elSelected.attr('id'));
path.attr('d', `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
=======
const path = defs.select("#textPath_" + elSelected.attr("id"));
path.attr("d", `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
>>>>>>> master
drawControlPointsAndLine();
}
function editLabelLegend() {
const id = elSelected.attr('id');
const name = elSelected.text();
editNotes(id, name);
}
function removeLabel() {
<<<<<<< HEAD
const message = 'Are you sure you want to remove the label? <br>This action cannot be reverted';
const onConfirm = () => {
defs.select('#textPath_' + elSelected.attr('id')).remove();
elSelected.remove();
$('#labelEditor').dialog('close');
};
confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm});
=======
alertMessage.innerHTML = "Are you sure you want to remove the label?";
$("#alert").dialog({
resizable: false,
title: "Remove label",
buttons: {
Remove: function () {
$(this).dialog("close");
defs.select("#textPath_" + elSelected.attr("id")).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
>>>>>>> master
}
function closeLabelEditor() {
debug.select('#controlPoints').remove();
unselect();
}
}

View file

@ -48,8 +48,7 @@ function changePreset(preset) {
.querySelectorAll('li')
.forEach(function (e) {
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
// turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
});
layersPreset.value = preset;
localStorage.setItem('preset', preset);
@ -122,6 +121,7 @@ function restoreLayers() {
if (layerIsOn('toggleReligions')) drawReligions();
if (layerIsOn('toggleIce')) drawIce();
if (layerIsOn('toggleEmblems')) drawEmblems();
if (layerIsOn('toggleMarkers')) drawMarkers();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn('toggleBorders')) borders.selectAll('path').remove();
@ -1419,8 +1419,8 @@ function toggleTexture(event) {
turnButtonOn('toggleTexture');
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll('*').size()) {
const x = +styleTextureShiftX.value,
y = +styleTextureShiftY.value;
const x = +styleTextureShiftX.value;
const y = +styleTextureShiftY.value;
const image = texture
.append('image')
.attr('id', 'textureImage')
@ -1430,16 +1430,13 @@ function toggleTexture(event) {
.attr('height', graphHeight - y)
.attr('xlink:href', getDefaultTexture())
.attr('preserveAspectRatio', 'xMidYMid slice');
if (styleTextureInput.value !== 'default') getBase64(styleTextureInput.value, (base64) => image.attr('xlink:href', base64));
getBase64(styleTextureInput.value, (base64) => image.attr('xlink:href', base64));
}
$('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
if (event && isCtrlClick(event)) editStyle('texture');
} else {
if (event && isCtrlClick(event)) {
editStyle('texture');
return;
}
if (event && isCtrlClick(event)) return editStyle('texture');
$('#texture').fadeOut();
turnButtonOff('toggleTexture');
}
@ -1459,14 +1456,16 @@ function toggleRivers(event) {
function drawRivers() {
TIME && console.time('drawRivers');
rivers.selectAll('*').remove();
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map((river) => {
const meanderedPoints = addMeandering(river.cells, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.sourceWidth || 0;
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
return `<path id="river${river.i}" d="${path}"/>`;
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
if (!cells || cells.length < 2) return;
const meanderedPoints = addMeandering(cells, points);
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
return `<path id="river${i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(''));
TIME && console.timeEnd('drawRivers');
@ -1505,18 +1504,51 @@ function toggleMilitary() {
function toggleMarkers(event) {
if (!layerIsOn('toggleMarkers')) {
turnButtonOn('toggleMarkers');
$('#markers').fadeIn();
drawMarkers();
if (event && isCtrlClick(event)) editStyle('markers');
} else {
if (event && isCtrlClick(event)) {
editStyle('markers');
return;
}
$('#markers').fadeOut();
if (event && isCtrlClick(event)) return editStyle('markers');
markers.selectAll('*').remove();
turnButtonOff('toggleMarkers');
}
}
function 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(''));
}
const getPin = (shape = 'bubble', fill = '#fff', stroke = '#000') => {
if (shape === 'bubble') return `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`;
if (shape === 'pin') return `<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}"/>`;
if (shape === 'square') return `<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}"/>`;
if (shape === 'squarish') return `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'diamond') return `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'hex') return `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'hexy') return `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'shieldy') return `<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}" />`;
if (shape === 'shield') return `<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}" />`;
if (shape === 'pentagon') return `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'heptagon') return `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'circle') return `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'no') return '';
};
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 pinHTML = getPin(pin, fill, stroke);
return `<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}"><g>${pinHTML}</g><text x="${dx}%" y="${dy}%" font-size="${px}px" >${icon}</text></svg>`;
}
function toggleLabels(event) {
if (!layerIsOn('toggleLabels')) {
turnButtonOn('toggleLabels');
@ -1620,21 +1652,21 @@ function drawEmblems() {
const validBurgs = burgs.filter((b) => b.i && !b.removed && b.coa && b.coaSize != 0);
const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById('emblemsStateSizeInput').value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('emblemsProvinceSizeInput').value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('emblemsBurgSizeInput').value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
@ -1685,11 +1717,9 @@ function drawEmblems() {
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('');

1970
modules/ui/layers.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,291 +1,261 @@
'use strict';
function editMarker() {
function editMarker(markerI) {
if (customization) return;
closeDialogs('#markerEditor, .stable');
$('#markerEditor').dialog();
closeDialogs(".stable");
const [element, marker] = getElement(markerI, d3.event);
if (!marker || !element) return;
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
elSelected = d3.select(d3.event.target).call(d3.drag().on('start', dragMarker)).classed('draggable', true);
updateInputs();
if (modules.editMarker) return;
modules.editMarker = true;
$('#markerEditor').dialog({
title: 'Edit Marker',
title: "Edit Marker",
resizable: false,
position: {my: 'center top+30', at: 'bottom', of: d3.event, collision: 'fit'},
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeMarkerEditor
});
// add listeners
document.getElementById('markerGroup').addEventListener('click', toggleGroupSection);
document.getElementById('markerAddGroup').addEventListener('click', toggleGroupInput);
document.getElementById('markerSelectGroup').addEventListener('change', changeGroup);
document.getElementById('markerInputGroup').addEventListener('change', createGroup);
document.getElementById('markerRemoveGroup').addEventListener('click', removeGroup);
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
listen(markerSize, "input", changeMarkerSize),
listen(markerPin, "change", changeMarkerPin),
listen(markerFill, "input", changePinFill),
listen(markerStroke, "input", changePinStroke),
listen(markerNotes, "click", editMarkerLegend),
listen(markerLock, "click", toggleMarkerLock),
listen(markerAdd, "click", toggleAddMarker),
listen(markerRemove, "click", confirmMarkerDeletion)
];
document.getElementById('markerIcon').addEventListener('click', toggleIconSection);
document.getElementById('markerIconSize').addEventListener('input', changeIconSize);
document.getElementById('markerIconShiftX').addEventListener('input', changeIconShiftX);
document.getElementById('markerIconShiftY').addEventListener('input', changeIconShiftY);
document.getElementById('markerIconSelect').addEventListener('click', selectMarkerIcon);
function getElement(markerI, event) {
if (event) {
const element = event.target?.closest("svg");
const marker = pack.markers.find(({i}) => Number(element.id.slice(6)) === i);
return [element, marker];
}
document.getElementById('markerStyle').addEventListener('click', toggleStyleSection);
document.getElementById('markerSize').addEventListener('input', changeMarkerSize);
document.getElementById('markerBaseStroke').addEventListener('input', changePinStroke);
document.getElementById('markerBaseFill').addEventListener('input', changePinFill);
document.getElementById('markerIconStrokeWidth').addEventListener('input', changeIconStrokeWidth);
document.getElementById('markerIconStroke').addEventListener('input', changeIconStroke);
document.getElementById('markerIconFill').addEventListener('input', changeIconFill);
const element = document.getElementById(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
document.getElementById('markerToggleBubble').addEventListener('click', togglePinVisibility);
document.getElementById('markerLegendButton').addEventListener('click', editMarkerLegend);
document.getElementById('markerAdd').addEventListener('click', toggleAddMarker);
document.getElementById('markerRemove').addEventListener('click', removeMarker);
updateGroupOptions();
function getSameTypeMarkers() {
const currentType = marker.type;
if (!currentType) return [marker];
return pack.markers.filter(({type}) => type === currentType);
}
function dragMarker() {
const tr = parseTransform(this.getAttribute('transform'));
const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
const dx = +this.getAttribute("x") - d3.event.x;
const dy = +this.getAttribute("y") - d3.event.y;
d3.event.on('drag', function () {
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
this.setAttribute('transform', transform);
const {x, y} = d3.event;
this.setAttribute("x", dx + x);
this.setAttribute("y", dy + y);
});
d3.event.on("end", function () {
const {x, y} = d3.event;
this.setAttribute("x", rn(dx + x, 2));
this.setAttribute("y", rn(dy + y, 2));
const size = marker.size || 30;
const zoomSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
marker.x = rn(x + dx + zoomSize / 2, 1);
marker.y = rn(y + dy + zoomSize, 1);
marker.cell = findCell(marker.x, marker.y);
.selectAll('symbol')
.each(function () {
});
}
function updateInputs() {
const id = elSelected.attr('data-id');
const symbol = d3.select('#defs-markers').select(id);
const icon = symbol.select('text');
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
markerSelectGroup.value = id.slice(1);
markerIconSize.value = parseFloat(icon.attr('font-size'));
markerIconShiftX.value = parseFloat(icon.attr('x'));
markerIconShiftY.value = parseFloat(icon.attr('y'));
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerSize.value = elSelected.attr('data-size');
markerBaseStroke.value = symbol.select('path').attr('fill');
markerBaseFill.value = symbol.select('circle').attr('fill');
markerIconStrokeWidth.value = icon.attr('stroke-width');
markerIconStroke.value = icon.attr('stroke');
markerIconFill.value = icon.attr('fill');
markerToggleBubble.className = symbol.select('circle').attr('opacity') === '0' ? 'icon-info' : 'icon-info-circled';
markerIconSelect.innerHTML = icon.text();
}
function toggleGroupSection() {
if (markerGroupSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerGroup)').forEach((b) => (b.style.display = 'inline-block'));
markerGroupSection.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerGroup)').forEach((b) => (b.style.display = 'none'));
markerGroupSection.style.display = 'inline-block';
}
}
function updateGroupOptions() {
markerSelectGroup.innerHTML = '';
d3.select('#defs-markers')
.selectAll('symbol')
.each(function () {
markerSelectGroup.options.add(new Option(this.id, this.id));
});
markerSelectGroup.value = elSelected.attr('data-id').slice(1);
}
function toggleGroupInput() {
if (markerInputGroup.style.display === 'inline-block') {
markerSelectGroup.style.display = 'inline-block';
markerInputGroup.style.display = 'none';
} else {
markerSelectGroup.style.display = 'none';
markerInputGroup.style.display = 'inline-block';
markerInputGroup.focus();
}
}
function changeGroup() {
elSelected.attr('xlink:href', '#' + this.value);
elSelected.attr('data-id', '#' + this.value);
}
function createGroup() {
let newGroup = this.value
.toLowerCase()
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (Number.isFinite(+newGroup.charAt(0))) newGroup = 'm' + newGroup;
if (document.getElementById(newGroup)) {
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
markerInputGroup.value = '';
// clone old group assigning new id
const id = elSelected.attr('data-id');
const clone = d3.select('#defs-markers').select(id).node().cloneNode(true);
clone.id = newGroup;
document.getElementById('defs-markers').insertBefore(clone, null);
elSelected.attr('xlink:href', '#' + newGroup).attr('data-id', '#' + newGroup);
// select new group
markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
toggleGroupInput();
}
function removeGroup() {
const id = elSelected.attr('data-id');
const used = document.querySelectorAll("use[data-id='" + id + "']");
const count = used.length === 1 ? '1 element' : used.length + ' elements';
const message = `Are you sure you want to remove all markers of that type (${count})? <br>This action cannot be reverted`;
const onConfirm = () => {
if (id !== '#marker0') d3.select('#defs-markers').select(id).remove();
used.forEach((e) => {
const index = notes.findIndex((n) => n.id === e.id);
if (index != -1) notes.splice(index, 1);
e.remove();
});
updateGroupOptions();
updateGroupOptions();
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker type', message, confirm: 'Remove', onConfirm});
function changeMarkerType() {
marker.type = this.value;
}
function toggleIconSection() {
if (markerIconSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'inline-block'));
markerIconSection.style.display = 'none';
markerIconSelect.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'none'));
markerIconSection.style.display = 'inline-block';
markerIconSelect.style.display = 'inline-block';
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
}
function selectMarkerIcon() {
selectIcon(this.innerHTML, (v) => {
this.innerHTML = v;
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').text(v);
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
});
}
function changeIconSize() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('font-size', this.value + 'px');
const px = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.px = px;
redrawIcon(marker);
});
}
function changeIconShiftX() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('x', this.value + '%');
const dx = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dx = dx;
redrawIcon(marker);
});
}
function changeIconShiftY() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('y', this.value + '%');
}
function toggleStyleSection() {
if (markerStyleSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerStyle)').forEach((b) => (b.style.display = 'inline-block'));
markerStyleSection.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerStyle)').forEach((b) => (b.style.display = 'none'));
markerStyleSection.style.display = 'inline-block';
}
const dy = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dy = dy;
redrawIcon(marker);
});
}
function changeMarkerSize() {
const id = elSelected.attr('data-id');
document.querySelectorAll("use[data-id='" + id + "']").forEach((e) => {
const x = +e.dataset.x,
y = +e.dataset.y;
const size = +this.value;
const rescale = +markers.attr("rescale");
const desired = (e.dataset.size = +markerSize.value);
const size = Math.max(desired * 5 + 25 / scale, 1);
e.setAttribute('x', x - size / 2);
e.setAttribute('y', y - size / 2);
e.setAttribute('width', size);
e.setAttribute('height', size);
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
invokeActiveZooming();
}
function changePinStroke() {
const id = elSelected.attr('data-id');
d3.select(id).select('path').attr('fill', this.value);
d3.select(id).select('circle').attr('stroke', this.value);
function changeMarkerPin() {
const pin = this.value;
getSameTypeMarkers().forEach(marker => {
marker.pin = pin;
redrawPin(marker);
});
}
function changePinFill() {
const id = elSelected.attr('data-id');
d3.select(id).select('circle').attr('fill', this.value);
const fill = this.value;
getSameTypeMarkers().forEach(marker => {
marker.fill = fill;
redrawPin(marker);
});
}
function changeIconStrokeWidth() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('stroke-width', this.value);
function changePinStroke() {
const stroke = this.value;
getSameTypeMarkers().forEach(marker => {
marker.stroke = stroke;
redrawPin(marker);
});
}
function changeIconStroke() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('stroke', this.value);
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
}
}
function changeIconFill() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('fill', this.value);
}
function togglePinVisibility() {
const id = elSelected.attr('data-id');
let show = 1;
if (this.className === 'icon-info-circled') {
this.className = 'icon-info';
show = 0;
} else this.className = 'icon-info-circled';
function redrawPin({i, hidden, pin = "bubble", fill = "#fff", stroke = "#000"}) {
const pinGroup = !hidden && document.querySelector(`#marker${i} > g`);
if (pinGroup) pinGroup.innerHTML = getPin(pin, fill, stroke);
d3.select(id).select('circle').attr('opacity', show);
d3.select(id).select('path').attr('opacity', show);
}
function editMarkerLegend() {
const id = elSelected.attr('id');
const id = element.id;
editNotes(id, id);
}
function toggleAddMarker() {
document.getElementById('addMarker').click();
function toggleMarkerLock() {
marker.lock = !marker.lock;
markerLock.classList.toggle("icon-lock-open");
markerLock.classList.toggle("icon-lock");
}
function removeMarker() {
const message = 'Are you sure you want to remove the marker? <br>This action cannot be reverted';
const onConfirm = () => {
const index = notes.findIndex((n) => n.id === elSelected.attr('id'));
if (index != -1) notes.splice(index, 1);
elSelected.remove();
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker', message, confirm: 'Remove', onConfirm});
function toggleAddMarker() {
markerAdd.classList.toggle("pressed");
addMarker.click();
}
function confirmMarkerDeletion() {
confirmationDialog({
title: "Remove marker",
message: "Are you sure you want to remove this marker? The action cannot be reverted",
confirm: "Remove",
onConfirm: deleteMarker
function deleteMarker() {
notes = notes.filter(note => note.id !== element.id);
pack.markers = pack.markers.filter(m => m.i !== marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {
listeners.forEach(removeListener => removeListener());
unselect();
if (addMarker.classList.contains('pressed')) addMarker.classList.remove('pressed');
if (markerAdd.classList.contains('pressed')) markerAdd.classList.remove('pressed');

View file

@ -0,0 +1,265 @@
'use strict';
function editMarker(markerI) {
if (customization) return;
closeDialogs(".stable");
const [element, marker] = getElement(markerI, d3.event);
if (!marker || !element) return;
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
updateInputs();
$('#markerEditor').dialog({
title: "Edit Marker",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeMarkerEditor
});
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
listen(markerSize, "input", changeMarkerSize),
listen(markerPin, "change", changeMarkerPin),
listen(markerFill, "input", changePinFill),
listen(markerStroke, "input", changePinStroke),
listen(markerNotes, "click", editMarkerLegend),
listen(markerLock, "click", toggleMarkerLock),
listen(markerAdd, "click", toggleAddMarker),
listen(markerRemove, "click", confirmMarkerDeletion)
];
function getElement(markerI, event) {
if (event) {
const element = event.target?.closest("svg");
const marker = pack.markers.find(({i}) => Number(element.id.slice(6)) === i);
return [element, marker];
}
const element = document.getElementById(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
function getSameTypeMarkers() {
const currentType = marker.type;
if (!currentType) return [marker];
return pack.markers.filter(({type}) => type === currentType);
}
function dragMarker() {
const dx = +this.getAttribute("x") - d3.event.x;
const dy = +this.getAttribute("y") - d3.event.y;
d3.event.on('drag', function () {
const {x, y} = d3.event;
this.setAttribute("x", dx + x);
this.setAttribute("y", dy + y);
});
d3.event.on("end", function () {
const {x, y} = d3.event;
this.setAttribute("x", rn(dx + x, 2));
this.setAttribute("y", rn(dy + y, 2));
const size = marker.size || 30;
const zoomSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
marker.x = rn(x + dx + zoomSize / 2, 1);
marker.y = rn(y + dy + zoomSize, 1);
marker.cell = findCell(marker.x, marker.y);
.selectAll('symbol')
.each(function () {
});
}
function updateInputs() {
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (Number.isFinite(+newGroup.charAt(0))) newGroup = 'm' + newGroup;
}
function changeMarkerType() {
marker.type = this.value;
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
}
function selectMarkerIcon() {
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
});
}
function changeIconSize() {
const px = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.px = px;
redrawIcon(marker);
});
}
function changeIconShiftX() {
const dx = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dx = dx;
redrawIcon(marker);
});
}
function changeIconShiftY() {
const dy = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dy = dy;
redrawIcon(marker);
});
}
function changeMarkerSize() {
const size = +this.value;
const rescale = +markers.attr("rescale");
const desired = (e.dataset.size = +markerSize.value);
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
}
function changeMarkerPin() {
const pin = this.value;
getSameTypeMarkers().forEach(marker => {
marker.pin = pin;
redrawPin(marker);
});
}
function changePinFill() {
const fill = this.value;
getSameTypeMarkers().forEach(marker => {
marker.fill = fill;
redrawPin(marker);
});
}
function changePinStroke() {
const stroke = this.value;
getSameTypeMarkers().forEach(marker => {
marker.stroke = stroke;
redrawPin(marker);
});
}
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
}
}
function redrawPin({i, hidden, pin = "bubble", fill = "#fff", stroke = "#000"}) {
const pinGroup = !hidden && document.querySelector(`#marker${i} > g`);
if (pinGroup) pinGroup.innerHTML = getPin(pin, fill, stroke);
d3.select(id).select('circle').attr('opacity', show);
d3.select(id).select('path').attr('opacity', show);
}
function editMarkerLegend() {
const id = element.id;
editNotes(id, id);
}
function toggleMarkerLock() {
marker.lock = !marker.lock;
markerLock.classList.toggle("icon-lock-open");
markerLock.classList.toggle("icon-lock");
}
function toggleAddMarker() {
markerAdd.classList.toggle("pressed");
addMarker.click();
}
function confirmMarkerDeletion() {
confirmationDialog({
title: "Remove marker",
message: "Are you sure you want to remove this marker? The action cannot be reverted",
confirm: "Remove",
onConfirm: deleteMarker
function deleteMarker() {
notes = notes.filter(note => note.id !== element.id);
pack.markers = pack.markers.filter(m => m.i !== marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {
listeners.forEach(removeListener => removeListener());
unselect();
addMarker.classList.remove("pressed");
markerAdd.classList.remove("pressed");
restoreDefaultEvents();
clearMainTip();
}
}

View file

@ -0,0 +1,196 @@
'use strict';
function overviewMarkers() {
if (customization) return;
closeDialogs('#markersOverview, .stable');
if (!layerIsOn('toggleMarkers')) toggleMarkers();
const markerGroup = document.getElementById('markers');
const body = document.getElementById('markersBody');
const markersInverPin = document.getElementById('markersInverPin');
const markersInverLock = document.getElementById('markersInverLock');
const markersFooterNumber = document.getElementById('markersFooterNumber');
const markersOverviewRefresh = document.getElementById('markersOverviewRefresh');
const markersAddFromOverview = document.getElementById('markersAddFromOverview');
const markersGenerationConfig = document.getElementById('markersGenerationConfig');
const markersRemoveAll = document.getElementById('markersRemoveAll');
const markersExport = document.getElementById('markersExport');
addLines();
$('#markersOverview').dialog({
title: 'Markers Overview',
resizable: false,
width: fitContent(),
close: close,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
const listeners = [
listen(body, 'click', handleLineClick),
listen(markersInverPin, 'click', invertPin),
listen(markersInverLock, 'click', invertLock),
listen(markersOverviewRefresh, 'click', addLines),
listen(markersAddFromOverview, 'click', toggleAddMarker),
listen(markersGenerationConfig, 'click', configMarkersGeneration),
listen(markersRemoveAll, 'click', triggerRemoveAll),
listen(markersExport, 'click', exportMarkers)
];
function handleLineClick(ev) {
const el = ev.target;
const i = +el.parentNode.dataset.i;
if (el.classList.contains('icon-pencil')) return openEditor(i);
if (el.classList.contains('icon-dot-circled')) return focusOnMarker(i);
if (el.classList.contains('icon-pin')) return pinMarker(el, i);
if (el.classList.contains('locks')) return toggleLockStatus(el, i);
if (el.classList.contains('icon-trash-empty')) return triggerRemove(i);
}
function addLines() {
const lines = pack.markers
.map(({i, type, icon, pinned, lock}) => {
return `<div class="states" data-i=${i} data-type="${type}">
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${pinned ? '' : 'inactive'}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${lock ? 'icon-lock' : 'icon-lock-open inactive'}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove marker" class="icon-trash-empty"></span>
</div>`;
})
.join('');
body.innerHTML = lines;
markersFooterNumber.innerText = pack.markers.length;
applySorting(markersHeader);
}
function invertPin() {
let anyPinned = false;
pack.markers.forEach((marker) => {
const pinned = !marker.pinned;
if (pinned) {
marker.pinned = true;
anyPinned = true;
} else delete marker.pinned;
});
markerGroup.setAttribute('pinned', anyPinned ? 1 : null);
drawMarkers();
addLines();
}
function invertLock() {
pack.markers = pack.markers.map((marker) => ({...marker, lock: !marker.lock}));
addLines();
}
function openEditor(i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (!marker) return;
const {x, y} = marker;
zoomTo(x, y, 8, 2000);
editMarker(i);
}
function focusOnMarker(i) {
highlightElement(document.getElementById(`marker${i}`), 2);
}
function pinMarker(el, i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (marker.pinned) {
delete marker.pinned;
const anyPinned = pack.markers.some((marker) => marker.pinned);
if (!anyPinned) markerGroup.removeAttribute('pinned');
} else {
marker.pinned = true;
markerGroup.setAttribute('pinned', 1);
}
el.classList.toggle('inactive');
drawMarkers();
}
function toggleLockStatus(el, i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (marker.lock) {
delete marker.lock;
el.className = 'locks pointer icon-lock-open inactive';
} else {
marker.lock = true;
el.className = 'locks pointer icon-lock';
}
}
function triggerRemove(i) {
confirmationDialog({
title: 'Remove marker',
message: 'Are you sure you want to remove this marker? The action cannot be reverted',
confirm: 'Remove',
onConfirm: () => removeMarker(i)
});
}
function toggleAddMarker() {
markersAddFromOverview.classList.toggle('pressed');
addMarker.click();
}
function removeMarker(i) {
notes = notes.filter((note) => note.id !== `marker${i}`);
pack.markers = pack.markers.filter((marker) => marker.i !== i);
document.getElementById(`marker${i}`)?.remove();
addLines();
}
function triggerRemoveAll() {
confirmationDialog({
title: 'Remove all markers',
message: 'Are you sure you want to remove all non-locked markers? The action cannot be reverted',
confirm: 'Remove all',
onConfirm: removeAllMarkers
});
}
function removeAllMarkers() {
pack.markers = pack.markers.filter(({i, lock}) => {
if (lock) return true;
const id = `marker${i}`;
document.getElementById(id)?.remove();
notes = notes.filter((note) => note.id !== id);
return false;
});
addLines();
}
function exportMarkers() {
const headers = 'Id,Type,Icon,Name,Note,X,Y\n';
const body = pack.markers.map((marker) => {
const {i, type, icon, x, y} = marker;
const id = `marker${i}`;
const note = notes.find((note) => note.id === id);
const legend = escape(note.legend);
return [id, type, icon, note.name, legend, x, y].join(',');
});
const data = headers + body.join('\n');
const fileName = getFileName('Markers') + '.csv';
downloadFile(data, fileName);
}
function close() {
listeners.forEach((removeListener) => removeListener());
addMarker.classList.remove('pressed');
markerAdd.classList.remove('pressed');
restoreDefaultEvents();
clearMainTip();
}
}

View file

@ -10,33 +10,33 @@ class Rulers {
}
toString() {
return this.data.map(ruler => ruler.toString()).join("; ");
return this.data.map((ruler) => ruler.toString()).join('; ');
}
fromString(string) {
this.data = [];
const rulers = string.split("; ");
const rulers = string.split('; ');
for (const rulerString of rulers) {
const [type, pointsString] = rulerString.split(": ");
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
const Type = type === "Ruler" ? Ruler : type === "Opisometer" ? Opisometer : type === "RouteOpisometer" ? RouteOpisometer : type === "Planimeter" ? Planimeter : null;
const [type, pointsString] = rulerString.split(': ');
const points = pointsString.split(' ').map((el) => el.split(',').map((n) => +n));
const Type = type === 'Ruler' ? Ruler : type === 'Opisometer' ? Opisometer : type === 'RouteOpisometer' ? RouteOpisometer : type === 'Planimeter' ? Planimeter : null;
this.create(Type, points);
}
}
draw() {
this.data.forEach(ruler => ruler.draw());
this.data.forEach((ruler) => ruler.draw());
}
undraw() {
this.data.forEach(ruler => ruler.undraw());
this.data.forEach((ruler) => ruler.undraw());
}
remove(id) {
if (id === undefined) return;
const ruler = this.data.find(ruler => ruler.id === id);
const ruler = this.data.find((ruler) => ruler.id === id);
ruler.undraw();
const rulerIndex = this.data.indexOf(ruler);
rulers.data.splice(rulerIndex, 1);
@ -50,7 +50,7 @@ class Measurer {
}
toString() {
return this.constructor.name + ": " + this.points.join(" ");
return this.constructor.name + ': ' + this.points.join(' ');
}
getSize() {
@ -62,13 +62,13 @@ class Measurer {
}
drag() {
const tr = parseTransform(this.getAttribute("transform"));
const tr = parseTransform(this.getAttribute('transform'));
const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
this.setAttribute("transform", transform);
this.setAttribute('transform', transform);
});
}
@ -111,7 +111,7 @@ class Ruler extends Measurer {
}
getPointsString() {
return this.points.join(" ");
return this.points.join(' ');
}
updatePoint(index, x, y) {
@ -119,7 +119,7 @@ class Ruler extends Measurer {
}
getPointId(x, y) {
return this.points.findIndex(el => el[0] == x && el[1] == y);
return this.points.findIndex((el) => el[0] == x && el[1] == y);
}
pushPoint(i) {
@ -128,42 +128,42 @@ class Ruler extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const points = this.getPointsString();
const size = this.getSize();
const dash = this.getDash();
const el = (this.el = ruler
.append("g")
.attr("class", "ruler")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("polyline")
.attr("points", points)
.attr("class", "white")
.attr("stroke-width", size)
.call(d3.drag().on("start", () => this.addControl(this)));
el.append("polyline")
.attr("points", points)
.attr("class", "gray")
.attr("stroke-width", rn(size * 1.2, 2))
.attr("stroke-dasharray", dash);
el.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
.append('g')
.attr('class', 'ruler')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('polyline')
.attr('points', points)
.attr('class', 'white')
.attr('stroke-width', size)
.call(d3.drag().on('start', () => this.addControl(this)));
el.append('polyline')
.attr('points', points)
.attr('class', 'gray')
.attr('stroke-width', rn(size * 1.2, 2))
.attr('stroke-dasharray', dash);
el.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.drawPoints(el);
this.updateLabel();
return this;
}
drawPoints(el) {
const g = el.select(".rulerPoints");
g.selectAll("circle").remove();
const g = el.select('.rulerPoints');
g.selectAll('circle').remove();
for (let i = 0; i < this.points.length; i++) {
const [x, y] = this.points[i];
@ -173,19 +173,19 @@ class Ruler extends Measurer {
drawPoint(el, x, y, i) {
const context = this;
el.append("circle")
.attr("r", "1em")
.attr("cx", x)
.attr("cy", y)
.attr("class", this.isEdge(i) ? "edge" : "control")
.on("click", function () {
el.append('circle')
.attr('r', '1em')
.attr('cx', x)
.attr('cy', y)
.attr('class', this.isEdge(i) ? 'edge' : 'control')
.on('click', function () {
context.removePoint(context, i);
})
.call(
d3
.drag()
.clickDistance(3)
.on("start", function () {
.on('start', function () {
context.dragControl(context, i);
})
);
@ -197,9 +197,9 @@ class Ruler extends Measurer {
updateLabel() {
const length = this.getLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
getLength() {
@ -215,13 +215,13 @@ class Ruler extends Measurer {
dragControl(context, pointId) {
let addPoint = context.isEdge(pointId) && d3.event.sourceEvent.ctrlKey;
let circle = context.el.select(`circle:nth-child(${pointId + 1})`);
const line = context.el.selectAll("polyline");
const line = context.el.selectAll('polyline');
let x0 = rn(d3.event.x, 1);
let y0 = rn(d3.event.y, 1);
let axis;
d3.event.on("drag", function () {
d3.event.on('drag', function () {
if (addPoint) {
if (d3.event.dx < 0.1 && d3.event.dy < 0.1) return;
context.pushPoint(pointId);
@ -232,10 +232,10 @@ class Ruler extends Measurer {
}
const shiftPressed = d3.event.sourceEvent.shiftKey;
if (shiftPressed && !axis) axis = Math.abs(d3.event.dx) > Math.abs(d3.event.dy) ? "x" : "y";
if (shiftPressed && !axis) axis = Math.abs(d3.event.dx) > Math.abs(d3.event.dy) ? 'x' : 'y';
const x = axis === "y" ? x0 : rn(d3.event.x, 1);
const y = axis === "x" ? y0 : rn(d3.event.y, 1);
const x = axis === 'y' ? x0 : rn(d3.event.x, 1);
const y = axis === 'x' ? y0 : rn(d3.event.y, 1);
if (!shiftPressed) {
axis = null;
@ -244,8 +244,8 @@ class Ruler extends Measurer {
}
context.updatePoint(pointId, x, y);
line.attr("points", context.getPointsString());
circle.attr("cx", x).attr("cy", y);
line.attr('points', context.getPointsString());
circle.attr('cx', x).attr('cy', y);
context.updateLabel();
});
}
@ -273,43 +273,43 @@ class Opisometer extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const dash = this.getDash();
const context = this;
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
.append('g')
.attr('class', 'opisometer')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('path').attr('class', 'white').attr('stroke-width', size);
el.append('path').attr('class', 'gray').attr('stroke-width', size).attr('stroke-dasharray', dash);
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -319,26 +319,26 @@ class Opisometer extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
const left = this.points[0];
const right = last(this.points);
this.el.select(".rulerPoints > circle:first-child").attr("cx", left[0]).attr("cy", left[1]);
this.el.select(".rulerPoints > circle:last-child").attr("cx", right[0]).attr("cy", right[1]);
this.el.select('.rulerPoints > circle:first-child').attr('cx', left[0]).attr('cy', left[1]);
this.el.select('.rulerPoints > circle:last-child').attr('cx', right[0]).attr('cy', right[1]);
}
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const length = this.el.select('path').node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
dragControl(context, rigth) {
const MIN_DIST = d3.event.sourceEvent.shiftKey ? 9 : 100;
let prev = rigth ? last(context.points) : context.points[0];
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const point = [d3.event.x | 0, d3.event.y | 0];
const dist2 = (prev[0] - point[0]) ** 2 + (prev[1] - point[1]) ** 2;
@ -351,7 +351,7 @@ class Opisometer extends Measurer {
context.updateLabel();
});
d3.event.on("end", function () {
d3.event.on('end', function () {
if (!d3.event.sourceEvent.shiftKey) context.optimize();
});
}
@ -361,7 +361,7 @@ class RouteOpisometer extends Measurer {
constructor(points) {
super(points);
if (pack.cells) {
this.cellStops = points.map(p => findCell(p[0], p[1]));
this.cellStops = points.map((p) => findCell(p[0], p[1]));
} else {
this.cellStops = null;
}
@ -369,7 +369,7 @@ class RouteOpisometer extends Measurer {
checkCellStops() {
if (!this.cellStops) {
this.cellStops = this.points.map(p => findCell(p[0], p[1]));
this.cellStops = this.points.map((p) => findCell(p[0], p[1]));
}
}
@ -412,42 +412,42 @@ class RouteOpisometer extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const dash = this.getDash();
const context = this;
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
.append('g')
.attr('class', 'opisometer')
.attr('font-size', 10 * size));
el.append('path').attr('class', 'white').attr('stroke-width', size);
el.append('path').attr('class', 'gray').attr('stroke-width', size).attr('stroke-dasharray', dash);
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -457,23 +457,23 @@ class RouteOpisometer extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
const left = this.points[0];
const right = last(this.points);
this.el.select(".rulerPoints > circle:first-child").attr("cx", left[0]).attr("cy", left[1]);
this.el.select(".rulerPoints > circle:last-child").attr("cx", right[0]).attr("cy", right[1]);
this.el.select('.rulerPoints > circle:first-child').attr('cx', left[0]).attr('cy', left[1]);
this.el.select('.rulerPoints > circle:last-child').attr('cx', right[0]).attr('cy', right[1]);
}
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const length = this.el.select('path').node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
dragControl(context, rigth) {
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const mousePoint = [d3.event.x | 0, d3.event.y | 0];
const cells = pack.cells;
@ -493,16 +493,16 @@ class Planimeter extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const el = (this.el = ruler
.append("g")
.attr("class", "planimeter")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "planimeter").attr("stroke-width", size);
el.append("text").on("click", () => rulers.remove(this.id));
.append('g')
.attr('class', 'planimeter')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('path').attr('class', 'planimeter').attr('stroke-width', size);
el.append('text').on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -512,32 +512,33 @@ class Planimeter extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRomClosed.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
}
updateLabel() {
if (this.points.length < 3) return;
const polygonArea = rn(Math.abs(d3.polygonArea(this.points)));
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + " " + unit;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + ' ' + unit;
const c = polylabel([this.points], 1.0);
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);
}
}
// Scale bar
function drawScaleBar() {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
function drawScaleBar(requestedScale) {
if (scaleBar.style('display') === 'none') return; // no need to re-draw hidden element
scaleBar.selectAll('*').remove(); // fully redraw every time
const scaleLevel = requestedScale || scale;
const dScale = distanceScaleInput.value;
const distanceScale = distanceScaleInput.value;
const unit = distanceUnitInput.value;
const size = +barSizeInput.value;
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
const size = +barSizeInput.value;
let val = (init * size * dScale) / scale; // bar length in distance unit
const init = 100;
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);
@ -545,81 +546,81 @@ function drawScaleBar() {
else if (val > 9) val = rn(val, -1);
// round to 10
else val = rn(val); // round to 1
const l = (val * scale) / dScale; // actual length in pixels on this scale
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
scaleBar
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", l + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
.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');
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", l + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
const dash = size + " " + rn(l / 5 - size, 2);
.append('line')
.attr('x1', 0)
.attr('y1', size)
.attr('x2', length + size)
.attr('y2', size)
.attr('stroke-width', size)
.attr('stroke', '#3d3d3d');
const dash = size + ' ' + rn(length / 5 - size, 2);
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", l + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", dash)
.attr("stroke", "#3d3d3d");
.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', dash)
.attr('stroke', '#3d3d3d');
const fontSize = rn(5 * size, 1);
scaleBar
.selectAll("text")
.selectAll('text')
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * l) / 5, 2))
.attr("y", 0)
.attr("dy", "-.5em")
.attr("font-size", fontSize)
.text(d => rn((((d * l) / 5) * dScale) / scale) + (d < 5 ? "" : " " + unit));
.append('text')
.attr('x', (d) => rn((d * length) / 5, 2))
.attr('y', 0)
.attr('dy', '-.5em')
.attr('font-size', fontSize)
.text((d) => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? '' : ' ' + unit));
if (barLabel.value !== "") {
if (barLabel.value !== '') {
scaleBar
.append("text")
.attr("x", (l + 1) / 2)
.attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize)
.append('text')
.attr('x', (length + 1) / 2)
.attr('y', 2 * size)
.attr('dominant-baseline', 'text-before-edge')
.attr('font-size', fontSize)
.text(barLabel.value);
}
const bbox = scaleBar.node().getBBox();
// append backbround rectangle
scaleBar
.insert("rect", ":first-child")
.attr("x", -10)
.attr("y", -20)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 15)
.attr("stroke-width", size)
.attr("stroke", "none")
.attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value)
.attr("opacity", +barBackOpacity.value);
.insert('rect', ':first-child')
.attr('x', -10)
.attr('y', -20)
.attr('width', bbox.width + 10)
.attr('height', bbox.height + 15)
.attr('stroke-width', size)
.attr('stroke', 'none')
.attr('filter', 'url(#blur5)')
.attr('fill', barBackColor.value)
.attr('opacity', +barBackOpacity.value);
fitScaleBar();
}
// fit ScaleBar to canvas size
function fitScaleBar() {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
if (!scaleBar.select('rect').size() || scaleBar.style('display') === 'none') return;
const px = isNaN(+barPosX.value) ? 0.99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 0.99 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const bbox = scaleBar.select('rect').node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10),
y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
scaleBar.attr('transform', `translate(${x},${y})`);
}

View file

@ -199,9 +199,9 @@ function overviewMilitary() {
function militaryCustomize() {
const types = ['melee', 'ranged', 'mounted', 'machinery', 'naval', 'armored', 'aviation', 'magical'];
const table = document.getElementById('militaryOptions').querySelector('tbody');
const tableBody = document.getElementById('militaryOptions').querySelector('tbody');
removeUnitLines();
options.military.map((u) => addUnitLine(u));
options.military.map((unit) => addUnitLine(unit));
$('#militaryOptions').dialog({
title: 'Edit Military Units',
@ -219,44 +219,127 @@ function overviewMilitary() {
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener('mousemove', () => tip('Add new military unit to the table'));
buttons[2].addEventListener('mousemove', () => tip('Restore default military units and settings'));
buttons[3].addEventListener('mousemove', () => tip('Close the window without saving the changes'));
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
tableBody.addEventListener('click', (event) => {
const el = event.target;
if (el.tagName !== 'BUTTON') return;
const type = el.dataset.type;
if (type === 'icon') return selectIcon(el.innerHTML, (v) => (el.innerHTML = v));
if (type === 'biomes') {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
return selectLimitation(el, biomes);
}
if (type === 'states') return selectLimitation(el, pack.states);
if (type === 'cultures') return selectLimitation(el, pack.cultures);
if (type === 'religions') return selectLimitation(el, pack.religions);
});
function removeUnitLines() {
table.querySelectorAll('tr').forEach((el) => el.remove());
tableBody.querySelectorAll('tr').forEach((el) => el.remove());
}
function addUnitLine(u) {
const row = document.createElement('tr');
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types
.map((t) => `<option ${u.type === t ? 'selected' : ''} value="${t}">${t}</option>`)
.join(' ')}</select></td>
function getLimitValue(attr) {
return attr?.join(',') || '';
}
function getLimitText(attr) {
return attr?.length ? 'some' : 'all';
}
function getLimitTip(attr, data) {
if (!attr || !attr.length) return '';
return attr.map((i) => data?.[i]?.name || '').join(', ');
}
function addUnitLine(unit) {
const typeOptions = types.map((t) => `<option ${unit.type === t ? 'selected' : ''} value="${t}">${t}</option>`).join(' ');
const getLimitButton = (attr) =>
`<button
data-tip="Select allowed ${attr}"
data-type="${attr}"
title="${getLimitTip(unit[attr], pack[attr])}"
data-value="${getLimitValue(unit[attr])}">
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
<td>${getLimitButton('biomes')}</td>
<td>${getLimitButton('states')}</td>
<td>${getLimitButton('cultures')}</td>
<td>${getLimitButton('religions')}</td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? 'checked' : ''}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? 'checked' : ''}>
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector('button').addEventListener('click', function (e) {
selectIcon(this.innerHTML, (v) => (this.innerHTML = v));
});
table.appendChild(row);
tableBody.appendChild(row);
}
function restoreDefaultUnits() {
removeUnitLines();
Military.getDefaultOptions().map((u) => addUnitLine(u));
Military.getDefaultOptions().map((unit) => addUnitLine(unit));
}
function selectLimitation(el, data) {
const type = el.dataset.type;
const value = el.dataset.value;
const initial = value ? value.split(',').map((v) => +v) : [];
const filtered = data.filter((datum) => datum.i && !datum.removed);
const lines = filtered.map(
({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? 'checked' : ''} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><table style="margin-top:.3em"><tbody>${lines.join('')}</tbody></table>`;
$('#alert').dialog({
width: fitContent(),
title: `Limit unit`,
buttons: {
Invert: function () {
alertMessage.querySelectorAll('input').forEach((el) => (el.checked = !el.checked));
},
Apply: function () {
const inputs = Array.from(alertMessage.querySelectorAll('input'));
const selected = inputs.reduce((acc, input) => {
if (input.checked) acc.push(input.dataset.i);
return acc;
}, []);
if (!selected.length) return tip('Select at least one element', false, 'error');
const allAreSelected = selected.length === inputs.length;
el.dataset.value = allAreSelected ? '' : selected.join(',');
el.innerHTML = allAreSelected ? 'all' : 'some';
el.setAttribute('title', getLimitTip(selected, data));
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll('tr'));
const unitLines = Array.from(tableBody.querySelectorAll('tr'));
const names = unitLines.map((r) => r.querySelector('input').value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'));
if (new Set(names).size !== names.length) {
tip('All units should have unique names', false, 'error');
@ -265,14 +348,22 @@ function overviewMilitary() {
$('#militaryOptions').dialog('close');
options.military = unitLines.map((r, i) => {
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll('input, select, button')).map((d) => {
let value = d.value;
if (d.type === 'number') value = +d.value || 0;
if (d.type === 'checkbox') value = +d.checked || 0;
if (d.type === 'button') value = d.innerHTML || '';
return value;
const elements = Array.from(r.querySelectorAll('input, button, select'));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map((el) => {
const {type, value} = el.dataset || {};
if (type === 'icon') return el.innerHTML || '';
if (type) return value ? value.split(',').map((v) => parseInt(v)) : null;
if (el.type === 'number') return +el.value || 0;
if (el.type === 'checkbox') return +el.checked || 0;
return el.value;
});
return {icon, name: names[i], rural, urban, crew, power, type, separate};
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
if (states) unit.states = states;
if (cultures) unit.cultures = cultures;
if (religions) unit.religions = religions;
return unit;
});
localStorage.setItem('military', JSON.stringify(options.military));
Military.generate();

View file

@ -0,0 +1,492 @@
'use strict';
function overviewMilitary() {
if (customization) return;
closeDialogs('#militaryOverview, .stable');
if (!layerIsOn('toggleStates')) toggleStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
if (!layerIsOn('toggleMilitary')) toggleMilitary();
const body = document.getElementById('militaryBody');
addLines();
$('#militaryOverview').dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
updateHeaders();
$('#militaryOverview').dialog({
title: 'Military Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('militaryOverviewRefresh').addEventListener('click', addLines);
document.getElementById('militaryPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('militaryOptionsButton').addEventListener('click', militaryCustomize);
document.getElementById('militaryRegimentsList').addEventListener('click', () => overviewRegiments(-1));
document.getElementById('militaryOverviewRecalculate').addEventListener('click', militaryRecalculate);
document.getElementById('militaryExport').addEventListener('click', downloadMilitaryData);
document.getElementById('militaryWiki').addEventListener('click', () => wiki('Military-Forces'));
body.addEventListener('change', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
changeAlert(state, line, +el.value);
});
body.addEventListener('click', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
if (el.tagName === 'SPAN') overviewRegiments(state);
});
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById('militaryHeader');
header.querySelectorAll('.removable').forEach((el) => el.remove());
const insert = (html) => document.getElementById('militaryTotal').insertAdjacentHTML('beforebegin', html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll('.removable').forEach(function (e) {
e.addEventListener('click', function () {
sortLines(this);
});
});
}
// add line for each state
function addLines() {
body.innerHTML = '';
let lines = '';
const states = pack.states.filter((s) => s.i && !s.removed);
for (const s of states) {
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = (total / population) * 100;
const sortData = options.military.map((u) => `data-${u.name}="${getForces(u)}"`).join(' ');
const lineData = options.military.map((u) => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(' ');
lines += `<div class="states" data-id=${s.i} data-state="${
s.name
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" class="fillRect"></svg>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
<div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(
s.alert,
2
)}">
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
updateFooter();
// add listeners
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => stateHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => stateHighlightOff(ev)));
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(militaryHeader);
}
function changeAlert(state, line, alert) {
const s = pack.states[state];
const dif = s.alert || alert ? alert / s.alert : 0; // modifier
s.alert = line.dataset.alert = alert;
s.military.forEach((r) => {
Object.keys(r.u).forEach((u) => (r.u[u] = rn(r.u[u] * dif))); // change units value
r.a = d3.sum(Object.values(r.u)); // change total
armies.select(`g>g#regiment${s.i}-${r.i}>text`).text(Military.getTotal(r)); // change icon text
});
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach((u) => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
const rate = (line.dataset.rate = (total / population) * 100);
line.querySelector("div[data-type='total']").innerHTML = si(total);
line.querySelector("div[data-type='rate']").innerHTML = rn(rate, 2) + '%';
updateFooter();
}
function updateFooter() {
const lines = Array.from(body.querySelectorAll(':scope > div'));
const statesNumber = (militaryFooterStates.innerHTML = pack.states.filter((s) => s.i && !s.removed).length);
const total = d3.sum(lines.map((el) => el.dataset.total));
militaryFooterForcesTotal.innerHTML = si(total);
militaryFooterForces.innerHTML = si(total / statesNumber);
militaryFooterRate.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.rate)) / statesNumber, 2) + '%';
militaryFooterAlert.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.alert)) / statesNumber, 2);
}
function stateHighlightOn(event) {
const state = +event.target.dataset.id;
if (customization || !state) return;
armies
.select('#army' + state)
.transition()
.duration(2000)
.style('fill', '#ff0000');
if (!layerIsOn('toggleStates')) return;
const d = regions.select('#state' + state).attr('d');
<<<<<<< HEAD
const path = debug.append('path').attr('class', 'highlight').attr('d', d).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 1).attr('opacity', 1).attr('filter', 'url(#blur1)');
=======
const path = debug
.append("path")
.attr("class", "highlight")
.attr("d", d)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("opacity", 1)
.attr("filter", "url(#blur1)");
>>>>>>> master
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString('0,' + l, l + ',' + l);
path
.transition()
.duration(dur)
.attrTween('stroke-dasharray', function () {
return (t) => i(t);
});
}
function stateHighlightOff(event) {
debug.selectAll('.highlight').each(function () {
d3.select(this).transition().duration(1000).attr('opacity', 0).remove();
});
const state = +event.target.dataset.id;
armies
.select('#army' + state)
.transition()
.duration(1000)
.style('fill', null);
}
function togglePercentageMode() {
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const lines = body.querySelectorAll(':scope > div');
const array = Array.from(lines),
cache = [];
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map((el) => +el.dataset[type]));
return cache[type];
};
lines.forEach(function (el) {
el.querySelectorAll('div').forEach(function (div) {
const type = div.dataset.type;
if (type === 'rate') return;
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + '%' : '0%';
});
});
} else {
body.dataset.type = 'absolute';
addLines();
}
}
function militaryCustomize() {
<<<<<<< HEAD
const types = ['melee', 'ranged', 'mounted', 'machinery', 'naval', 'armored', 'aviation', 'magical'];
const table = document.getElementById('militaryOptions').querySelector('tbody');
removeUnitLines();
options.military.map((u) => addUnitLine(u));
=======
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
const tableBody = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
options.military.map(unit => addUnitLine(unit));
>>>>>>> master
$('#militaryOptions').dialog({
title: 'Edit Military Units',
resizable: false,
width: fitContent(),
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Apply: applyMilitaryOptions,
Add: () => addUnitLine({icon: '🛡️', name: 'custom' + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: 'melee'}),
Restore: restoreDefaultUnits,
Cancel: function () {
$(this).dialog('close');
}
},
open: function () {
<<<<<<< HEAD
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener('mousemove', () => tip('Add new military unit to the table'));
buttons[2].addEventListener('mousemove', () => tip('Restore default military units and settings'));
buttons[3].addEventListener('mousemove', () => tip('Close the window without saving the changes'));
=======
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () =>
tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>")
);
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
>>>>>>> master
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
tableBody.addEventListener("click", event => {
const el = event.target;
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
return selectLimitation(el, biomes);
}
if (type === "states") return selectLimitation(el, pack.states);
if (type === "cultures") return selectLimitation(el, pack.cultures);
if (type === "religions") return selectLimitation(el, pack.religions);
});
function removeUnitLines() {
<<<<<<< HEAD
table.querySelectorAll('tr').forEach((el) => el.remove());
}
function addUnitLine(u) {
const row = document.createElement('tr');
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types
.map((t) => `<option ${u.type === t ? 'selected' : ''} value="${t}">${t}</option>`)
.join(' ')}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? 'checked' : ''}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector('button').addEventListener('click', function (e) {
selectIcon(this.innerHTML, (v) => (this.innerHTML = v));
});
table.appendChild(row);
=======
tableBody.querySelectorAll("tr").forEach(el => el.remove());
}
function getLimitValue(attr) {
return attr?.join(",") || "";
}
function getLimitText(attr) {
return attr?.length ? "some" : "all";
}
function getLimitTip(attr, data) {
if (!attr || !attr.length) return "";
return attr.map(i => data?.[i]?.name || "").join(", ");
}
function addUnitLine(unit) {
const row = document.createElement("tr");
const typeOptions = types.map(t => `<option ${unit.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ");
const getLimitButton = attr =>
`<button
data-tip="Select allowed ${attr}"
data-type="${attr}"
title="${getLimitTip(unit[attr], pack[attr])}"
data-value="${getLimitValue(unit[attr])}">
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || " "}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
<td>${getLimitButton("cultures")}</td>
<td>${getLimitButton("religions")}</td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? "checked" : ""}>
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
tableBody.appendChild(row);
>>>>>>> master
}
function restoreDefaultUnits() {
removeUnitLines();
<<<<<<< HEAD
Military.getDefaultOptions().map((u) => addUnitLine(u));
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll('tr'));
const names = unitLines.map((r) => r.querySelector('input').value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'));
=======
Military.getDefaultOptions().map(unit => addUnitLine(unit));
}
function selectLimitation(el, data) {
const type = el.dataset.type;
const value = el.dataset.value;
const initial = value ? value.split(",").map(v => +v) : [];
const filtered = data.filter(datum => datum.i && !datum.removed);
const lines = filtered.map(
({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><table style="margin-top:.3em"><tbody>${lines.join("")}</tbody></table>`;
$("#alert").dialog({
width: fitContent(),
title: `Limit unit`,
buttons: {
Invert: function () {
alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked));
},
Apply: function () {
const inputs = Array.from(alertMessage.querySelectorAll("input"));
const selected = inputs.reduce((acc, input) => {
if (input.checked) acc.push(input.dataset.i);
return acc;
}, []);
if (!selected.length) return tip("Select at least one element", false, "error");
const allAreSelected = selected.length === inputs.length;
el.dataset.value = allAreSelected ? "" : selected.join(",");
el.innerHTML = allAreSelected ? "all" : "some";
el.setAttribute("title", getLimitTip(selected, data));
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function applyMilitaryOptions() {
const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
>>>>>>> master
if (new Set(names).size !== names.length) {
tip('All units should have unique names', false, 'error');
return;
}
$('#militaryOptions').dialog('close');
options.military = unitLines.map((r, i) => {
<<<<<<< HEAD
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll('input, select, button')).map((d) => {
let value = d.value;
if (d.type === 'number') value = +d.value || 0;
if (d.type === 'checkbox') value = +d.checked || 0;
if (d.type === 'button') value = d.innerHTML || '';
return value;
=======
const elements = Array.from(r.querySelectorAll("input, button, select"));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.innerHTML || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;
return el.value;
>>>>>>> master
});
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
if (states) unit.states = states;
if (cultures) unit.cultures = cultures;
if (religions) unit.religions = religions;
return unit;
});
localStorage.setItem('military', JSON.stringify(options.military));
Military.generate();
updateHeaders();
addLines();
}
}
function militaryRecalculate() {
alertMessage.innerHTML = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
$('#alert').dialog({
resizable: false,
title: 'Remove regiment',
buttons: {
Recalculate: function () {
$(this).dialog('close');
Military.generate();
addLines();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function downloadMilitaryData() {
const units = options.military.map((u) => u.name);
let data = 'Id,State,' + units.map((u) => capitalize(u)).join(',') + ',Total,Population,Rate,War Alert\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.state + ',';
data += units.map((u) => el.dataset[u]).join(',') + ',';
data += el.dataset.total + ',';
data += el.dataset.population + ',';
data += rn(el.dataset.rate, 2) + '%,';
data += el.dataset.alert + '\n';
});
const name = getFileName('Military') + '.csv';
downloadFile(data, name);
}
}

View file

@ -1,4 +1,5 @@
'use strict';
function editNotes(id, name) {
// update list of objects
const select = document.getElementById('notesSelect');
@ -8,11 +9,12 @@ function editNotes(id, name) {
}
// initiate pell (html editor)
const notesText = document.getElementById('notesText');
notesText.innerHTML = '';
const editor = Pell.init({
element: document.getElementById('notesText'),
element: notesText,
onChange: (html) => {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
const note = notes.find((note) => note.id === select.value);
if (!note) return;
note.legend = html;
showNote(note);
@ -43,8 +45,7 @@ function editNotes(id, name) {
title: 'Notes Editor',
minWidth: '40em',
width: '50vw',
position: {my: 'center', at: 'center', of: 'svg'},
close: () => (notesText.innerHTML = '')
position: {my: 'center', at: 'center', of: 'svg'}
});
if (modules.editNotes) return;
@ -107,7 +108,7 @@ function editNotes(id, name) {
return;
}
highlightElement(element); // if element is found
highlightElement(element, 3); // if element is found
}
function downloadLegends() {

View file

@ -0,0 +1,183 @@
<<<<<<< HEAD
'use strict';
=======
"use strict";
>>>>>>> master
function editNotes(id, name) {
// update list of objects
const select = document.getElementById('notesSelect');
select.options.length = 0;
for (const note of notes) {
select.options.add(new Option(note.id, note.id));
}
// initiate pell (html editor)
const notesText = document.getElementById("notesText");
notesText.innerHTML = "";
const editor = Pell.init({
<<<<<<< HEAD
element: document.getElementById('notesText'),
onChange: (html) => {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
=======
element: notesText,
onChange: html => {
const note = notes.find(note => note.id === select.value);
>>>>>>> master
if (!note) return;
note.legend = html;
showNote(note);
}
});
// select an object
if (notes.length || id) {
if (!id) id = notes[0].id;
let note = notes.find((note) => note.id === id);
if (note === undefined) {
if (!name) name = id;
note = {id, name, legend: ''};
notes.push(note);
select.options.add(new Option(id, id));
}
select.value = id;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
showNote(note);
} else {
editor.content.innerHTML = 'There are no added notes. Click on element (e.g. label) and add a free text note';
document.getElementById('notesName').value = '';
}
// open a dialog
<<<<<<< HEAD
$('#notesEditor').dialog({
title: 'Notes Editor',
minWidth: '40em',
width: '50vw',
position: {my: 'center', at: 'center', of: 'svg'},
close: () => (notesText.innerHTML = '')
=======
$("#notesEditor").dialog({
title: "Notes Editor",
minWidth: "40em",
width: "50vw",
position: {my: "center", at: "center", of: "svg"}
>>>>>>> master
});
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById('notesSelect').addEventListener('change', changeObject);
document.getElementById('notesName').addEventListener('input', changeName);
document.getElementById('notesPin').addEventListener('click', () => (options.pinNotes = !options.pinNotes));
document.getElementById('notesSpeak').addEventListener('click', () => speak(editor.content.innerHTML));
document.getElementById('notesFocus').addEventListener('click', validateHighlightElement);
document.getElementById('notesDownload').addEventListener('click', downloadLegends);
document.getElementById('notesUpload').addEventListener('click', () => legendsToLoad.click());
document.getElementById('legendsToLoad').addEventListener('change', function () {
uploadFile(this, uploadLegends);
});
document.getElementById('notesClearStyle').addEventListener('click', clearStyle);
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
function showNote(note) {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
}
function changeObject() {
const note = notes.find((note) => note.id === this.value);
if (!note) return;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
}
function changeName() {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
if (!note) return;
note.name = this.value;
showNote(note);
}
function validateHighlightElement() {
const select = document.getElementById('notesSelect');
const element = document.getElementById(select.value);
if (element === null) {
alertMessage.innerHTML = 'Related element is not found. Would you like to remove the note?';
$('#alert').dialog({
resizable: false,
title: 'Element not found',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
return;
}
highlightElement(element, 3); // if element is found
}
function downloadLegends() {
const data = JSON.stringify(notes);
const name = getFileName('Notes') + '.txt';
downloadFile(data, name);
}
function uploadLegends(dataLoaded) {
if (!dataLoaded) {
tip('Cannot load the file. Please check the data format', false, 'error');
return;
}
notes = JSON.parse(dataLoaded);
document.getElementById('notesSelect').options.length = 0;
editNotes(notes[0].id, notes[0].name);
}
function clearStyle() {
editor.content.innerHTML = editor.content.textContent;
}
function triggerNotesRemove() {
alertMessage.innerHTML = 'Are you sure you want to remove the selected note?';
$('#alert').dialog({
resizable: false,
title: 'Remove note',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
}
function removeLegend() {
const select = document.getElementById('notesSelect');
const index = notes.findIndex((n) => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {
$('#notesEditor').dialog('close');
return;
}
notesText.innerHTML = '';
editNotes(notes[0].id, notes[0].name);
}
}

View file

@ -89,17 +89,19 @@ function showSupporters() {
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught,
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '')
@ -148,7 +150,9 @@ optionsContent.addEventListener('input', function (event) {
else if (id === 'regionsInput' || id === 'regionsOutput') changeStatesNumber(value);
else if (id === 'emblemShape') changeEmblemShape(value);
else if (id === 'tooltipSizeInput' || id === 'tooltipSizeOutput') changeTooltipSize(value);
else if (id === 'transparencyInput') changeDialogsTransparency(value);
else if (id === "themeHueInput") changeThemeHue(value);
else if (id === "themeColorInput") changeDialogsTheme(themeColorInput.value, transparencyInput.value);
else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
});
optionsContent.addEventListener('change', function (event) {
@ -156,23 +160,24 @@ optionsContent.addEventListener('change', function (event) {
const value = event.target.value;
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
if (id === 'shapeRendering') viewbox.attr('shape-rendering', value);
else if (id === 'yearInput') changeYear();
else if (id === 'eraInput') changeEra();
else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
});
optionsContent.addEventListener('click', function (event) {
const id = event.target.id;
if (id === 'toggleFullscreen') toggleFullscreen();
else if (id === 'optionsSeedGenerate') generateMapWithSeed();
else if (id === 'optionsMapHistory') showSeedHistoryDialog();
else if (id === 'optionsCopySeed') copyMapURL();
else if (id === 'optionsEraRegenerate') regenerateEra();
else if (id === 'zoomExtentDefault') restoreDefaultZoomExtent();
else if (id === 'translateExtent') toggleTranslateExtent(event.target);
else if (id === 'speakerTest') testSpeaker();
else if (id === "themeColorRestore") restoreDefaultThemeColor();
});
function mapSizeInputChange() {
@ -206,8 +211,8 @@ function changeMapSize() {
// just apply canvas size that was already set
function applyMapSize() {
const zoomMin = +zoomExtentMin.value,
zoomMax = +zoomExtentMax.value;
const zoomMin = +zoomExtentMin.value;
const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value;
graphHeight = +mapHeightInput.value;
svgWidth = Math.min(graphWidth, window.innerWidth);
@ -275,12 +280,9 @@ function testSpeaker() {
speechSynthesis.speak(speaker);
}
function generateMapWithSeed() {
if (optionsSeed.value == seed) {
tip('The current map already has this seed', false, 'error');
return;
}
regeneratePrompt();
function generateMapWithSeed(source) {
if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
regeneratePrompt(source);
}
function showSeedHistoryDialog() {
@ -311,7 +313,7 @@ function restoreSeed(id) {
mapHeightInput.value = mapHistory[id].height;
templateInput.value = mapHistory[id].template;
if (locked('template')) unlock('template');
regeneratePrompt();
regeneratePrompt("seed history");
}
function restoreDefaultZoomExtent() {
@ -415,7 +417,7 @@ function changeUIsize(value) {
if (+value > max) value = max;
uiSizeInput.value = uiSizeOutput.value = value;
document.getElementsByTagName('body')[0].style.fontSize = value * 11 + 'px';
document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px";
document.getElementById('options').style.width = value * 300 + 'px';
}
@ -427,26 +429,56 @@ function changeTooltipSize(value) {
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
}
// change transparency for modal windows
function changeDialogsTransparency(value) {
transparencyInput.value = transparencyOutput.value = value;
const alpha = (100 - +value) / 100;
const optionsColor = 'rgba(164, 139, 149, ' + alpha + ')';
const dialogsColor = 'rgba(255, 255, 255, ' + alpha + ')';
const optionButtonsColor = 'rgba(145, 110, 127, ' + Math.min(alpha + 0.3, 1) + ')';
const optionLiColor = 'rgba(153, 123, 137, ' + Math.min(alpha + 0.3, 1) + ')';
document.getElementById('options').style.backgroundColor = optionsColor;
document.getElementById('dialogs').style.backgroundColor = dialogsColor;
document.querySelectorAll('.tabcontent button').forEach((el) => (el.style.backgroundColor = optionButtonsColor));
document.querySelectorAll('.tabcontent li').forEach((el) => (el.style.backgroundColor = optionLiColor));
document.querySelectorAll('button.options').forEach((el) => (el.style.backgroundColor = optionLiColor));
const THEME_COLOR = "#997787";
function restoreDefaultThemeColor() {
localStorage.removeItem("themeColor");
changeDialogsTheme(THEME_COLOR, transparencyInput.value);
}
function changeThemeHue(hue) {
const {s, l} = d3.hsl(themeColorInput.value);
const newColor = d3.hsl(+hue, s, l).hex();
changeDialogsTheme(newColor, transparencyInput.value);
}
// change color and transparency for modal windows
function changeDialogsTheme(themeColor, transparency) {
transparencyInput.value = transparencyOutput.value = transparency;
const alpha = (100 - +transparency) / 100;
const alphaReduced = Math.min(alpha + 0.3, 1);
const {h, s, l} = d3.hsl(themeColor || THEME_COLOR);
themeColorInput.value = themeColor || THEME_COLOR;
themeHueInput.value = h;
const getRGBA = (hue, saturation, lightness, alpha) => {
const color = d3.hsl(hue, saturation, lightness, alpha);
return color.toString();
};
const theme = [
{name: "--bg-main", h, s, l, alpha},
{name: "--bg-lighter", h, s, l: l + 0.02, alpha},
{name: "--bg-light", h, s: s - 0.02, l: l + 0.06, alpha},
{name: "--light-solid", h, s: s + 0.01, l: l + 0.05, alpha: 1},
{name: "--dark-solid", h, s, l: l - 0.2, alpha: 1},
{name: "--header", h, s: s, l: l - 0.03, alpha: alphaReduced},
{name: "--header-active", h, s: s, l: l - 0.09, alpha: alphaReduced},
{name: "--bg-disabled", h, s: s - 0.04, l: l + 0.09, alphaReduced},
{name: "--bg-dialogs", h: 0, s: 0, l: 0.98, alpha}
];
const sx = document.documentElement.style;
theme.forEach(({name, h, s, l, alpha}) => {
sx.setProperty(name, getRGBA(h, s, l, alpha));
});
}
function changeZoomExtent(value) {
const min = Math.max(+zoomExtentMin.value, 0.01),
max = Math.min(+zoomExtentMax.value, 200);
const min = Math.max(+zoomExtentMin.value, 0.01);
const max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const scale = Math.max(Math.min(+value, 200), 0.01);
const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale);
}
@ -482,13 +514,12 @@ function applyStoredOptions() {
.map((w) => +w);
if (localStorage.getItem('military')) options.military = JSON.parse(localStorage.getItem('military'));
changeDialogsTransparency(localStorage.getItem('transparency') || 5);
if (localStorage.getItem('tooltipSize')) changeTooltipSize(localStorage.getItem('tooltipSize'));
if (localStorage.getItem('regions')) changeStatesNumber(localStorage.getItem('regions'));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (localStorage.getItem('uiSize')) changeUIsize(localStorage.getItem('uiSize'));
else changeUIsize(Math.max(Math.min(rn(mapWidthInput.value / 1280, 1), 2.5), 1));
else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
const params = new URL(window.location.href).searchParams;
@ -497,8 +528,14 @@ function applyStoredOptions() {
if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height;
const transparency = localStorage.getItem("transparency") || 5;
const themeColor = localStorage.getItem("themeColor");
changeDialogsTheme(themeColor, transparency);
// set shape rendering
viewbox.attr('shape-rendering', shapeRendering.value);
options.stateLabelsMode = stateLabelsModeInput.value;
}
// randomize options if randomization is allowed (not locked or options='default')
@ -529,10 +566,9 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === 'en-US';
const UK = navigator.language === 'en-GB';
if (randomize || !locked('distanceScale')) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored('distanceUnit')) distanceUnitInput.value = US || UK ? 'mi' : 'km';
if (!stored('heightUnit')) heightUnit.value = US || UK ? 'ft' : 'm';
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored('temperatureScale')) temperatureScale.value = US ? '°F' : '°C';
// World settings
@ -619,22 +655,16 @@ function restoreDefaultOptions() {
// Sticked menu Options listeners
document.getElementById('sticked').addEventListener('click', function (event) {
const id = event.target.id;
if (id === 'newMapButton') regeneratePrompt();
if (id === "newMapButton") regeneratePrompt("sticky button");
else if (id === 'saveButton') showSavePane();
else if (id === 'loadButton') showLoadPane();
else if (id === "exportButton") showExportPane();
else if (id === 'zoomReset') resetZoom(1000);
});
function regeneratePrompt() {
if (customization) {
tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
return;
}
function regeneratePrompt(source) {
if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
regenerateMap();
return;
}
if (workingTime < 5) return regenerateMap(source);
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`;
@ -647,19 +677,20 @@ function regeneratePrompt() {
},
Generate: function () {
closeDialogs();
regenerateMap();
regenerateMap(source);
}
}
});
}
function showSavePane() {
document.getElementById('showLabels').checked = !hideLabels.checked;
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
sharableLinkContainer.style.display = "none";
$('#saveMapData').dialog({
title: 'Save map',
resizable: false,
width: '30em',
width: "25em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Close: function () {
@ -669,21 +700,21 @@ function showSavePane() {
});
}
// download map data as GeoJSON
function saveGeoJSON() {
alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
Check out ${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export', 'wiki-page')} for guidance`;
function copyLinkToClickboard() {
const shrableLink = document.getElementById("sharableLink");
const link = shrableLink.getAttribute("href");
navigator.clipboard.writeText(link).then(() => tip("Link is copied to the clipboard", true, "success", 8000));
}
$('#alert').dialog({
title: 'GIS data export',
function showExportPane() {
document.getElementById("showLabels").checked = !hideLabels.checked;
$("#exportMapData").dialog({
title: "Export map data",
resizable: false,
width: '35em',
width: "26em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Cells: saveGeoJSON_Cells,
Routes: saveGeoJSON_Routes,
Rivers: saveGeoJSON_Rivers,
Markers: saveGeoJSON_Markers,
Close: function () {
$(this).dialog('close');
}
@ -691,11 +722,11 @@ function saveGeoJSON() {
});
}
function showLoadPane() {
async function showLoadPane() {
$('#loadMapData').dialog({
title: 'Load map',
resizable: false,
width: '17em',
width: "24em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Close: function () {
@ -703,6 +734,25 @@ function showLoadPane() {
}
}
});
const loadFromDropboxButtons = document.getElementById("loadFromDropboxButtons");
const fileSelect = document.getElementById("loadFromDropboxSelect");
const files = await Cloud.providers.dropbox.list();
if (!files) {
loadFromDropboxButtons.style.display = "none";
fileSelect.innerHTML = `<option value="" disabled selected>Save files to Dropbox first</option>`;
return;
}
loadFromDropboxButtons.style.display = "block";
fileSelect.innerHTML = "";
files.forEach(file => {
const opt = document.createElement("option");
opt.innerText = file.name;
opt.value = file.path;
fileSelect.appendChild(opt);
});
}
function loadURL() {
@ -747,7 +797,9 @@ function openSaveTiles() {
status.innerHTML = '';
let loading = null;
$('#saveTilesScreen').dialog({
const inputs = document.getElementById("saveTilesScreen").querySelectorAll("input");
inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
resizable: false,
title: 'Download tiles',
width: '23em',
@ -767,17 +819,12 @@ function openSaveTiles() {
}
},
close: () => {
debug.selectAll('*').remove();
inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
clearInterval(loading);
}
});
}
document
.getElementById('saveTilesScreen')
.querySelectorAll('input')
.forEach((el) => el.addEventListener('input', updateTilesOptions));
function updateTilesOptions() {
if (this?.tagName === 'INPUT') {
const {nextElementSibling: next, previousElementSibling: prev} = this;

1187
modules/ui/options.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,7 @@ function editProvinces() {
document.getElementById('provincesManually').addEventListener('click', enterProvincesManualAssignent);
document.getElementById('provincesManuallyApply').addEventListener('click', applyProvincesManualAssignent);
document.getElementById('provincesManuallyCancel').addEventListener('click', () => exitProvincesManualAssignment());
document.getElementById('provincesAdd').addEventListener('click', enterAddProvinceMode);
document.getElementById('provincesRelease').addEventListener('click', triggerProvincesRelease);
document.getElementById('provincesRecolor').addEventListener('click', recolorProvinces);
body.addEventListener('click', function (ev) {
@ -148,7 +148,6 @@ function editProvinces() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Declare province independence (turn non-capital province with burgs into a new state)" class="icon-flag-empty ${separable ? '' : 'placeholder'} hide"></span>
<span data-tip="Toggle province focus" class="icon-pin ${focused ? '' : ' inactive'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`;
}
@ -228,74 +227,63 @@ function editProvinces() {
function capitalZoomIn(p) {
const capital = pack.provinces[p].burg;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr('x'),
y = +l.attr('y');
const x = +l.attr('x');
const y = +l.attr('y');
zoomTo(x, y, 8, 2000);
}
function triggerIndependencePromps(p) {
alertMessage.innerHTML = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
$('#alert').dialog({
resizable: false,
confirmationDialog({
title: 'Declare independence',
buttons: {
Declare: function () {
declareProvinceIndependence(p);
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
message: 'Are you sure you want to declare province independence? <br>It will turn province into a new state',
confirm: 'Declare',
onConfirm: () => {
const [oldStateId, newStateId] = declareProvinceIndependence(p);
updateStatesPostRelease([oldStateId], [newStateId]);
}
});
}
function declareProvinceIndependence(p) {
const states = pack.states,
provinces = pack.provinces,
cells = pack.cells;
if (provinces[p].burgs.some((b) => pack.burgs[b].capital)) {
tip('Cannot declare independence of a province having capital burg. Please change capital first', false, 'error');
return;
}
function declareProvinceIndependence(provinceId) {
const {states, provinces, cells, burgs} = pack;
const province = provinces[provinceId];
const {name, burg: burgId, burgs: provinceBurgs} = province;
const oldState = pack.provinces[p].state;
const newState = pack.states.length;
if (provinceBurgs.some((b) => burgs[b].capital)) return tip('Cannot declare independence of a province having capital burg. Please change capital first', false, 'error');
if (!burgId) return tip('Cannot declare independence of a province without burg', false, 'error');
const oldStateId = province.state;
const newStateId = states.length;
// turn province burg into a capital
const burg = provinces[p].burg;
if (!burg) return;
pack.burgs[burg].capital = 1;
moveBurgToGroup(burg, 'cities');
burgs[burgId].capital = 1;
moveBurgToGroup(burgId, 'cities');
// move all burgs to a new state
provinces[p].burgs.forEach((b) => (pack.burgs[b].state = newState));
province.burgs.forEach((b) => (burgs[b].state = newStateId));
// difine new state attributes
const center = pack.burgs[burg].cell;
const culture = pack.burgs[burg].culture;
const name = provinces[p].name;
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = provinces[p].coa;
const coaEl = document.getElementById('provinceCOA' + p);
if (coaEl) coaEl.id = 'stateCOA' + newState;
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
const coa = province.coa;
const coaEl = document.getElementById('provinceCOA' + provinceId);
if (coaEl) coaEl.id = 'stateCOA' + newStateId;
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
// update cells
cells.i
.filter((i) => cells.province[i] === p)
.filter((i) => cells.province[i] === provinceId)
.forEach((i) => {
cells.province[i] = 0;
cells.state[i] = newState;
cells.state[i] = newStateId;
});
// update diplomacy and reverse relations
const diplomacy = states.map((s) => {
if (!s.i || s.removed) return 'x';
let relations = states[oldState].diplomacy[s.i]; // relations between Nth state and old overlord
if (s.i === oldState) relations = 'Enemy';
// new state is Enemy to its old overlord
let relations = states[oldStateId].diplomacy[s.i]; // relations between Nth state and old overlord
// new state is Enemy to its old owner
if (s.i === oldStateId) relations = 'Enemy';
else if (relations === 'Ally') relations = 'Suspicion';
else if (relations === 'Friendly') relations = 'Suspicion';
else if (relations === 'Suspicion') relations = 'Neutral';
@ -307,28 +295,51 @@ function editProvinces() {
return relations;
});
diplomacy.push('x');
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]);
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}`]);
// create new state
states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: 'Generic', center, culture, military: [], alert: 1, coa});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
if (layerIsOn('toggleProvinces')) toggleProvinces();
if (!layerIsOn('toggleStates')) toggleStates();
else drawStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
BurgsAndStates.drawStateLabels([newState, oldState]);
states.push({
i: newStateId,
name,
diplomacy,
provinces: [],
color,
expansionism: 0.5,
capital: burgId,
type: 'Generic',
center,
culture,
military: [],
alert: 1,
coa
});
// remove old province
unfog('focusProvince' + p);
if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1);
provinces[p] = {i: p, removed: true};
states[oldStateId].provinces = states[oldStateId].provinces.filter((p) => p !== provinceId);
provinces[provinceId] = {i: provinceId, removed: true};
// draw emblem
COArenderer.add('state', newState, coa, pack.states[newState].pole[0], pack.states[newState].pole[1]);
return [oldStateId, newStateId];
}
function updateStatesPostRelease(oldStates, newStates) {
const allStates = unique([...oldStates, ...newStates]);
layerIsOn('toggleProvinces') && toggleProvinces();
layerIsOn('toggleStates') ? drawStates() : toggleStates();
layerIsOn('toggleBorders') ? drawBorders() : toggleBorders();
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms(newStates);
BurgsAndStates.drawStateLabels(allStates);
// redraw emblems
allStates.forEach((stateId) => {
emblems.select(`#stateEmblems > use[data-i='${stateId}']`)?.remove();
const {coa, pole} = pack.states[stateId];
COArenderer.add('state', stateId, coa, ...pole);
});
unfog();
closeDialogs();
editStates();
}
@ -547,7 +558,17 @@ function editProvinces() {
const provinces = pack.provinces
.filter((p) => p.i && !p.removed)
.map((p) => {
return {id: p.i + states.length - 1, i: p.i, state: p.state, color: p.color, name: p.name, fullName: p.fullName, area: p.area, urban: p.urban, rural: p.rural};
return {
id: p.i + states.length - 1,
i: p.i,
state: p.state,
color: p.color,
name: p.name,
fullName: p.fullName,
area: p.area,
urban: p.urban,
rural: p.rural
};
});
const data = states.concat(provinces);
const root = d3
@ -571,8 +592,6 @@ function editProvinces() {
</select>`;
alertMessage.innerHTML += `<div id='provinceInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3.select('#alertMessage').insert('svg', '#provinceInfo').attr('id', 'provincesTree').attr('width', width).attr('height', height).attr('font-size', '10px');
const graph = svg.append('g').attr('transform', `translate(10, 0)`);
document.getElementById('provincesTreeType').addEventListener('change', updateChart);
treeLayout(root);
@ -694,6 +713,34 @@ function editProvinces() {
provs.selectAll('text').call(d3.drag().on('drag', dragLabel)).classed('draggable', true);
}
function triggerProvincesRelease() {
confirmationDialog({
title: 'Release provinces',
message: `Are you sure you want to release all provinces?
</br>It will turn all separable provinces into independent states.
</br>Capital province and provinces without any burgs will state as they are`,
confirm: 'Release',
onConfirm: () => {
const oldStateIds = [];
const newStateIds = [];
body.querySelectorAll(':scope > div').forEach((el) => {
const provinceId = +el.dataset.id;
const province = pack.provinces[provinceId];
if (!province.burg) return;
if (province.burg === pack.states[province.state].capital) return;
if (province.burgs.some((burgId) => pack.burgs[burgId].capital)) return;
const [oldStateId, newStateId] = declareProvinceIndependence(provinceId);
oldStateIds.push(oldStateId);
newStateIds.push(newStateId);
});
updateStatesPostRelease(unique(oldStateIds), newStateIds);
}
});
}
function enterProvincesManualAssignent() {
if (!layerIsOn('toggleProvinces')) toggleProvinces();
if (!layerIsOn('toggleBorders')) toggleBorders();
@ -852,10 +899,8 @@ function editProvinces() {
}
function enterAddProvinceMode() {
if (this.classList.contains('pressed')) {
exitAddProvinceMode();
return;
}
if (this.classList.contains('pressed')) return exitAddProvinceMode();
customization = 12;
this.classList.add('pressed');
tip('Click on the map to place a new province center', true);
@ -864,24 +909,16 @@ function editProvinces() {
}
function addProvince() {
const cells = pack.cells,
provinces = pack.provinces;
const {cells, provinces} = pack;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (cells.h[center] < 20) {
tip('You cannot place province into the water. Please click on a land cell', false, 'error');
return;
}
if (cells.h[center] < 20) return tip('You cannot place province into the water. Please click on a land cell', false, 'error');
const oldProvince = cells.province[center];
if (oldProvince && provinces[oldProvince].center === center) {
tip('The cell is already a center of a different province. Select other cell', false, 'error');
return;
}
if (oldProvince && provinces[oldProvince].center === center) return tip('The cell is already a center of a different province. Select other cell', false, 'error');
const state = cells.state[center];
if (!state) {
tip('You cannot create a province in neutral lands. Please assign this land to a state first', false, 'error');
return;
}
if (!state) return tip('You cannot create a province in neutral lands. Please assign this land to a state first', false, 'error');
if (d3.event.shiftKey === false) exitAddProvinceMode();
@ -892,8 +929,8 @@ function editProvinces() {
const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const formName = oldProvince ? provinces[oldProvince].formName : 'Province';
const fullName = name + ' ' + formName;
const stateColor = pack.states[state].color,
rndColor = getRandomColor();
const stateColor = pack.states[state].color;
const rndColor = getRandomColor();
const color = stateColor[0] === '#' ? d3.color(d3.interpolate(stateColor, rndColor)(0.2)).hex() : rndColor;
// generate emblem
@ -947,20 +984,20 @@ function editProvinces() {
function downloadProvincesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Province,Form,State,Color,Capital,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
let data = 'Id,Province,Full Name,Form,State,Color,Capital,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
let key = parseInt(el.dataset.id);
data += el.dataset.id + ',';
const key = parseInt(el.dataset.id);
const provincePack = pack.provinces[key];
data += el.dataset.name + ',';
data += el.dataset.form + ',';
data += el.dataset.state + ',';
data += provincePack.fullName + ',';
data += el.dataset.color + ',';
data += el.dataset.capital + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.provinces[key].rural * populationRate)},`;
data += `${Math.round(pack.provinces[key].urban * populationRate * urbanization)}\n`;
data += `${Math.round(provincePack.rural * populationRate)},`;
data += `${Math.round(provincePack.urban * populationRate * urbanization)}\n`;
});
const name = getFileName('Provinces') + '.csv';

File diff suppressed because it is too large Load diff

View file

@ -1,49 +1,53 @@
"use strict";
'use strict';
function overviewRegiments(state) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleMilitary")) toggleMilitary();
closeDialogs('.stable');
if (!layerIsOn('toggleMilitary')) toggleMilitary();
const body = document.getElementById("regimentsBody");
const body = document.getElementById('regimentsBody');
updateFilter(state);
addLines();
$("#regimentsOverview").dialog();
$('#regimentsOverview').dialog();
if (modules.overviewRegiments) return;
modules.overviewRegiments = true;
updateHeaders();
$("#regimentsOverview").dialog({
title: "Regiments Overview", resizable: false, width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#regimentsOverview').dialog({
title: 'Regiments Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("regimentsOverviewRefresh").addEventListener("click", addLines);
document.getElementById("regimentsPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regimentsAddNew").addEventListener("click", toggleAdd);
document.getElementById("regimentsExport").addEventListener("click", downloadRegimentsData);
document.getElementById("regimentsFilter").addEventListener("change", addLines);
document.getElementById('regimentsOverviewRefresh').addEventListener('click', addLines);
document.getElementById('regimentsPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('regimentsAddNew').addEventListener('click', toggleAdd);
document.getElementById('regimentsExport').addEventListener('click', downloadRegimentsData);
document.getElementById('regimentsFilter').addEventListener('change', addLines);
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById("regimentsHeader");
header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
const header = document.getElementById('regimentsHeader');
header.querySelectorAll('.removable').forEach((el) => el.remove());
const insert = (html) => document.getElementById('regimentsTotal').insertAdjacentHTML('beforebegin', html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll(".removable").forEach(function(e) {
e.addEventListener("click", function() {sortLines(this);});
header.querySelectorAll('.removable').forEach(function (e) {
e.addEventListener('click', function () {
sortLines(this);
});
});
}
// add line for each state
function addLines() {
const state = +regimentsFilter.value;
body.innerHTML = "";
let lines = "";
body.innerHTML = '';
let lines = '';
const regiments = [];
for (const s of pack.states) {
@ -51,8 +55,8 @@ function overviewRegiments(state) {
if (state !== -1 && s.i !== state) continue; // specific state is selected
for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name]||0}`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name]||0}</div>`).join(" ");
const sortData = options.military.map((u) => `data-${u.name}=${r.u[u.name] || 0}`).join(' ');
const lineData = options.military.map((u) => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`).join(' ');
lines += `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
@ -70,90 +74,98 @@ function overviewRegiments(state) {
lines += `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name]||0)))}</div>`).join(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
${options.military.map((u) => `<div style="width:5em">${si(d3.sum(regiments.map((r) => r.u[u.name] || 0)))}</div>`).join(' ')}
<div style="width:5em">${si(d3.sum(regiments.map((r) => r.a)))}</div>
</div>`;
body.insertAdjacentHTML("beforeend", lines);
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
body.insertAdjacentHTML('beforeend', lines);
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(regimentsHeader);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => regimentHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => regimentHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => regimentHighlightOff(ev)));
}
function updateFilter(state) {
const filter = document.getElementById("regimentsFilter");
const filter = document.getElementById('regimentsFilter');
filter.options.length = 0; // remove all options
filter.options.add(new Option(`all`, -1, false, state === -1));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
statesSorted.forEach(s => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
const statesSorted = pack.states.filter((s) => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach((s) => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
}
function regimentHighlightOn(event) {
const state = +event.target.dataset.s;
const id = +event.target.dataset.id;
if (customization || !state) return;
armies.select(`g > g#regiment${state}-${id}`).transition().duration(2000).style("fill", "#ff0000");
armies.select(`g > g#regiment${state}-${id}`).transition().duration(2000).style('fill', '#ff0000');
}
function regimentHighlightOff(event) {
const state = +event.target.dataset.s;
const id = +event.target.dataset.id;
armies.select(`g > g#regiment${state}-${id}`).transition().duration(1000).style("fill", null);
armies.select(`g > g#regiment${state}-${id}`).transition().duration(1000).style('fill', null);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const lines = body.querySelectorAll(":scope > div:not(.totalLine)");
const array = Array.from(lines), cache = [];
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const lines = body.querySelectorAll(':scope > div:not(.totalLine)');
const array = Array.from(lines),
cache = [];
const total = function(type) {
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map(el => +el.dataset[type]));
cache[type] = d3.sum(array.map((el) => +el.dataset[type]));
return cache[type];
}
};
lines.forEach(function(el) {
el.querySelectorAll("div").forEach(function(div) {
lines.forEach(function (el) {
el.querySelectorAll('div').forEach(function (div) {
const type = div.dataset.type;
if (type === "rate") return;
div.textContent = total(type) ? rn(+el.dataset[type] / total(type) * 100) + "%" : "0%";
if (type === 'rate') return;
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + '%' : '0%';
});
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
addLines();
}
}
function toggleAdd() {
document.getElementById("regimentsAddNew").classList.toggle("pressed");
if (document.getElementById("regimentsAddNew").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
if (regimentAdd.offsetParent) regimentAdd.classList.add("pressed");
document.getElementById('regimentsAddNew').classList.toggle('pressed');
if (document.getElementById('regimentsAddNew').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', addRegimentOnClick);
tip('Click on map to create new regiment or fleet', true);
if (regimentAdd.offsetParent) regimentAdd.classList.add('pressed');
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
viewbox.on('click', clicked).style('cursor', 'default');
addLines();
if (regimentAdd.offsetParent) regimentAdd.classList.remove("pressed");
if (regimentAdd.offsetParent) regimentAdd.classList.remove('pressed');
}
}
function addRegimentOnClick() {
const state = +regimentsFilter.value;
if (state === -1) {tip("Please select state from the list", false, "error"); return;}
if (state === -1) {
tip('Please select state from the list', false, 'error');
return;
}
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const x = pack.cells.p[cell][0],
y = pack.cells.p[cell][1];
const military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: '🛡️'};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@ -162,19 +174,18 @@ function overviewRegiments(state) {
}
function downloadRegimentsData() {
const units = options.military.map(u => u.name);
let data = "State,Id,Name,"+units.map(u => capitalize(u)).join(",")+",Total\n"; // headers
const units = options.military.map((u) => u.name);
let data = 'State,Id,Name,' + units.map((u) => capitalize(u)).join(',') + ',Total\n'; // headers
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function(el) {
data += el.dataset.state + ",";
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + "\n";
body.querySelectorAll(':scope > div:not(.totalLine)').forEach(function (el) {
data += el.dataset.state + ',';
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += units.map((u) => el.dataset[u]).join(',') + ',';
data += el.dataset.total + '\n';
});
const name = getFileName("Regiments") + ".csv";
const name = getFileName('Regiments') + '.csv';
downloadFile(data, name);
}
}

View file

@ -1,23 +1,23 @@
"use strict";
'use strict';
function createRiver() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRivers")) toggleRivers();
if (!layerIsOn('toggleRivers')) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
tip("Click to add river point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
viewbox.style("cursor", "crosshair").on("click", onCellClick);
tip('Click to add river point, click again to remove', true);
debug.append('g').attr('id', 'controlCells');
viewbox.style('cursor', 'crosshair').on('click', onCellClick);
createRiver.cells = [];
const body = document.getElementById("riverCreatorBody");
const body = document.getElementById('riverCreatorBody');
$("#riverCreator").dialog({
title: "Create River",
$('#riverCreator').dialog({
title: 'Create River',
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverCreator
});
@ -25,14 +25,14 @@ function createRiver() {
modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
body.addEventListener("click", function (ev) {
document.getElementById('riverCreatorComplete').addEventListener('click', addRiver);
document.getElementById('riverCreatorCancel').addEventListener('click', () => $('#riverCreator').dialog('close'));
body.addEventListener('click', function (ev) {
const el = ev.target;
const cl = el.classList;
const cell = +el.parentNode.dataset.cell;
if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
else if (cl.contains("icon-trash-empty")) removeCell(cell);
if (cl.contains('editFlux')) pack.cells.fl[cell] = +el.value;
else if (cl.contains('icon-trash-empty')) removeCell(cell);
});
function onCellClick() {
@ -57,19 +57,19 @@ function createRiver() {
}
function removeCell(cell) {
createRiver.cells = createRiver.cells.filter(c => c !== cell);
createRiver.cells = createRiver.cells.filter((c) => c !== cell);
drawCells(createRiver.cells);
body.querySelector(`div[data-cell='${cell}']`)?.remove();
}
function drawCells(cells) {
debug
.select("#controlCells")
.select('#controlCells')
.selectAll(`polygon`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", "current");
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', 'current');
}
function addRiver() {
@ -77,12 +77,12 @@ function createRiver() {
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
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 = rivers.length ? last(rivers).i + 1 : 1;
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
riverCells.forEach((cell) => {
if (!cells.r[cell]) cells.r[cell] = riverId;
});
@ -99,27 +99,24 @@ function createRiver() {
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: 'River'});
const id = 'river' + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox
.select("#rivers")
.append("path")
.attr("id", "river" + riverId)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
viewbox.select('#rivers').append('path').attr('id', id).attr('d', getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(riverId);
editRiver(id);
}
function closeRiverCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
body.innerHTML = '';
debug.select('#controlCells').remove();
restoreDefaultEvents();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -8,9 +8,9 @@ function editRiver(id) {
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
elSelected = d3.select('#' + id);
elSelected = d3.select('#' + id).on('click', addControlPoint);
tip('Drag control points to change the river course. For major changes please create a new river instead', true);
tip('Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead', true);
debug.append('g').attr('id', 'controlCells');
debug.append('g').attr('id', 'controlPoints');
@ -19,8 +19,8 @@ function editRiver(id) {
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
drawControlPoints(riverPoints);
drawCells(cells);
$('#riverEditor').dialog({
title: 'Edit River',
@ -92,37 +92,35 @@ function editRiver(id) {
document.getElementById('riverWidth').value = width;
}
function drawControlPoints(points, cells) {
function drawControlPoints(points) {
debug
.select('#controlPoints')
.selectAll('circle')
.data(points)
.enter()
.append('circle')
.join('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
.call(d3.drag().on('start', dragControlPoint))
.on('click', removeControlPoint);
}
function drawCells(cells, type) {
function drawCells(cells) {
const validCells = [...new Set(cells)].filter((i) => pack.cells.i[i]);
debug
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.selectAll(`polygon`)
.data(validCells)
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
.attr('points', (d) => getPackPolygon(d));
}
function dragControlPoint() {
const {i, r, fl} = pack.cells;
const {r, fl} = pack.cells;
const river = getRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
const {x: x0, y: y0} = d3.event;
const initCell = findCell(x0, y0);
let movedToCell = null;
@ -136,22 +134,18 @@ function editRiver(id) {
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
drawCells(river.cells);
});
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
if (movedToCell && !r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
redrawRiver();
}
});
}
@ -159,8 +153,10 @@ function editRiver(id) {
function redrawRiver() {
const river = getRiver();
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
@ -170,6 +166,27 @@ function editRiver(id) {
if (modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const point = [rn(x, 1), rn(y, 1)];
const river = getRiver();
if (!river.points) river.points = debug.selectAll('#controlPoints > *').data();
const index = getSegmentId(river.points, point, 2);
river.points.splice(index, 0, point);
drawControlPoints(river.points);
redrawRiver();
}
function removeControlPoint() {
this.remove();
redrawRiver();
const {cells} = getRiver();
drawCells(cells);
}
function changeName() {
getRiver().name = this.value;
}
@ -244,6 +261,8 @@ function editRiver(id) {
function closeRiverEditor() {
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
elSelected.on('click', null);
unselect();
clearMainTip();

View file

@ -0,0 +1,334 @@
'use strict';
function editRiver(id) {
if (customization) return;
if (elSelected && id === elSelected.attr('id')) return;
closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
<<<<<<< HEAD
elSelected = d3.select('#' + id);
tip('Drag control points to change the river course. For major changes please create a new river instead', true);
debug.append('g').attr('id', 'controlCells');
debug.append('g').attr('id', 'controlPoints');
=======
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
>>>>>>> master
updateRiverData();
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
<<<<<<< HEAD
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
=======
drawControlPoints(riverPoints);
drawCells(cells);
>>>>>>> master
$('#riverEditor').dialog({
title: 'Edit River',
resizable: false,
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverEditor
});
if (modules.editRiver) return;
modules.editRiver = true;
// add listeners
document.getElementById('riverCreateSelectingCells').addEventListener('click', createRiver);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById('riverLegend').addEventListener('click', editRiverLegend);
document.getElementById('riverRemove').addEventListener('click', removeRiver);
document.getElementById('riverName').addEventListener('input', changeName);
document.getElementById('riverType').addEventListener('input', changeType);
document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom);
document.getElementById('riverMainstem').addEventListener('change', changeParent);
document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth);
document.getElementById('riverWidthFactor').addEventListener('input', changeWidthFactor);
function getRiver() {
const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId);
return river;
}
function updateRiverData() {
const r = getRiver();
document.getElementById('riverName').value = r.name;
document.getElementById('riverType').value = r.type;
const parentSelect = document.getElementById('riverMainstem');
parentSelect.options.length = 0;
const parent = r.parent || r.i;
const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1));
sortedRivers.forEach((river) => {
const opt = new Option(river.name, river.i, false, river.i === parent);
parentSelect.options.add(opt);
});
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
document.getElementById('riverDischarge').value = r.discharge + ' m³/s';
document.getElementById('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
}
function updateRiverLength(river) {
river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
document.getElementById('riverLength').value = lengthUI;
}
function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river;
const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
document.getElementById('riverWidth').value = width;
}
function drawControlPoints(points) {
debug
.select('#controlPoints')
.selectAll('circle')
.data(points)
<<<<<<< HEAD
.enter()
.append('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
=======
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.call(d3.drag().on("start", dragControlPoint))
.on("click", removeControlPoint);
>>>>>>> master
}
function drawCells(cells) {
const validCells = [...new Set(cells)].filter(i => pack.cells.i[i]);
debug
<<<<<<< HEAD
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
=======
.select("#controlCells")
.selectAll(`polygon`)
.data(validCells)
.join("polygon")
.attr("points", d => getPackPolygon(d));
>>>>>>> master
}
function dragControlPoint() {
const {r, fl} = pack.cells;
const river = getRiver();
const {x: x0, y: y0} = d3.event;
const initCell = findCell(x0, y0);
let movedToCell = null;
d3.event.on('drag', function () {
const {x, y} = d3.event;
const currentCell = findCell(x, y);
movedToCell = initCell !== currentCell ? currentCell : null;
this.setAttribute('cx', x);
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
drawCells(river.cells);
});
<<<<<<< HEAD
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
=======
d3.event.on("end", () => {
if (movedToCell && !r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
redrawRiver();
>>>>>>> master
}
});
}
function redrawRiver() {
const river = getRiver();
<<<<<<< HEAD
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
=======
river.points = debug.selectAll("#controlPoints > *").data();
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
>>>>>>> master
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
elSelected.attr('d', path);
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const point = [rn(x, 1), rn(y, 1)];
const river = getRiver();
if (!river.points) river.points = debug.selectAll("#controlPoints > *").data();
const index = getSegmentId(river.points, point, 2);
river.points.splice(index, 0, point);
drawControlPoints(river.points);
redrawRiver();
}
function removeControlPoint() {
this.remove();
redrawRiver();
const {cells} = getRiver();
drawCells(cells);
}
function changeName() {
getRiver().name = this.value;
}
function changeType() {
getRiver().type = this.value;
}
function generateNameCulture() {
const r = getRiver();
r.name = riverName.value = Rivers.getName(r.mouth);
}
function generateNameRandom() {
const r = getRiver();
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length - 1));
}
function changeParent() {
const r = getRiver();
r.parent = +this.value;
r.basin = pack.rivers.find((river) => river.i === r.parent).basin;
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
}
function changeSourceWidth() {
const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function changeWidthFactor() {
const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function showElevationProfile() {
modules.elevation = true;
showEPForRiver(elSelected.node());
}
function editRiverLegend() {
const id = elSelected.attr('id');
const river = getRiver();
editNotes(id, river.name + ' ' + river.type);
}
function removeRiver() {
alertMessage.innerHTML = 'Are you sure you want to remove the river and all its tributaries';
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river and tributaries',
buttons: {
Remove: function () {
$(this).dialog('close');
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove();
$('#riverEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeRiverEditor() {
<<<<<<< HEAD
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
=======
debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
>>>>>>> master
unselect();
clearMainTip();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -91,7 +91,7 @@ function overviewRivers() {
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
const river = rivers.select('#river' + r).node();
highlightElement(river);
highlightElement(river, 3);
}
function toggleBasinsHightlight() {

View file

@ -0,0 +1,187 @@
'use strict';
function overviewRivers() {
if (customization) return;
closeDialogs('#riversOverview, .stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const body = document.getElementById('riversBody');
riversOverviewAddLines();
$('#riversOverview').dialog();
if (modules.overviewRivers) return;
modules.overviewRivers = true;
$('#riversOverview').dialog({
title: 'Rivers Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riverCreateNew').addEventListener('click', createRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
// add line for each river
function riversOverviewAddLines() {
body.innerHTML = '';
let lines = '';
const unit = distanceUnitInput.value;
for (const r of pack.rivers) {
const discharge = r.discharge + ' m³/s';
const length = rn(r.length * distanceScaleInput.value) + ' ' + unit;
const width = rn(r.width * distanceScaleInput.value, 3) + ' ' + unit;
const basin = pack.rivers.find((river) => river.i === r.basin)?.name;
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-type="${r.type}" data-discharge="${r.discharge}" data-length="${r.length}" data-width="${r.width}" data-basin="${basin}">
<span data-tip="Click to focus on river" class="icon-dot-circled pointer"></span>
<div data-tip="River name" class="riverName">${r.name}</div>
<div data-tip="River type name" class="riverType">${r.type}</div>
<div data-tip="River discharge (flux power)" class="biomeArea">${discharge}</div>
<div data-tip="River length from source to mouth" class="biomeArea">${length}</div>
<div data-tip="River mouth width" class="biomeArea">${width}</div>
<input data-tip="River basin (name of the main stem)" class="stateName" value="${basin}" disabled>
<span data-tip="Edit river" class="icon-pencil"></span>
<span data-tip="Remove river" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
// update footer
riversFooterNumber.innerHTML = pack.rivers.length;
const averageDischarge = rn(d3.mean(pack.rivers.map((r) => r.discharge)));
riversFooterDischarge.innerHTML = averageDischarge + ' m³/s';
const averageLength = rn(d3.mean(pack.rivers.map((r) => r.length)));
riversFooterLength.innerHTML = averageLength * distanceScaleInput.value + ' ' + unit;
const averageWidth = rn(d3.mean(pack.rivers.map((r) => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 3) + ' ' + unit;
// add listeners
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => riverHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => riverHighlightOff(ev)));
body.querySelectorAll('div > span.icon-dot-circled').forEach((el) => el.addEventListener('click', zoomToRiver));
body.querySelectorAll('div > span.icon-pencil').forEach((el) => el.addEventListener('click', openRiverEditor));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', triggerRiverRemove));
applySorting(riversHeader);
}
function riverHighlightOn(event) {
if (!layerIsOn('toggleRivers')) toggleRivers();
const r = +event.target.dataset.id;
rivers
.select('#river' + r)
.attr('stroke', 'red')
.attr('stroke-width', 1);
}
function riverHighlightOff(e) {
const r = +e.target.dataset.id;
rivers
.select('#river' + r)
.attr('stroke', null)
.attr('stroke-width', null);
}
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
<<<<<<< HEAD
const river = rivers.select('#river' + r).node();
highlightElement(river);
=======
const river = rivers.select("#river" + r).node();
highlightElement(river, 3);
>>>>>>> master
}
function toggleBasinsHightlight() {
if (rivers.attr('data-basin') === 'hightlighted') {
rivers.selectAll('*').attr('fill', null);
rivers.attr('data-basin', null);
} else {
rivers.attr('data-basin', 'hightlighted');
const basins = [...new Set(pack.rivers.map((r) => r.basin))];
const colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'];
basins.forEach((b, i) => {
const color = colors[i % colors.length];
pack.rivers
.filter((r) => r.basin === b)
.forEach((r) => {
rivers.select('#river' + r.i).attr('fill', color);
});
});
}
}
function downloadRiversData() {
let data = 'Id,River,Type,Discharge,Length,Width,Basin\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
const d = el.dataset;
const discharge = d.discharge + ' m³/s';
const length = rn(d.length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const width = rn(d.width * distanceScaleInput.value, 3) + ' ' + distanceUnitInput.value;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(',') + '\n';
});
const name = getFileName('Rivers') + '.csv';
downloadFile(data, name);
}
function openRiverEditor() {
const id = 'river' + this.parentNode.dataset.id;
editRiver(id);
}
function triggerRiverRemove() {
const river = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the river?
All tributaries will be auto-removed`;
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river',
buttons: {
Remove: function () {
Rivers.remove(river);
riversOverviewAddLines();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function triggerAllRiversRemove() {
alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rivers',
buttons: {
Remove: function () {
$(this).dialog('close');
removeAllRivers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function removeAllRivers() {
pack.rivers = [];
pack.cells.r = new Uint16Array(pack.cells.i.length);
rivers.selectAll('*').remove();
riversOverviewAddLines();
}
}

View file

@ -502,6 +502,7 @@ function editStates() {
pack.cells.province.forEach((pr, i) => {
if (pr === p) pack.cells.province[i] = 0;
});
const coaId = 'provinceCOA' + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
@ -568,19 +569,20 @@ function editStates() {
function showStatesChart() {
// build hierarchy tree
const data = pack.states.filter((s) => !s.removed);
const statesData = pack.states.filter((s) => !s.removed);
if (statesData.length < 2) return tip('There are no states to show', false, 'error');
const root = d3
.stratify()
.id((d) => d.i)
.parentId((d) => (d.i ? 0 : null))(data)
.parentId((d) => (d.i ? 0 : null))(statesData)
.sum((d) => d.area)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value,
height = 150 + 200 * uiSizeOutput.value;
const size = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: 0, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
const w = size - margin.left - margin.right;
const h = size - margin.top - margin.bottom;
const treeLayout = d3.pack().size([w, h]).padding(3);
// prepare svg
@ -592,12 +594,13 @@ function editStates() {
<option value="burgs">Burgs number</option>
</select>`;
alertMessage.innerHTML += `<div id='statesInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3
.select('#alertMessage')
.insert('svg', '#statesInfo')
.attr('id', 'statesTree')
.attr('width', width)
.attr('height', height)
.attr('width', size)
.attr('height', size)
.style('font-family', 'Almendra SC')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central');
@ -819,9 +822,9 @@ function editStates() {
}
function applyStatesManualAssignent() {
const cells = pack.cells,
affectedStates = [],
affectedProvinces = [];
const {cells} = pack;
const affectedStates = [];
const affectedProvinces = [];
statesBody
.select('#temp')
@ -837,77 +840,143 @@ function editStates() {
if (affectedStates.length) {
refreshStatesEditor();
if (!layerIsOn('toggleStates')) toggleStates();
else drawStates();
layerIsOn('toggleStates') ? drawStates() : toggleStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
layerIsOn('toggleBorders') ? drawBorders() : toggleBorders();
if (layerIsOn('toggleProvinces')) drawProvinces();
}
exitStatesManualAssignment();
}
function adjustProvinces(affectedProvinces) {
const {cells, provinces, states} = pack;
const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
affectedProvinces.forEach((p) => {
if (!p) return; // do nothing if neutral lands are captured
const old = provinces[p].state;
// remove province from state provinces list
if (states[old]?.provinces?.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
const {cells, provinces, states, burgs} = pack;
affectedProvinces.forEach((provinceId) => {
// find states owning at least 1 province cell
const provCells = cells.i.filter((i) => cells.province[i] === p);
const provCells = cells.i.filter((i) => cells.state[i] && cells.province[i] === provinceId);
const provStates = [...new Set(provCells.map((i) => cells.state[i]))];
// assign province to its center owner; if center is neutral, remove province
const owner = cells.state[provinces[p].center];
if (owner) {
const name = provinces[p].name;
// province is captured completely => change owner or remove
if (provinceId && provStates.length === 1) return changeProvinceOwner(provinceId, provStates[0], provCells);
// if province is a historical part of another state's province, unite with old province
const part = states[owner].provinces.find((n) => name.includes(provinces[n].name));
if (part) {
provinces[p].removed = true;
provCells.filter((i) => cells.state[i] === owner).forEach((i) => (cells.province[i] = part));
} else {
provinces[p].state = owner;
states[owner].provinces.push(p);
provinces[p].color = getMixedColor(states[owner].color);
}
} else {
provinces[p].removed = true;
provCells.filter((i) => !cells.state[i]).forEach((i) => (cells.province[i] = 0));
}
// create new provinces for non-main part
provStates
.filter((s) => s && s !== owner)
.forEach((s) =>
createProvince(
p,
s,
provCells.filter((i) => cells.state[i] === s)
)
);
// province is captured partially => split province
splitProvince(provinceId, provStates, provCells);
});
function createProvince(initProv, state, provCells) {
const province = provinces.length;
provCells.forEach((i) => (cells.province[i] = province));
function changeProvinceOwner(provinceId, newOwnerId, provinceCells) {
const province = provinces[provinceId];
const prevOwner = states[province.state];
const burgCell = provCells.find((i) => cells.burg[i]);
const center = burgCell ? burgCell : provCells[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
// remove province from old owner list
prevOwner.provinces = prevOwner.provinces.filter((province) => province !== provinceId);
const name = burgCell && P(0.7) ? getAdjective(pack.burgs[burg].name) : getAdjective(states[state].name) + ' ' + provinces[initProv].name.split(' ').slice(-1)[0];
const formName = name.split(' ').length > 1 ? provinces[initProv].formName : rw(form);
const fullName = name + ' ' + formName;
const color = getMixedColor(states[state].color);
provinces.push({i: province, state, center, burg, name, formName, fullName, color});
if (newOwnerId) {
// new owner is a state => change owner
province.state = newOwnerId;
states[newOwnerId].provinces.push(provinceId);
} else {
// new owner is neutral => remove province
provinces[provinceId] = {i: provinceId, removed: true};
provinceCells.forEach((i) => {
cells.province[i] = 0;
});
}
}
function splitProvince(provinceId, provinceStates, provinceCells) {
const province = provinces[provinceId];
const prevOwner = states[province.state];
const provinceCenterOwner = cells.state[province.center];
provinceStates.forEach((stateId) => {
const stateProvinceCells = provinceCells.filter((i) => cells.state[i] === stateId);
if (stateId === provinceCenterOwner) {
// province center is owned by the same state => do nothing for this state
if (stateId === prevOwner.i) return;
// province center is captured by neutrals => remove state
if (!stateId) {
provinces[provinceId] = {i: provinceId, removed: true};
stateProvinceCells.forEach((i) => {
cells.province[i] = 0;
});
return;
}
// reassign province ownership to province center owner
prevOwner.provinces = prevOwner.provinces.filter((province) => province !== provinceId);
province.state = stateId;
province.color = getMixedColor(states[stateId].color);
states[stateId].provinces.push(provinceId);
return;
}
// province cells captured by neutrals => clear province
if (!stateId) {
stateProvinceCells.forEach((i) => {
cells.province[i] = 0;
});
return;
}
// a few province cells owned by state => add to closes province
if (stateProvinceCells.length < 20) {
const closestProvince = findClosestProvince(provinceId, stateId, stateProvinceCells);
if (closestProvince) {
stateProvinceCells.forEach((i) => {
cells.province[i] = closestProvince;
});
return;
}
}
// some province cells owned by state => create new province
createProvince(province, stateId, stateProvinceCells);
});
}
function createProvince(oldProvince, stateId, provinceCells) {
const newProvinceId = provinces.length;
const burgCell = provinceCells.find((i) => cells.burg[i]);
const center = burgCell ? burgCell : provinceCells[0];
const burgId = burgCell ? cells.burg[burgCell] : 0;
const burg = burgId ? burgs[burgId] : null;
const culture = cells.culture[center];
const nameByBurg = burgCell && P(0.5);
const name = nameByBurg ? burg.name : oldProvince.name || Names.getState(Names.getCultureShort(culture), culture);
const formOptions = ['Zone', 'Area', 'Territory', 'Province'];
const formName = burgCell && oldProvince.formName ? oldProvince.formName : ra(formOptions);
const color = getMixedColor(states[stateId].color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = BurgsAndStates.getType(center, burg?.port);
const coa = COA.generate(burg?.coa || states[stateId].coa, kinship, burg ? null : 0.9, type);
coa.shield = COA.getShield(culture, stateId);
provinces.push({i: newProvinceId, state: stateId, center, burg: burgId, name, formName, fullName: `${name} ${formName}`, color, coa});
provinceCells.forEach((i) => {
cells.province[i] = newProvinceId;
});
states[stateId].provinces.push(newProvinceId);
}
function findClosestProvince(provinceId, stateId, sourceCells) {
const borderCell = sourceCells.find((i) =>
cells.c[i].some((c) => {
return cells.state[c] === stateId && cells.province[c] && cells.province[c] !== provinceId;
})
);
const closesProvince = borderCell && cells.c[borderCell].map((c) => cells.province[c]).find((province) => province && province !== provinceId);
return closesProvince;
}
}
@ -1007,7 +1076,22 @@ function editStates() {
cells.state[center] = newState;
cells.province[center] = 0;
states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: 'Generic', center, culture, military: [], alert: 1, coa, pole});
states.push({
i: newState,
name,
diplomacy,
provinces: [],
color,
expansionism: 0.5,
capital: burg,
type: 'Generic',
center,
culture,
military: [],
alert: 1,
coa,
pole
});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
adjustProvinces([cells.province[center]]);
@ -1050,12 +1134,13 @@ function editStates() {
function downloadStatesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,State,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
let data = 'Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
const key = parseInt(el.dataset.id);
const statePack = pack.states[key];
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += (statePack.fullName ? statePack.fullName : '') + ',';
data += el.dataset.form + ',';
data += el.dataset.color + ',';
data += el.dataset.capital + ',';
@ -1066,8 +1151,8 @@ function editStates() {
data += el.dataset.burgs + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.states[key].rural * populationRate)},`;
data += `${Math.round(pack.states[key].urban * populationRate * urbanization)}\n`;
data += `${Math.round(statePack.rural * populationRate)},`;
data += `${Math.round(statePack.urban * populationRate * urbanization)}\n`;
});
const name = getFileName('States') + '.csv';

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

1701
modules/ui/style.js.orig Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,15 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
'use strict';
// module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener('click', function (event) {
if (customization) {
tip('Please exit the customization mode first', false, 'warning');
return;
}
if (event.target.tagName !== 'BUTTON') return;
if (!['BUTTON', 'I'].includes(event.target.tagName)) return;
const button = event.target.id;
// Click to open Editor buttons
// click on open Editor buttons
if (button === 'editHeightmapButton') editHeightmap();
else if (button === 'editBiomesButton') editBiomes();
else if (button === 'editStatesButton') editStates();
@ -17,7 +17,6 @@ toolsContent.addEventListener('click', function (event) {
else if (button === 'editDiplomacyButton') editDiplomacy();
else if (button === 'editCulturesButton') editCultures();
else if (button === 'editReligions') editReligions();
else if (button === 'editResources') editResources();
else if (button === 'editEmblemButton') openEmblemEditor();
else if (button === 'editNamesBaseButton') editNamesbase();
else if (button === 'editUnitsButton') editUnits();
@ -26,9 +25,10 @@ toolsContent.addEventListener('click', function (event) {
else if (button === 'overviewBurgsButton') overviewBurgs();
else if (button === 'overviewRiversButton') overviewRivers();
else if (button === 'overviewMilitaryButton') overviewMilitary();
else if (button === 'overviewMarkersButton') overviewMarkers();
else if (button === 'overviewCellsButton') viewCellDetails();
// Click to Regenerate buttons
// click on Regenerate buttons
if (event.target.parentNode.id === 'regenerateFeature') {
if (sessionStorage.getItem('regenerateFeatureDontAsk')) {
processFeatureRegeneration(event, button);
@ -61,7 +61,10 @@ toolsContent.addEventListener('click', function (event) {
});
}
// Click to Add buttons
// click on Configure regenerate buttons
if (button === 'configRegenerateMarkers') configMarkersGeneration();
// click on Add buttons
if (button === 'addLabel') toggleAddLabel();
else if (button === 'addBurgTool') toggleAddBurg();
else if (button === 'addRiver') toggleAddRiver();
@ -84,13 +87,12 @@ function processFeatureRegeneration(event, button) {
else if (button === 'regenerateStates') regenerateStates();
else if (button === 'regenerateProvinces') regenerateProvinces();
else if (button === 'regenerateBurgs') regenerateBurgs();
else if (button === 'regenerateResources') regenerateResources();
else if (button === 'regenerateEmblems') regenerateEmblems();
else if (button === 'regenerateReligions') regenerateReligions();
else if (button === 'regenerateCultures') regenerateCultures();
else if (button === 'regenerateMilitary') regenerateMilitary();
else if (button === 'regenerateIce') regenerateIce();
else if (button === 'regenerateMarkers') regenerateMarkers(event);
else if (button === 'regenerateMarkers') regenerateMarkers();
else if (button === 'regenerateZones') regenerateZones(event);
}
@ -119,6 +121,7 @@ function regenerateRivers() {
Lakes.defineGroup();
Rivers.specify();
if (!layerIsOn('toggleRivers')) toggleRivers();
else drawRivers();
}
function recalculatePopulation() {
@ -137,21 +140,11 @@ function recalculatePopulation() {
function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (!burgs.length) {
tip('No burgs to generate states. Please create burgs first', false, 'error');
return;
}
if (burgs.length < +regionsInput.value) {
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, 'warn');
}
// burg local ids sorted by a bit randomized population:
const sorted = burgs
.map((b, i) => [i, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map((b) => b[0]);
const capitalsTree = d3.quadtree();
const statesCount = +regionsInput.value;
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (!burgs.length) return tip('There are no any burgs to generate states. Please create burgs first', false, 'error');
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, 'warn');
// turn all old capitals into towns
burgs
@ -168,8 +161,7 @@ function regenerateStates() {
unfog();
// if desired states number is 0
if (regionsInput.value == 0) {
if (!statesCount) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, 'warn');
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
@ -185,26 +177,34 @@ function regenerateStates() {
return;
}
const neutral = pack.states[0].name;
const count = Math.min(+regionsInput.value, burgs.length);
// burg local ids sorted by a bit randomized population:
const sortedBurgs = burgs
.map((b, i) => [b, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map((b) => b[0]);
const capitalsTree = d3.quadtree();
const neutral = pack.states[0].name; // neutrals name
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
pack.states = d3.range(count).map((i) => {
if (!i) return {i, name: neutral};
let capital = null,
x = 0,
y = 0;
for (const i of sorted) {
capital = burgs[i];
(x = capital.x), (y = capital.y);
if (capitalsTree.find(x, y, spacing) === undefined) break;
let capital = null;
for (const burg of sortedBurgs) {
const {x, y} = burg;
if (capitalsTree.find(x, y, spacing) === undefined) {
burg.capital = 1;
capital = burg;
capitalsTree.add([x, y]);
moveBurgToGroup(burg.i, 'cities');
break;
}
spacing = Math.max(spacing - 1, 1);
}
capitalsTree.add([x, y]);
capital.capital = 1;
moveBurgToGroup(capital.i, 'cities');
const culture = capital.culture;
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, '', 0);
const name = Names.getState(basename, culture);
@ -337,13 +337,6 @@ function regenerateBurgs() {
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
}
function regenerateResources() {
Resources.generate();
goods.selectAll('*').remove();
if (layerIsOn('toggleResources')) drawResources();
refreshAllEditors();
}
function regenerateEmblems() {
// remove old emblems
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
@ -424,23 +417,11 @@ function regenerateIce() {
drawIce();
}
function regenerateMarkers(event) {
if (isCtrlClick(event)) prompt('Please provide markers number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2));
function addNumberOfMarkers(number) {
// remove existing markers and assigned notes
markers
.selectAll('use')
.each(function () {
const index = notes.findIndex((n) => n.id === this.id);
if (index != -1) notes.splice(index, 1);
})
.remove();
addMarkers(number);
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
function regenerateMarkers() {
Markers.regenerate();
turnButtonOn('toggleMarkers');
drawMarkers();
if (document.getElementById('markersOverviewRefresh').offsetParent) markersOverviewRefresh.click();
}
function regenerateZones(event) {
@ -485,7 +466,10 @@ function addLabelOnClick() {
const name = Names.getCulture(culture);
const id = getNextId('label');
let group = labels.select('#addedLabels');
// use most recently selected label group
let selected = labelGroupSelect.value;
const symbol = selected ? '#' + selected : '#addedLabels';
let group = labels.select(symbol);
if (!group.size())
group = labels
.append('g')
@ -495,7 +479,6 @@ function addLabelOnClick() {
.attr('stroke', '#3a3a3a')
.attr('stroke-width', 0)
.attr('font-family', 'Almendra SC')
.attr('data-font', 'Almendra+SC')
.attr('font-size', 18)
.attr('data-size', 18)
.attr('filter', null);
@ -697,7 +680,7 @@ function addRouteOnClick() {
}
function toggleAddMarker() {
const pressed = document.getElementById('addMarker').classList.contains('pressed');
const pressed = document.getElementById('addMarker')?.classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
return;
@ -705,45 +688,115 @@ function toggleAddMarker() {
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addMarker.classList.add('pressed');
closeDialogs('.stable');
markersAddFromOverview.classList.add('pressed');
viewbox.style('cursor', 'crosshair').on('click', addMarkerOnClick);
tip('Click on map to add a marker. Hold Shift to add multiple', true);
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
function addMarkerOnClick() {
const {markers} = pack;
const point = d3.mouse(this);
const x = rn(point[0], 2),
y = rn(point[1], 2);
const id = getNextId('markerElement');
const x = rn(point[0], 2);
const y = rn(point[1], 2);
const i = last(markers).i + 1;
const selected = markerSelectGroup.value;
const valid =
selected &&
d3
.select('#defs-markers')
.select('#' + selected)
.size();
const symbol = valid ? '#' + selected : '#marker0';
const added = markers.select("[data-id='" + symbol + "']").size();
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr('data-size') : 1;
if (isNaN(desired)) desired = 1;
const size = desired * 5 + 25 / scale;
const isMarkerSelected = elSelected?.node()?.parentElement?.id === 'markers';
const selectedMarker = isMarkerSelected ? markers.find((marker) => marker.i === +elSelected.attr('id').slice(6)) : null;
const baseMarker = selectedMarker || {icon: '❓'};
const marker = {...baseMarker, i, x, y};
markers
.append('use')
.attr('id', id)
.attr('xlink:href', symbol)
.attr('data-id', symbol)
.attr('data-x', x)
.attr('data-y', y)
.attr('x', x - size / 2)
.attr('y', y - size)
.attr('data-size', desired)
.attr('width', size)
.attr('height', size);
markers.push(marker);
const markersElement = document.getElementById('markers');
const rescale = +markersElement.getAttribute('rescale');
markersElement.insertAdjacentHTML('beforeend', drawMarker(marker, rescale));
if (d3.event.shiftKey === false) unpressClickToAddButton();
if (d3.event.shiftKey === false) {
document.getElementById('markerAdd').classList.remove('pressed');
document.getElementById('markersAddFromOverview').classList.remove('pressed');
unpressClickToAddButton();
}
}
function configMarkersGeneration() {
drawConfigTable();
function drawConfigTable() {
const {markers} = pack;
const config = Markers.getConfig();
const headers = `<thead style='font-weight:bold'><tr>
<td data-tip="Marker type name">Type</td>
<td data-tip="Marker icon">Icon</td>
<td data-tip="Marker number multiplier">Multiplier</td>
<td data-tip="Number of markers of that type on the current map">Number</td>
</tr></thead>`;
const lines = config.map(({type, icon, multiplier}, index) => {
const inputId = `markerIconInput${index}`;
return `<tr>
<td><input value="${type}" /></td>
<td>
<input id="${inputId}" style="width: 5em" value="${icon}" />
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i>
</td>
<td><input type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${markers.filter((marker) => marker.type === type).length}</td>
</tr>`;
});
const table = `<table class="table">${headers}<tbody>${lines.join('')}</tbody></table>`;
alertMessage.innerHTML = table;
alertMessage.querySelectorAll('i').forEach((selectIconButton) => {
selectIconButton.addEventListener('click', function () {
const input = this.previousElementSibling;
selectIcon(input.value, (icon) => (input.value = icon));
});
});
}
const applyChanges = () => {
const rows = alertMessage.querySelectorAll('tbody > tr');
const rowsData = Array.from(rows).map((row) => {
const inputs = row.querySelectorAll('input');
return {
type: inputs[0].value,
icon: inputs[1].value,
multiplier: parseFloat(inputs[2].value)
};
});
const config = Markers.getConfig();
const newConfig = config.map((markerType, index) => {
const {type, icon, multiplier} = rowsData[index];
return {...markerType, type, icon, multiplier};
});
Markers.setConfig(newConfig);
};
$('#alert').dialog({
resizable: false,
title: 'Markers generation settings',
position: {my: 'left top', at: 'left+10 top+10', of: 'svg', collision: 'fit'},
buttons: {
Regenerate: () => {
applyChanges();
regenerateMarkers();
drawConfigTable();
},
Close: function () {
$(this).dialog('close');
}
},
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip('Apply changes and regenerate markers'));
buttons[1].addEventListener('mousemove', () => tip('Close the window'));
},
close: function () {
$(this).dialog('destroy');
}
});
}
function viewCellDetails() {

1014
modules/ui/tools.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,8 @@ function editUnits() {
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
document.getElementById('urbanDensityOutput').addEventListener('input', changeUrbanDensity);
document.getElementById('urbanDensityInput').addEventListener('change', changeUrbanDensity);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
@ -93,6 +95,10 @@ function editUnits() {
urbanization = +this.value;
}
function changeUrbanDensity() {
urbanDensity = +this.value;
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById('distanceScaleOutput').value = 3;
@ -135,8 +141,9 @@ function editUnits() {
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
localStorage.removeItem('populationRate');
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
localStorage.removeItem('urbanization');
localStorage.removeItem('urbanDensity');
}
function addRuler() {

View file

@ -0,0 +1,329 @@
'use strict';
function editUnits() {
closeDialogs('#unitsEditor, .stable');
$('#unitsEditor').dialog();
if (modules.editUnits) return;
modules.editUnits = true;
$('#unitsEditor').dialog({
title: 'Units Editor',
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
<<<<<<< HEAD
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
document.getElementById('barSizeOutput').addEventListener('input', drawScaleBar);
document.getElementById('barSizeInput').addEventListener('input', drawScaleBar);
document.getElementById('barLabel').addEventListener('input', drawScaleBar);
document.getElementById('barPosX').addEventListener('input', fitScaleBar);
document.getElementById('barPosY').addEventListener('input', fitScaleBar);
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
document.getElementById('addRouteOpisometer').addEventListener('click', toggleRouteOpisometerMode);
document.getElementById('addPlanimeter').addEventListener('click', togglePlanimeterMode);
document.getElementById('removeRulers').addEventListener('click', removeAllRulers);
document.getElementById('unitsRestore').addEventListener('click', restoreDefaultUnits);
=======
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", drawScaleBar);
document.getElementById("barSizeInput").addEventListener("input", drawScaleBar);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRateInput").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanizationInput").addEventListener("change", changeUrbanizationRate);
document.getElementById("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
document.getElementById("urbanDensityInput").addEventListener("change", changeUrbanDensity);
document.getElementById("addLinearRuler").addEventListener("click", addRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
>>>>>>> master
function changeDistanceUnit() {
if (this.value === 'custom_name') {
prompt('Provide a custom name for a distance unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('distanceUnit');
drawScaleBar();
calculateFriendlyGridSize();
});
return;
}
drawScaleBar();
calculateFriendlyGridSize();
}
function changeDistanceScale() {
drawScaleBar();
calculateFriendlyGridSize();
}
function changeHeightUnit() {
if (this.value !== 'custom_name') return;
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('heightUnit');
});
}
function changeHeightExponent() {
calculateTemperatures();
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeTemperatureScale() {
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeScaleBarOpacity() {
scaleBar.select('rect').attr('opacity', this.value);
}
function changeScaleBarColor() {
scaleBar.select('rect').attr('fill', this.value);
}
function changePopulationRate() {
populationRate = +this.value;
}
function changeUrbanizationRate() {
urbanization = +this.value;
}
function changeUrbanDensity() {
urbanDensity = +this.value;
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById('distanceScaleOutput').value = 3;
document.getElementById('distanceScaleInput').value = 3;
unlock('distanceScale');
// units
const US = navigator.language === 'en-US';
const UK = navigator.language === 'en-GB';
distanceUnitInput.value = US || UK ? 'mi' : 'km';
heightUnit.value = US || UK ? 'ft' : 'm';
temperatureScale.value = US ? '°F' : '°C';
areaUnit.value = 'square';
localStorage.removeItem('distanceUnit');
localStorage.removeItem('heightUnit');
localStorage.removeItem('temperatureScale');
localStorage.removeItem('areaUnit');
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
localStorage.removeItem('heightExponent');
calculateTemperatures();
// scale bar
barSizeOutput.value = barSizeInput.value = 2;
barLabel.value = '';
barBackOpacity.value = 0.2;
barBackColor.value = '#ffffff';
barPosX.value = barPosY.value = 99;
localStorage.removeItem('barSize');
localStorage.removeItem('barLabel');
localStorage.removeItem('barBackOpacity');
localStorage.removeItem('barBackColor');
localStorage.removeItem('barPosX');
localStorage.removeItem('barPosY');
drawScaleBar();
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
<<<<<<< HEAD
localStorage.removeItem('populationRate');
localStorage.removeItem('urbanization');
=======
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
localStorage.removeItem("urbanDensity");
>>>>>>> master
}
function addRuler() {
if (!layerIsOn('toggleRulers')) toggleRulers();
const pt = document.getElementById('map').createSVGPoint();
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = graphWidth / 4 / scale;
const dy = (rulers.data.length * 40) % (graphHeight / 2);
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw();
}
function toggleOpisometerMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure length. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const opisometer = rulers.create(Opisometer, [point]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
opisometer.addPoint(point);
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addOpisometer.classList.remove('pressed');
if (opisometer.points.length < 2) rulers.remove(opisometer.id);
if (!d3.event.sourceEvent.shiftKey) opisometer.optimize();
});
})
);
}
}
function toggleRouteOpisometerMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve along routes to measure length. Hold Shift to measure away from roads.', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
const routeOpisometer = rulers.create(RouteOpisometer, [[x, y]]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove('pressed');
if (routeOpisometer.points.length < 2) {
rulers.remove(routeOpisometer.id);
}
});
} else {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove('pressed');
tip('Must start in a cell with a route in it', false, 'error');
}
})
);
}
}
function togglePlanimeterMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure its area. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const planimeter = rulers.create(Planimeter, [point]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
planimeter.addPoint(point);
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addPlanimeter.classList.remove('pressed');
if (planimeter.points.length < 3) rulers.remove(planimeter.id);
else if (!d3.event.sourceEvent.shiftKey) planimeter.optimize();
});
})
);
}
}
function removeAllRulers() {
if (!rulers.data.length) return;
alertMessage.innerHTML = `
Are you sure you want to remove all placed rulers?
<br>If you just want to hide rulers, toggle the Rulers layer off in Menu`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rulers',
buttons: {
Remove: function () {
$(this).dialog('close');
rulers.undraw();
rulers = new Rulers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
}

View file

@ -1,25 +1,33 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: "42em",
$('#worldConfigurator').dialog({
title: 'Configure World',
resizable: false,
width: '42em',
buttons: {
"Whole World": () => applyWorldPreset(100, 50),
"Northern": () => applyWorldPreset(33, 25),
"Tropical": () => applyWorldPreset(33, 50),
"Southern": () => applyWorldPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
'Whole World': () => applyWorldPreset(100, 50),
Northern: () => applyWorldPreset(33, 25),
Tropical: () => applyWorldPreset(33, 50),
Southern: () => applyWorldPreset(33, 75),
'Restore Winds': restoreDefaultWinds
},
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip('Click to set map size to cover the whole World'));
buttons[1].addEventListener('mousemove', () => tip('Click to set map size to cover the Northern latitudes'));
buttons[2].addEventListener('mousemove', () => tip('Click to set map size to cover the Tropical latitudes'));
buttons[3].addEventListener('mousemove', () => tip('Click to set map size to cover the Southern latitudes'));
buttons[4].addEventListener('mousemove', () => tip('Click to restore default wind directions'));
},
close: function () {
$(this).dialog('destroy');
}
});
const globe = d3.select("#globe");
const globe = d3.select('#globe');
const clr = d3.scaleSequential(d3.interpolateSpectral);
const tMax = 30, tMin = -25; // temperature extremes
const tMax = 30,
tMin = -25; // temperature extremes
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
const path = d3.geoPath(projection);
@ -29,15 +37,15 @@ function editWorld() {
if (modules.editWorld) return;
modules.editWorld = true;
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
document.getElementById('worldControls').addEventListener('input', (e) => updateWorld(e.target));
globe.select('#globeWindArrows').on('click', changeWind);
globe.select('#globeGraticule').attr('d', round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
function updateWorld(el) {
if (el) {
document.getElementById(el.dataset.stored+"Input").value = el.value;
document.getElementById(el.dataset.stored+"Output").value = el.value;
document.getElementById(el.dataset.stored + 'Input').value = el.value;
document.getElementById(el.dataset.stored + 'Output').value = el.value;
if (el.dataset.stored) lock(el.dataset.stored);
}
@ -52,84 +60,94 @@ function editWorld() {
pack.cells.h = new Float32Array(heights);
defineBiomes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
if (layerIsOn('toggleTemp')) drawTemp();
if (layerIsOn('togglePrec')) drawPrec();
if (layerIsOn('toggleBiomes')) drawBiomes();
if (layerIsOn('toggleCoordinates')) drawCoordinates();
if (layerIsOn('toggleRivers')) drawRivers();
if (document.getElementById('canvas3d')) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
const size = +document.getElementById("mapSizeOutput").value;
const eqD = graphHeight / 2 * 100 / size;
const size = +document.getElementById('mapSizeOutput').value;
const eqD = ((graphHeight / 2) * 100) / size;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const scale = +distanceScaleInput.value,
unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
document.getElementById('mapSize').innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById('mapSizeFriendly').innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById('meridianLength').innerHTML = rn(eqD * 2);
document.getElementById('meridianLengthFriendly').innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById('meridianLengthEarth').innerHTML = meridian ? ' = ' + rn(meridian / 200) + '%🌏' : '';
document.getElementById('mapCoordinates').innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
if (unit === "km") return v;
else if (unit === "mi") return v * 1.60934;
else if (unit === "lg") return v * 5.556;
else if (unit === "vr") return v * 1.0668;
if (unit === 'km') return v;
else if (unit === 'mi') return v * 1.60934;
else if (unit === 'lg') return v * 5.556;
else if (unit === 'vr') return v * 1.0668;
return 0; // 0 if distanceUnitInput is a custom unit
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
function lat(lat) {
return lat > 0 ? Math.abs(rn(lat)) + '°N' : Math.abs(rn(lat)) + '°S';
} // parse latitude value
const area = d3.geoGraticule().extent([
[mc.lonW, mc.latN],
[mc.lonE, mc.latS]
]);
globe.select('#globeArea').attr('d', round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
const tEq = +document.getElementById('temperatureEquatorOutput').value;
document.getElementById('temperatureEquatorF').innerHTML = rn((tEq * 9) / 5 + 32);
const tPole = +document.getElementById('temperaturePoleOutput').value;
document.getElementById('temperaturePoleF').innerHTML = rn((tPole * 9) / 5 + 32);
globe.selectAll('.tempGradient90').attr('stop-color', clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll('.tempGradient60').attr('stop-color', clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
globe.selectAll('.tempGradient30').attr('stop-color', clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
globe.select('.tempGradient0').attr('stop-color', clr(1 - (tEq - tMin) / (tMax - tMin)));
}
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
globe
.select('#globeWindArrows')
.selectAll('path')
.each(function (d, i) {
const tr = parseTransform(this.getAttribute('transform'));
this.setAttribute('transform', `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
const arrow = d3.event.target.nextElementSibling;
const tier = +arrow.dataset.tier;
options.winds[tier] = (options.winds[tier] + 45) % 360;
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const tr = parseTransform(arrow.getAttribute('transform'));
arrow.setAttribute('transform', `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem('winds', options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map((c) => ((90 - c) / 30) | 0);
if (mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map((c) => ((90 - c) / 30) | 0);
const update = mapTiers.some((t) => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
}
function applyWorldPreset(size, lat) {
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
document.getElementById('mapSizeInput').value = document.getElementById('mapSizeOutput').value = size;
document.getElementById('latitudeInput').value = document.getElementById('latitudeOutput').value = lat;
lock('mapSize');
lock('latitude');
updateWorld();
}
}

View file

@ -1,83 +1,83 @@
"use strict";
'use strict';
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
if (!layerIsOn('toggleZones')) toggleZones();
const body = document.getElementById('zonesBodySection');
zonesEditorAddLines();
if (modules.editZones) return;
modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor",
$('#zonesEditor').dialog({
title: 'Zones Editor',
resizable: false,
width: fitContent(),
close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
close: () => exitZonesManualAssignment('close'),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
document.getElementById('zonesEditorRefresh').addEventListener('click', zonesEditorAddLines);
document.getElementById('zonesEditStyle').addEventListener('click', () => editStyle('zones'));
document.getElementById('zonesLegend').addEventListener('click', toggleLegend);
document.getElementById('zonesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('zonesManually').addEventListener('click', enterZonesManualAssignent);
document.getElementById('zonesManuallyApply').addEventListener('click', applyZonesManualAssignent);
document.getElementById('zonesManuallyCancel').addEventListener('click', cancelZonesManualAssignent);
document.getElementById('zonesAdd').addEventListener('click', addZonesLayer);
document.getElementById('zonesExport').addEventListener('click', downloadZonesData);
document.getElementById('zonesRemove').addEventListener('click', toggleEraseMode);
body.addEventListener("click", function (ev) {
body.addEventListener('click', function (ev) {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
if (cl.contains("culturePopulation")) {
if (cl.contains('culturePopulation')) {
changePopulation(zone);
return;
}
if (cl.contains("icon-trash-empty")) {
if (cl.contains('icon-trash-empty')) {
zoneRemove(zone);
return;
}
if (cl.contains("icon-eye")) {
if (cl.contains('icon-eye')) {
toggleVisibility(el);
return;
}
if (cl.contains("icon-pin")) {
if (cl.contains('icon-pin')) {
toggleFog(zone, cl);
return;
}
if (cl.contains("fillRect")) {
if (cl.contains('fillRect')) {
changeFill(el);
return;
}
if (customization) selectZone(el);
});
body.addEventListener("input", function (ev) {
body.addEventListener('input', function (ev) {
const el = ev.target,
zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
if (el.classList.contains('religionName')) zones.select('#' + zone).attr('data-description', el.value);
});
// add line for each zone
function zonesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '';
zones.selectAll("g").each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
zones.selectAll('g').each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(',').map((c) => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const fill = this.getAttribute('fill');
const area = d3.sum(c.map((i) => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map((i) => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map((i) => pack.cells.burg[i]).map((b) => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus" + this.id).size();
const inactive = this.style.display === 'none';
const focused = defs.select('#fog #focus' + this.id).size();
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
@ -89,8 +89,8 @@ function editZones() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? '' : ' inactive'} hide ${c.length ? '' : ' placeholder'}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? ' inactive' : ''} hide ${c.length ? '' : ' placeholder'}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
});
@ -99,73 +99,73 @@ function editZones() {
// update footer
const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter((b) => !b.removed).map((b) => b.population)) * urbanization) * populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterNumber.innerHTML = zones.selectAll('g').size();
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => zoneHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
$("#zonesEditor").dialog({width: fitContent()});
$('#zonesEditor').dialog({width: fitContent()});
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", "1px solid red");
zones.select('#' + zone).style('outline', '1px solid red');
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", null);
zones.select('#' + zone).style('outline', null);
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
$(body).sortable({items: 'div.states', handle: '.icon-resize-vertical', containment: 'parent', axis: 'y', update: movezone});
function movezone(ev, ui) {
const zone = $("#" + ui.item.attr("data-id"));
const prev = $("#" + ui.item.prev().attr("data-id"));
const zone = $('#' + ui.item.attr('data-id'));
const prev = $('#' + ui.item.prev().attr('data-id'));
if (prev) {
zone.insertAfter(prev);
return;
}
const next = $("#" + ui.item.next().attr("data-id"));
const next = $('#' + ui.item.next().attr('data-id'));
if (next) zone.insertBefore(next);
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
if (!layerIsOn('toggleZones')) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
document.querySelectorAll('#zonesBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('zonesManuallyButtons').style.display = 'inline-block';
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
zonesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
zonesFooter.style.display = 'none';
body.querySelectorAll('div > input, select, svg').forEach((e) => (e.style.pointerEvents = 'none'));
$('#zonesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush);
tip('Click to select a zone, drag to paint a zone', true);
viewbox.style('cursor', 'crosshair').on('click', selectZoneOnMapClick).call(d3.drag().on('start', dragZoneBrush)).on('touchmove mousemove', moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function () {
this.setAttribute("data-init", this.getAttribute("data-cells"));
body.querySelector('div').classList.add('selected');
zones.selectAll('g').each(function () {
this.setAttribute('data-init', this.getAttribute('data-cells'));
});
}
function selectZone(el) {
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
el.classList.add('selected');
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
if (d3.event.target.parentElement.parentElement.id !== 'zones') return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
@ -174,7 +174,7 @@ function editZones() {
function dragZoneBrush() {
const r = +zonesBrush.value;
d3.event.on("drag", () => {
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -182,34 +182,34 @@ function editZones() {
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
const selected = body.querySelector("div.selected");
const zone = zones.select("#" + selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const selected = body.querySelector('div.selected');
const zone = zones.select('#' + selected.dataset.id);
const base = zone.attr('id') + '_'; // id generic part
const dataCells = zone.attr('data-cells');
let cells = dataCells ? dataCells.split(',').map((i) => +i) : [];
const erase = document.getElementById("zonesRemove").classList.contains("pressed");
const erase = document.getElementById('zonesRemove').classList.contains('pressed');
if (erase) {
// remove
selection.forEach(i => {
selection.forEach((i) => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
zone.select('polygon#' + base + i).remove();
cells.splice(index, 1);
});
} else {
// add
selection.forEach(i => {
selection.forEach((i) => {
if (cells.includes(i)) return;
cells.push(i);
zone
.append("polygon")
.attr("points", getPackPolygon(i))
.attr("id", base + i);
.append('polygon')
.attr('points', getPackPolygon(i))
.attr('id', base + i);
});
}
zone.attr("data-cells", cells);
zone.attr('data-cells', cells);
});
}
@ -221,11 +221,11 @@ function editZones() {
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
if (this.dataset.cells) return;
// all zone cells are removed
unfog("focusZone" + this.id);
this.style.display = "block";
unfog('focusZone' + this.id);
this.style.display = 'block';
});
zonesEditorAddLines();
@ -234,20 +234,20 @@ function editZones() {
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
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("*")
.selectAll('*')
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
.append('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('id', (d) => base + d);
});
exitZonesManualAssignment();
@ -256,97 +256,97 @@ function editZones() {
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
document.querySelectorAll('#zonesBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('zonesManuallyButtons').style.display = 'none';
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
zonesEditor.querySelectorAll('.hide:not(.show)').forEach((el) => el.classList.remove('hidden'));
zonesFooter.style.display = 'block';
body.querySelectorAll('div > input, select, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (!close) $('#zonesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function () {
this.removeAttribute("data-init");
zones.selectAll('g').each(function () {
this.removeAttribute('data-init');
});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
}
function changeFill(el) {
const fill = el.getAttribute("fill");
const fill = el.getAttribute('fill');
const callback = function (fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
el.setAttribute('fill', fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute('fill', fill);
};
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#" + el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
const zone = zones.select('#' + el.parentNode.dataset.id);
const inactive = zone.style('display') === 'none';
inactive ? zone.style('display', 'block') : zone.style('display', 'none');
el.classList.toggle('inactive');
}
function toggleFog(z, cl) {
const dataCells = zones.select("#" + z).attr("data-cells");
const dataCells = zones.select('#' + z).attr('data-cells');
if (!dataCells) return;
const path =
"M" +
'M' +
dataCells
.split(",")
.map(c => getPackPolygon(+c))
.join("M") +
"Z",
id = "focusZone" + z;
cl.contains("inactive") ? fog(id, path) : unfog(id);
cl.toggle("inactive");
.split(',')
.map((c) => getPackPolygon(+c))
.join('M') +
'Z',
id = 'focusZone' + z;
cl.contains('inactive') ? fog(id, path) : unfog(id);
cl.toggle('inactive');
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const data = [];
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const fill = this.getAttribute('fill');
data.push([id, fill, description]);
});
drawLegend("Zones", data);
drawLegend('Zones', data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +zonesFooterCells.innerHTML;
const totalArea = +zonesFooterArea.dataset.area;
const totalPopulation = +zonesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
body.querySelectorAll(':scope > div').forEach(function (el) {
el.querySelector('.stateCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + '%';
el.querySelector('.culturePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + '%';
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
zonesEditorAddLines();
}
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const id = getNextId('zone');
const description = 'Unknown zone';
const fill = 'url(#hatch' + (id.slice(4) % 14) + ')';
zones.append('g').attr('id', id).attr('data-description', description).attr('data-cells', '').attr('fill', fill);
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
@ -363,53 +363,53 @@ function editZones() {
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
body.insertAdjacentHTML('beforeend', line);
zonesFooterNumber.innerHTML = zones.selectAll('g').size();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area " + unit + ",Population\n"; // headers
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Fill,Description,Cells,Area ' + unit + ',Population\n'; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.fill + ',';
data += el.dataset.description + ',';
data += el.dataset.cells + ',';
data += el.dataset.area + ',';
data += el.dataset.population + '\n';
});
const name = getFileName("Zones") + ".csv";
const name = getFileName('Zones') + '.csv';
downloadFile(data, name);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
this.classList.toggle('pressed');
}
function changePopulation(zone) {
const dataCells = zones.select("#" + zone).attr("data-cells");
const dataCells = zones.select('#' + zone).attr('data-cells');
const cells = dataCells
? dataCells
.split(",")
.map(i => +i)
.filter(i => pack.cells.h[i] >= 20)
.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");
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 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 urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization);
const rural = rn(d3.sum(cells.map((i) => pack.cells.pop[i])) * populationRate);
const urban = rn(d3.sum(cells.map((i) => pack.cells.burg[i]).map((b) => pack.burgs[b].population)) * populationRate * urbanization);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
const l = (n) => Number(n).toLocaleString();
alertMessage.innerHTML = `
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"}>
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? '' : 'disabled'}>
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
@ -422,41 +422,41 @@ function editZones() {
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$("#alert").dialog({
$('#alert').dialog({
resizable: false,
title: "Change zone population",
width: "24em",
title: 'Change zone population',
width: '24em',
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog("close");
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog("close");
$(this).dialog('close');
}
},
position: {my: "center", at: "center", of: "svg"}
position: {my: 'center', at: 'center', of: 'svg'}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const pop = rn(points / cells.length);
cells.forEach(i => (pack.cells.pop[i] = pop));
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => (b.population = population));
burgs.forEach((b) => (b.population = population));
}
zonesEditorAddLines();
@ -464,8 +464,8 @@ function editZones() {
}
function zoneRemove(zone) {
zones.select("#" + zone).remove();
unfog("focusZone" + zone);
zones.select('#' + zone).remove();
unfog('focusZone' + zone);
zonesEditorAddLines();
}
}

File diff suppressed because one or more lines are too long

View file

@ -10,27 +10,26 @@ class Voronoi {
this.delaunay = delaunay;
this.points = points;
this.pointsN = pointsN;
this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
this.cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
this.vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
// Half-edges are the indices into the delaunator outputs:
// delaunay.triangles[e] gives the point ID where the half-edge starts
// delaunay.triangles[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
// delaunay.halfedges[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
for (let e = 0; e < this.delaunay.triangles.length; e++) {
const p = this.delaunay.triangles[this.nextHalfedge(e)];
if (p < this.pointsN && !this.cells.c[p]) {
const edges = this.edgesAroundPoint(e);
this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
this.cells.v[p] = edges.map((e) => this.triangleOfEdge(e)); // cell: adjacent vertex
this.cells.c[p] = edges.map((e) => this.delaunay.triangles[e]).filter((c) => c < this.pointsN); // cell: adjacent valid cells
this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
}
const t = this.triangleOfEdge(e);
if (!this.vertices.p[t]) {
this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
}
}
}
@ -41,7 +40,7 @@ class Voronoi {
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
*/
pointsOfTriangle(t) {
return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
return this.edgesOfTriangle(t).map((edge) => this.delaunay.triangles[edge]);
}
/**
@ -80,7 +79,7 @@ class Voronoi {
* @returns {[number, number]}
*/
triangleCenter(t) {
let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
let vertices = this.pointsOfTriangle(t).map((p) => this.points[p]);
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
}
@ -89,28 +88,36 @@ class Voronoi {
* @param {number} t The index of the triangle
* @returns {[number, number, number]} The edges of the triangle.
*/
edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
edgesOfTriangle(t) {
return [3 * t, 3 * t + 1, 3 * t + 2];
}
/**
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
* @param {number} e The index of the edge
* @returns {number} The index of the triangle
*/
triangleOfEdge(e) { return Math.floor(e / 3); }
triangleOfEdge(e) {
return Math.floor(e / 3);
}
/**
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the next half edge
*/
nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
nextHalfedge(e) {
return e % 3 === 2 ? e - 2 : e + 1;
}
/**
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the previous half edge
*/
prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
prevHalfedge(e) {
return e % 3 === 0 ? e + 2 : e - 1;
}
/**
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
@ -127,9 +134,6 @@ class Voronoi {
const bd = bx * bx + by * by;
const cd = cx * cx + cy * cy;
const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
return [
Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
];
return [Math.floor((1 / D) * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))), Math.floor((1 / D) * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))];
}
}

15
node_modules/.bin/acorn generated vendored Normal file
View file

@ -0,0 +1,15 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
ret=$?
else
node "$basedir/../acorn/bin/acorn" "$@"
ret=$?
fi
exit $ret

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