Compare commits

...

30 commits

Author SHA1 Message Date
Marc Emmanuel
8ba29b2561
refactor: migrate zones (#1300)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* refactor: migrate zones

* refactor: remove duplicate markers property from PackedGraph interface
2026-02-03 17:22:25 +01:00
Marc Emmanuel
86fc62da03
fix: rename feature path functions and update global declarations (#1303)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* fix: rename feature path functions and update global declarations

* chore: lint
2026-02-03 16:59:08 +01:00
Marc Emmanuel
b73557d624
fix: include ice generation in resampling process (#1302)
* feat: include ice generation in resampling process

* chore: update version to 1.112.1 in versioning.js and resample.js script reference
2026-02-03 16:46:19 +01:00
Marc Emmanuel
844fc15891
refactor: migrate religions (#1299)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* refactor: migrate religions

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-02 14:53:59 +01:00
Marc Emmanuel
3ba8338508
refactor: migrate renderers to ts (#1296)
* refactor: migrate renderers to ts

* fix: copilot review
2026-02-02 11:32:08 +01:00
Marc Emmanuel
e8b0b19ff0
feat: show total land percentage in biomes editor footer (#1301)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* feat: show total land percentage in biomes editor footer

* feat: update version to 1.112.0 in versioning.js and biomes-editor.js
2026-02-01 22:18:05 +01:00
Marc Emmanuel
0f19902a56
refactor: migrate provinces generator to new module structure (#1295)
* refactor: migrate provinces generator to new module structure

* fix: after merge fixes of state

* refactor: fixed a bug so had to update tests
2026-02-01 22:16:04 +01:00
Marc Emmanuel
454178fa99
refactor: migrate routes (#1294)
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Code quality / quality (push) Has been cancelled
* refactor: migrate routes

* refactor: format findPath call for improved readability

* refactor: update findPath call to include pack parameter

* refactor: optimize route deletion logic in RoutesModule
2026-01-30 18:29:44 +01:00
Marc Emmanuel
88c70b9264
refactor: migrate states generator (#1291)
* refactor: migrate states generator

* Update src/modules/states-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/states-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 16:44:09 +01:00
Marc Emmanuel
363c82ee30
fix: update port type from string to number and add tests for inland burgs (#1292)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
2026-01-30 13:42:33 +01:00
Marc Emmanuel
e938bc7802
refactor: migrate burg module (#1288)
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Code quality / quality (push) Has been cancelled
* refactor: migrate burg module

* Update src/modules/burgs-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: lint

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 00:11:22 +01:00
Marc Emmanuel
3807903cae
refactor: migrate cultures generator (#1287)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* refactor: migrate cultures generator

* Update src/modules/cultures-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore:lint

* fix: wrong call structure

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-27 21:00:55 +01:00
Marc Emmanuel
260ccd76a3
refactor: migrate names-generator (#1285)
* refactor: migrate names-generator

* Update src/types/global.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/index.ts

* fix: failing builds after merge

* chore: update biome version to 2.3.13 and adjust name validation regex for ASCII characters

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-27 19:29:37 +01:00
Marc Emmanuel
9db40a5230
chore: add biome for linting/formatting + CI action for linting in SRC folder (#1284)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* chore: add npm + vite for progressive enhancement

* fix: update Dockerfile to copy only the dist folder contents

* fix: update Dockerfile to use multi-stage build for optimized production image

* fix: correct nginx config file copy command in Dockerfile

* chore: add netlify configuration for build and redirects

* fix: add NODE_VERSION to environment in Netlify configuration

* remove wrong dist folder

* Update package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: split public and src

* migrating all util files from js to ts

* feat: Implement HeightmapGenerator and Voronoi module

- Added HeightmapGenerator class for generating heightmaps with various tools (Hill, Pit, Range, Trough, Strait, etc.).
- Introduced Voronoi class for creating Voronoi diagrams using Delaunator.
- Updated index.html to include new modules.
- Created index.ts to manage module imports.
- Enhanced arrayUtils and graphUtils with type definitions and improved functionality.
- Added utility functions for generating grids and calculating Voronoi cells.

* chore: add GitHub Actions workflow for deploying to GitHub Pages

* fix: update branch name in GitHub Actions workflow from 'main' to 'master'

* chore: update package.json to specify Node.js engine version and remove unused launch.json

* Initial plan

* Update copilot guidelines to reflect NPM/Vite/TypeScript migration

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

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/utils/graphUtils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: Add TIME and ERROR variables to global scope in HeightmapGenerator

* fix: Update base path in vite.config.ts for Netlify deployment

* refactor: Migrate features to a new module and remove legacy script reference

* refactor: Update feature interfaces and improve type safety in FeatureModule

* refactor: Add documentation for markupPack and defineGroups methods in FeatureModule

* refactor: Remove legacy ocean-layers.js and migrate functionality to ocean-layers.ts

* refactor: Remove river-generator.js script reference and migrate river generation logic to river-generator.ts

* refactor: Remove river-generator.js reference and add biomes module

* refactor: Migrate lakes functionality to lakes.ts and update related interfaces

* refactor: clean up global variable declarations and improve type definitions

* refactor: update shoreline calculation and improve type imports in PackedGraph

* fix: e2e tests

* chore: add biome for linting/formatting

* chore: add linting workflow using Biome

* refactor: improve code readability by standardizing string quotes and simplifying function calls

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <maxganiev@yandex.com>
Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
2026-01-26 22:30:28 +01:00
Marc Emmanuel
e37fce1eed
fix: GeoJSON export (#1283)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
* fix: use global vars instead of window.

* feat: add GitHub Actions workflow for unit tests

* fix: change mapCoordinates declaration from let to var for compatibility
2026-01-26 18:34:35 +01:00
Marc Emmanuel
29bc2832e0
Refactor/migrate first modules (#1273)
* chore: add npm + vite for progressive enhancement

* fix: update Dockerfile to copy only the dist folder contents

* fix: update Dockerfile to use multi-stage build for optimized production image

* fix: correct nginx config file copy command in Dockerfile

* chore: add netlify configuration for build and redirects

* fix: add NODE_VERSION to environment in Netlify configuration

* remove wrong dist folder

* Update package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: split public and src

* migrating all util files from js to ts

* feat: Implement HeightmapGenerator and Voronoi module

- Added HeightmapGenerator class for generating heightmaps with various tools (Hill, Pit, Range, Trough, Strait, etc.).
- Introduced Voronoi class for creating Voronoi diagrams using Delaunator.
- Updated index.html to include new modules.
- Created index.ts to manage module imports.
- Enhanced arrayUtils and graphUtils with type definitions and improved functionality.
- Added utility functions for generating grids and calculating Voronoi cells.

* chore: add GitHub Actions workflow for deploying to GitHub Pages

* fix: update branch name in GitHub Actions workflow from 'main' to 'master'

* chore: update package.json to specify Node.js engine version and remove unused launch.json

* Initial plan

* Update copilot guidelines to reflect NPM/Vite/TypeScript migration

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

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/utils/graphUtils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: Add TIME and ERROR variables to global scope in HeightmapGenerator

* fix: Update base path in vite.config.ts for Netlify deployment

* refactor: Migrate features to a new module and remove legacy script reference

* refactor: Update feature interfaces and improve type safety in FeatureModule

* refactor: Add documentation for markupPack and defineGroups methods in FeatureModule

* refactor: Remove legacy ocean-layers.js and migrate functionality to ocean-layers.ts

* refactor: Remove river-generator.js script reference and migrate river generation logic to river-generator.ts

* refactor: Remove river-generator.js reference and add biomes module

* refactor: Migrate lakes functionality to lakes.ts and update related interfaces

* refactor: clean up global variable declarations and improve type definitions

* refactor: update shoreline calculation and improve type imports in PackedGraph

* fix: e2e tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <maxganiev@yandex.com>
Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
2026-01-26 17:07:54 +01:00
Marc Emmanuel
9903f0b9aa
Test/add e2e and unit testing (#1282)
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
* feat: add string utility tests and vitest browser configuration

* feat: add Playwright for end-to-end testing and update snapshots

- Introduced Playwright for E2E testing with a new configuration file.
- Added test scripts to package.json for running E2E tests.
- Updated package-lock.json and package.json with new dependencies for Playwright and types.
- Created new SVG snapshot files for various layers (ruler, scaleBar, temperature, terrain, vignette, zones) to support visual testing.
- Excluded e2e directory from TypeScript compilation.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add SVG layer snapshots for various components

- Added ruler layer snapshot with hidden display.
- Added scale bar layer snapshot with detailed structure and styling.
- Added temperature layer snapshot with opacity and stroke settings.
- Added terrain layer snapshot with ocean and land heights groups.
- Added vignette layer snapshot with mask and opacity settings.
- Added zones layer snapshot with specified opacity and stroke settings.

* fix: update Playwright browser installation command to use npx

* Update snapshots

* refactor: remove unused layer tests and their corresponding snapshots as fonts are unpredictable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 16:50:21 +01:00
Marc Emmanuel
c590c168f4
fix: update colorUtils and probabilityUtils to use seeded randomness (#1280) 2026-01-23 13:47:35 +01:00
Marc Emmanuel
70ed9aec56
fix: bind HeightmapGenerator methods for correct context in editHeightmap (#1281) 2026-01-23 13:47:13 +01:00
kruschen
4b341a6590
Data model ice (#1279)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
* prototype for ice seperation

* feat: migrate ice data to new data model and update version to 1.110.0

* refactor: update ice data handling and rendering for improved performance

* feat: integrate ice generation and recalculation in heightmap editing

* fix ice selection(hopefully)

* fix ice selection better(pls)

* refactor: remove redundant element selection in ice editing functions

* fix: clear ice data before generating glaciers and icebergs

* sparse array implementation with reduced updates

* fix logic chech in modules/dynamic/auto-update.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: migrate ice data to new data model structure

* refactor: streamline ice generation process and clean up rendering functions

* refactor: simplify ice rendering logic by removing redundant clearing of old SVG

* fix: update editIce function to accept element parameter and improve logic for glacier handling

* ice drawing with only type on less occuring glaciers

* feat: add compactPackData function to filter out undefined glaciers and icebergs

* fix: clear existing ice elements before redrawing in editHeightmap function

* fix compact problems on autosave

* refactor: unify ice data structure and streamline ice element handling

* refactor: improve getNextId function to fill gaps in ice element IDs(optional commit)

* just to be sure

* bump version in html

* fix index.html script import

* feat: add ice module script to index.html

* fix migration check

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 22:24:34 +01:00
Marc Emmanuel
81c1ba2963
fix: initialize heights array if not already set in HeightmapGenerator (#1277)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
2026-01-22 18:28:09 +01:00
Azgaar
b228a8f610
Revert "Ice Layer Data Model (#1262)" (#1275)
This reverts commit e597d905eb.
2026-01-22 17:51:20 +01:00
kruschen
e597d905eb
Ice Layer Data Model (#1262)
* prototype for ice seperation

* feat: migrate ice data to new data model and update version to 1.110.0

* refactor: update ice data handling and rendering for improved performance

* feat: integrate ice generation and recalculation in heightmap editing

* fix ice selection(hopefully)

* fix ice selection better(pls)

* refactor: remove redundant element selection in ice editing functions

* fix: clear ice data before generating glaciers and icebergs

* sparse array implementation with reduced updates

* fix logic chech in modules/dynamic/auto-update.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: migrate ice data to new data model structure

* refactor: streamline ice generation process and clean up rendering functions

* refactor: simplify ice rendering logic by removing redundant clearing of old SVG

* fix: update editIce function to accept element parameter and improve logic for glacier handling

* ice drawing with only type on less occuring glaciers

* feat: add compactPackData function to filter out undefined glaciers and icebergs

* fix: clear existing ice elements before redrawing in editHeightmap function

* fix compact problems on autosave

* refactor: unify ice data structure and streamline ice element handling

* refactor: improve getNextId function to fill gaps in ice element IDs(optional commit)

* just to be sure

* bump version in html

* fix index.html script import

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 17:33:30 +01:00
Marc Emmanuel
b223dc62da
fix: implement quadtree search for points within a radius (#1274) 2026-01-22 17:24:02 +01:00
Azgaar
f30ffd812e
Overview dialogs search (#1260)
* feat: add search functionality to overview components

* feat: enhance search functionality

* chore: correct typo in pull request template

* chore: update version to 1.110.0 and add peer dependencies in package-lock.json; enhance versioning.js with new features

* Fix null safety and performance in overview dialogs search (#1272)

* Initial plan

* fix: add optional chaining and optimize performance in overview dialogs

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
2026-01-22 13:06:13 +01:00
Azgaar
9e0eb03618
[Migration] NPM (#1266)
* chore: add npm + vite for progressive enhancement

* fix: update Dockerfile to copy only the dist folder contents

* fix: update Dockerfile to use multi-stage build for optimized production image

* fix: correct nginx config file copy command in Dockerfile

* chore: add netlify configuration for build and redirects

* fix: add NODE_VERSION to environment in Netlify configuration

* remove wrong dist folder

* Update package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: split public and src

* migrating all util files from js to ts

* feat: Implement HeightmapGenerator and Voronoi module

- Added HeightmapGenerator class for generating heightmaps with various tools (Hill, Pit, Range, Trough, Strait, etc.).
- Introduced Voronoi class for creating Voronoi diagrams using Delaunator.
- Updated index.html to include new modules.
- Created index.ts to manage module imports.
- Enhanced arrayUtils and graphUtils with type definitions and improved functionality.
- Added utility functions for generating grids and calculating Voronoi cells.

* chore: add GitHub Actions workflow for deploying to GitHub Pages

* fix: update branch name in GitHub Actions workflow from 'main' to 'master'

* chore: update package.json to specify Node.js engine version and remove unused launch.json

* Initial plan

* Update copilot guidelines to reflect NPM/Vite/TypeScript migration

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

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/utils/graphUtils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: Add TIME and ERROR variables to global scope in HeightmapGenerator

* fix: Update base path in vite.config.ts for Netlify deployment

* fix: Update Node.js version in Dockerfile to 24-alpine

---------

Co-authored-by: Marc Emmanuel <marc.emmanuel@tado.com>
Co-authored-by: Marc Emmanuel <marcwissler@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
2026-01-22 12:20:12 +01:00
Azgaar
0c26f0831f fix: enhance population constraints in UI and calculations 2026-01-09 23:21:43 +01:00
Azgaar
fa8fd58259 fix: if group is missing, recreate all labels or icons 2026-01-09 22:50:58 +01:00
Azgaar
753db70283 fix: restore MFCG link 2026-01-09 22:26:48 +01:00
Azgaar
d0395624af fix: update group style for old versions 2026-01-08 18:36:21 +01:00
780 changed files with 17932 additions and 8999 deletions

View file

@ -1,58 +1,76 @@
# Fantasy Map Generator
Azgaar's Fantasy Map Generator is a client-side JavaScript web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
Azgaar's Fantasy Map Generator is a web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
- Run the application using HTTP server (required - cannot run with file:// protocol):
- `python3 -m http.server 8000` - takes 2-3 seconds to start
- Access at: `http://localhost:8000`
- The project uses NPM, Vite, and TypeScript for development and building.
- **Setup**: Run `npm install` to install dependencies (requires Node.js >= 24.0.0)
- **Development**: Run `npm run dev` to start the Vite development server
- Access at: `http://localhost:5173` (Vite's default port)
- Hot module replacement (HMR) enabled - changes are reflected immediately
- **Building**: Run `npm run build` to compile TypeScript and build for production
- TypeScript compilation runs first (`tsc`)
- Vite builds the application to `dist/` directory
- **Preview**: Run `npm run preview` to preview the production build locally
## Validation
- Always manually validate any changes by:
1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
2. Navigate to the application in browser
1. Run `npm run dev` to start the development server (wait for "ready" message)
2. Navigate to the application in browser (typically `http://localhost:5173`)
3. Click the "►" button to open the menu and generate a new map
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
5. Test UI interaction: click "Layers" button, verify layer controls work
6. Test regeneration: click "New Map!" button, verify new map generates correctly
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
- For production build validation: run `npm run build` followed by `npm run preview`
## Repository Structure
### Core Files
- `index.html` - Main application entry point
- `main.js` - Core application logic
- `versioning.js` - Version management and update handling
- `package.json` - NPM package configuration with scripts and dependencies
- `vite.config.ts` - Vite build configuration
- `tsconfig.json` - TypeScript compiler configuration
### Key Directories
### Source Directories
- `modules/` - core functionality modules:
- `src/` - Source code directory (build input)
- `src/index.html` - Main application entry point
- `src/utils/` - TypeScript utility modules (migrated from JS)
- `public/` - Static assets (copied to build output)
- `public/main.js` - Core application logic
- `public/versioning.js` - Version management and update handling
- `public/modules/` - Core functionality modules:
- `modules/ui/` - UI components (editors, tools, style management)
- `modules/dynamic/` - runtime modules (export, installation)
- `modules/renderers/` - drawing and rendering logic
- `utils/` - utility libraries (math, arrays, strings, etc.)
- `styles/` - visual style presets (JSON files)
- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
- `images/` - backgrounds, UI elements
- `charges/` - heraldic symbols and coat of arms elements
- `config/` - Heightmap templates and configurations
- `heightmaps/` - Terrain generation data
- `public/styles/` - Visual style presets (JSON files)
- `public/libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
- `public/images/` - Backgrounds, UI elements
- `public/charges/` - Heraldic symbols and coat of arms elements
- `public/config/` - Heightmap templates and configurations
- `public/heightmaps/` - Terrain generation data
- `dist/` - Production build output (generated by `npm run build`)
## Common Tasks
### Making Code Changes
1. Edit JavaScript files directly (no compilation needed)
2. Refresh browser to see changes immediately
3. **ALWAYS test map generation** after making changes
4. Update version in `versioning.js` for all changes
5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
1. **TypeScript files** (`src/utils/*.ts`):
- Edit TypeScript files in the `src/utils/` directory
- Changes are automatically recompiled and hot-reloaded in dev mode
- Run `npm run build` to verify TypeScript compilation succeeds
2. **JavaScript files** (`public/*.js`, `public/modules/*.js`):
- Edit JavaScript files directly in the `public/` directory
- Changes are automatically reflected in dev mode via HMR
- **Note**: Core application logic is still in JavaScript and gradually being migrated
3. **Always test map generation** after making changes
4. Update version in `public/versioning.js` for all changes
5. For production builds, update file hashes in `src/index.html` if needed (format: `file.js?v=1.108.1`)
### Debugging Map Generation
@ -71,19 +89,30 @@ Always reference these instructions first and fallback to search or bash command
### Application Won't Load
- Ensure using HTTP server (not file://)
- Check console for JavaScript errors
- Run `npm install` to ensure dependencies are installed
- Run `npm run dev` to start the development server
- Check console for JavaScript/TypeScript errors
- Verify all files are present in repository
- Ensure Node.js version >= 24.0.0 (`node --version`)
### Build Failures
- Check TypeScript compilation errors (`tsc` output)
- Verify all dependencies are installed (`npm install`)
- Check `tsconfig.json` for configuration issues
- Look for import/module resolution errors
### Map Generation Fails
- Check browser console for error messages
- Look for specific module failures in generation logs
- Try refreshing page and generating new map
- Verify build completed successfully if using production build
### Performance Issues
- Map generation should complete in ~1 second for standard configurations
- If slower, check browser console for errors
- Development mode may be slower due to HMR overhead
Remember: This is a sophisticated client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality.
Remember: This is a sophisticated application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. The project is gradually migrating from vanilla JavaScript to TypeScript. Always validate that your changes preserve the core map generation functionality.

View file

@ -4,7 +4,7 @@
# Type of change
<!-- Please put X into brackers of required option OR delete options that are not relevant -->
<!-- Please put X into brackets of required option OR delete options that are not relevant -->
- [ ] Bug fix
- [ ] New feature

51
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,51 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ['master']
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: 'pages'
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
# Upload dist folder
path: './dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

22
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Code quality
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest
- name: Run Biome
run: biome ci .

25
.github/workflows/playwright.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Playwright Tests
on:
pull_request:
branches: [ master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '24'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

17
.github/workflows/unit-tests.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Unit Tests
on:
pull_request:
branches: [ master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '24'
- name: Install dependencies
run: npm ci
- name: Run Unit tests
run: npm run test

3
.gitignore vendored
View file

@ -1,5 +1,8 @@
.vscode
.idea
/node_modules
*/node_modules
/dist
/coverage
/playwright-report
/test-results

11
.vscode/launch.json vendored
View file

@ -1,11 +0,0 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Debug",
"type": "chrome",
"request": "launch",
"file": "${workspaceFolder}/index.html"
}
]
}

View file

@ -1,7 +1,27 @@
# Build stage
FROM node:24-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY ./src ./src
COPY ./public ./public
COPY vite.config.js .
# Build the application
RUN npm run build
# Production stage
FROM nginx:stable-alpine
# Copy the contents of the repo to the container
COPY . /usr/share/nginx/html
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Move the customized nginx config file to the nginx folder
RUN mv /usr/share/nginx/html/.docker/default.conf /etc/nginx/conf.d/default.conf
# Copy the customized nginx config file to the nginx folder
COPY .docker/default.conf /etc/nginx/conf.d/default.conf

58
biome.json Normal file
View file

@ -0,0 +1,58 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["src/**/*.ts"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useTemplate": {
"level": "warn",
"fix": "safe"
},
"noNonNullAssertion": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noGlobalIsNan": {
"level": "error",
"fix": "safe"
}
},
"correctness": {
"noUnusedVariables": {
"level": "error",
"fix": "safe"
},
"useParseIntRadix": {
"fix": "safe",
"level": "error"
}
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

View file

@ -1,128 +0,0 @@
"use strict";
window.Biomes = (function () {
const MIN_LAND_HEIGHT = 20;
const getDefault = () => {
const name = [
"Marine",
"Hot desert",
"Cold desert",
"Savanna",
"Grassland",
"Tropical seasonal forest",
"Temperate deciduous forest",
"Tropical rainforest",
"Temperate rainforest",
"Taiga",
"Tundra",
"Glacier",
"Wetland"
];
const color = [
"#466eab",
"#fbe79f",
"#b5b887",
"#d2d082",
"#c8d68f",
"#b6d95d",
"#29bc56",
"#7dcb35",
"#409c43",
"#4b6b32",
"#96784b",
"#d5e7eb",
"#0b9131"
];
const habitability = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
const iconsDensity = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
const icons = [
{},
{dune: 3, cactus: 6, deadTree: 1},
{dune: 9, deadTree: 1},
{acacia: 1, grass: 9},
{grass: 1},
{acacia: 8, palm: 1},
{deciduous: 1},
{acacia: 5, palm: 3, deciduous: 1, swamp: 1},
{deciduous: 6, swamp: 1},
{conifer: 1},
{grass: 1},
{},
{swamp: 1}
];
const cost = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
const biomesMartix = [
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10])
];
// parse icons weighted array into a simple array
for (let i = 0; i < icons.length; i++) {
const parsed = [];
for (const icon in icons[i]) {
for (let j = 0; j < icons[i][icon]; j++) {
parsed.push(icon);
}
}
icons[i] = parsed;
}
return {i: d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
};
// assign biome id for each cell
function define() {
TIME && console.time("defineBiomes");
const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
const {temp, prec} = grid.cells;
pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array
for (let cellId = 0; cellId < heights.length; cellId++) {
const height = heights[cellId];
const moisture = height < MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
const temperature = temp[gridReference[cellId]];
pack.cells.biome[cellId] = getId(moisture, temperature, height, Boolean(riverIds[cellId]));
}
function calculateMoisture(cellId) {
let moisture = prec[gridReference[cellId]];
if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2);
const moistAround = neighbors[cellId]
.filter(neibCellId => heights[neibCellId] >= MIN_LAND_HEIGHT)
.map(c => prec[gridReference[c]])
.concat([moisture]);
return rn(4 + d3.mean(moistAround));
}
TIME && console.timeEnd("defineBiomes");
}
function getId(moisture, temperature, height, hasRiver) {
if (height < 20) return 0; // all water cells: marine biome
if (temperature < -5) return 11; // too cold: permafrost biome
if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome
if (isWetland(moisture, temperature, height)) return 12; // too wet: wetland biome
// in other cases use biome matrix
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];
}
function isWetland(moisture, temperature, height) {
if (temperature <= -2) return false; // too cold
if (moisture > 40 && height < 25) return true; // near coast
if (moisture > 24 && height > 24 && height < 60) return true; // off coast
return false;
}
return {getDefault, define, getId};
})();

View file

@ -1,597 +0,0 @@
"use strict";
window.Burgs = (() => {
const generate = () => {
TIME && console.time("generateBurgs");
const {cells} = pack;
let burgs = [0]; // burgs array
cells.burg = new Uint16Array(cells.i.length);
const populatedCells = cells.i.filter(i => cells.s[i] > 0 && cells.culture[i]);
if (!populatedCells.length) {
ERROR && console.error("There is no populated cells with culture assigned. Cannot generate states");
return burgs;
}
let quadtree = d3.quadtree();
generateCapitals();
generateTowns();
pack.burgs = burgs;
shift();
TIME && console.timeEnd("generateBurgs");
function generateCapitals() {
const randomize = score => score * (0.5 + Math.random() * 0.5);
const score = new Int16Array(cells.s.map(randomize));
const sorted = populatedCells.sort((a, b) => score[b] - score[a]);
const capitalsNumber = getCapitalsNumber();
let spacing = (graphWidth + graphHeight) / 2 / capitalsNumber; // min distance between capitals
for (let i = 0; burgs.length <= capitalsNumber; i++) {
const cell = sorted[i];
const [x, y] = cells.p[cell];
if (quadtree.find(x, y, spacing) === undefined) {
burgs.push({cell, x, y});
quadtree.add([x, y]);
}
// reset if all cells were checked
if (i === sorted.length - 1) {
WARN && console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
quadtree = d3.quadtree();
i = -1;
burgs = [0];
spacing /= 1.2;
}
}
burgs.forEach((burg, burgId) => {
if (!burgId) return;
burg.i = burgId;
burg.state = burgId;
burg.culture = cells.culture[burg.cell];
burg.name = Names.getCultureShort(burg.culture);
burg.feature = cells.f[burg.cell];
burg.capital = 1;
cells.burg[burg.cell] = burgId;
});
}
function generateTowns() {
const randomize = score => score * gauss(1, 3, 0, 20, 3);
const score = new Int16Array(cells.s.map(randomize));
const sorted = populatedCells.sort((a, b) => score[b] - score[a]);
const burgsNumber = getTownsNumber();
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** 0.7 / 66); // min distance between town
for (let added = 0; added < burgsNumber && spacing > 1; ) {
for (let i = 0; added < burgsNumber && i < sorted.length; i++) {
if (cells.burg[sorted[i]]) continue;
const cell = sorted[i];
const [x, y] = cells.p[cell];
const minSpacing = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make placement not uniform
if (quadtree.find(x, y, minSpacing) !== undefined) continue; // to close to existing burg
const burgId = burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
const feature = cells.f[cell];
burgs.push({cell, x, y, i: burgId, state: 0, culture, name, feature, capital: 0});
added++;
cells.burg[cell] = burgId;
}
spacing *= 0.5;
}
}
function getCapitalsNumber() {
let number = +byId("statesNumber").value;
if (populatedCells.length < number * 10) {
number = Math.floor(populatedCells.length / 10);
WARN && console.warn(`Not enough populated cells. Generating only ${number} capitals/states`);
}
return number;
}
function getTownsNumber() {
const manorsInput = byId("manorsInput");
const isAuto = manorsInput.value === "1000"; // '1000' is considered as auto
if (isAuto) return rn(populatedCells.length / 5 / (grid.points.length / 10000) ** 0.8);
return Math.min(manorsInput.valueAsNumber, populatedCells.length);
}
};
// define port status and shift ports and burgs on rivers close to the edge of the water body
function shift() {
const {cells, features, burgs} = pack;
const temp = grid.cells.temp;
// port is a capital with any harbor OR any burg with a safe harbor
// safe harbor is a cell having just one adjacent water cell
const featurePortCandidates = {};
for (const burg of burgs) {
if (!burg.i || burg.lock) continue;
delete burg.port; // reset port status
const cellId = burg.cell;
const haven = cells.haven[cellId];
const harbor = cells.harbor[cellId];
const featureId = cells.f[haven];
if (!featureId) continue; // no adjacent water body
const isMulticell = features[featureId].cells > 1;
const isHarbor = (harbor && burg.capital) || harbor === 1;
const isFrozen = temp[cells.g[cellId]] <= 0;
if (isMulticell && isHarbor && !isFrozen) {
if (!featurePortCandidates[featureId]) featurePortCandidates[featureId] = [];
featurePortCandidates[featureId].push(burg);
}
}
// shift ports to the edge of the water body
Object.entries(featurePortCandidates).forEach(([featureId, burgs]) => {
if (burgs.length < 2) return; // only one port on water body - skip
burgs.forEach(burg => {
burg.port = featureId;
const haven = cells.haven[burg.cell];
const [x, y] = getCloseToEdgePoint(burg.cell, haven);
burg.x = x;
burg.y = y;
});
});
// shift non-port river burgs a bit
for (const burg of burgs) {
if (!burg.i || burg.lock || burg.port || !cells.r[burg.cell]) continue;
const cellId = burg.cell;
const shift = Math.min(cells.fl[cellId] / 150, 1);
burg.x = cellId % 2 ? rn(burg.x + shift, 2) : rn(burg.x - shift, 2);
burg.y = cells.r[cellId] % 2 ? rn(burg.y + shift, 2) : rn(burg.y - shift, 2);
}
function getCloseToEdgePoint(cell1, cell2) {
const {cells, vertices} = pack;
const [x0, y0] = cells.p[cell1];
const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
const [x1, y1] = vertices.p[commonVertices[0]];
const [x2, y2] = vertices.p[commonVertices[1]];
const xEdge = (x1 + x2) / 2;
const yEdge = (y1 + y2) / 2;
const x = rn(x0 + 0.95 * (xEdge - x0), 2);
const y = rn(y0 + 0.95 * (yEdge - y0), 2);
return [x, y];
}
}
const specify = () => {
TIME && console.time("specifyBurgs");
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed || burg.lock) return;
definePopulation(burg);
defineEmblem(burg);
defineFeatures(burg);
});
const populations = pack.burgs
.filter(b => b.i && !b.removed)
.map(b => b.population)
.sort((a, b) => a - b); // ascending
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
defineGroup(burg, populations);
});
TIME && console.timeEnd("specifyBurgs");
};
const getType = (cellId, port) => {
const {cells, features} = pack;
if (port) return "Naval";
const haven = cells.haven[cellId];
if (haven !== undefined && features[cells.f[haven]].type === "lake") return "Lake";
if (cells.h[cellId] > 60) return "Highland";
if (cells.r[cellId] && cells.fl[cellId] >= 100) return "River";
const biome = cells.biome[cellId];
const population = cells.pop[cellId];
if (!cells.burg[cellId] || population <= 5) {
if (population < 5 && [1, 2, 3, 4].includes(biome)) return "Nomadic";
if (biome > 4 && biome < 10) return "Hunting";
}
return "Generic";
};
function definePopulation(burg) {
const cellId = burg.cell;
let population = pack.cells.s[cellId] / 5;
if (burg.capital) population *= 1.5;
const connectivityRate = Routes.getConnectivityRate(cellId);
if (connectivityRate) population *= connectivityRate;
population *= gauss(1, 1, 0.25, 4, 5); // randomize
population += ((burg.i % 100) - (cellId % 100)) / 1000; // unround
burg.population = rn(Math.max(population, 0.01), 3);
}
function defineEmblem(burg) {
burg.type = getType(burg.cell, burg.port);
const state = pack.states[burg.state];
const stateCOA = state.coa;
let kinship = 0.25;
if (burg.capital) kinship += 0.1;
else if (burg.port) kinship -= 0.1;
if (burg.culture !== state.culture) kinship -= 0.25;
const type = burg.capital && P(0.2) ? "Capital" : burg.type === "Generic" ? "City" : burg.type;
burg.coa = COA.generate(stateCOA, kinship, null, type);
burg.coa.shield = COA.getShield(burg.culture, burg.state);
}
function defineFeatures(burg) {
const pop = burg.population;
burg.citadel = Number(burg.capital || (pop > 50 && P(0.75)) || (pop > 15 && P(0.5)) || P(0.1));
burg.plaza = Number(
Routes.isCrossroad(burg.cell) || (Routes.hasRoad(burg.cell) && P(0.7)) || pop > 20 || (pop > 10 && P(0.8))
);
burg.walls = Number(burg.capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.1));
burg.shanty = Number(pop > 60 || (pop > 40 && P(0.75)) || (pop > 20 && burg.walls && P(0.4)));
const religion = pack.cells.religion[burg.cell];
const theocracy = pack.states[burg.state].form === "Theocracy";
burg.temple = Number(
(religion && theocracy && P(0.5)) || pop > 50 || (pop > 35 && P(0.75)) || (pop > 20 && P(0.5))
);
}
const getDefaultGroups = () => [
{name: "capital", active: true, order: 9, features: {capital: true}, preview: "watabou-city"},
{name: "city", active: true, order: 8, percentile: 90, min: 5, preview: "watabou-city"},
{
name: "fort",
active: true,
features: {citadel: true, walls: false, plaza: false, port: false},
order: 6,
max: 1
},
{
name: "monastery",
active: true,
features: {temple: true, walls: false, plaza: false, port: false},
order: 5,
max: 0.8
},
{
name: "caravanserai",
active: true,
features: {port: false, plaza: true},
order: 4,
max: 0.8,
biomes: [1, 2, 3]
},
{
name: "trading_post",
active: true,
order: 3,
features: {plaza: true},
max: 0.8,
biomes: [5, 6, 7, 8, 9, 10, 11, 12]
},
{
name: "village",
active: true,
order: 2,
min: 0.1,
max: 2,
preview: "watabou-village"
},
{
name: "hamlet",
active: true,
order: 1,
features: {plaza: false},
max: 0.1,
preview: "watabou-village"
},
{name: "town", active: true, order: 7, isDefault: true, preview: "watabou-city"}
];
function defineGroup(burg, populations) {
if (burg.lock && burg.group) {
// locked burgs: don't change group if it still exists
const group = options.burgs.groups.find(g => g.name === burg.group);
if (group) return;
}
const defaultGroup = options.burgs.groups.find(g => g.isDefault);
if (!defaultGroup) {
ERROR && console.error("No default group defined");
return;
}
burg.group = defaultGroup.name;
for (const group of options.burgs.groups) {
if (!group.active) continue;
if (group.min) {
const isFit = burg.population >= group.min;
if (!isFit) continue;
}
if (group.max) {
const isFit = burg.population <= group.max;
if (!isFit) continue;
}
if (group.features) {
const isFit = Object.entries(group.features).every(([feature, value]) => Boolean(burg[feature]) === value);
if (!isFit) continue;
}
if (group.biomes) {
const isFit = group.biomes.includes(pack.cells.biome[burg.cell]);
if (!isFit) continue;
}
if (group.percentile) {
const index = populations.indexOf(burg.population);
const isFit = index >= Math.floor((populations.length * group.percentile) / 100);
if (!isFit) continue;
}
burg.group = group.name; // apply fitting group
return;
}
}
const previewGeneratorsMap = {
"watabou-city": createWatabouCityLinks,
"watabou-village": createWatabouVillageLinks,
"watabou-dwelling": createWatabouDwellingLinks
};
function getPreview(burg) {
if (burg.link) return {link: burg.link, preview: burg.link};
const group = options.burgs.groups.find(g => g.name === burg.group);
if (!group?.preview || !previewGeneratorsMap[group.preview]) return {link: null, preview: null};
return previewGeneratorsMap[group.preview](burg);
}
function createWatabouCityLinks(burg) {
const cells = pack.cells;
const {i, name, population: burgPopulation, cell} = burg;
const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0);
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
const population = rn(burgPopulation * populationRate * urbanization);
const river = cells.r[cell] ? 1 : 0;
const coast = Number(burg.port > 0);
const sea = (() => {
if (!coast || !cells.haven[cell]) return null;
// calculate see direction: 0 = east, 0.5 = north, 1 = west, 1.5 = south
const [x1, y1] = cells.p[cell];
const [x2, y2] = cells.p[cells.haven[cell]];
const deg = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
if (deg <= 0) return normalize(Math.abs(deg), 0, 180);
return 2 - normalize(deg, 0, 180);
})();
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
const farms = +arableBiomes.includes(cells.biome[cell]);
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
const hub = Routes.isCrossroad(cell);
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shantytown = +burg.shanty;
const style = "natural";
const url = new URL("https://watabou.github.io/city-generator/");
url.search = new URLSearchParams({
name,
population,
size,
seed: burgSeed,
river,
coast,
farms,
citadel,
urban_castle,
hub,
plaza,
temple,
walls,
shantytown,
gates: -1,
style
});
if (sea) url.searchParams.append("sea", sea);
const link = url.toString();
return {link, preview: link + "&preview=1"};
}
function createWatabouVillageLinks(burg) {
const {cells, features} = pack;
const {i, population, cell} = burg;
const burgSeed = seed + String(i).padStart(4, 0);
const pop = rn(population * populationRate * urbanization);
const tags = [];
if (cells.r[cell] && cells.haven[cell]) tags.push("estuary");
else if (cells.haven[cell] && features[cells.f[cell]].cells === 1) tags.push("island,district");
else if (burg.port) tags.push("coast");
else if (cells.conf[cell]) tags.push("confluence");
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const connectivityRate = Routes.getConnectivityRate(cell);
tags.push(connectivityRate > 1 ? "highway" : connectivityRate === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
if (!arableBiomes.includes(biome)) tags.push("uncultivated");
else if (each(6)(cell)) tags.push("farmland");
const temp = grid.cells.temp[cells.g[cell]];
if (temp <= 0 || temp > 28 || (temp > 25 && each(3)(cell))) tags.push("no orchards");
if (!burg.plaza) tags.push("no square");
if (burg.walls) tags.push("palisade");
if (pop < 100) tags.push("sparse");
else if (pop > 300) tags.push("dense");
const width = (() => {
if (pop > 1500) return 1600;
if (pop > 1000) return 1400;
if (pop > 500) return 1000;
if (pop > 200) return 800;
if (pop > 100) return 600;
return 400;
})();
const height = rn(width / 2.05);
const style = (() => {
if ([1, 2].includes(biome)) return "sand";
if (temp <= 5 || [9, 10, 11].includes(biome)) return "snow";
return "default";
})();
const url = new URL("https://watabou.github.io/village-generator/");
url.search = new URLSearchParams({pop, name: burg.name, seed: burgSeed, width, height, style, tags});
const link = url.toString();
return {link, preview: link + "&preview=1"};
}
function createWatabouDwellingLinks(burg) {
const burgSeed = seed + String(burg.i).padStart(4, 0);
const pop = rn(burg.population * populationRate * urbanization);
const tags = (() => {
if (pop > 200) return ["large", "tall"];
if (pop > 100) return ["large"];
if (pop > 50) return ["tall"];
if (pop > 20) return ["low"];
return ["small"];
})();
const url = new URL("https://watabou.github.io/dwellings/");
url.search = new URLSearchParams({pop, name: "", seed: burgSeed, tags});
const link = url.toString();
return {link, preview: link + "&preview=1"};
}
function add([x, y]) {
const {cells} = pack;
const burgId = pack.burgs.length;
const cellId = findCell(x, y);
const culture = cells.culture[cellId];
const name = Names.getCulture(culture);
const state = cells.state[cellId];
const feature = cells.f[cellId];
const burg = {
cell: cellId,
x,
y,
i: burgId,
state,
culture,
name,
feature,
capital: 0,
port: 0
};
definePopulation(burg);
defineEmblem(burg);
defineFeatures(burg);
const populations = pack.burgs
.filter(b => b.i && !b.removed)
.map(b => b.population)
.sort((a, b) => a - b); // ascending
defineGroup(burg, populations);
pack.burgs.push(burg);
cells.burg[cellId] = burgId;
const newRoute = Routes.connect(cellId);
if (newRoute && layerIsOn("toggleRoutes")) drawRoute(newRoute);
drawBurgIcon(burg);
drawBurgLabel(burg);
return burgId;
}
function changeGroup(burg, group) {
if (group) {
burg.group = group;
} else {
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
const populations = validBurgs.map(b => b.population).sort((a, b) => a - b);
defineGroup(burg, populations);
}
drawBurgIcon(burg);
drawBurgLabel(burg);
}
function remove(burgId) {
const burg = pack.burgs[burgId];
if (!burg) return tip(`Burg ${burgId} not found`, false, "error");
pack.cells.burg[burg.cell] = 0;
burg.removed = true;
const noteId = notes.findIndex(note => note.id === `burg${burgId}`);
if (noteId !== -1) notes.splice(noteId, 1);
if (burg.coa) {
byId("burgCOA" + burgId)?.remove();
emblems.select(`#burgEmblems > use[data-i='${burgId}']`).remove();
delete burg.coa;
}
removeBurgIcon(burg.i);
removeBurgLabel(burg.i);
}
return {generate, getDefaultGroups, shift, specify, defineGroup, getPreview, getType, add, changeGroup, remove};
})();

View file

@ -1,618 +0,0 @@
"use strict";
window.Cultures = (function () {
let cells;
const generate = function () {
TIME && console.time("generateCultures");
cells = pack.cells;
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
const culturesInputNumber = +byId("culturesInput").value;
const culturesInSetNumber = +byId("culturesSet").selectedOptions[0].dataset.max;
let count = Math.min(culturesInputNumber, culturesInSetNumber);
const populated = cells.i.filter(i => cells.s[i]); // populated cells
if (populated.length < count * 25) {
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"}];
cells.culture = cultureIds;
alertMessage.innerHTML = /* html */ `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({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
return;
} else {
WARN && console.warn(`Not enough populated cells (${populated.length}). Will generate only ${count} cultures`);
alertMessage.innerHTML = /* html */ ` 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({
resizable: false,
title: "Extreme climate warning",
buttons: {
Ok: function () {
$(this).dialog("close");
}
}
});
}
}
const cultures = (pack.cultures = selectCultures(count));
const centers = d3.quadtree();
const colors = getColors(count);
const emblemShape = document.getElementById("emblemShape").value;
const codes = [];
cultures.forEach(function (c, i) {
const newId = i + 1;
if (c.lock) {
codes.push(c.code);
centers.add(c.center);
for (const i of cells.i) {
if (cells.culture[i] === c.i) cultureIds[i] = newId;
}
c.i = newId;
return;
}
const sortingFn = c.sort ? c.sort : i => cells.s[i];
const center = placeCenter(sortingFn);
centers.add(cells.p[center]);
c.center = center;
c.i = newId;
delete c.odd;
delete c.sort;
c.color = colors[i];
c.type = defineCultureType(center);
c.expansionism = defineCultureExpansionism(c.type);
c.origins = [0];
c.code = abbreviate(c.name, codes);
codes.push(c.code);
cultureIds[center] = newId;
if (emblemShape === "random") c.shield = getRandomShield();
});
cells.culture = cultureIds;
function placeCenter(sortingFn) {
let spacing = (graphWidth + graphHeight) / 2 / count;
const MAX_ATTEMPTS = 100;
const sorted = [...populated].sort((a, b) => sortingFn(b) - sortingFn(a));
const max = Math.floor(sorted.length / 2);
let cellId = 0;
for (let i = 0; i < MAX_ATTEMPTS; i++) {
cellId = sorted[biased(0, max, 5)];
spacing *= 0.9;
if (!cultureIds[cellId] && !centers.find(cells.p[cellId][0], cells.p[cellId][1], spacing)) break;
}
return cellId;
}
// the first culture with id 0 is for wildlands
cultures.unshift({name: "Wildlands", i: 0, base: 1, origins: [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");
nameBases = Names.getNameBases();
}
cultures.forEach(c => (c.base = c.base % nameBases.length));
function selectCultures(culturesNumber) {
let defaultCultures = getDefault(culturesNumber);
const cultures = [];
pack.cultures?.forEach(function (culture) {
if (culture.lock && !culture.removed) cultures.push(culture);
});
if (!cultures.length) {
if (culturesNumber === defaultCultures.length) return defaultCultures;
if (defaultCultures.every(d => d.odd === 1)) return defaultCultures.splice(0, culturesNumber);
}
for (let culture, rnd, i = 0; cultures.length < culturesNumber && defaultCultures.length > 0; ) {
do {
rnd = rand(defaultCultures.length - 1);
culture = defaultCultures[rnd];
i++;
} while (i < 200 && !P(culture.odd));
cultures.push(culture);
defaultCultures.splice(rnd, 1);
}
return cultures;
}
// 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
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";
}
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;
return rn(((Math.random() * byId("sizeVariety").value) / 2 + 1) * base, 1);
}
TIME && console.timeEnd("generateCultures");
};
const add = function (center) {
const defaultCultures = getDefault();
let culture, base, name;
if (pack.cultures.length < defaultCultures.length) {
// add one of the default cultures
culture = pack.cultures.length;
base = defaultCultures[culture].base;
name = defaultCultures[culture].name;
} else {
// add random culture besed on one of the current ones
culture = rand(pack.cultures.length - 1);
name = Names.getCulture(culture, 5, 8, "");
base = pack.cultures[culture].base;
}
const code = abbreviate(
name,
pack.cultures.map(c => c.code)
);
const i = pack.cultures.length;
const color = getRandomColor();
// define emblem shape
let shield = culture.shield;
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,
origins: [pack.cells.culture[center]],
code,
shield
});
};
const getDefault = function (count) {
// generic sorting functions
const cells = pack.cells,
s = cells.s,
sMax = d3.max(s),
t = cells.t,
h = cells.h,
temp = grid.cells.temp;
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
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"}
];
}
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"}
];
}
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"}
];
}
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: 42, odd: 0.3, sort: i => n(i) / td(i, 20) / sf(i), shield: "oval"}, // Levantine
{name: "Hebrew", base: 42, odd: 0.2, sort: i => (n(i) / td(i, 19)) * sf(i), shield: "oval"}, // Levantine
{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") {
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
// 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"
}
];
}
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"},
// 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"},
// 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"},
// 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
];
}
if (culturesSet.value === "random") {
return d3.range(count).map(function () {
const rnd = rand(nameBases.length - 1);
const name = Names.getBaseShort(rnd);
return {name, base: rnd, odd: 1, shield: getRandomShield()};
});
}
// 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: "Hebrew", base: 42, odd: 0.2, sort: i => (n(i) / td(i, 18)) * sf(i), shield: "oval"} // Levantine
];
};
// expand cultures across the map (Dijkstra-like algorithm)
const expand = function () {
TIME && console.time("expandCultures");
const {cells, cultures} = pack;
const queue = new FlatQueue();
const cost = [];
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
const maxExpansionCost = cells.i.length * 0.6 * neutralRate; // limit cost for culture growth
// remove culture from all cells except of locked
const hasLocked = cultures.some(c => !c.removed && c.lock);
if (hasLocked) {
for (const cellId of cells.i) {
const culture = cultures[cells.culture[cellId]];
if (culture.lock) continue;
cells.culture[cellId] = 0;
}
} else {
cells.culture = new Uint16Array(cells.i.length);
}
for (const culture of cultures) {
if (!culture.i || culture.removed || culture.lock) continue;
queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0);
}
while (queue.length) {
const {cellId, priority, cultureId} = queue.pop();
const {type, expansionism} = cultures[cultureId];
cells.c[cellId].forEach(neibCellId => {
if (hasLocked) {
const neibCultureId = cells.culture[neibCellId];
if (neibCultureId && cultures[neibCultureId].lock) return; // do not overwrite cell of locked culture
}
const biome = cells.biome[neibCellId];
const biomeCost = getBiomeCost(cultureId, biome, type);
const biomeChangeCost = biome === cells.biome[neibCellId] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(neibCellId, cells.h[neibCellId], type);
const riverCost = getRiverCost(cells.r[neibCellId], neibCellId, type);
const typeCost = getTypeCost(cells.t[neibCellId], type);
const cellCost = (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / expansionism;
const totalCost = priority + cellCost;
if (totalCost > maxExpansionCost) return;
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
cost[neibCellId] = totalCost;
queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
}
});
}
function getBiomeCost(c, biome, type) {
if (cells.biome[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
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 (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 (h >= 67) return 200; // general mountains crossing penalty
if (h >= 44) return 30; // general hills crossing penalty
return 0;
}
function getRiverCost(riverId, cellId, type) {
if (type === "River") return riverId ? 0 : 100; // penalty for river cultures
if (!riverId) return 0; // no penalty for others if there is no river
return minmax(cells.fl[cellId] / 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
return 0;
}
TIME && console.timeEnd("expandCultures");
};
const getRandomShield = function () {
const type = rw(COA.shields.types);
return rw(COA.shields[type]);
};
return {generate, add, expand, getDefault, getRandomShield};
})();

View file

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

View file

@ -1,543 +0,0 @@
"use strict";
window.HeightmapGenerator = (function () {
let grid = null;
let heights = null;
let blobPower;
let linePower;
const setGraph = graph => {
const {cellsDesired, cells, points} = graph;
heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length});
blobPower = getBlobPower(cellsDesired);
linePower = getLinePower(cellsDesired);
grid = graph;
};
const getHeights = () => heights;
const clearData = () => {
heights = null;
grid = null;
};
const fromTemplate = (graph, id) => {
const templateString = heightmapTemplates[id]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
setGraph(graph);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
addStep(...elements);
}
return heights;
};
const fromPrecreated = (graph, id) => {
return new Promise(resolve => {
// create canvas where 1px corresponts to a cell
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const {cellsX, cellsY} = graph;
canvas.width = cellsX;
canvas.height = cellsY;
// load heightmap into image and render to canvas
const img = new Image();
img.src = `./heightmaps/${id}.png`;
img.onload = () => {
ctx.drawImage(img, 0, 0, cellsX, cellsY);
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
setGraph(graph);
getHeightsFromImageData(imageData.data);
canvas.remove();
img.remove();
resolve(heights);
};
});
};
const generate = async function (graph) {
TIME && console.time("defineHeightmap");
const id = byId("templateInput").value;
Math.random = aleaPRNG(seed);
const isTemplate = id in heightmapTemplates;
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
TIME && console.timeEnd("defineHeightmap");
clearData();
return heights;
};
function addStep(tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(a2, a3, a4, a5);
if (tool === "Pit") return addPit(a2, a3, a4, a5);
if (tool === "Range") return addRange(a2, a3, a4, a5);
if (tool === "Trough") return addTrough(a2, a3, a4, a5);
if (tool === "Strait") return addStrait(a2, a3);
if (tool === "Mask") return mask(a2);
if (tool === "Invert") return invert(a2, a3);
if (tool === "Add") return modify(a3, +a2, 1);
if (tool === "Multiply") return modify(a3, 0, +a2);
if (tool === "Smooth") return smooth(a2);
}
function getBlobPower(cells) {
const blobPowerMap = {
1000: 0.93,
2000: 0.95,
5000: 0.97,
10000: 0.98,
20000: 0.99,
30000: 0.991,
40000: 0.993,
50000: 0.994,
60000: 0.995,
70000: 0.9955,
80000: 0.996,
90000: 0.9964,
100000: 0.9973
};
return blobPowerMap[cells] || 0.98;
}
function getLinePower(cells) {
const linePowerMap = {
1000: 0.75,
2000: 0.77,
5000: 0.79,
10000: 0.81,
20000: 0.82,
30000: 0.83,
40000: 0.84,
50000: 0.86,
60000: 0.87,
70000: 0.88,
80000: 0.91,
90000: 0.92,
100000: 0.93
};
return linePowerMap[cells] || 0.81;
}
const addHill = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count);
while (count > 0) {
addOneHill();
count--;
}
function addOneHill() {
const change = new Uint8Array(heights.length);
let limit = 0;
let start;
let h = lim(getNumberInRange(height));
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] + h > 90 && limit < 50);
change[start] = h;
const queue = [start];
while (queue.length) {
const q = queue.shift();
for (const c of grid.cells.c[q]) {
if (change[c]) continue;
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
if (change[c] > 1) queue.push(c);
}
}
heights = heights.map((h, i) => lim(h + change[i]));
}
};
const addPit = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count);
while (count > 0) {
addOnePit();
count--;
}
function addOnePit() {
const used = new Uint8Array(heights.length);
let limit = 0,
start;
let h = lim(getNumberInRange(height));
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
const queue = [start];
while (queue.length) {
const q = queue.shift();
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
if (h < 1) return;
grid.cells.c[q].forEach(function (c, i) {
if (used[c]) return;
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
used[c] = 1;
queue.push(c);
});
}
}
};
// fromCell, toCell are options cell ids
const addRange = (count, height, rangeX, rangeY, startCell, endCell) => {
count = getNumberInRange(count);
while (count > 0) {
addOneRange();
count--;
}
function addOneRange() {
const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height));
if (rangeX && rangeY) {
// find start and end points
const startX = getPointInRange(rangeX, graphWidth);
const startY = getPointInRange(rangeY, graphHeight);
let dist = 0,
limit = 0,
endX,
endY;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCell = findGridCell(startX, startY, grid);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.85) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
if (min === Infinity) return range;
range.push(cur);
used[cur] = 1;
}
return range;
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
});
h = h ** linePower - 1;
if (h < 2) break;
frontier.forEach(f => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
}
});
});
}
// generate prominences
range.forEach((cur, d) => {
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
cur = min;
}
});
}
};
const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => {
count = getNumberInRange(count);
while (count > 0) {
addOneTrough();
count--;
}
function addOneTrough() {
const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height));
if (rangeX && rangeY) {
// find start and end points
let limit = 0,
startX,
startY,
dist = 0,
endX,
endY;
do {
startX = getPointInRange(rangeX, graphWidth);
startY = getPointInRange(rangeY, graphHeight);
startCell = findGridCell(startX, startY, grid);
limit++;
} while (heights[startCell] < 20 && limit < 50);
limit = 0;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
if (min === Infinity) return range;
range.push(cur);
used[cur] = 1;
}
return range;
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
});
h = h ** linePower - 1;
if (h < 2) break;
frontier.forEach(f => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
}
});
});
}
// generate prominences
range.forEach((cur, d) => {
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
cur = min;
}
});
}
};
const addStrait = (width, direction = "vertical") => {
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return;
const used = new Uint8Array(heights.length);
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;
const endY = vert
? graphHeight - 5
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY, grid);
let range = getRange(start, end);
const query = [];
function getRange(cur, end) {
const range = [];
const p = grid.points;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
range.push(cur);
}
return range;
}
const step = 0.1 / width;
while (width > 0) {
const exp = 0.9 - step * width;
range.forEach(function (r) {
grid.cells.c[r].forEach(function (e) {
if (used[e]) return;
used[e] = 1;
query.push(e);
heights[e] **= exp;
if (heights[e] > 100) heights[e] = 5;
});
});
range = query.slice();
width--;
}
};
const modify = (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];
const isLand = min === 20;
heights = heights.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 = (fr = 2, add = 0) => {
heights = heights.map((h, i) => {
const a = [h];
grid.cells.c[i].forEach(c => a.push(heights[c]));
if (fr === 1) return d3.mean(a) + add;
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
});
};
const mask = (power = 1) => {
const fr = power ? Math.abs(power) : 1;
heights = heights.map((h, i) => {
const [x, y] = grid.points[i];
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
const masked = h * distance;
return lim((h * (fr - 1) + masked) / fr);
});
};
const invert = (count, axes) => {
if (!P(count)) return;
const invertX = axes !== "y";
const invertY = axes !== "x";
const {cellsX, cellsY} = grid;
const inverted = heights.map((h, i) => {
const x = i % cellsX;
const y = Math.floor(i / cellsX);
const nx = invertX ? cellsX - x - 1 : x;
const ny = invertY ? cellsY - y - 1 : y;
const invertedI = nx + ny * cellsX;
return heights[invertedI];
});
heights = inverted;
};
function getPointInRange(range, length) {
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;
return rand(min * length, max * length);
}
function getHeightsFromImageData(imageData) {
for (let i = 0; i < heights.length; i++) {
const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
heights[i] = minmax(Math.floor(powered * 100), 0, 100);
}
}
return {
setGraph,
getHeights,
generate,
fromTemplate,
fromPrecreated,
addHill,
addRange,
addTrough,
addStrait,
addPit,
smooth,
modify,
mask,
invert
};
})();

View file

@ -1,123 +0,0 @@
"use strict";
window.Lakes = (function () {
const LAKE_ELEVATION_DELTA = 0.1;
// check if lake can be potentially open (not in deep depression)
const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
delete feature.closed;
const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
if (MAX_ELEVATION > 99) {
feature.closed = false;
return;
}
let isDeep = true;
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [lowestShorelineCell];
const checked = [];
checked[lowestShorelineCell] = true;
while (queue.length && isDeep) {
const cellId = queue.pop();
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
}
checked[neibCellId] = true;
queue.push(neibCellId);
}
}
feature.closed = isDeep;
});
};
const defineClimateData = function (heights) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell] = feature.i;
});
return lakeOutCells;
function getFlux(lake) {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
function getLakeTemp(lake) {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
}
function getLakeEvaporation(lake) {
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
function getLowestShoreCell(lake) {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
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));
if (!inlets || !inlets.length) delete feature.inlets;
else feature.inlets = inlets;
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
};
const getHeight = function (feature) {
const heights = pack.cells.h;
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
};
const defineNames = function () {
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
feature.name = getName(feature);
});
};
const getName = function (feature) {
const landCell = feature.shoreline[0];
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
};
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName};
})();

View file

@ -1,328 +0,0 @@
"use strict";
window.Names = (function () {
let chains = [];
// calculate Markov chain for a namesbase
const calculateChain = function (string) {
const chain = [];
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
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 (vowel(that)) v = 1; // check if letter is vowel
// do not split some diphthongs
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 (vowel(that) === next) break; // two same vowels in a row
if (v && vowel(name[c + 2])) break; // syllable has vowel and additional vowel is expected soon
}
if (chain[prev] === undefined) chain[prev] = [];
chain[prev].push(syllable);
}
}
return chain;
};
const updateChain = i => {
chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
};
const clearChains = () => {
chains = [];
};
// generate name using Markov's chain
const getBase = function (base, min, max, dupl) {
if (base === undefined) return ERROR && console.error("Please define a base");
if (nameBases[base] === undefined) {
if (nameBases[0]) {
WARN && console.warn("Namebase " + base + " is not found. First available namebase will be used");
base = 0;
} else {
ERROR && console.error("Namebase " + base + " is not found");
return "ERROR";
}
}
if (!chains[base]) updateChain(base);
const data = chains[base];
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;
let v = data[""],
cur = ra(v),
w = "";
for (let i = 0; i < 20; i++) {
if (cur === "") {
// end of word
if (w.length < min) {
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[""];
}
w += cur;
cur = ra(v);
}
// 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
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 (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))
name = name
.split(" ")
.map((p, i) => (i ? p.toLowerCase() : p))
.join("");
if (name.length < 2) {
ERROR && console.error("Name is too short! Random name will be selected");
name = ra(nameBases[base].b.split(","));
}
return name;
};
// generate name for culture
const getCulture = function (culture, min, max, dupl) {
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) return ERROR && console.error("Please define a culture");
return getBaseShort(pack.cultures[culture].base);
};
// generate short name for base
const getBaseShort = function (base) {
const min = nameBases[base] ? nameBases[base].min - 1 : null;
const max = min ? Math.max(nameBases[base].max - 2, min) : null;
return getBase(base, min, max, "", 0);
};
// generate state name based on capital or random name and culture-specific suffix
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 (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(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(0.4)) return name; // 60% for cc and vc
// define suffix
let suffix = "ia"; // standard suffix
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 ? "yurt" : "eli";
// 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
const s1 = suffix.charAt(0);
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2, -1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
return name + suffix;
}
// generato name for the map
const getMapName = function (force) {
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 "";
}
const min = nameBases[base].min - 1;
const max = Math.max(nameBases[base].max - 3, min);
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));
return validateSuffix(name, suffix);
}
const getNameBases = function () {
// name, min length, max length, letters to allow duplication, multi-word name rate [deprecated]
// prettier-ignore
return [
// real-world bases by Azgaar:
{name: "German", i: 0, min: 5, max: 12, d: "lt", m: 0, b: "Achern,Aichhalden,Aitern,Albbruck,Alpirsbach,Altensteig,Althengstett,Appenweier,Auggen,Badenen,Badenweiler,Baiersbronn,Ballrechten,Bellingen,Berghaupten,Bernau,Biberach,Biederbach,Binzen,Birkendorf,Birkenfeld,Bischweier,Blumberg,Bollen,Bollschweil,Bonndorf,Bosingen,Braunlingen,Breisach,Breisgau,Breitnau,Brigachtal,Buchenbach,Buggingen,Buhl,Buhlertal,Calw,Dachsberg,Dobel,Donaueschingen,Dornhan,Dornstetten,Dottingen,Dunningen,Durbach,Durrheim,Ebhausen,Ebringen,Efringen,Egenhausen,Ehrenkirchen,Ehrsberg,Eimeldingen,Eisenbach,Elzach,Elztal,Emmendingen,Endingen,Engelsbrand,Enz,Enzklosterle,Eschbronn,Ettenheim,Ettlingen,Feldberg,Fischerbach,Fischingen,Fluorn,Forbach,Freiamt,Freiburg,Freudenstadt,Friedenweiler,Friesenheim,Frohnd,Furtwangen,Gaggenau,Geisingen,Gengenbach,Gernsbach,Glatt,Glatten,Glottertal,Gorwihl,Gottenheim,Grafenhausen,Grenzach,Griesbach,Gutach,Gutenbach,Hag,Haiterbach,Hardt,Harmersbach,Hasel,Haslach,Hausach,Hausen,Hausern,Heitersheim,Herbolzheim,Herrenalb,Herrischried,Hinterzarten,Hochenschwand,Hofen,Hofstetten,Hohberg,Horb,Horben,Hornberg,Hufingen,Ibach,Ihringen,Inzlingen,Kandern,Kappel,Kappelrodeck,Karlsbad,Karlsruhe,Kehl,Keltern,Kippenheim,Kirchzarten,Konigsfeld,Krozingen,Kuppenheim,Kussaberg,Lahr,Lauchringen,Lauf,Laufenburg,Lautenbach,Lauterbach,Lenzkirch,Liebenzell,Loffenau,Loffingen,Lorrach,Lossburg,Mahlberg,Malsburg,Malsch,March,Marxzell,Marzell,Maulburg,Monchweiler,Muhlenbach,Mullheim,Munstertal,Murg,Nagold,Neubulach,Neuenburg,Neuhausen,Neuried,Neuweiler,Niedereschach,Nordrach,Oberharmersbach,Oberkirch,Oberndorf,Oberbach,Oberried,Oberwolfach,Offenburg,Ohlsbach,Oppenau,Ortenberg,otigheim,Ottenhofen,Ottersweier,Peterstal,Pfaffenweiler,Pfalzgrafenweiler,Pforzheim,Rastatt,Renchen,Rheinau,Rheinfelden,Rheinmunster,Rickenbach,Rippoldsau,Rohrdorf,Rottweil,Rummingen,Rust,Sackingen,Sasbach,Sasbachwalden,Schallbach,Schallstadt,Schapbach,Schenkenzell,Schiltach,Schliengen,Schluchsee,Schomberg,Schonach,Schonau,Schonenberg,Schonwald,Schopfheim,Schopfloch,Schramberg,Schuttertal,Schwenningen,Schworstadt,Seebach,Seelbach,Seewald,Sexau,Simmersfeld,Simonswald,Sinzheim,Solden,Staufen,Stegen,Steinach,Steinen,Steinmauern,Straubenhardt,Stuhlingen,Sulz,Sulzburg,Teinach,Tiefenbronn,Tiengen,Titisee,Todtmoos,Todtnau,Todtnauberg,Triberg,Tunau,Tuningen,uhlingen,Unterkirnach,Reichenbach,Utzenfeld,Villingen,Villingendorf,Vogtsburg,Vohrenbach,Waldachtal,Waldbronn,Waldkirch,Waldshut,Wehr,Weil,Weilheim,Weisenbach,Wembach,Wieden,Wiesental,Wildbad,Wildberg,Winzeln,Wittlingen,Wittnau,Wolfach,Wutach,Wutoschingen,Wyhlen,Zavelstein"},
{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,Ivre,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"},
{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,Arcinazzo,Ariccia,Arpino,Arsoli,Ausonia,Bagnoregio,Bassiano,Bellegra,Belmonte,Bolsena,Bomarzo,Borgorose,Boville,Bracciano,Broccostella,Calcata,Camerata,Campagnano,Campoli,Canale,Canino,Cantalice,Cantalupo,Capranica,Caprarola,Carbognano,Casalattico,Casalvieri,Castelforte,Castelnuovo,Castiglione,Castro,Castrocielo,Ceccano,Celleno,Cellere,Cerreto,Cervara,Cerveteri,Ciampino,Ciciliano,Cittaducale,Cittareale,Civita,Civitella,Colfelice,Colleferro,Collepardo,Colonna,Concerviano,Configni,Contigliano,Cori,Cottanello,Esperia,Faleria,Farnese,Ferentino,Fiamignano,Filacciano,Fiuggi,Fiumicino,Fondi,Fontana,Fonte,Fontechiari,Formia,Frascati,Frasso,Frosinone,Fumone,Gaeta,Gallese,Gavignano,Genazzano,Giuliano,Gorga,Gradoli,Grottaferrata,Grotte,Guarcino,Guidonia,Ischia,Isola,Labico,Labro,Ladispoli,Latera,Lenola,Leonessa,Licenza,Longone,Lubriano,Maenza,Magliano,Marano,Marcellina,Marcetelli,Marino,Mazzano,Mentana,Micigliano,Minturno,Montalto,Montasola,Montebuono,Monteflavio,Montelanico,Monteleone,Montenero,Monterosi,Moricone,Morlupo,Nazzano,Nemi,Nerola,Nespolo,Nettuno,Norma,Olevano,Onano,Oriolo,Orte,Orvinio,Paganico,Paliano,Palombara,Patrica,Pescorocchiano,Petrella,Piansano,Picinisco,Pico,Piedimonte,Piglio,Pignataro,Poggio,Poli,Pomezia,Pontecorvo,Pontinia,Ponzano,Posta,Pozzaglia,Priverno,Proceno,Rignano,Riofreddo,Ripi,Rivodutri,Rocca,Roccagorga,Roccantica,Roccasecca,Roiate,Ronciglione,Roviano,Salisano,Sambuci,Santa,Santini,Scandriglia,Segni,Selci,Sermoneta,Serrone,Settefrati,Sezze,Sgurgola,Sonnino,Sora,Soriano,Sperlonga,Spigno,Subiaco,Supino,Sutri,Tarano,Tarquinia,Terelle,Terracina,Tivoli,Toffia,Tolfa,Torrice,Torricella,Trevi,Trevignano,Trivigliano,Turania,Tuscania,Valentano,Vallecorsa,Vallemaio,Vallepietra,Vallerano,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: "Ajofrin,Alameda,Alaminos,Albares,Albarreal,Albendiego,Alcanizo,Alcaudete,Alcolea,Aldea,Aldeanueva,Algar,Algora,Alhondiga,Almadrones,Almendral,Alovera,Anguita,Arbancon,Argecilla,Arges,Arroyo,Atanzon,Atienza,Azuqueca,Baides,Banos,Bargas,Barriopedro,Belvis,Berninches,Brihuega,Buenaventura,Burgos,Burguillos,Bustares,Cabanillas,Calzada,Camarena,Campillo,Cantalojas,Cardiel,Carmena,Casas,Castejon,Castellar,Castilforte,Castillo,Castilnuevo,Cazalegas,Centenera,Cervera,Checa,Chozas,Chueca,Cifuentes,Cincovillas,Ciruelas,Cogollor,Cogolludo,Consuegra,Copernal,Corral,Cuerva,Domingo,Dosbarrios,Driebes,Duron,Escalona,Escalonilla,Escamilla,Escopete,Espinosa,Esplegares,Esquivias,Estables,Estriegana,Fontanar,Fuembellida,Fuensalida,Fuentelsaz,Gajanejos,Galvez,Gascuena,Gerindote,Guadamur,Heras,Herreria,Herreruela,Hinojosa,Hita,Hombrados,Hontanar,Hormigos,Huecas,Huerta,Humanes,Illana,Illescas,Iniestola,Irueste,Jadraque,Jirueque,Lagartera,Ledanca,Lillo,Lominchar,Loranca,Lucillos,Luzaga,Luzon,Madrid,Magan,Malaga,Malpica,Manzanar,Maqueda,Masegoso,Matillas,Medranda,Megina,Mejorada,Millana,Milmarcos,Mirabueno,Miralrio,Mocejon,Mochales,Molina,Mondejar,Montarron,Mora,Moratilla,Morenilla,Navas,Negredo,Noblejas,Numancia,Nuno,Ocana,Ocentejo,Olias,Olmeda,Ontigola,Orea,Orgaz,Oropesa,Otero,Palma,Pardos,Paredes,Penalver,Pepino,Peralejos,Pinilla,Pioz,Piqueras,Portillo,Poveda,Pozo,Pradena,Prados,Puebla,Puerto,Quero,Quintanar,Rebollosa,Retamoso,Riba,Riofrio,Robledo,Romanillos,Romanones,Rueda,Salmeron,Santiuste,Santo,Sauca,Segura,Selas,Semillas,Sesena,Setiles,Sevilla,Siguenza,Solanillos,Somolinos,Sonseca,Sotillo,Talavera,Taravilla,Tembleque,Tendilla,Tierzo,Torralba,Torre,Torrejon,Torrijos,Tortola,Tortuera,Totanes,Trillo,Uceda,Ugena,Urda,Utande,Valdesotos,Valhermoso,Valtablado,Valverde,Velada,Viana,Yebra,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,Acharnae,Aegae,Aegina,Agrinion,Aigosthena,Akragas,Akroinon,Akrotiri,Alalia,Alexandria,Amarynthos,Amaseia,Amphicaea,Amphigeneia,Amphipolis,Antipatrea,Antiochia,Apamea,Aphidna,Apollonia,Argos,Artemita,Argyropolis,Asklepios,Athenai,Athmonia,Bhrytos,Borysthenes,Brauron,Byblos,Byzantion,Bythinion,Calydon,Chamaizi,Chalcis,Chios,Cleona,Corcyra,Croton,Cyrene,Cythera,Decelea,Delos,Delphi,Dicaearchia,Didyma,Dion,Dioscurias,Dodona,Dorylaion,Elateia,Eleusis,Eleutherna,Emporion,Ephesos,Epidamnos,Epidauros,Epizephyrian,Erythrae,Eubea,Golgi,Gonnos,Gorgippia,Gournia,Gortyn,Gytion,Hagios,Halicarnassos,Heliopolis,Hellespontos,Heloros,Heraclea,Hierapolis,Himera,Histria,Hubla,Hyele,Ialysos,Iasos,Idalion,Imbros,Iolcos,Itanos,Ithaca,Juktas,Kallipolis,Kameiros,Karistos,Kasmenai,Kepoi,Kimmerikon,Knossos,Korinthos,Kos,Kourion,Kydonia,Kyrenia,Lamia,Lampsacos,Laodicea,Lapithos,Larissa,Lebena,Lefkada,Lekhaion,Leibethra,Leontinoi,Lilaea,Lindos,Lissos,Magnesia,Mantineia,Marathon,Marmara,Massalia,Megalopolis,Megara,Metapontion,Methumna,Miletos,Morgantina,Mulai,Mukenai,Myonia,Myra,Myrmekion,Myos,Nauplios,Naucratis,Naupaktos,Naxos,Neapolis,Nemea,Nicaea,Nicopolis,Nymphaion,Nysa,Odessos,Olbia,Olympia,Olynthos,Opos,Orchomenos,Oricos,Orestias,Oreos,Onchesmos,Pagasae,Palaikastro,Pandosia,Panticapaion,Paphos,Pargamon,Paros,Pegai,Pelion,Peiraies,Phaistos,Phaleron,Pharos,Pithekussa,Philippopolis,Phocaea,Pinara,Pisa,Pitane,Plataea,Poseidonia,Potidaea,Pseira,Psychro,Pteleos,Pydna,Pylos,Pyrgos,Rhamnos,Rhithymna,Rhypae,Rizinia,Rodos,Salamis,Samos,Skyllaion,Seleucia,Semasos,Sestos,Scidros,Sicyon,,Sinope,Siris,Smyrna,Sozopolis,Sparta,Stagiros,Stratos,Stymphalos,Sybaris,Surakousai,Taras,Tanagra,Tanais,Tauromenion,Tegea,Temnos,Teos,Thapsos,Thassos,Thebai,Theodosia,Therma,Thespian,Thronion,Thoricos,Thurii,Thyreum,Thyria,Tithoraea,Tomis,Tragurion,Tripolis,Troliton,Troy,Tylissos,Tyros,Vathypetros,Zakynthos,Zakros"},
{name: "Roman", i: 8, min: 6, max: 11, d: "ln", m: .1, b: "Abila,Adflexum,Adnicrem,Aelia,Aelius,Aeminium,Aequum,Agrippina,Agrippinae,Ala,Albanianis,Aleria,Ambianum,Andautonia,Apulum,Aquae,Aquaegranni,Aquensis,Aquileia,Aquincum,Arae,Argentoratum,Ariminum,Ascrivium,Asturica,Atrebatum,Atuatuca,Augusta,Aurelia,Aurelianorum,Batavar,Batavorum,Belum,Biriciana,Blestium,Bonames,Bonna,Bononia,Borbetomagus,Bovium,Bracara,Brigantium,Burgodunum,Caesaraugusta,Caesarea,Caesaromagus,Calleva,Camulodunum,Cannstatt,Cantiacorum,Capitolina,Caralis,Castellum,Castra,Castrum,Cibalae,Clausentum,Colonia,Concangis,Condate,Confluentes,Conimbriga,Corduba,Coria,Corieltauvorum,Corinium,Coriovallum,Cornoviorum,Danum,Deva,Dianium,Divodurum,Dobunnorum,Drusi,Dubris,Dumnoniorum,Durnovaria,Durocobrivis,Durocornovium,Duroliponte,Durovernum,Durovigutum,Eboracum,Ebusus,Edetanorum,Emerita,Emona,Emporiae,Euracini,Faventia,Flaviae,Florentia,Forum,Gerulata,Gerunda,Gesoscribate,Glevensium,Hadriani,Herculanea,Isca,Italica,Iulia,Iuliobrigensium,Iuvavum,Lactodurum,Lagentium,Lapurdum,Lauri,Legionis,Lemanis,Lentia,Lepidi,Letocetum,Lindinis,Lindum,Lixus,Londinium,Lopodunum,Lousonna,Lucus,Lugdunum,Luguvalium,Lutetia,Mancunium,Marsonia,Martius,Massa,Massilia,Matilo,Mattiacorum,Mediolanum,Mod,Mogontiacum,Moridunum,Mursa,Naissus,Nervia,Nida,Nigrum,Novaesium,Noviomagus,Olicana,Olisippo,Ovilava,Parisiorum,Partiscum,Paterna,Pistoria,Placentia,Pollentia,Pomaria,Pompeii,Pons,Portus,Praetoria,Praetorium,Pullum,Ragusium,Ratae,Raurica,Ravenna,Regina,Regium,Regulbium,Rigomagus,Roma,Romula,Rutupiae,Salassorum,Salernum,Salona,Scalabis,Segovia,Silurum,Sirmium,Siscia,Sorviodurum,Sumelocenna,Tarraco,Taurinorum,Theranda,Traiectum,Treverorum,Tungrorum,Turicum,Ulpia,Valentia,Venetiae,Venta,Verulamium,Vesontio,Vetera,Victoriae,Victrix,Villa,Viminacium,Vindelicorum,Vindobona,Vinovia,Viroconium"},
{name: "Finnic", i: 9, min: 5, max: 11, d: "akiut", m: 0, b: "Aanekoski,Ahlainen,Aholanvaara,Ahtari,Aijala,Akaa,Alajarvi,Antsla,Aspo,Bennas,Bjorkoby,Elva,Emasalo,Espoo,Esse,Evitskog,Forssa,Haapamaki,Haapavesi,Haapsalu,Hameenlinna,Hanko,Harjavalta,Hattuvaara,Hautajarvi,Havumaki,Heinola,Hetta,Hinkabole,Hirmula,Hossa,Huittinen,Husula,Hyryla,Hyvinkaa,Ikaalinen,Iskmo,Itakoski,Jamsa,Jarvenpaa,Jeppo,Jioesuu,Jiogeva,Joensuu,Jokikyla,Jungsund,Jyvaskyla,Kaamasmukka,Kajaani,Kalajoki,Kallaste,Kankaanpaa,Karkku,Karpankyla,Kaskinen,Kasnas,Kauhajoki,Kauhava,Kauniainen,Kauvatsa,Kehra,Kellokoski,Kelottijarvi,Kemi,Kemijarvi,Kerava,Keuruu,Kiljava,Kiuruvesi,Kivesjarvi,Kiviioli,Kivisuo,Klaukkala,Klovskog,Kohtlajarve,Kokemaki,Kokkola,Kolho,Koskue,Kotka,Kouva,Kaupunki,Kuhmo,Kunda,Kuopio,Kuressaare,Kurikka,Kuusamo,Kylmalankyla,Lahti,Laitila,Lankipohja,Lansikyla,Lapua,Laurila,Lautiosaari,Lempaala,Lepsama,Liedakkala,Lieksa,Littoinen,Lohja,Loimaa,Loksa,Loviisa,Malmi,Mantta,Matasvaara,Maula,Miiluranta,Mioisakula,Munapirtti,Mustvee,Muurahainen,Naantali,Nappa,Narpio,Niinimaa,Niinisalo,Nikkila,Nilsia,Nivala,Nokia,Nummela,Nuorgam,Nuvvus,Obbnas,Oitti,Ojakkala,Onninen,Orimattila,Orivesi,Otanmaki,Otava,Otepaa,Oulainen,Oulu,Paavola,Paide,Paimio,Pakankyla,Paldiski,Parainen,Parkumaki,Parola,Perttula,Pieksamaki,Pioltsamaa,Piolva,Pohjavaara,Porhola,Porrasa,Porvoo,Pudasjarvi,Purmo,Pyhajarvi,Raahe,Raasepori,Raisio,Rajamaki,Rakvere,Rapina,Rapla,Rauma,Rautio,Reposaari,Riihimaki,Rovaniemi,Roykka,Ruonala,Ruottala,Rutalahti,Saarijarvi,Salo,Sastamala,Saue,Savonlinna,Seinajoki,Sillamae,Siuntio,Sompujarvi,Suonenjoki,Suurejaani,Syrjantaka,Tamsalu,Tapa,Temmes,Tiorva,Tormasenvaara,Tornio,Tottijarvi,Tulppio,Turenki,Turi,Tuukkala,Tuurala,Tuuri,Tuuski,Tuusniemi,Ulvila,Unari,Upinniemi,Utti,Uusikaupunki,Vaaksy,Vaalimaa,Vaarinmaja,Vaasa,Vainikkala,Valga,Valkeakoski,Vantaa,Varkaus,Vehkapera,Vehmasmaki,Vieki,Vierumaki,Viitasaari,Viljandi,Vilppula,Viohma,Vioru,Virrat,Ylike,Ylivieska,Ylojarvi"},
{name: "Korean", i: 10, min: 5, max: 11, d: "", m: 0, b: "Anjung,Ansan,Anseong,Anyang,Aphae,Apo,Baekseok,Baeksu,Beolgyo,Boeun,Boseong,Busan,Buyeo,Changnyeong,Changwon,Cheonan,Cheongdo,Cheongjin,Cheongsong,Cheongyang,Cheorwon,Chirwon,Chuncheon,Chungju,Daedeok,Daegaya,Daejeon,Damyang,Dangjin,Dasa,Donghae,Dongsong,Doyang,Eonyang,Gaeseong,Ganggyeong,Ganghwa,Gangneung,Ganseong,Gaun,Geochang,Geoje,Geoncheon,Geumho,Geumil,Geumwang,Gijang,Gimcheon,Gimhwa,Gimje,Goa,Gochang,Gohan,Gongdo,Gongju,Goseong,Goyang,Gumi,Gunpo,Gunsan,Guri,Gurye,Gwangju,Gwangyang,Gwansan,Gyeongseong,Hadong,Hamchang,Hampyeong,Hamyeol,Hanam,Hapcheon,Hayang,Heungnam,Hongnong,Hongseong,Hwacheon,Hwando,Hwaseong,Hwasun,Hwawon,Hyangnam,Incheon,Inje,Iri,Janghang,Jangheung,Jangseong,Jangseungpo,Jangsu,Jecheon,Jeju,Jeomchon,Jeongeup,Jeonggwan,Jeongju,Jeongok,Jeongseon,Jeonju,Jido,Jiksan,Jinan,Jincheon,Jindo,Jingeon,Jinjeop,Jinnampo,Jinyeong,Jocheon,Jochiwon,Jori,Maepo,Mangyeong,Mokpo,Muju,Munsan,Naesu,Naju,Namhae,Namwon,Namyang,Namyangju,Nongong,Nonsan,Ocheon,Okcheon,Okgu,Onam,Onsan,Onyang,Opo,Paengseong,Pogok,Poseung,Pungsan,Pyeongchang,Pyeonghae,Pyeongyang,Sabi,Sacheon,Samcheok,Samho,Samrye,Sancheong,Sangdong,Sangju,Sapgyo,Sariwon,Sejong,Seocheon,Seogwipo,Seonghwan,Seongjin,Seongju,Seongnam,Seongsan,Seosan,Seungju,Siheung,Sindong,Sintaein,Soheul,Sokcho,Songak,Songjeong,Songnim,Songtan,Suncheon,Taean,Taebaek,Tongjin,Uijeongbu,Uiryeong,Uiwang,Uljin,Ulleung,Unbong,Ungcheon,Ungjin,Waegwan,Wando,Wayang,Wiryeseong,Wondeok,Yangju,Yangsan,Yangyang,Yecheon,Yeomchi,Yeoncheon,Yeongam,Yeongcheon,Yeongdeok,Yeongdong,Yeonggwang,Yeongju,Yeongwol,Yeongyang,Yeonil,Yongin,Yongjin,Yugu"},
{name: "Chinese", i: 11, min: 5, max: 10, d: "", m: 0, b: "Anding,Anlu,Anqing,Anshun,Baixing,Banyang,Baoqing,Binzhou,Caozhou,Changbai,Changchun,Changde,Changling,Changsha,Changzhou,Chengdu,Chenzhou,Chizhou,Chongqing,Chuxiong,Chuzhou,Dading,Daming,Datong,Daxing,Dengzhou,Deqing,Dihua,Dingli,Dongan,Dongchang,Dongchuan,Dongping,Duyun,Fengtian,Fengxiang,Fengyang,Fenzhou,Funing,Fuzhou,Ganzhou,Gaoyao,Gaozhou,Gongchang,Guangnan,Guangning,Guangping,Guangxin,Guangzhou,Guiyang,Hailong,Hangzhou,Hanyang,Hanzhong,Heihe,Hejian,Henan,Hengzhou,Hezhong,Huaian,Huaiqing,Huanglong,Huangzhou,Huining,Hulan,Huzhou,Jiading,Jian,Jianchang,Jiangning,Jiankang,Jiaxing,Jiayang,Jilin,Jinan,Jingjiang,Jingzhao,Jinhua,Jinzhou,Jiujiang,Kaifeng,Kaihua,Kangding,Kuizhou,Laizhou,Lianzhou,Liaoyang,Lijiang,Linan,Linhuang,Lintao,Liping,Liuzhou,Longan,Longjiang,Longxing,Luan,Lubin,Luzhou,Mishan,Nanan,Nanchang,Nandian,Nankang,Nanyang,Nenjiang,Ningbo,Ningguo,Ningwu,Ningxia,Ningyuan,Pingjiang,Pingliang,Pingyang,Puer,Puzhou,Qianzhou,Qingyang,Qingyuan,Qingzhou,Qujing,Quzhou,Raozhou,Rende,Ruian,Ruizhou,Shafeng,Shajing,Shaoqing,Shaowu,Shaoxing,Shaozhou,Shinan,Shiqian,Shouchun,Shuangcheng,Shulei,Shunde,Shuntian,Shuoping,Sicheng,Sinan,Sizhou,Songjiang,Suiding,Suihua,Suining,Suzhou,Taian,Taibei,Taiping,Taiwan,Taiyuan,Taizhou,Taonan,Tengchong,Tingzhou,Tongchuan,Tongqing,Tongzhou,Weihui,Wensu,Wenzhou,Wuchang,Wuding,Wuzhou,Xian,Xianchun,Xianping,Xijin,Xiliang,Xincheng,Xingan,Xingde,Xinghua,Xingjing,Xingyi,Xingyuan,Xingzhong,Xining,Xinmen,Xiping,Xuanhua,Xunzhou,Xuzhou,Yanan,Yangzhou,Yanji,Yanping,Yanzhou,Yazhou,Yichang,Yidu,Yilan,Yili,Yingchang,Yingde,Yingtian,Yingzhou,Yongchang,Yongping,Yongshun,Yuanzhou,Yuezhou,Yulin,Yunnan,Yunyang,Zezhou,Zhang,Zhangzhou,Zhaoqing,Zhaotong,Zhenan,Zhending,Zhenhai,Zhenjiang,Zhenxi,Zhenyun,Zhongshan,Zunyi"},
{name: "Japanese", i: 12, min: 4, max: 10, d: "", m: 0, b: "Abira,Aga,Aikawa,Aizumisato,Ajigasawa,Akkeshi,Amagi,Ami,Ando,Asakawa,Ashikita,Bandai,Biratori,Chonan,Esashi,Fuchu,Fujimi,Funagata,Genkai,Godo,Goka,Gonohe,Gyokuto,Haboro,Hamatonbetsu,Harima,Hashikami,Hayashima,Heguri,Hidaka,Higashiura,Hiranai,Hirogawa,Hiroo,Hodatsushimizu,Hoki,Hokuei,Hokuryu,Horokanai,Ibigawa,Ichikai,Ichikawa,Ichinohe,Iijima,Iizuna,Ikawa,Inagawa,Itakura,Iwaizumi,Iwate,Kaisei,Kamifurano,Kamiita,Kamijima,Kamikawa,Kamishihoro,Kamiyama,Kanda,Kanna,Kasagi,Kasuya,Katsuura,Kawabe,Kawamoto,Kawanehon,Kawanishi,Kawara,Kawasaki,Kawatana,Kawazu,Kihoku,Kikonai,Kin,Kiso,Kitagata,Kitajima,Kiyama,Kiyosato,Kofu,Koge,Kohoku,Kokonoe,Kora,Kosa,Kotohira,Kudoyama,Kumejima,Kumenan,Kumiyama,Kunitomi,Kurate,Kushimoto,Kutchan,Kyonan,Kyotamba,Mashike,Matsumae,Mifune,Mihama,Minabe,Minami,Minamiechizen,Minamitane,Misaki,Misasa,Misato,Miyashiro,Miyoshi,Mori,Moseushi,Mutsuzawa,Nagaizumi,Nagatoro,Nagayo,Nagomi,Nakadomari,Nakanojo,Nakashibetsu,Namegawa,Nanbu,Nanporo,Naoshima,Nasu,Niseko,Nishihara,Nishiizu,Nishikatsura,Nishikawa,Nishinoshima,Nishiwaga,Nogi,Noto,Nyuzen,Oarai,Obuse,Odai,Ogawara,Oharu,Oirase,Oishida,Oiso,Oizumi,Oji,Okagaki,Okutama,Omu,Ono,Osaka,Otobe,Otsuki,Owani,Reihoku,Rifu,Rikubetsu,Rishiri,Rokunohe,Ryuo,Saka,Sakuho,Samani,Satsuma,Sayo,Saza,Setana,Shakotan,Shibayama,Shikama,Shimamoto,Shimizu,Shintomi,Shirakawa,Shisui,Shitara,Sobetsu,Sue,Sumita,Suooshima,Suttsu,Tabuse,Tachiarai,Tadami,Tadaoka,Taiji,Taiki,Takachiho,Takahama,Taketoyo,Taragi,Tateshina,Tatsugo,Tawaramoto,Teshikaga,Tobe,Tokigawa,Toma,Tomioka,Tonosho,Tosa,Toyokoro,Toyotomi,Toyoyama,Tsubata,Tsubetsu,Tsukigata,Tsuno,Tsuwano,Umi,Wakasa,Yamamoto,Yamanobe,Yamatsuri,Yanaizu,Yasuda,Yoichi,Yonaguni,Yoro,Yoshino,Yubetsu,Yugawara,Yuni,Yusuhara,Yuza"},
{name: "Portuguese", i: 13, min: 5, max: 11, d: "", m: .1, b: "Abrigada,Afonsoeiro,Agueda,Aguilada,Alagoas,Alagoinhas,Albufeira,Alcanhoes,Alcobaca,Alcoutim,Aldoar,Alenquer,Alfeizerao,Algarve,Almada,Almagreira,Almeirim,Alpalhao,Alpedrinha,Alvorada,Amieira,Anapolis,Apelacao,Aranhas,Arganil,Armacao,Assenceira,Aveiro,Avelar,Balsas,Barcarena,Barreiras,Barretos,Batalha,Beira,Benavente,Betim,Braga,Braganca,Brasilia,Brejo,Cabeceiras,Cabedelo,Cachoeiras,Cadafais,Calhandriz,Calheta,Caminha,Campinas,Canidelo,Canoas,Capinha,Carmoes,Cartaxo,Carvalhal,Carvoeiro,Cascavel,Castanhal,Caxias,Chapadinha,Chaves,Cocais,Coentral,Coimbra,Comporta,Conde,Coqueirinho,Coruche,Damaia,Dourados,Enxames,Ericeira,Ervidel,Escalhao,Esmoriz,Espinhal,Estela,Estoril,Eunapolis,Evora,Famalicao,Fanhoes,Faro,Fatima,Felgueiras,Ferreira,Figueira,Flecheiras,Florianopolis,Fornalhas,Fortaleza,Freiria,Freixeira,Fronteira,Fundao,Gracas,Gradil,Grainho,Gralheira,Guimaraes,Horta,Ilhavo,Ilheus,Lages,Lagos,Laranjeiras,Lavacolhos,Leiria,Limoeiro,Linhares,Lisboa,Lomba,Lorvao,Lourical,Lourinha,Luziania,Macedo,Machava,Malveira,Marinhais,Maxial,Mealhada,Milharado,Mira,Mirandela,Mogadouro,Montalegre,Mourao,Nespereira,Nilopolis,Obidos,Odemira,Odivelas,Oeiras,Oleiros,Olhalvo,Olinda,Olival,Oliveira,Oliveirinha,Palheiros,Palmeira,Palmital,Pampilhosa,Pantanal,Paradinha,Parelheiros,Pedrosinho,Pegoes,Penafiel,Peniche,Pinhao,Pinheiro,Pombal,Pontal,Pontinha,Portel,Portimao,Quarteira,Queluz,Ramalhal,Reboleira,Recife,Redinha,Ribadouro,Ribeira,Ribeirao,Rosais,Sabugal,Sacavem,Sagres,Sandim,Sangalhos,Santarem,Santos,Sarilhos,Seixas,Seixezelo,Seixo,Silvares,Silveira,Sinhaem,Sintra,Sobral,Sobralinho,Tabuaco,Tabuleiro,Taveiro,Teixoso,Telhado,Telheiro,Tomar,Torreira,Trancoso,Troviscal,Vagos,Varzea,Velas,Viamao,Viana,Vidigal,Vidigueira,Vidual,Vilamar,Vimeiro,Vinhais,Vitoria"},
{name: "Nahuatl", i: 14, min: 6, max: 13, d: "l", m: 0, b: "Acapulco,Acatepec,Acatlan,Acaxochitlan,Acolman,Actopan,Acuamanala,Ahuacatlan,Almoloya,Amacuzac,Amanalco,Amaxac,Apaxco,Apetatitlan,Apizaco,Atenco,Atizapan,Atlacomulco,Atlapexco,Atotonilco,Axapusco,Axochiapan,Axocomanitla,Axutla,Azcapotzalco,Aztahuacan,Calimaya,Calnali,Calpulalpan,Camotlan,Capulhuac,Chalco,Chapulhuacan,Chapultepec,Chiapan,Chiautempan,Chiconautla,Chihuahua,Chilcuautla,Chimalhuacan,Cholollan,Cihuatlan,Coahuila,Coatepec,Coatetelco,Coatlan,Coatlinchan,Coatzacoalcos,Cocotitlan,Cohetzala,Colima,Colotlan,Coyoacan,Coyohuacan,Cuapiaxtla,Cuauhnahuac,Cuauhtemoc,Cuauhtitlan,Cuautepec,Cuautla,Cuaxomulco,Culhuacan,Ecatepec,Eloxochitlan,Epatlan,Epazoyucan,Huamantla,Huascazaloya,Huatlatlauca,Huautla,Huehuetlan,Huehuetoca,Huexotla,Hueyapan,Hueyotlipan,Hueypoxtla,Huichapan,Huimilpan,Huitzilac,Ixtapallocan,Iztacalco,Iztaccihuatl,Iztapalapa,Lolotla,Malinalco,Mapachtlan,Mazatepec,Mazatlan,Metepec,Metztitlan,Mexico,Miacatlan,Michoacan,Minatitlan,Mixcoac,Mixtla,Molcaxac,Nanacamilpa,Naucalpan,Naupan,Nextlalpan,Nezahualcoyotl,Nopalucan,Oaxaca,Ocotepec,Ocotitlan,Ocotlan,Ocoyoacac,Ocuilan,Ocuituco,Omitlan,Otompan,Otzoloapan,Pacula,Pahuatlan,Panotla,Papalotla,Patlachican,Piaztla,Popocatepetl,Sultepec,Tecamac,Tecolotlan,Tecozautla,Temamatla,Temascalapa,Temixco,Temoac,Temoaya,Tenayuca,Tenochtitlan,Teocuitlatlan,Teotihuacan,Teotlalco,Tepeacac,Tepeapulco,Tepehuacan,Tepetitlan,Tepeyanco,Tepotzotlan,Tepoztlan,Tetecala,Tetlatlahuca,Texcalyacac,Texcoco,Tezontepec,Tezoyuca,Timilpan,Tizapan,Tizayuca,Tlacopan,Tlacotenco,Tlahuac,Tlahuelilpan,Tlahuiltepa,Tlalmanalco,Tlalnepantla,Tlalpan,Tlanchinol,Tlatelolco,Tlaxcala,Tlaxcoapan,Tlayacapan,Tocatlan,Tolcayuca,Toluca,Tonanitla,Tonantzintla,Tonatico,Totolac,Totolapan,Tototlan,Tuchtlan,Tulantepec,Tultepec,Tzompantepec,Xalatlaco,Xaloztoc,Xaltocan,Xiloxoxtla,Xochiatipan,Xochicoatlan,Xochimilco,Xochitepec,Xolotlan,Xonacatlan,Yahualica,Yautepec,Yecapixtla,Yehaultepec,Zacatecas,Zacazonapan,Zacoalco,Zacualpan,Zacualtipan,Zapotlan,Zimapan,Zinacantepec,Zoyaltepec,Zumpahuacan"},
{name: "Hungarian", i: 15, min: 6, max: 13, d: "", m: 0.1, b: "Aba,Abadszalok,Adony,Ajak,Albertirsa,Alsozsolca,Aszod,Babolna,Bacsalmas,Baktaloranthaza,Balassagyarmat,Balatonalmadi,Balatonboglar,Balkany,Balmazujvaros,Barcs,Bataszek,Batonyterenye,Battonya,Bekes,Berettyoujfalu,Berhida,Biatorbagy,Bicske,Biharkeresztes,Bodajk,Boly,Bonyhad,Budakalasz,Budakeszi,Celldomolk,Csakvar,Csenger,Csongrad,Csorna,Csorvas,Csurgo,Dabas,Demecser,Derecske,Devavanya,Devecser,Dombovar,Dombrad,Dunafoldvar,Dunaharaszti,Dunavarsany,Dunavecse,Edeleny,Elek,Emod,Encs,Enying,Ercsi,Fegyvernek,Fehergyarmat,Felsozsolca,Fertoszentmiklos,Fonyod,Fot,Fuzesabony,Fuzesgyarmat,Gardony,God,Gyal,Gyomaendrod,Gyomro,Hajdudorog,Hajduhadhaz,Hajdusamson,Hajduszoboszlo,Halasztelek,Harkany,Hatvan,Heves,Heviz,Ibrany,Isaszeg,Izsak,Janoshalma,Janossomorja,Jaszapati,Jaszarokszallas,Jaszfenyszaru,Jaszkiser,Kaba,Kalocsa,Kapuvar,Karcag,Kecel,Kemecse,Kenderes,Kerekegyhaza,Keszthely,Kisber,Kiskunmajsa,Kistarcsa,Kistelek,Kisujszallas,Kisvarda,Komadi,Komarom,Komlo,Kormend,Korosladany,Koszeg,Kozarmisleny,Kunhegyes,Kunszentmarton,Kunszentmiklos,Labatlan,Lajosmizse,Lenti,Letavertes,Letenye,Lorinci,Maglod,Mako,Mandok,Marcali,Martonvasar,Mateszalka,Melykut,Mezobereny,Mezocsat,Mezohegyes,Mezokeresztes,Mezokovesd,Mezotur,Mindszent,Mohacs,Monor,Mor,Morahalom,Nadudvar,Nagyatad,Nagyecsed,Nagyhalasz,Nagykallo,Nagykoros,Nagymaros,Nyekladhaza,Nyergesujfalu,Nyirbator,Nyirmada,Nyirtelek,Ocsa,Orkeny,Oroszlany,Paks,Pannonhalma,Paszto,Pecel,Pecsvarad,Pilisvorosvar,Polgar,Polgardi,Pomaz,Puspokladany,Pusztaszabolcs,Putnok,Racalmas,Rackeve,Rakamaz,Rakoczifalva,Sajoszent,Sandorfalva,Sarbogard,Sarkad,Sarospatak,Sarvar,Satoraljaujhely,Siklos,Simontornya,Soltvadkert,Sumeg,Szabadszallas,Szarvas,Szazhalombatta,Szecseny,Szeghalom,Szentgotthard,Szentlorinc,Szerencs,Szigethalom,Szigetvar,Szikszo,Tab,Tamasi,Tapioszele,Tapolca,Teglas,Tet,Tiszafoldvar,Tiszafured,Tiszakecske,Tiszalok,Tiszaujvaros,Tiszavasvari,Tokaj,Tokol,Tompa,Torokbalint,Torokszentmiklos,Totkomlos,Tura,Turkeve,Ujkigyos,ujszasz,Vamospercs,Varpalota,Vasarosnameny,Vasvar,Vecses,Veresegyhaz,Verpelet,Veszto,Zahony,Zalaszentgrot,Zirc,Zsambek"},
{name: "Turkish", i: 16, min: 4, max: 10, d: "", m: 0, b: "Yelkaya,Buyrukkaya,Erdemtepe,Alakesen,Baharbeyli,Bozbay,Karaoklu,Altunbey,Yalkale,Yalkut,Akardere,Altayburnu,Esentepe,Okbelen,Derinsu,Alaoba,Yamanbeyli,Aykor,Ekinova,Saztepe,Baharkale,Devrekdibi,Alpseki,Ormanseki,Erkale,Yalbelen,Aytay,Yamanyaka,Altaydelen,Esen,Yedieli,Alpkor,Demirkor,Yediyol,Erdemkaya,Yayburnu,Ganiler,Bayatyurt,Kopuzteke,Aytepe,Deniz,Ayan,Ayazdere,Tepe,Kayra,Ayyaka,Deren,Adatepe,Kalkaneli,Bozkale,Yedidelen,Kocayolu,Sazdere,Bozkesen,Oguzeli,Yayladibi,Uluyol,Altay,Ayvar,Alazyaka,Yaloba,Suyaka,Baltaberi,Poyrazdelen,Eymir,Yediyuva,Kurt,Yeltepe,Oktar,Kara Ok,Ekinberi,Er Yurdu,Eren,Erenler,Ser,Oguz,Asay,Bozokeli,Aykut,Ormanyol,Yazkaya,Kalkanova,Yazbeyli,Dokuz Teke,Bilge,Ertensuyu,Kopuzyuva,Buyrukkut,Akardiken,Aybaray,Aslanbeyli,Altun Kaynak,Atikobasi,Yayla Eli,Kor Tepe,Salureli,Kor Kaya,Aybarberi,Kemerev,Yanaray,Beydileli,Buyrukoba,Yolduman,Tengri Tepe,Dokuzsu,Uzunkor,Erdem Yurdu,Kemer,Korteke,Bozokev,Bozoba,Ormankale,Askale,Oguztoprak,Yolberi,Kumseki,Esenobasi,Turkbelen,Ayazseki,Cereneli,Taykut,Bayramdelen,Beydilyaka,Boztepe,Uluoba,Yelyaka,Ulgardiken,Esensu,Baykale,Cerenkor,Bozyol,Duranoba,Aladuman,Denizli,Bahar,Yarkesen,Dokuzer,Yamankaya,Kocatarla,Alayaka,Toprakeli,Sarptarla,Sarpkoy,Serkaynak,Adayaka,Ayazkaynak,Kopuz,Turk,Kart,Kum,Erten,Buyruk,Yel,Ada,Alazova,Ayvarduman,Buyrukok,Ayvartoprak,Uzuntepe,Binseki,Yedibey,Durankale,Alaztoprak,Sarp Ok,Yaparobasi,Yaytepe,Asberi,Kalkankor,Beydiltepe,Adaberi,Bilgeyolu,Ganiyurt,Alkanteke,Esenerler,Asbey,Erdemkale,Erenkaynak,Oguzkoyu,Ayazoba,Boynuztoprak,Okova,Yaloklu,Sivriberi,Yuladiken,Sazbey,Karakaynak,Kopuzkoyu,Buyrukay,Kocakaya,Tepeduman,Yanarseki,Atikyurt,Esenev,Akarbeyli,Yayteke,Devreksungur,Akseki,Baykut,Kalkandere,Ulgarova,Devrekev,Yulabey,Bayatev,Yazsu,Vuraleli,Sivribeyli,Alaova,Alpobasi,Yalyurt,Elmatoprak,Alazkaynak,Esenay,Ertenev,Salurkor,Ekinok,Yalbey,Yeldere,Ganibay,Altaykut,Baltaboy,Ereli,Ayvarsu,Uzunsaz,Bayeli,Erenyol,Kocabay,Derintay,Ayazyol,Aslanoba,Esenkaynak,Ekinlik,Alpyolu,Alayunt,Bozeski,Erkil,Duransuyu,Yulak,Kut,Dodurga,Kutlubey,Kutluyurt,Boynuz,Alayol,Aybar,Aslaneli,Kemerseki,Baltasuyu,Akarer,Ayvarburnu,Boynuzbeyli,Adasungur,Esenkor,Yamanoba,Toprakkor,Uzunyurt,Sungur,Bozok,Kemerli,Alaz,Demirci,Kartepe"},
{name: "Berber", i: 17, min: 4, max: 10, d: "s", m: .2, b: "Abkhouch,Adrar,Aeraysh,Afrag,Agadir,Agelmam,Aghmat,Agrakal,Agulmam,Ahaggar,Ait Baha,Ajdir,Akka,Almou,Amegdul,Amizmiz,Amknas,Amlil,Amurakush,Anfa,Annaba,Aousja,Arbat,Arfud,Argoub,Arif,Asfi,Asfru,Ashawen,Assamer,Assif,Awlluz,Ayt Melel,Azaghar,Azila,Azilal,Azmour,Azro,Azrou,Beccar,Beja,Bennour,Benslimane,Berkane,Berrechid,Bizerte,Bjaed,Bouayach,Boudenib,Boufrah,Bouskoura,Boutferda,Darallouch,Dar Bouazza,Darchaabane,Dcheira,Demnat,Denden,Djebel,Djedeida,Drargua,Elhusima,Essaouira,Ezzahra,Fas,Fnideq,Ghezeze,Goubellat,Grisaffen,Guelmim,Guercif,Hammamet,Harrouda,Hdifa,Hoceima,Houara,Idhan,Idurar,Ifendassen,Ifoghas,Ifrane,Ighoud,Ikbir,Imilchil,Imzuren,Inezgane,Irherm,Izoughar,Jendouba,Kacem,Kelibia,Kenitra,Kerrando,Khalidia,Khemisset,Khenifra,Khouribga,Khourigba,Kidal,Korba,Korbous,Lahraouyine,Larache,Leyun,Lqliaa,Manouba,Martil,Mazagan,Mcherga,Mdiq,Megrine,Mellal,Melloul,Midelt,Misur,Mohammedia,Mornag,Mrirt,Nabeul,Nadhour,Nador,Nawaksut,Nefza,Ouarzazate,Ouazzane,Oued Zem,Oujda,Ouladteima,Qsentina,Rades,Rafraf,Safi,Sefrou,Sejnane,Settat,Sijilmassa,Skhirat,Slimane,Somaa,Sraghna,Susa,Tabarka,Tadrart,Taferka,Tafilalt,Tafrawt,Tafza,Tagbalut,Tagerdayt,Taghzut,Takelsa,Taliouine,Tanja,Tantan,Taourirt,Targuist,Taroudant,Tarudant,Tasfelalayt,Tassort,Tata,Tattiwin,Tawnat,Taza,Tazagurt,Tazerka,Tazizawt,Taznakht,Tebourba,Teboursouk,Temara,Testour,Tetouan,Tibeskert,Tifelt,Tijdit,Tinariwen,Tinduf,Tinja,Tittawan,Tiznit,Toubkal,Trables,Tubqal,Tunes,Ultasila,Urup,Wagguten,Wararni,Warzazat,Watlas,Wehran,Wejda,Xamida,Yedder,Youssoufia,Zaghouan,Zahret,Zemmour,Zriba"},
{name: "Arabic", i: 18, min: 4, max: 9, d: "ae", m: .2, b: "Abha,Ajman,Alabar,Alarjam,Alashraf,Alawali,Albawadi,Albirk,Aldhabiyah,Alduwaid,Alfareeq,Algayed,Alhazim,Alhrateem,Alhudaydah,Alhuwaya,Aljahra,Aljubail,Alkhafah,Alkhalas,Alkhawaneej,Alkhen,Alkhobar,Alkhuznah,Allisafah,Almshaykh,Almurjan,Almuwayh,Almuzaylif,Alnaheem,Alnashifah,Alqah,Alqouz,Alqurayyat,Alradha,Alraqmiah,Alsadyah,Alsafa,Alshagab,Alshuqaiq,Alsilaa,Althafeer,Alwasqah,Amaq,Amran,Annaseem,Aqbiyah,Arafat,Arar,Ardah,Asfan,Ashayrah,Askar,Ayaar,Aziziyah,Baesh,Bahrah,Balhaf,Banizayd,Bidiyah,Bisha,Biyatah,Buqhayq,Burayda,Dafiyat,Damad,Dammam,Dariyah,Dhafar,Dhahran,Dhalkut,Dhurma,Dibab,Doha,Dukhan,Duwaibah,Enaker,Fadhla,Fahaheel,Fanateer,Farasan,Fardah,Fujairah,Ghalilah,Ghar,Ghizlan,Ghomgyah,Ghran,Hadiyah,Haffah,Hajanbah,Hajrah,Haqqaq,Haradh,Hasar,Hawiyah,Hebaa,Hefar,Hijal,Husnah,Huwailat,Huwaitah,Irqah,Isharah,Ithrah,Jamalah,Jarab,Jareef,Jazan,Jeddah,Jiblah,Jihanah,Jilah,Jizan,Joraibah,Juban,Jumeirah,Kamaran,Keyad,Khab,Khaiybar,Khasab,Khathirah,Khawarah,Khulais,Kumzar,Limah,Linah,Madrak,Mahab,Mahalah,Makhtar,Mashwar,Masirah,Masliyah,Mastabah,Mazhar,Medina,Meeqat,Mirbah,Mokhtara,Muharraq,Muladdah,Musaykah,Mushayrif,Musrah,Mussafah,Nafhan,Najran,Nakhab,Nizwa,Oman,Qadah,Qalhat,Qamrah,Qasam,Qosmah,Qurain,Quriyat,Qurwa,Radaa,Rafha,Rahlah,Rakamah,Rasheedah,Rasmadrakah,Risabah,Rustaq,Ryadh,Sabtaljarah,Sadah,Safinah,Saham,Saihat,Salalah,Salmiya,Shabwah,Shalim,Shaqra,Sharjah,Sharurah,Shatifiyah,Shidah,Shihar,Shoqra,Shuwaq,Sibah,Sihmah,Sinaw,Sirwah,Sohar,Suhailah,Sulaibiya,Sunbah,Tabuk,Taif,Taqah,Tarif,Tharban,Thuqbah,Thuwal,Tubarjal,Turaif,Turbah,Tuwaiq,Ubar,Umaljerem,Urayarah,Urwah,Wabrah,Warbah,Yabreen,Yadamah,Yafur,Yarim,Yemen,Yiyallah,Zabid,Zahwah,Zallaq,Zinjibar,Zulumah"},
{name: "Inuit", i: 19, min: 5, max: 15, d: "alutsn", m: 0, b: "Aaluik,Aappilattoq,Aasiaat,Agissat,Agssaussat,Akuliarutsip,Akunnaaq,Alluitsup,Alluttoq,Amitsorsuaq,Ammassalik,Anarusuk,Anguniartarfik,Annertussoq,Annikitsoq,Apparsuit,Apusiaajik,Arsivik,Arsuk,Atammik,Ateqanaq,Atilissuaq,Attu,Augpalugtoq,Aukarnersuaq,Aumat,Auvilkikavsaup,Avadtlek,Avallersuaq,Bjornesk,Blabaerdalen,Blomsterdalen,Brattalhid,Bredebrae,Brededal,Claushavn,Edderfulegoer,Egger,Eqalugalinnguit,Eqalugarssuit,Eqaluit,Eqqua,Etah,Graah,Hakluyt,Haredalen,Hareoen,Hundeo,Igaliku,Igdlorssuit,Igdluluarssuk,Iginniafik,Ikamiut,Ikarissat,Ikateq,Ikermiut,Ikermoissuaq,Ikorfarssuit,Ilimanaq,Illorsuit,Illunnguit,Iluileq,Ilulissat,Imaarsivik,Imartunarssuk,Immikkoortukajik,Innaarsuit,Inneruulalik,Inussullissuaq,Iperaq,Ippik,Iqek,Isortok,Isungartussoq,Itileq,Itissaalik,Itivdleq,Ittit,Ittoqqortoormiit,Ivingmiut,Ivittuut,Kanajoorartuut,Kangaamiut,Kangeq,Kangerluk,Kangerlussuaq,Kanglinnguit,Kapisillit,Kekertamiut,Kiatak,Kiataussaq,Kigatak,Kinaussak,Kingittorsuaq,Kitak,Kitsissuarsuit,Kitsissut,Klenczner,Kook,Kraulshavn,Kujalleq,Kullorsuaq,Kulusuk,Kuurmiit,Kuusuaq,Laksedalen,Maniitsoq,Marrakajik,Mattaangassut,Mernoq,Mittivakkat,Moriusaq,Myggbukta,Naajaat,Nangissat,Nanuuseq,Nappassoq,Narsarmijt,Narsarsuaq,Narssaq,Nasiffik,Natsiarsiorfik,Naujanguit,Niaqornaarsuk,Niaqornat,Nordfjordspasset,Nugatsiaq,Nunarssit,Nunarsuaq,Nunataaq,Nunatakavsaup,Nutaarmiut,Nuugaatsiaq,Nuuk,Nuukullak,Olonkinbyen,Oodaaq,Oqaatsut,Oqaitsunguit,Oqonermiut,Paagussat,Paamiut,Paatuut,Palungataq,Pamialluk,Perserajoq,Pituffik,Puugutaa,Puulkuip,Qaanaq,Qaasuitsup,Qaersut,Qajartalik,Qallunaat,Qaneq,Qaqortok,Qasigiannguit,Qassimiut,Qeertartivaq,Qeqertaq,Qeqertasussuk,Qeqqata,Qernertoq,Qernertunnguit,Qianarreq,Qingagssat,Qoornuup,Qorlortorsuaq,Qullikorsuit,Qunnerit,Qutdleq,Ravnedalen,Ritenbenk,Rypedalen,Saarloq,Saatorsuaq,Saattut,Salliaruseq,Sammeqqat,Sammisoq,Sanningassoq,Saqqaq,Saqqarlersuaq,Saqqarliit,Sarfannguit,Sattiaatteq,Savissivik,Serfanguaq,Sermersooq,Sermiligaaq,Sermilik,Sermitsiaq,Simitakaja,Simiutaq,Singamaq,Siorapaluk,Sisimiut,Sisuarsuit,Sullorsuaq,Suunikajik,Sverdrup,Taartoq,Takiseeq,Tasirliaq,Tasiusak,Tiilerilaaq,Timilersua,Timmiarmiut,Tukingassoq,Tussaaq,Tuttulissuup,Tuujuk,Uiivaq,Uilortussoq,Ujuaakajiip,Ukkusissat,Upernavik,Uttorsiutit,Uumannaq,Uunartoq,Uvkusigssat,Ymer"},
{name: "Basque", i: 20, min: 4, max: 11, d: "r", m: .1, b: "Agurain,Aia,Aiara,Albiztur,Alkiza,Altzaga,Amorebieta,Amurrio,Andoain,Anoeta,Antzuola,Arakaldo,Arantzazu,Arbatzegi,Areatza,Arratzua,Arrieta,Artea,Artziniega,Asteasu,Astigarraga,Ataun,Atxondo,Aulesti,Azkoitia,Azpeitia,Bakio,Baliarrain,Barakaldo,Barrika,Barrundia,Basauri,Beasain,Bedia,Beizama,Belauntza,Berastegi,Bergara,Bermeo,Bernedo,Berriatua,Berriz,Bidania,Bilar,Bilbao,Busturia,Deba,Derio,Donostia,Dulantzi,Durango,Ea,Eibar,Elantxobe,Elduain,Elgeta,Elgoibar,Elorrio,Erandio,Ergoitia,Ermua,Errenteria,Errezil,Eskoriatza,Eskuernaga,Etxebarri,Etxebarria,Ezkio,Forua,Gabiria,Gaintza,Galdakao,Gamiz,Garai,Gasteiz,Gatzaga,Gaubea,Gautegiz,Gaztelu,Gernika,Gerrikaitz,Getaria,Getxo,Gizaburuaga,Goiatz,Gorliz,Gorriaga,Harana,Hernani,Hondarribia,Ibarra,Ibarrangelu,Idiazabal,Iekora,Igorre,Ikaztegieta,Irun,Irura,Iruraiz,Itsaso,Itsasondo,Iurreta,Izurtza,Jatabe,Kanpezu,Karrantza,Kortezubi,Kripan,Kuartango,Lanestosa,Lantziego,Larrabetzu,Lasarte,Laukiz,Lazkao,Leaburu,Legazpi,Legorreta,Legutio,Leintz,Leioa,Lekeitio,Lemoa,Lemoiz,Leza,Lezama,Lezo,Lizartza,Maeztu,Mallabia,Manaria,Markina,Maruri,Menaka,Mendaro,Mendata,Mendexa,Morga,Mundaka,Mungia,Munitibar,Murueta,Muskiz,Mutiloa,Mutriku,Nabarniz,Oiartzun,Oion,Okondo,Olaberria,Onati,Ondarroa,Ordizia,Orendain,Orexa,Oria,Orio,Ormaiztegi,Orozko,Ortuella,Otegi,Otxandio,Pasaia,Plentzia,Santurtzi,Sestao,Sondika,Soraluze,Sukarrieta,Tolosa,Trapagaran,Turtzioz,Ubarrundia,Ubide,Ugao,Urdua,Urduliz,Urizaharra,Urkabustaiz,Urnieta,Urretxu,Usurbil,Xemein,Zabaleta,Zaia,Zaldibar,Zambrana,Zamudio,Zaratamo,Zarautz,Zeberio,Zegama,Zerain,Zestoa,Zierbena,Zigoitia,Ziortza,Zuia,Zumaia,Zumarraga"},
{name: "Nigerian", i: 21, min: 4, max: 10, d: "", m: .3, b: "Abadogo,Abafon,Adealesu,Adeto,Adyongo,Afaga,Afamju,Agigbigi,Agogoke,Ahute,Aiyelaboro,Ajebe,Ajola,Akarekwu,Akunuba,Alawode,Alkaijji,Amangam,Amgbaye,Amtasa,Amunigun,Animahun,Anyoko,Arapagi,Asande,Awgbagba,Awhum,Awodu,Babateduwa,Bandakwai,Bangdi,Bilikani,Birnindodo,Braidu,Bulakawa,Buriburi,Cainnan,Chakum,Chondugh,Dagwarga,Darpi,Dokatofa,Dozere,Ebelibri,Efem,Ekoku,Ekpe,Ewhoeviri,Galea,Gamen,Ganjin,Gantetudu,Gargar,Garinbode,Gbure,Gerti,Gidan,Gitabaremu,Giyagiri,Giyawa,Gmawa,Golakochi,Golumba,Gunji,Gwambula,Gwodoti,Hayinlere,Hayinmaialewa,Hirishi,Hombo,Ibefum,Iberekodo,Icharge,Idofin,Idofinoka,Igbogo,Ijoko,Ijuwa,Ikawga,Ikhin,Ikpakidout,Ikpeoniong,Imuogo,Ipawo,Ipinlerere,Isicha,Itakpa,Jangi,Jare,Jataudakum,Jaurogomki,Jepel,Kafinmalama,Katab,Katanga,Katinda,Katirije,Kaurakimba,Keffinshanu,Kellumiri,Kiagbodor,Kirbutu,Kita,Kogogo,Kopje,Korokorosei,Kotoku,Kuata,Kujum,Kukau,Kunboon,Kuonubogbene,Kurawe,Kushinahu,Kwaramakeri,Ladimeji,Lafiaro,Lahaga,Laindebajanle,Laindegoro,Lakati,Litenswa,Maba,Madarzai,Maianita,Malikansaa,Mata,Megoyo,Meku,Miama,Modi,Mshi,Msugh,Muduvu,Murnachehu,Namnai,Ndamanma,Ndiwulunbe,Ndonutim,Ngbande,Nguengu,Ntoekpe,Nyajo,Nyior,Odajie,Ogbaga,Ogultu,Ogunbunmi,Ojopode,Okehin,Olugunna,Omotunde,Onipede,Onma,Orhere,Orya,Otukwang,Otunade,Rampa,Rimi,Rugan,Rumbukawa,Sabiu,Sangabama,Sarabe,Seboregetore,Shafar,Shagwa,Shata,Shengu,Sokoron,Sunnayu,Tafoki,Takula,Talontan,Tarhemba,Tayu,Ter,Timtim,Timyam,Tindirke,Tokunbo,Torlwam,Tseakaadza,Tseanongo,Tsebeeve,Tsepaegh,Tuba,Tumbo,Tungalombo,Tunganyakwe,Uhkirhi,Umoru,Umuabai,Umuajuju,Unchida,Ungua,Unguwar,Unongo,Usha,Utongbo,Vembera,Wuro,Yanbashi,Yanmedi,Yoku,Zarunkwari,Zilumo,Zulika"},
{name: "Celtic", i: 22, min: 4, max: 12, d: "nld", m: 0, b: "Aberaman,Aberangell,Aberarth,Aberavon,Aberbanc,Aberbargoed,Aberbeeg,Abercanaid,Abercarn,Abercastle,Abercegir,Abercraf,Abercregan,Abercych,Abercynon,Aberdare,Aberdaron,Aberdaugleddau,Aberdeen,Aberdulais,Aberdyfi,Aberedw,Abereiddy,Abererch,Abereron,Aberfan,Aberffraw,Aberffrwd,Abergavenny,Abergele,Aberglasslyn,Abergorlech,Abergwaun,Abergwesyn,Abergwili,Abergwynfi,Abergwyngregyn,Abergynolwyn,Aberhafesp,Aberhonddu,Aberkenfig,Aberllefenni,Abermain,Abermaw,Abermorddu,Abermule,Abernant,Aberpennar,Aberporth,Aberriw,Abersoch,Abersychan,Abertawe,Aberteifi,Aberthin,Abertillery,Abertridwr,Aberystwyth,Achininver,Afonhafren,Alisaha,Anfosadh,Antinbhearmor,Ardenna,Attacon,Banwen,Beira,Bhrura,Bleddfa,Boioduro,Bona,Boskyny,Boslowenpolbrogh,Boudobriga,Bravon,Brigant,Briganta,Briva,Brosnach,Caersws,Cambodunum,Cambra,Caracta,Catumagos,Centobriga,Ceredigion,Chalain,Chearbhallain,Chlasaigh,Chormaic,Cuileannach,Dinn,Diwa,Dubingen,Duibhidighe,Duro,Ebora,Ebruac,Eburodunum,Eccles,Egloskuri,Eighe,Eireann,Elerghi,Ferkunos,Fhlaithnin,Gallbhuaile,Genua,Ghrainnse,Gwyles,Heartsease,Hebron,Hordh,Inbhear,Inbhir,Inbhirair,Innerleithen,Innerleven,Innerwick,Inver,Inveraldie,Inverallan,Inveralmond,Inveramsay,Inveran,Inveraray,Inverarnan,Inverbervie,Inverclyde,Inverell,Inveresk,Inverfarigaig,Invergarry,Invergordon,Invergowrie,Inverhaddon,Inverkeilor,Inverkeithing,Inverkeithney,Inverkip,Inverleigh,Inverleith,Inverloch,Inverlochlarig,Inverlochy,Invermay,Invermoriston,Inverness,Inveroran,Invershin,Inversnaid,Invertrossachs,Inverugie,Inveruglas,Inverurie,Iubhrach,Karardhek,Kilninver,Kirkcaldy,Kirkintilloch,Krake,Lanngorrow,Latense,Leming,Lindomagos,Llanaber,Llandidiwg,Llandyrnog,Llanfarthyn,Llangadwaldr,Llansanwyr,Lochinver,Lugduno,Magoduro,Mheara,Monmouthshire,Nanshiryarth,Narann,Novioduno,Nowijonago,Octoduron,Penning,Pheofharain,Ponsmeur,Raithin,Ricomago,Rossinver,Salodurum,Seguia,Sentica,Theorsa,Tobargeal,Trealaw,Trefesgob,Trewedhenek,Trewythelan,Tuaisceart,Uige,Vitodurum,Windobona"},
{name: "Mesopotamian", i: 23, min: 4, max: 9, d: "srpl", m: .1, b: "Adab,Adamndun,Adma,Admatum,Agrab,Akkad,Akshak,Amnanum,Andarig,Anshan,Apiru,Apum,Arantu,Arbid,Arpachiyah,Arpad,Arrapha,Ashlakka,Assur,Awan,Babilim,Bad-Tibira,Balawat,Barsip,Birtu,Bit-Bunakki,Borsippa,Chuera,Dashrah,Der,Dilbat,Diniktum,Doura,Dur-Kurigalzu,Dur-Sharrukin,Dur-Untash,Dûr-gurgurri,Ebla,Ekallatum,Ekalte,Emar,Erbil,Eresh,Eridu,Eshnunn,Eshnunna,Gargamish,Gasur,Gawra,Gibil,Girsu,Gizza,Habirun,Habur,Hadatu,Hakkulan,Halab,Halabit,Hamazi,Hamoukar,Haradum,Harbidum,Harran,Harranu,Hassuna,Hatarikka,Hatra,Hissar,Hiyawa,Hormirzad,Ida-Maras,Idamaraz,Idu,Imerishu,Imgur-Enlil,Irisagrig,Irnina,Irridu,Isin,Issinnitum,Iturungal,Izubitum,Jarmo,Jemdet,Kabnak,Kadesh,Kahat,Kalhu,Kar-Shulmanu-Asharedu,Kar-Tukulti-Ninurta,Kar-shulmanu-asharedu,Karana,Karatepe,Kartukulti,Kazallu,Kesh,Kidsha,Kinza,Kish,Kisiga,Kisurra,Kuara,Kurda,Kurruhanni,Kutha,Lagaba,Lagash,Larak,Larsa,Leilan,Malgium,Marad,Mardaman,Mari,Marlik,Mashkan,Mashkan-shapir,Matutem,Me-Turan,Meliddu,Mumbaqat,Nabada,Nagar,Nanagugal,Nerebtum,Nigin,Nimrud,Nina,Nineveh,Ninua,Nippur,Niru,Niya,Nuhashe,Nuhasse,Nuzi,Puzrish-Dagan,Qalatjarmo,Qatara,Qatna,Qattunan,Qidshu,Rapiqum,Rawda,Sagaz,Shaduppum,Shaggaratum,Shalbatu,Shanidar,Sharrukin,Shawwan,Shehna,Shekhna,Shemshara,Shibaniba,Shubat-Enlil,Shurkutir,Shuruppak,Shusharra,Shushin,Sikan,Sippar,Sippar-Amnanum,Sippar-sha-Annunitum,Subatum,Susuka,Tadmor,Tarbisu,Telul,Terqa,Tirazish,Tisbon,Tuba,Tushhan,Tuttul,Tutub,Ubaid,Umma,Ur,Urah,Urbilum,Urkesh,Ursa'um,Uruk,Urum,Uzarlulu,Warka,Washukanni,Zabalam,Zarri-Amnan"},
{name: "Iranian", i: 24, min: 5, max: 11, d: "", m: .1, b: "Abali,Abrisham,Absard,Abuzeydabad,Afus,Alavicheh,Alikosh,Amol,Anarak,Anbar,Andisheh,Anshan,Aran,Ardabil,Arderica,Ardestan,Arjomand,Asgaran,Asgharabad,Ashian,Awan,Babajan,Badrud,Bafran,Baghestan,Baghshad,Bahadoran,Baharan Shahr,Baharestan,Bakun,Bam,Baqershahr,Barzok,Bastam,Behistun,Bitistar,Bumahen,Bushehr,Chadegan,Chahardangeh,Chamgardan,Chermahin,Choghabonut,Chugan,Damaneh,Damavand,Darabgard,Daran,Dastgerd,Dehaq,Dehaqan,Dezful,Dizicheh,Dorcheh,Dowlatabad,Duruntash,Ecbatana,Eslamshahr,Estakhr,Ezhiyeh,Falavarjan,Farrokhi,Fasham,Ferdowsieh,Fereydunshahr,Ferunabad,Firuzkuh,Fuladshahr,Ganjdareh,Ganzak,Gaz,Geoy,Godin,Goldasht,Golestan,Golpayegan,Golshahr,Golshan,Gorgab,Guged,Habibabad,Hafshejan,Hajjifiruz,Hana,Harand,Hasanabad,Hasanlu,Hashtgerd,Hecatompylos,Hormirzad,Imanshahr,Isfahan,Jandaq,Javadabad,Jiroft,Jowsheqan ,Jowzdan,Kabnak,Kahrizak,Kahriz Sang,Kangavar,Karaj,Karkevand,Kashan,Kelishad,Kermanshah,Khaledabad,Khansar,Khorramabad,Khur,Khvorzuq,Kilan,Komeh,Komeshcheh,Konar,Kuhpayeh,Kul,Kushk,Lavasan,Laybid,Liyan,Lyan,Mahabad,Mahallat,Majlesi,Malard,Manzariyeh,Marlik,Meshkat,Meymeh,Miandasht,Mish,Mobarakeh,Nahavand,Nain,Najafabad,Naqshe,Narezzash,Nasimshahr,Nasirshahr,Nasrabad,Natanz,Neyasar,Nikabad,Nimvar,Nushabad,Pakdasht,Parand,Pardis,Parsa,Pasargadai,Patigrabana,Pir Bakran,Pishva,Qahderijan,Qahjaverestan,Qamsar,Qarchak,Qods,Rabat,Ray-shahr,Rezvanshahr,Rhages,Robat Karim,Rozveh,Rudehen,Sabashahr,Safadasht,Sagzi,Salehieh,Sandal,Sarvestan,Sedeh,Sefidshahr,Semirom,Semnan,Shadpurabad,Shah,Shahdad,Shahedshahr,Shahin,Shahpour,Shahr,Shahreza,Shahriar,Sharifabad,Shemshak,Shiraz,Shushan,Shushtar,Sialk,Sin,Sukhteh,Tabas,Tabriz,Takhte,Talkhuncheh,Talli,Tarq,Temukan,Tepe,Tiran,Tudeshk,Tureng,Urmia,Vahidieh,Vahrkana,Vanak,Varamin,Varnamkhast,Varzaneh,Vazvan,Yahya,Yarim,Yasuj,Zarrin Shahr,Zavareh,Zayandeh,Zazeran,Ziar,Zibashahr,Zranka"},
{name: "Hawaiian", i: 25, min: 5, max: 10, d: "auo", m: 1, b: "Aapueo,Ahoa,Ahuakaio,Ahupau,Alaakua,Alae,Alaeloa,Alamihi,Aleamai,Alena,Alio,Aupokopoko,Halakaa,Haleu,Haliimaile,Hamoa,Hanakaoo,Hanaulu,Hanawana,Hanehoi,Haou,Hikiaupea,Hokuula,Honohina,Honokahua,Honokeana,Honokohau,Honolulu,Honomaele,Hononana,Honopou,Hoolawa,Huelo,Kaalaea,Kaapahu,Kaeo,Kahalehili,Kahana,Kahuai,Kailua,Kainehe,Kakalahale,Kakanoni,Kalenanui,Kaleoaihe,Kalialinui,Kalihi,Kalimaohe,Kaloi,Kamani,Kamehame,Kanahena,Kaniaula,Kaonoulu,Kapaloa,Kapohue,Kapuaikini,Kapunakea,Kauau,Kaulalo,Kaulanamoa,Kauluohana,Kaumakani,Kaumanu,Kaunauhane,Kaupakulua,Kawaloa,Keaa,Keaaula,Keahua,Keahuapono,Kealahou,Keanae,Keauhou,Kelawea,Keokea,Keopuka,Kikoo,Kipapa,Koakupuna,Koali,Kolokolo,Kopili,Kou,Kualapa,Kuhiwa,Kuholilea,Kuhua,Kuia,Kuikui,Kukoae,Kukohia,Kukuiaeo,Kukuipuka,Kukuiula,Kulahuhu,Lapakea,Lapueo,Launiupoko,Lole,Maalo,Mahinahina,Mailepai,Makaakini,Makaalae,Makaehu,Makaiwa,Makaliua,Makapipi,Makapuu,Maluaka,Manawainui,Mehamenui,Moalii,Moanui,Mohopili,Mokae,Mokuia,Mokupapa,Mooiki,Mooloa,Moomuku,Muolea,Nakaaha,Nakalepo,Nakaohu,Nakapehu,Nakula,Napili,Niniau,Nuu,Oloewa,Olowalu,Omaopio,Onau,Onouli,Opaeula,Opana,Opikoula,Paakea,Paeahu,Paehala,Paeohi,Pahoa,Paia,Pakakia,Palauea,Palemo,Paniau,Papaaea,Papaanui,Papaauhau,Papaka,Papauluana,Pauku,Paunau,Pauwalu,Pauwela,Pohakanele,Polaiki,Polanui,Polapola,Poopoo,Poponui,Poupouwela,Puahoowali,Puakea,Puako,Pualaea,Puehuehu,Pueokauiki,Pukaauhuhu,Pukuilua,Pulehu,Puolua,Puou,Puuhaehae,Puuiki,Puuki,Puulani,Puunau,Puuomaile,Uaoa,Uhao,Ukumehame,Ulaino,Ulumalu,Wahikuli,Waianae,Waianu,Waiawa,Waiehu,Waieli,Waikapu,Wailamoa,Wailaulau,Wainee,Waiohole,Waiohonu,Waiohuli,Waiokama,Waiokila,Waiopai,Waiopua,Waipao,Waipionui,Waipouli"},
{name: "Karnataka", i: 26, min: 5, max: 11, d: "tnl", m: 0, b: "Adityapatna,Adyar,Afzalpur,Aland,Alnavar,Alur,Ambikanagara,Anekal,Ankola,Annigeri,Arkalgud,Arsikere,Athni,Aurad,Badami,Bagalkot,Bagepalli,Bail,Bajpe,Bangalore,Bangarapet,Bankapura,Bannur,Bantval,Basavakalyan,Basavana,Belgaum,Beltangadi,Belur,Bhadravati,Bhalki,Bhatkal,Bhimarayanagudi,Bidar,Bijapur,Bilgi,Birur,Bommasandra,Byadgi,Challakere,Chamarajanagar,Channagiri,Channapatna,Channarayapatna,Chik,Chikmagalur,Chiknayakanhalli,Chikodi,Chincholi,Chintamani,Chitapur,Chitgoppa,Chitradurga,Dandeli,Dargajogihalli,Devadurga,Devanahalli,Dod,Donimalai,Gadag,Gajendragarh,Gangawati,Gauribidanur,Gokak,Gonikoppal,Gubbi,Gudibanda,Gulbarga,Guledgudda,Gundlupet,Gurmatkal,Haliyal,Hangal,Harapanahalli,Harihar,Hassan,Hatti,Haveri,Hebbagodi,Heggadadevankote,Hirekerur,Holalkere,Hole,Homnabad,Honavar,Honnali,Hoovina,Hosakote,Hosanagara,Hosdurga,Hospet,Hubli,Hukeri,Hungund,Hunsur,Ilkal,Indi,Jagalur,Jamkhandi,Jevargi,Jog,Kadigenahalli,Kadur,Kalghatgi,Kamalapuram,Kampli,Kanakapura,Karkal,Karwar,Khanapur,Kodiyal,Kolar,Kollegal,Konnur,Koppa,Koppal,Koratagere,Kotturu,Krishnarajanagara,Krishnarajasagara,Krishnarajpet,Kudchi,Kudligi,Kudremukh,Kumta,Kundapura,Kundgol,Kunigal,Kurgunta,Kushalnagar,Kushtagi,Lakshmeshwar,Lingsugur,Londa,Maddur,Madhugiri,Madikeri,Mahalingpur,Malavalli,Mallar,Malur,Mandya,Mangalore,Manvi,Molakalmuru,Mudalgi,Mudbidri,Muddebihal,Mudgal,Mudhol,Mudigere,Mulbagal,Mulgund,Mulki,Mulur,Mundargi,Mundgod,Munirabad,Mysore,Nagamangala,Nanjangud,Narasimharajapura,Naregal,Nargund,Navalgund,Nipani,Pandavapura,Pavagada,Piriyapatna,Pudu,Puttur,Rabkavi,Raichur,Ramanagaram,Ramdurg,Ranibennur,Raybag,Robertson,Ron,Sadalgi,Sagar,Sakleshpur,Saligram,Sandur,Sankeshwar,Saundatti,Savanur,Sedam,Shahabad,Shahpur,Shaktinagar,Shiggaon,Shikarpur,Shirhatti,Shorapur,Shrirangapattana,Siddapur,Sidlaghatta,Sindgi,Sindhnur,Sira,Siralkoppa,Sirsi,Siruguppa,Somvarpet,Sorab,Sringeri,Srinivaspur,Sulya,Talikota,Tarikere,Tekkalakote,Terdal,Thumbe,Tiptur,Tirthahalli,Tirumakudal,Tumkur,Turuvekere,Udupi,Vijayapura,Wadi,Yadgir,Yelandur,Yelbarga,Yellapur,Yenagudde"},
{name: "Quechua", i: 27, min: 6, max: 12, d: "l", m: 0, b: "Alpahuaycco,Anchihuay,Anqea,Apurimac,Arequipa,Atahuallpa,Atawalpa,Atico,Ayacucho,Ayahuanco,Ayllu,Cajamarca,Canayre,Canchacancha,Carapo,Carhuac,Carhuacatac,Cashan,Caullaraju,Caxamalca,Cayesh,Ccahuasno,Ccarhuacc,Ccopayoc,Chacchapunta,Chacraraju,Challhuamayo,Champara,Chanchan,Chekiacraju,Chillihua,Chinchey,Chontah,Chopicalqui,Chucuito,Chuito,Chullo,Chumpi,Chuncho,Chupahuacho,Chuquiapo,Chuquisaca,Churup,Cocapata,Cochabamba,Cojup,Collota,Conococha,Corihuayrachina,Cuchoquesera,Cusichaca,Haika,Hanpiq,Hatun,Haywarisqa,Huaca,Huachinga,Hualcan,Hualchancca,Huamanga,Huamashraju,Huancarhuas,Huandoy,Huantsan,Huanupampa,Huarmihuanusca,Huascaran,Huaylas,Huayllabamba,Huayrana,Huaytara,Huichajanca,Huinayhuayna,Huinche,Huinioch,Illiasca,Intipunku,Iquicha,Ishinca,Jahuacocha,Jirishanca,Juli,Jurau,Kakananpunta,Kamasqa,Karpay,Kausay,Khuya,Kuelap,Lanccochayocc,Llaca,Llactapata,Llanganuco,Llaqta,Lloqllasca,Llupachayoc,Luricocha,Machu,Mallku,Matarraju,Mechecc,Mikhuy,Milluacocha,Morochuco,Munay,Ocshapalca,Ollantaytambo,Oroccahua,Oronccoy,Oyolo,Pacamayo,Pacaycasa,Paccharaju,Pachacamac,Pachakamaq,Pachakuteq,Pachakuti,Pachamama,Paititi,Pajaten,Palcaraju,Pallccas,Pampa,Panaka,Paqarina,Paqo,Parap,Paria,Patahuasi,Patallacta,Patibamba,Pisac,Pisco,Pongos,Pucacolpa,Pucahirca,Pucaranra,Pumatambo,Puscanturpa,Putaca,Puyupatamarca,Qawaq,Qayqa,Qochamoqo,Qollana,Qorihuayrachina,Qorimoqo,Qotupuquio,Quenuaracra,Queshque,Quillcayhuanca,Quillya,Quitaracsa,Quitaraju,Qusqu,Rajucolta,Rajutakanan,Rajutuna,Ranrahirca,Ranrapalca,Raria,Rasac,Rimarima,Riobamba,Runkuracay,Rurec,Sacsa,Sacsamarca,Saiwa,Sarapo,Sayacmarca,Sayripata,Sinakara,Sonccopa,Taripaypacha,Taulliraju,Tawantinsuyu,Taytanchis,Tiwanaku,Tocllaraju,Tsacra,Tuco,Tucubamba,Tullparaju,Tumbes,Uchuraccay,Uchuraqay,Ulta,Urihuana,Uruashraju,Vallunaraju,Vilcabamba,Wacho,Wankawillka,Wayra,Yachay,Yahuarraju,Yanamarey,Yanaqucha,Yanesha,Yerupaja"},
{name: "Swahili", i: 28, min: 4, max: 9, d: "", m: 0, b: "Abim,Adjumani,Alebtong,Amolatar,Amuru,Apac,Arua,Arusha,Babati,Baragoi,Bombo,Budaka,Bugembe,Bugiri,Buikwe,Bukedea,Bukoba,Bukomansimbi,Bukungu,Buliisa,Bundibugyo,Bungoma,Busembatya,Bushenyi,Busia,Busolwe,Butaleja,Butambala,Butere,Buwenge,Buyende,Dadaab,Dodoma,Dokolo,Eldoret,Elegu,Emali,Embu,Entebbe,Garissa,Gede,Gulu,Handeni,Hima,Hoima,Hola,Ibanda,Iganga,Iringa,Isingiro,Isiolo,Jinja,Kaabong,Kabuyanda,Kabwohe,Kagadi,Kajiado,Kakinga,Kakiri,Kakuma,Kalangala,Kaliro,Kalongo,Kalungu,Kampala,Kamwenge,Kanungu,Kapchorwa,Kasese,Kasulu,Katakwi,Kayunga,Keroka,Kiambu,Kibaale,Kibaha,Kibingo,Kibwezi,Kigoma,Kihiihi,Kilifi,Kiruhura,Kiryandongo,Kisii,Kisoro,Kisumu,Kitale,Kitgum,Kitui,Koboko,Korogwe,Kotido,Kumi,Kyazanga,Kyegegwa,Kyenjojo,Kyotera,Lamu,Langata,Lindi,Lodwar,Lokichoggio,Londiani,Loyangalani,Lugazi,Lukaya,Luweero,Lwakhakha,Lwengo,Lyantonde,Machakos,Mafinga,Makambako,Makindu,Malaba,Malindi,Manafwa,Mandera,Marsabit,Masaka,Masindi,Masulita,Matugga,Mayuge,Mbale,Mbarara,Mbeya,Meru,Mitooma,Mityana,Mombasa,Morogoro,Moroto,Moyale,Moyo,Mpanda,Mpigi,Mpondwe,Mtwara,Mubende,Mukono,Muranga,Musoma,Mutomo,Mutukula,Mwanza,Nagongera,Nairobi,Naivasha,Nakapiripirit,Nakaseke,Nakasongola,Nakuru,Namanga,Namayingo,Namutumba,Nansana,Nanyuki,Narok,Naromoru,Nebbi,Ngora,Njeru,Njombe,Nkokonjeru,Ntungamo,Nyahururu,Nyeri,Oyam,Pader,Paidha,Pakwach,Pallisa,Rakai,Ruiru,Rukungiri,Rwimi,Sanga,Sembabule,Shimoni,Shinyanga,Singida,Sironko,Songea,Soroti,Ssabagabo,Sumbawanga,Tabora,Takaungu,Tanga,Thika,Tororo,Tunduma,Vihiga,Voi,Wajir,Wakiso,Watamu,Webuye,Wobulenzi,Wote,Wundanyi,Yumbe,Zanzibar"},
{name: "Vietnamese", i: 29, min: 3, max: 12, d: "", m: 1, b: "An Giang,Anh Son,An Khe,An Nhon,Ayun Pa,Bac Giang,Bac Kan,Bac Lieu,Bac Ninh,Ba Don,Bao Loc,Ba Ria,Ba Ria-Vung Tau,Ba Thuoc,Ben Cat,Ben Tre,Bien Hoa,Bim Son,Binh Dinh,Binh Duong,Binh Long,Binh Minh,Binh Phuoc,Binh Thuan,Buon Ho,Buon Ma Thuot,Cai Lay,Ca Mau,Cam Khe,Cam Pha,Cam Ranh,Cam Thuy,Can Tho,Cao Bang,Cao Lanh,Cao Phong,Chau Doc,Chi Linh,Con Cuong,Cua Lo,Da Bac,Dak Lak,Da Lat,Da Nang,Di An,Dien Ban,Dien Bien,Dien Bien Phu,Dien Chau,Do Luong,Dong Ha,Dong Hoi,Dong Trieu,Duc Pho,Duyen Hai,Duy Tien,Gia Lai,Gia Nghia,Gia Rai,Go Cong,Ha Giang,Ha Hoa,Hai Duong,Hai Phong,Ha Long,Ha Nam,Ha Noi,Ha Tinh,Ha Trung,Hau Giang,Hoa Binh,Hoang Mai,Hoa Thanh,Ho Chi Minh,Hoi An,Hong Linh,Hong Ngu,Hue,Hung Nguyen,Hung Yen,Huong Thuy,Huong Tra,Khanh Hoa,Kien Tuong,Kim Boi,Kinh Mon,Kon Tum,Ky Anh,Ky Son,Lac Son,Lac Thuy,La Gi,Lai Chau,Lam Thao,Lang Chanh,Lang Son,Lao Cai,Long An,Long Khanh,Long My,Long Xuyen,Luong Son,Mai Chau,Mong Cai,Muong Lat,Muong Lay,My Hao,My Tho,Nam Dan,Nam Dinh,Nga Bay,Nga Nam,Nga Son,Nghe An,Nghia Dan,Nghia Lo,Nghi Loc,Nghi Son,Ngoc Lac,Nha Trang,Nhu Thanh,Nhu Xuan,Ninh Binh,Ninh Hoa,Nong Cong,Phan Rang Thap Cham,Phan Thiet,Pho Yen,Phu Ly,Phu My,Phu Ninh,Phuoc Long,Phu Tho,Phu Yen,Pleiku,Quang Binh,Quang Nam,Quang Ngai,Quang Ninh,Quang Tri,Quang Xuong,Quang Yen,Quan Hoa,Quan Son,Que Phong,Quy Chau,Quy Hop,Quynh Luu,Quy Nhon,Rach Gia,Sa Dec,Sai Gon,Sam Son,Sa Pa,Soc Trang,Song Cau,Song Cong,Son La,Son Tay,Tam Diep,Tam Ky,Tan An,Tan Chau,Tan Ky,Tan Lac,Tan Son,Tan Uyen,Tay Ninh,Thach Thanh,Thai Binh,Thai Hoa,Thai Nguyen,Thanh Chuong,Thanh Hoa,Thieu Hoa,Thuan An,Thua Thien-Hue,Thu Dau Mot,Thu Duc,Thuong Xuan,Tien Giang,Trang Bang,Tra Vinh,Trieu Son,Tu Son,Tuyen Quang,Tuy Hoa,Uong Bi,Viet Tri,Vinh,Vinh Chau,Vinh Loc,Vinh Long,Vinh Yen,Vi Thanh,Vung Tau,Yen Bai,Yen Dinh,Yen Thanh,Yen Thuy"},
{name: "Cantonese", i: 30, min: 5, max: 11, d: "", m: 0, b: "Chaiwan,Chingchung,Chinghoi,Chingsen,Chingshing,Chiunam,Chiuon,Chiuyeung,Chiyuen,Choihung,Chuehoi,Chuiman,Chungfu,Chungsan,Chunguktsuen,Dakhing,Daopo,Daumun,Dingwu,Dinpak,Donggun,Dongyuen,Duenchau,Fachau,Fanling,Fatgong,Fatshan,Fotan,Fuktien,Fumun,Funggong,Funghoi,Fungshun,Fungtei,Gamtin,Gochau,Goming,Gonghoi,Gongshing,Goyiu,Hanghau,Hangmei,Hengon,Heungchau,Heunggong,Heungkiu,Hingning,Hohfuktong,Hoichue,Hoifung,Hoiping,Hokong,Hokshan,Hoyuen,Hunghom,Hungshuikiu,Jiuling,Kamsheung,Kamwan,Kaulongtong,Keilun,Kinon,Kinsang,Kityeung,Kongmun,Kukgong,Kwaifong,Kwaihing,Kwongchau,Kwongling,Kwongming,Kwuntong,Laichikok,Laiking,Laiwan,Lamtei,Lamtin,Leitung,Leungking,Limkong,Linping,Linshan,Loding,Lokcheong,Lokfu,Longchuen,Longgong,Longmun,Longping,Longwa,Longwu,Lowu,Luichau,Lukfung,Lukho,Lungmun,Macheung,Maliushui,Maonshan,Mauming,Maunam,Meifoo,Mingkum,Mogong,Mongkok,Muichau,Muigong,Muiyuen,Naiwai,Namcheong,Namhoi,Namhong,Namsha,Nganwai,Ngautaukok,Ngchuen,Ngwa,Onting,Pakwun,Paotoishan,Pingshan,Pingyuen,Poklo,Pongon,Poning,Potau,Puito,Punyue,Saiwanho,Saiyingpun,Samshing,Samshui,Samtsen,Samyuenlei,Sanfung,Sanhing,Sanhui,Sanwai,Seiwui,Shamshuipo,Shanmei,Shantau,Shauking,Shekmun,Shekpai,Sheungshui,Shingkui,Shiuhing,Shundak,Shunyi,Shupinwai,Simshing,Siuhei,Siuhong,Siukwan,Siulun,Suikai,Taihing,Taikoo,Taipo,Taishuihang,Taiwai,Taiwohau,Tinhau,Tinshuiwai,Tiukengleng,Toishan,Tongfong,Tonglowan,Tsakyoochung,Tsamgong,Tsangshing,Tseungkwano,Tsimshatsui,Tsinggong,Tsingshantsuen,Tsingwun,Tsingyi,Tsingyuen,Tsiuchau,Tsuenshekshan,Tsuenwan,Tuenmun,Tungchung,Waichap,Waichau,Waidong,Wailoi,Waishing,Waiyeung,Wanchai,Wanfau,Wanshing,Wingon,Wongpo,Wongtaisin,Woping,Wukaisha,Yano,Yaumatei,Yautong,Yenfa,Yeungchun,Yeungdong,Yeungsai,Yeungshan,Yimtin,Yingdak,Yiuping,Yongshing,Yongyuen,Yuenlong,Yuenshing,Yuetsau,Yuknam,Yunping"},
{name: "Mongolian", i: 31, min: 5, max: 12, d: "aou", m: .3, b: "Adaatsag,Airag,Alag Erdene,Altai,Altanshiree,Altantsogts,Arbulag,Baatsagaan,Batnorov,Batshireet,Battsengel,Bayan Adarga,Bayan Agt,Bayanbulag,Bayandalai,Bayandun,Bayangovi,Bayanjargalan,Bayankhongor,Bayankhutag,Bayanlig,Bayanmonkh,Bayannur,Bayannuur,Bayan Ondor,Bayan Ovoo,Bayantal,Bayantsagaan,Bayantumen,Bayan Uul,Bayanzurkh,Berkh,Biger,Binder,Bogd,Bombogor,Bor Ondor,Bugat,Bugt,Bulgan,Buregkhangai,Burentogtokh,Buutsagaan,Buyant,Chandmani,Chandmani Ondor,Choibalsan,Chuluunkhoroot,Chuluut,Dadal,Dalanjargalan,Dalanzadgad,Darhan Muminggan,Darkhan,Darvi,Dashbalbar,Dashinchilen,Delger,Delgerekh,Delgerkhaan,Delgerkhangai,Delgertsogt,Deluun,Deren,Dorgon,Duut,Erdene,Erdenebulgan,Erdeneburen,Erdenedalai,Erdenemandal,Erdenetsogt,Galshar,Galt,Galuut,Govi Ugtaal,Gurvan,Gurvanbulag,Gurvansaikhan,Gurvanzagal,Hinggan,Hodong,Holingol,Hondlon,Horin Ger,Horqin,Hulunbuir,Hure,Ikhkhet,Ikh Tamir,Ikh Uul,Jargalan,Jargalant,Jargaltkhaan,Jarud,Jinst,Khairkhan,Khalhgol,Khaliun,Khanbogd,Khangai,Khangal,Khankh,Khankhongor,Khashaat,Khatanbulag,Khatgal,Kherlen,Khishig Ondor,Khokh,Kholonbuir,Khongor,Khotont,Khovd,Khovsgol,Khuld,Khureemaral,Khurmen,Khutag Ondor,Luus,Mandakh,Mandal Ovoo,Mankhan,Manlai,Matad,Mogod,Monkhkhairkhan,Moron,Most,Myangad,Nogoonnuur,Nomgon,Norovlin,Noyon,Ogii,Olgii,Olziit,Omnodelger,Ondorkhaan,Ondorshil,Ondor Ulaan,Ongniud,Ordos,Orgon,Orkhon,Rashaant,Renchinlkhumbe,Sagsai,Saikhan,Saikhandulaan,Saikhan Ovoo,Sainshand,Saintsagaan,Selenge,Sergelen,Sevrei,Sharga,Sharyngol,Shine Ider,Shinejinst,Shiveegovi,Sumber,Taishir,Tarialan,Tariat,Teshig,Togrog,Togtoh,Tolbo,Tomorbulag,Tonkhil,Tosontsengel,Tsagaandelger,Tsagaannuur,Tsagaan Ovoo,Tsagaan Uur,Tsakhir,Tseel,Tsengel,Tsenkher,Tsenkhermandal,Tsetseg,Tsetserleg,Tsogt,Tsogt Ovoo,Tsogttsetsii,Tumed,Tunel,Tuvshruulekh,Ulaanbadrakh,Ulaankhus,Ulaan Uul,Ulanhad,Ulanqab,Uyench,Yesonbulag,Zag,Zalainur,Zamyn Uud,Zereg"},
// fantasy bases by Dopu:
{name: "Human Generic", i: 32, min: 6, max: 11, d: "peolst", m: 0, b: "Amberglen,Angelhand,Arrowden,Autumnband,Autumnkeep,Basinfrost,Basinmore,Bayfrost,Beargarde,Bearmire,Bellcairn,Bellport,Bellreach,Blackwatch,Bleakward,Bonemouth,Boulder,Bridgefalls,Bridgeforest,Brinepeak,Brittlehelm,Bronzegrasp,Castlecross,Castlefair,Cavemire,Claymond,Claymouth,Clearguard,Cliffgate,Cliffshear,Cliffshield,Cloudbay,Cloudcrest,Cloudwood,Coldholde,Cragbury,Crowgrove,Crowvault,Crystalrock,Crystalspire,Cursefield,Curseguard,Cursespell,Dawnforest,Dawnwater,Deadford,Deadkeep,Deepcairn,Deerchill,Demonfall,Dewglen,Dewmere,Diredale,Direden,Dirtshield,Dogcoast,Dogmeadow,Dragonbreak,Dragonhold,Dragonward,Dryhost,Dustcross,Dustwatch,Eaglevein,Earthfield,Earthgate,Earthpass,Ebonfront,Edgehaven,Eldergate,Eldermere,Embervault,Everchill,Evercoast,Falsevale,Faypond,Fayvale,Fayyard,Fearpeak,Flameguard,Flamewell,Freyshell,Ghostdale,Ghostpeak,Gloomburn,Goldbreach,Goldyard,Grassplains,Graypost,Greeneld,Grimegrove,Grimeshire,Heartfall,Heartford,Heartvault,Highbourne,Hillpass,Hollowstorm,Honeywater,Houndcall,Houndholde,Iceholde,Icelight,Irongrave,Ironhollow,Knightlight,Knighttide,Lagoonpass,Lakecross,Lastmere,Laststar,Lightvale,Limeband,Littlehall,Littlehold,Littlemire,Lostcairn,Lostshield,Loststar,Madfair,Madham,Midholde,Mightglen,Millstrand,Mistvault,Mondpass,Moonacre,Moongulf,Moonwell,Mosshand,Mosstide,Mosswind,Mudford,Mudwich,Mythgulch,Mythshear,Nevercrest,Neverfront,Newfalls,Nighthall,Oakenbell,Oakenrun,Oceanstar,Oldreach,Oldwall,Oldwatch,Oxbrook,Oxlight,Pearlhaven,Pinepond,Pondfalls,Pondtown,Pureshell,Quickbell,Quickpass,Ravenside,Roguehaven,Roseborn,Rosedale,Rosereach,Rustmore,Saltmouth,Sandhill,Scorchpost,Scorchstall,Shadeforest,Shademeadow,Shadeville,Shimmerrun,Shimmerwood,Shroudrock,Silentkeep,Silvercairn,Silvergulch,Smallmire,Smoothcliff,Smoothgrove,Smoothtown,Snakemere,Snowbay,Snowshield,Snowtown,Southbreak,Springmire,Springview,Stagport,Steammouth,Steamwall,Steepmoor,Stillhall,Stoneguard,Stonespell,Stormhand,Stormhorn,Sungulf,Sunhall,Swampmaw,Swangarde,Swanwall,Swiftwell,Thorncairn,Thornhelm,Thornyard,Timberside,Tradewick,Westmeadow,Westpoint,Whiteshore,Whitvalley,Wildeden,Wildwell,Wildyard,Winterhaven,Wolfpass"},
{name: "Elven", i: 33, min: 6, max: 12, d: "lenmsrg", m: 0, b: "Adrindest,Aethel,Afranthemar,Aiqua,Alari,Allanar,Almalian,Alora,Alyanasari,Alyelona,Alyran,Ammar,Anyndell,Arasari,Aren,Ashmebel,Aymlume,Bel-Didhel,Brinorion,Caelora,Chaulssad,Chaundra,Cyhmel,Cyrang,Dolarith,Dolonde,Draethe,Dranzan,Draugaust,E'ana,Eahil,Edhil,Eebel,Efranluma,Eld-Sinnocrin,Elelthyr,Ellanalin,Ellena,Ellorthond,Eltaesi,Elunore,Emyranserine,Entheas,Eriargond,Esari,Esath,Eserius,Eshsalin,Eshthalas,Evraland,Faellenor,Famelenora,Filranlean,Filsaqua,Gafetheas,Gaf Serine,Geliene,Gondorwin,Guallu,Haeth,Hanluna,Haulssad,Heloriath,Himlarien,Himliene,Hinnead,Hlinas,Hloireenil,Hluihei,Hlurthei,Hlynead,Iaenarion,Iaron,Illanathaes,Illfanora,Imlarlon,Imyse,Imyvelian,Inferius,Inlurth,innsshe,Iralserin,Irethtalos,Irholona,Ishal,Ishlashara,Ithelion,Ithlin,Iulil,Jaal,Jamkadi,Kaalume,Kaansera,Karanthanil,Karnosea,Kasethyr,Keatheas,Kelsya,Keth Aiqua,Kmlon,Kyathlenor,Kyhasera,Lahetheas,Lefdorei,Lelhamelle,Lilean,Lindeenil,Lindoress,Litys,Llaughei,Lya,Lyfa,Lylharion,Lynathalas,Machei,Masenoris,Mathethil,Mathentheas,Meethalas,Menyamar,Mithlonde,Mytha,Mythsemelle,Mythsthas,Naahona,Nalore,Nandeedil,Nasad Ilaurth,Nasin,Nathemar,Neadar,Neilon,Nelalon,Nellean,Nelnetaesi,Nilenathyr,Nionande,Nylm,Nytenanas,Nythanlenor,O'anlenora,Obeth,Ofaenathyr,Ollmnaes,Ollsmel,Olwen,Olyaneas,Omanalon,Onelion,Onelond,Orlormel,Ormrion,Oshana,Oshvamel,Raethei,Rauguall,Reisera,Reslenora,Ryanasera,Rymaserin,Sahnor,Saselune,Sel-Zedraazin,Selananor,Sellerion,Selmaluma,Shaeras,Shemnas,Shemserin,Sheosari,Sileltalos,Siriande,Siriathil,Srannor,Sshanntyr,Sshaulu,Syholume,Sylharius,Sylranbel,Taesi,Thalor,Tharenlon,Thelethlune,Thelhohil,Themar,Thene,Thilfalean,Thilnaenor,Thvethalas,Thylathlond,Tiregul,Tlauven,Tlindhe,Ulal,Ullve,Ulmetheas,Ulssin,Umnalin,Umye,Umyheserine,Unanneas,Unarith,Undraeth,Unysarion,Vel-Shonidor,Venas,Vin Argor,Wasrion,Wlalean,Yaeluma,Yeelume,Yethrion,Ymserine,Yueghed,Yuerran,Yuethin"},
{name: "Dark Elven", i: 34, min: 6, max: 14, d: "nrslamg", m: .2, b: "Abaethaggar,Abburth,Afranthemar,Aharasplit,Aidanat,Ald'ruhn,Ashamanu,Ashesari,Ashletheas,Baerario,Baereghel,Baethei,Bahashae,Balmora,Bel-Didhel,Borethanil,Buiyrandyn,Caellagith,Caellathala,Caergroth,Caldras,Chaggar,Chaggaust,Channtar,Charrvhel'raugaust,Chaulssin,Chaundra,ChedNasad,ChetarIthlin,ChethRrhinn,Chymaer,Clarkarond,Cloibbra,Commoragh,Cyrangroth,Cilben,D'eldarc,Daedhrog,Dalkyn,Do'Urden,Doladress,Dolarith,Dolonde,Draethe,Dranzan,Dranzithl,Draugaust,Dreghei,Drelhei,Dryndlu,Dusklyngh,DyonG'ennivalz,Edraithion,Eld-Sinnocrin,Ellorthond,Enhethyr,Entheas,ErrarIthinn,Eryndlyn,Faladhell,Faneadar,Fethalas,Filranlean,Formarion,Ferdor,Gafetheas,Ghrond,Gilranel,Glamordis,Gnaarmok,Gnisis,Golothaer,Gondorwin,Guallidurth,Guallu,Gulshin,Haeth,Haggraef,Harganeth,Harkaldra,Haulssad,Haundrauth,Heloriath,Hlammachar,Hlaughei,Hloireenil,Hluitar,Inferius,Innsshe,Ithilaughym,Iz'aiogith,Jaal,Jhachalkhyn,Kaerabrae,Karanthanil,Karondkar,Karsoluthiyl,Kellyth,Khuul,Lahetheas,Lidurth,Lindeenil,Lirillaquen,LithMy'athar,LlurthDreier,Lolth,Lothuial,Luihaulen'tar,Maeralyn,Maerimydra,Mathathlona,Mathethil,Mellodona,Menagith,Menegwen,Menerrendil,Menzithl,Menzoberranzan,Mila-Nipal,Mithryn,Molagmar,Mundor,Myvanas,Naggarond,Nandeedil,NasadIlaurth,Nauthor,Navethas,Neadar,Nurtaleewe,Nidiel,Noruiben,Olwen,O'lalona,Obeth,Ofaenathyr,Orlormel,Orlytlar,Pelagiad,Raethei,Raugaust,Rauguall,Rilauven,Rrharrvhei,Sadrith,Sel-Zedraazin,Seydaneen,Shaz'rir,Skaal,Sschindylryn,Shamath,Shamenz,Shanntur,Sshanntynlan,Sshanntyr,Shaulssin,SzithMorcane,Szithlin,Szobaeth,Sirdhemben,T'lindhet,Tebh'zhor,Telmere,Telnarquel,Tharlarast,Thylathlond,Tlaughe,Trizex,Tyrybblyn,Ugauth,Ughym,Uhaelben,Ullmatalos,Ulmetheas,Ulrenserine,Uluitur,Undraeth,Undraurth,Undrek'Thoz,Ungethal,UstNatha,Uthaessien,V'elddrinnsshar,Vaajha,Vel-Shonidor,Velddra,Velothi,Venead,Vhalth'vha,Vinargothr,Vojha,Waethe,Waethei,Xaalkis,Yakaridan,Yeelume,Yridhremben,Yuethin,Yuethindrynn,Zirnakaynin"},
{name: "Dwarven", i: 35, min: 4, max: 11, d: "dk", m: 0, b: "Addundad,Ahagzad,Ahazil,Akil,Akzizad,Anumush,Araddush,Arar,Arbhur,Badushund,Baragzig,Baragzund,Barakinb,Barakzig,Barakzinb,Barakzir,Baramunz,Barazinb,Barazir,Bilgabar,Bilgatharb,Bilgathaz,Bilgila,Bilnaragz,Bilnulbar,Bilnulbun,Bizaddum,Bizaddush,Bizanarg,Bizaram,Bizinbiz,Biziram,Bunaram,Bundinar,Bundushol,Bundushund,Bundushur,Buzaram,Buzundab,Buzundush,Gabaragz,Gabaram,Gabilgab,Gabilgath,Gabizir,Gabunal,Gabunul,Gabuzan,Gatharam,Gatharbhur,Gathizdum,Gathuragz,Gathuraz,Gila,Giledzir,Gilukkhath,Gilukkhel,Gunala,Gunargath,Gunargil,Gundumunz,Gundusharb,Gundushizd,Kharbharbiln,Kharbhatharb,Kharbhela,Kharbilgab,Kharbuzadd,Khatharbar,Khathizdin,Khathundush,Khazanar,Khazinbund,Khaziragz,Khaziraz,Khizdabun,Khizdusharbh,Khizdushath,Khizdushel,Khizdushur,Kholedzar,Khundabiln,Khundabuz,Khundinarg,Khundushel,Khuragzig,Khuramunz,Kibarak,Kibilnal,Kibizar,Kibunarg,Kibundin,Kibuzan,Kinbadab,Kinbaragz,Kinbarakz,Kinbaram,Kinbizah,Kinbuzar,Nala,Naledzar,Naledzig,Naledzinb,Naragzah,Naragzar,Naragzig,Narakzah,Narakzar,Naramunz,Narazar,Nargabad,Nargabar,Nargatharb,Nargila,Nargundum,Nargundush,Nargunul,Narukthar,Narukthel,Nula,Nulbadush,Nulbaram,Nulbilnarg,Nulbunal,Nulbundab,Nulbundin,Nulbundum,Nulbuzah,Nuledzah,Nuledzig,Nulukkhaz,Nulukkhund,Nulukkhur,Sharakinb,Sharakzar,Sharamunz,Sharbarukth,Shatharbhizd,Shatharbiz,Shathazah,Shathizdush,Shathola,Shaziragz,Shizdinar,Shizdushund,Sholukkharb,Shundinulb,Shundushund,Shurakzund,Shuramunz,Tumunzadd,Tumunzan,Tumunzar,Tumunzinb,Tumunzir,Ukthad,Ulbirad,Ulbirar,Ulunzar,Ulur,Umunzad,Undalar,Undukkhil,Undun,Undur,Unduzur,Unzar,Unzathun,Usharar,Zaddinarg,Zaddushur,Zaharbad,Zaharbhizd,Zarakib,Zarakzar,Zaramunz,Zarukthel,Zinbarukth,Zirakinb,Zirakzir,Ziramunz,Ziruktharbh,Zirukthur,Zundumunz"},
{name: "Goblin", i: 36, min: 4, max: 9, d: "eag", m: 0, b: "Asinx,Bhiagielt,Biokvish,Blix,Blus,Bratliaq,Breshass,Bridvelb,Brybsil,Bugbig,Buyagh,Cel,Chalk,Chiafzia,Chox,Cielb,Cosvil,Crekork,Crild,Croibieq,Diervaq,Dobruing,Driord,Eebligz,Een,Enissee,Esz,Far,Felhob,Froihiofz,Fruict,Fygsee,Gagablin,Gigganqi,Givzieqee,Glamzofs,Glernaahx,Gneabs,Gnoklig,Gobbledak,gobbok,Gobbrin,Heszai,Hiszils,Hobgar,Honk,Iahzaarm,Ialsirt,Ilm,Ish,Jasheafta,Joimtoilm,Kass,Katmelt,Kleabtong,Kleardeek,Klilm,Kluirm,Kuipuinx,Moft,Mogg,Nilbog,Oimzoishai,Onq,Ozbiard,Paas,Phax,Phigheldai,Preang,Prolkeh,Pyreazzi,Qeerags,Qosx,Rekx,Shaxi,Sios,Slehzit,Slofboif,Slukex,Srefs,Srurd,Stiaggaltia,Stiolx,Stioskurt,Stroir,Strytzakt,Stuikvact,Styrzangai,Suirx,Swaxi,Taxai,Thelt,Thresxea,Thult,Traglila,Treaq,Ulb,Ulm,Utha,Utiarm,Veekz,Vohniots,Vreagaald,Watvielx,Wrogdilk,Wruilt,Xurx,Ziggek,Zriokots"},
{name: "Orc", i: 37, min: 4, max: 8, d: "gzrcu", m: 0, b: "Adgoz,Adgril-Gha,Adog,Adzurd,Agkadh,Agzil-Ghal,Akh,Ariz-Dru,Arkugzo,Arrordri,Ashnedh,Azrurdrekh,Bagzildre,Bashnud,Bedgez-Graz,Bhakh,Bhegh,Bhiccozdur,Bhicrur,Bhirgoshbel,Bhog,Bhurkrukh,Bod-Rugniz,Bogzel,Bozdra,Bozgrun,Bozziz,Bral-Lazogh,Brazadh,Brogved,Brogzozir,Brolzug,Brordegeg,Brorkril-Zrog,Brugroz,Brukh-Zrabrul,Brur-Korre,Bulbredh,Bulgragh,Chaz-Charard,Chegan-Khed,Chugga,Chuzar,Dhalgron-Mog,Dhazon-Ner,Dhezza,Dhoddud,Dhodh-Brerdrodh,Dhodh-Ghigin,Dhoggun-Bhogh,Dhulbazzol,Digzagkigh,Dirdrurd,Dodkakh,Dorgri,Drizdedh,Drobagh,Drodh-Ashnugh,Drogvukh-Drodh,Drukh-Qodgoz,Drurkuz,Dududh,Dur-Khaddol,Egmod,Ekh-Beccon,Ekh-Krerdrugh,Ekh-Mezred,Gagh-Druzred,Gazdrakh-Vrard,Gegnod,Gerkradh,Ghagrocroz,Ghared-Krin,Ghedgrolbrol,Gheggor,Ghizgil,Gho-Ugnud,Gholgard,Gidh-Ucceg,Goccogmurd,Golkon,Graz-Khulgag,Gribrabrokh,Gridkog,Grigh-Kaggaz,Grirkrun-Qur,Grughokh,Grurro,Gugh-Zozgrod,Gur-Ghogkagh,Ibagh-Chol,Ibruzzed,Ibul-Brad,Iggulzaz,Ikh-Ugnan,Irdrelzug,Irmekh-Bhor,Kacruz,Kalbrugh,Karkor-Zrid,Kazzuz-Zrar,Kezul-Bruz,Kharkiz,Khebun,Khorbric,Khuldrerra,Khuzdraz,Kirgol,Koggodh,Korkrir-Grar,Kraghird,Krar-Zurmurd,Krigh-Bhurdin,Kroddadh,Krudh-Khogzokh,Kudgroccukh,Kudrukh,Kudzal,Kuzgrurd-Dedh,Larud,Legvicrodh,Lorgran,Lugekh,Lulkore,Mazgar,Merkraz,Mocculdrer,Modh-Odod,Morbraz,Mubror,Muccug-Ghuz,Mughakh-Chil,Murmad,Nazad-Ludh,Negvidh,Nelzor-Zroz,Nirdrukh,Nogvolkar,Nubud,Nuccag,Nudh-Kuldra,Nuzecro,Oddigh-Krodh,Okh-Uggekh,Ordol,Orkudh-Bhur,Orrad,Qashnagh,Qiccad-Chal,Qiddolzog,Qidzodkakh,Qirzodh,Rarurd,Reradgri,Rezegh,Rezgrugh,Rodrekh,Rogh-Chirzaz,Rordrushnokh,Rozzez,Ruddirgrad,Rurguz-Vig,Ruzgrin,Ugh-Vruron,Ughudadh,Uldrukh-Bhudh,Ulgor,Ulkin,Ummugh-Ekh,Uzaggor,Uzdriboz,Uzdroz,Uzord,Uzron,Vaddog,Vagord-Khod,Velgrudh,Verrugh,Vrazin,Vrobrun,Vrugh-Nardrer,Vrurgu,Vuccidh,Vun-Gaghukh,Zacrad,Zalbrez,Zigmorbredh,Zordrordud,Zorrudh,Zradgukh,Zragmukh,Zragrizgrakh,Zraldrozzuz,Zrard-Krodog,Zrazzuz-Vaz,Zrigud,Zrulbukh-Dekh,Zubod-Ur,Zulbriz,Zun-Bergrord"},
{name: "Giant", i: 38, min: 5, max: 10, d: "kdtng", m: 0, b: "Addund,Aerora,Agane,Anumush,Arangrim,Bahourg,Baragzund,Barakinb,Barakzig,Barakzinb,Baramunz,Barazinb,Beornelde,Beratira,Borgbert,Botharic,Bremrol,Brerstin,Brildung,Brozu,Bundushund,Burthug,Chazruc,Chergun,Churtec,Dagdhor,Dankuc,Darnaric,Debuch,Dina,Dinez,Diru,Drard,Druguk,Dugfast,Duhal,Dulkun,Eldond,Enuz,Eraddam,Eradhelm,Froththorn,Fynwyn,Gabaragz,Gabaram,Gabizir,Gabuzan,Gagkake,Galfald,Galgrim,Gatal,Gazin,Geru,Gila,Giledzir,Girkun,Glumvat,Gluthmark,Gomruch,Gorkege,Gortho,Gostuz,Grimor,Grimtira,Guddud,Gudgiz,Gulwo,Gunargath,Gundusharb,Guril,Gurkale,Guruge,Guzi,Hargarth,Hartreo,Heimfara,Hildlaug,Idgurth,Inez,Inginy,Iora,Irkin,Jaldhor,Jarwar,Jornangar,Jornmoth,Kakkek,Kaltoch,Kegkez,Kengord,Kharbharbiln,Khatharbar,Khathizdin,Khazanar,Khaziragz,Khizdabun,Khizdushel,Khundinarg,Kibarak,Kibizar,Kigine,Kilfond,Kilkan,Kinbadab,Kinbuzar,Koril,Kostand,Kuzake,Lindira,Lingarth,Maerdis,Magald,Marbold,Marbrand,Memron,Minu,Mistoch,Morluch,Mornkin,Morntaric,Nagu,Naragzah,Naramunz,Narazar,Nargabar,Nargatharb,Nargundush,Nargunul,Natan,Natil,Neliz,Nelkun,Noluch,Norginny,Nulbaram,Nulbilnarg,Nuledzah,Nuledzig,Nulukkhaz,Nulukkhur,Nurkel,Oci,Olane,Oldstin,Orga,Ranava,Ranhera,Rannerg,Rirkan,Rizen,Rurki,Rurkoc,Sadgach,Sgandrol,Sharakzar,Shatharbiz,Shathizdush,Shathola,Shizdinar,Sholukkharb,Shundushund,Shurakzund,Sidga,Sigbeorn,Sigbi,Solfod,Somrud,Srokvan,Stighere,Sulduch,Talkale,Theoddan,Theodgrim,Throtrek,Tigkiz,Tolkeg,Toren,Tozage,Tulkug,Tumunzar,Umunzad,Undukkhil,Usharar,Valdhere,Varkud,Velfirth,Velhera,Vigkan,Vorkige,Vozig,Vylwed,Widhyrde,Wylaeya,Yili,Yotane,Yudgor,Yulkake,Zigez,Zugkan,Zugke"},
{name: "Draconic", i: 39, min: 6, max: 14, d: "aliuszrox", m: 0, b: "Aaronarra,Adalon,Adamarondor,Aeglyl,Aerosclughpalar,Aghazstamn,Aglaraerose,Agoshyrvor,Alduin,Alhazmabad,Altagos,Ammaratha,Amrennathed,Anaglathos,Andrathanach,Araemra,Araugauthos,Arauthator,Arharzel,Arngalor,Arveiaturace,Athauglas,Augaurath,Auntyrlothtor,Azarvilandral,Azhaq,Balagos,Baratathlaer,Bleucorundum,BrazzPolis,Canthraxis,Capnolithyl,Charvekkanathor,Chellewis,Chelnadatilar,Cirrothamalan,Claugiyliamatar,Cragnortherma,Dargentum,Dendeirmerdammarar,Dheubpurcwenpyl,Domborcojh,Draconobalen,Dragansalor,Dupretiskava,Durnehviir,Eacoathildarandus,Eldrisithain,Enixtryx,Eormennoth,Esmerandanna,Evenaelorathos,Faenphaele,Felgolos,Felrivenser,Firkraag,Fll'Yissetat,Furlinastis,Galadaeros,Galglentor,Garnetallisar,Garthammus,Gaulauntyr,Ghaulantatra,Glouroth,Greshrukk,Guyanothaz,Haerinvureem,Haklashara,Halagaster,Halaglathgar,Havarlan,Heltipyre,Hethcypressarvil,Hoondarrh,Icehauptannarthanyx,Iiurrendeem,Ileuthra,Iltharagh,Ingeloakastimizilian,Irdrithkryn,Ishenalyr,Iymrith,Jaerlethket,Jalanvaloss,Jharakkan,Kasidikal,Kastrandrethilian,Khavalanoth,Khuralosothantar,Kisonraathiisar,Kissethkashaan,Kistarianth,Klauth,Klithalrundrar,Krashos,Kreston,Kriionfanthicus,Krosulhah,Krustalanos,Kruziikrel,Kuldrak,Lareth,Latovenomer,Lhammaruntosz,Llimark,Ma'fel'no'sei'kedeh'naar,MaelestorRex,Magarovallanthanz,Mahatnartorian,Mahrlee,Malaeragoth,Malagarthaul,Malazan,Maldraedior,Maldrithor,MalekSalerno,Maughrysear,Mejas,Meliordianix,Merah,Mikkaalgensis,Mirmulnir,Mistinarperadnacles,Miteach,Mithbarazak,Morueme,Moruharzel,Naaslaarum,Nahagliiv,Nalavarauthatoryl,Naxorlytaalsxar,Nevalarich,Nolalothcaragascint,Nurvureem,Nymmurh,Odahviing,Olothontor,Ormalagos,Otaaryliakkarnos,Paarthurnax,Pelath,Pelendralaar,Praelorisstan,Praxasalandos,Protanther,Qiminstiir,Quelindritar,Ralionate,Rathalylaug,Rathguul,Rauglothgor,Raumorthadar,Relonikiv,Ringreemeralxoth,Roraurim,Rynnarvyx,Sablaxaahl,Sahloknir,Sahrotaar,Samdralyrion,Saryndalaghlothtor,Sawaka,Shalamalauth,Shammagar,Sharndrel,Shianax,Skarlthoon,Skurge,Smergadas,Ssalangan,Sssurist,Sussethilasis,Sylvallitham,Tamarand,Tantlevgithus,Tarlacoal,Tenaarlaktor,Thalagyrt,Tharas'kalagram,Thauglorimorgorus,Thoklastees,Thyka,Tsenshivah,Ueurwen,Uinnessivar,Urnalithorgathla,Velcuthimmorhar,Velora,Vendrathdammarar,Venomindhar,Viinturuth,Voaraghamanthar,Voslaarum,Vr'tark,Vrondahorevos,Vuljotnaak,Vulthuryol,Wastirek,Worlathaugh,Xargithorvar,Xavarathimius,Yemere,Ylithargathril,Ylveraasahlisar,Za-Jikku,Zarlandris,Zellenesterex,Zilanthar,Zormapalearath,Zundaerazylym,Zz'Pzora"},
{name: "Arachnid", i: 40, min: 4, max: 10, d: "erlsk", m: 0, b: "Aaqok'ser,Aiced,Aizachis,Allinqel,As'taq,Ashrash,Caaqtos,Ceek'sax,Ceezuq,Cek'sier,Cen'qi,Ceqzocer,Cezeed,Chachocaq,Charis,Chashilieth,Checib,Chernul,Chezi,Chiazu,Chishros,Chixhi,Chizhi,Chollash,Choq'sha,Cinchichail,Collul,Ecush'taid,Ekiqe,Eqas,Er'uria,Erikas,Es'tase,Esrub,Exha,Haqsho,Hiavheesh,Hitha,Hok'thi,Hossa,Iacid,Iciever,Illuq,Isnir,Keezut,Kheellavas,Kheizoh,Khiachod,Khika,Khirzur,Khonrud,Khrakku,Khraqshis,Khrethish'ti,Khriashus,Khrika,Khrirni,Klashirel,Kleil'sha,Klishuth,Krarnit,Kras'tex,Krotieqas,Lais'tid,Laizuh,Lasnoth,Len'qeer,Leqanches,Lezad,Lhilir,Lhivhath,Lhok'thu,Lialliesed,Liaraq,Liceva,Lichorro,Lilla,Lokieqib,Nakur,Neerhaca,Neet'er,Neezoh,Nenchiled,Nerhalneth,Nir'ih,Nizus,Noreeqo,On'qix,Qalitho,Qas'tor,Qasol,Qavrud,Qavud,Qazar,Qazru,Qekno,Qeqravee,Qes'tor,Qhaik'sal,Qhak'sish,Qhazsakais,Qheliva,Qhenchaqes,Qherazal,Qhon'qos,Qhosh,Qish'tur,Qisih,Qorhoci,Qranchiq,Racith,Rak'zes,Ranchis,Rarhie,Rarzi,Rarzisiaq,Ras'tih,Ravosho,Recad,Rekid,Rernee,Rertachis,Rezhokketh,Reziel,Rhacish,Rhail'shel,Rhairhizse,Rhakivex,Rhaqeer,Rhartix,Rheciezsei,Rheevid,Rhel'shir,Rhevhie,Rhiavekot,Rhikkos,Rhiqese,Rhiqi,Rhiqracar,Rhisned,Rhousnateb,Riakeesnex,Rintachal,Rir'ul,Rourk'u,Rouzakri,Sailiqei,Sanchiqed,Saqshu,Sat'ier,Sazi,Seiqas,Shieth'i,Shiqsheh,Shizha,Shrachuvo,Shranqo,Shravhos,Shravuth,Shreerhod,Shrethuh,Shriantieth,Shronqash,Shrovarhir,Shrozih,Siacaqoh,Siezosh,Siq'sha,Sirro,Sornosi,Srachussi,Szaca,Szacih,Szaqova,Szasu,Szazhilos,Szeerrud,Szeezsad,Szeknur,Szesir,Szezhirros,Szilshith,Szon'qol,Szornuq,Xeekke,Yeek'su,Yeeq'zox,Yeqil,Yeqroq,Yeveed,Yevied,Yicaveeh,Yirresh,Yisie,Yithik'thaih,Yorhaqshes,Zacheek'sa,Zakkasa,Zelraq,Zeqo,Zharuncho,Zhath'arhish,Zhavirrit,Zhazilraq,Zhazsachiel,Zhek'tha,Zhequ,Zhias'ted,Zhicat,Zhicur,Zhirhacil,Zhizri,Zhochizses,Ziarih,Zirnib"},
{name: "Serpents", i: 41, min: 5, max: 11, d: "slrk", m: 0, b: "Aj'ha,Aj'i,Aj'tiss,Ajakess,Aksas,Aksiss,Al'en,An'jeshe,Apjige,Arkkess,Athaz,Atus,Azras,Caji,Cakrasar,Cal'arrun,Capji,Cathras,Cej'han,Ces,Cez'jenta,Cij'te,Cinash,Cizran,Coth'jus,Cothrash,Culzanek,Cunaless,Ej'tesh,Elzazash,Ergek,Eshjuk,Ethris,Gan'jas,Gapja,Gar'thituph,Gopjeguss,Gor'thesh,Gragishaph,Grar'theness,Grath'ji,Gressinas,Grolzesh,Grorjar,Grozrash,Guj'ika,Harji,Hej'hez,Herkush,Horgarrez,Illuph,Ipjar,Ithashin,Kaj'ess,Kar'kash,Kepjusha,Ki'kintus,Kissere,Koph,Kopjess,Kra'kasher,Krak,Krapjez,Krashjuless,Kraz'ji,Krirrigis,Krussin,Ma'lush,Mage,Maj'tak,Mal'a,Mapja,Mar'kash,Mar'kis,Marjin,Mas,Mathan,Men'jas,Meth'jaresh,Mij'hegak,Min'jash,Mith'jas,Monassu,Moss,Naj'hass,Najugash,Nak,Napjiph,Nar'ka,Nar'thuss,Narrusha,Nash,Nashjekez,Nataph,Nij'ass,Nij'tessiph,Nishjiss,Norkkuss,Nus,Olluruss,Or'thi,Or'thuss,Paj'a,Parkka,Pas,Pathujen,Paz'jaz,Pepjerras,Pirkkanar,Pituk,Porjunek,Pu'ke,Ragen,Ran'jess,Rargush,Razjuph,Rilzan,Riss,Rithruz,Rorgiss,Rossez,Rraj'asesh,Rraj'tass,Rrar'kess,Rrar'thuph,Rras,Rrazresh,Rrej'hish,Rrigelash,Rris,Rris,Rroksurrush,Rukrussush,Rurri,Russa,Ruth'jes,Sa'kitesh,Sar'thass,Sarjas,Sazjuzush,Ser'thez,Sezrass,Shajas,Shas,Shashja,Shass,Shetesh,Shijek,Shun'jaler,Shurjarri,Skaler,Skalla,Skallentas,Skaph,Skar'kerriz,Skath'jeruk,Sker'kalas,Skor,Skoz'ji,Sku'lu,Skuph,Skur'thur,Slalli,Slalt'har,Slelziress,Slil'ar,Sloz'jisa,Sojesh,Solle,Sorge,Sral'e,Sran'ji,Srapjess,Srar'thazur,Srash,Srath'jess,Srathrarre,Srerkkash,Srus,Sruss'tugeph,Sun,Suss'tir,Uzrash,Vargush,Vek,Vess'tu,Viph,Vult'ha,Vupjer,Vushjesash,Xagez,Xassa,Xulzessu,Zaj'tiss,Zan'jer,Zarriss,Zassegus,Zirres,Zsor,Zurjass"},
// additional by Avengium:
{name: "Levantine", i: 42, min: 4, max: 12, d: "ankprs", m: 0, b: "Adme,Adramet,Agadir,Akko,Akzib,Alimas,Alis-Ubbo,Alqosh,Amid,Ammon,Ampi,Amurru,Andarig,Anpa,Araden,Aram,Arwad,Ashkelon,Athar,Atiq,Aza,Azeka,Baalbek,Babel,Batrun,Beerot,Beersheba,Beit Shemesh,Berytus,Bet Agus,Bet Anya,Beth-Horon,Bethel,Bethlehem,Bethuel,Bet Nahrin,Bet Nohadra,Bet Zalin,Birmula,Biruta,Bit Agushi,Bitan,Bit Zamani,Cerne,Dammeseq,Darmsuq,Dor,Eddial,Eden Ekron,Elah,Emek,Emun,Ephratah,Eyn Ganim,Finike,Gades,Galatia,Gaza,Gebal,Gedera,Gerizzim,Gethsemane,Gibeon,Gilead,Gilgal,Golgotha,Goshen,Gytte,Hagalil,Haifa,Halab,Haqel Dma,Har Habayit,Har Nevo,Har Pisga,Havilah,Hazor,Hebron,Hormah,Iboshim,Iriho,Irinem,Irridu,Israel,Kadesh,Kanaan,Kapara,Karaly,Kart-Hadasht,Keret Chadeshet,Kernah,Kesed,Keysariya,Kfar,Kfar Nahum,Khalibon,Khalpe,Khamat,Kiryat,Kittim,Kurda,Lapethos,Larna,Lepqis,Lepriptza,Liksos,Lod,Luv,Malaka,Malet,Marat,Megido,Melitta,Merdin,Metsada,Mishmarot,Mitzrayim,Moab,Mopsos,Motye,Mukish,Nampigi,Nampigu,Natzrat,Nimrud,Nineveh,Nob,Nuhadra,Oea,Ofir,Oyat,Phineka,Phoenicus,Pleshet,Qart-Tubah Sarepta,Qatna,Rabat Amon,Rakkath,Ramat Aviv,Ramitha,Ramta,Rehovot,Reshef,Rushadir,Rushakad,Samrin,Sefarad,Sehyon,Sepat,Sexi,Sharon,Shechem,Shefelat,Shfanim,Shiloh,Shmaya,Shomron,Sidon,Sinay,Sis,Solki,Sur,Suria,Tabetu,Tadmur,Tarshish,Tartus,Teberya,Tefessedt,Tekoa,Teyman,Tinga,Tipasa,Tsabratan,Tur Abdin,Tzarfat,Tziyon,Tzor,Ugarit,Unubaal,Ureshlem,Urhay,Urushalim,Vaga,Yaffa,Yamhad,Yam hamelach,Yam Kineret,Yamutbal,Yathrib,Yaudi,Yavne,Yehuda,Yerushalayim,Yev,Yevus,Yizreel,Yurdnan,Zarefat,Zeboim,Zeurta,Zeytim,Zikhron,Zmurna"}
];
};
return {
getBase,
getCulture,
getCultureShort,
getBaseShort,
getState,
updateChain,
clearChains,
getNameBases,
getMapName,
calculateChain
};
})();

View file

@ -1,92 +0,0 @@
"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");
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 chains = [];
const opacity = rn(0.4 / limits.length, 2);
used = new Uint8Array(pointsN); // to detect already passed cells
for (const i of cells.i) {
const t = cells.t[i];
if (t > 0) continue;
if (used[i] || !limits.includes(t)) continue;
const start = findStart(i, t);
if (!start) continue;
used[i] = 1;
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));
if (relaxed.length < 4) continue;
const points = clipPoly(
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").attr("fill-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])];
}
TIME && console.timeEnd("drawOceanLayers");
};
function randomizeOutline() {
const limits = [];
let odd = 0.2;
for (let l = -9; l < 0; l++) {
if (P(odd)) {
odd = 0.2;
limits.push(l);
} else {
odd *= 2;
}
}
return limits;
}
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.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;
const c2 = !cells.t[c[2]] || cells.t[c[2]] === t - 1;
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
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");
break;
}
}
chain.push(chain[0]); // push first vertex as the last one
return chain;
}
return OceanLayers;
})();

View file

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

View file

@ -1,921 +0,0 @@
"use strict";
window.Religions = (function () {
// name generation approach and relative chance to be selected
const approach = {
Number: 1,
Being: 3,
Adjective: 5,
"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
};
// 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 = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: [
"Ancestor",
"Ancient",
"Avatar",
"Brother",
"Champion",
"Chief",
"Council",
"Creator",
"Deity",
"Divine One",
"Elder",
"Enlightened Being",
"Father",
"Forebear",
"Forefather",
"Giver",
"God",
"Goddess",
"Guardian",
"Guide",
"Hierach",
"Lady",
"Lord",
"Maker",
"Master",
"Mother",
"Numen",
"Oracle",
"Overlord",
"Protector",
"Reaper",
"Ruler",
"Sage",
"Seer",
"Sister",
"Spirit",
"Supreme Being",
"Transcendent",
"Virgin"
],
animal: [
"Antelope",
"Ape",
"Badger",
"Basilisk",
"Bear",
"Beaver",
"Bison",
"Boar",
"Buffalo",
"Camel",
"Cat",
"Centaur",
"Cerberus",
"Chimera",
"Cobra",
"Cockatrice",
"Crane",
"Crocodile",
"Crow",
"Cyclope",
"Deer",
"Dog",
"Direwolf",
"Drake",
"Dragon",
"Eagle",
"Elephant",
"Elk",
"Falcon",
"Fox",
"Goat",
"Goose",
"Gorgon",
"Gryphon",
"Hare",
"Hawk",
"Heron",
"Hippogriff",
"Horse",
"Hound",
"Hyena",
"Ibis",
"Jackal",
"Jaguar",
"Kitsune",
"Kraken",
"Lark",
"Leopard",
"Lion",
"Manticore",
"Mantis",
"Marten",
"Minotaur",
"Moose",
"Mule",
"Narwhal",
"Owl",
"Ox",
"Panther",
"Pegasus",
"Phoenix",
"Python",
"Rat",
"Raven",
"Roc",
"Rook",
"Scorpion",
"Serpent",
"Shark",
"Sheep",
"Snake",
"Sphinx",
"Spider",
"Swan",
"Tiger",
"Turtle",
"Unicorn",
"Viper",
"Vulture",
"Walrus",
"Wolf",
"Wolverine",
"Worm",
"Wyvern",
"Yeti"
],
adjective: [
"Aggressive",
"Almighty",
"Ancient",
"Beautiful",
"Benevolent",
"Big",
"Blind",
"Blond",
"Bloody",
"Brave",
"Broken",
"Brutal",
"Burning",
"Calm",
"Celestial",
"Cheerful",
"Crazy",
"Cruel",
"Dead",
"Deadly",
"Devastating",
"Distant",
"Disturbing",
"Divine",
"Dying",
"Eternal",
"Ethernal",
"Empyreal",
"Enigmatic",
"Enlightened",
"Evil",
"Explicit",
"Fair",
"Far",
"Fat",
"Fatal",
"Favorable",
"Flying",
"Friendly",
"Frozen",
"Giant",
"Good",
"Grateful",
"Great",
"Happy",
"High",
"Holy",
"Honest",
"Huge",
"Hungry",
"Illustrious",
"Immutable",
"Ineffable",
"Infallible",
"Inherent",
"Last",
"Latter",
"Lost",
"Loud",
"Lucky",
"Mad",
"Magical",
"Main",
"Major",
"Marine",
"Mythical",
"Mystical",
"Naval",
"New",
"Noble",
"Old",
"Otherworldly",
"Patient",
"Peaceful",
"Pregnant",
"Prime",
"Proud",
"Pure",
"Radiant",
"Resplendent",
"Sacred",
"Sacrosanct",
"Sad",
"Scary",
"Secret",
"Selected",
"Serene",
"Severe",
"Silent",
"Sleeping",
"Slumbering",
"Sovereign",
"Strong",
"Sunny",
"Superior",
"Supernatural",
"Sustainable",
"Transcendent",
"Transcendental",
"Troubled",
"Unearthly",
"Unfathomable",
"Unhappy",
"Unknown",
"Unseen",
"Waking",
"Wild",
"Wise",
"Worried",
"Young"
],
genitive: [
"Cold",
"Day",
"Death",
"Doom",
"Fate",
"Fire",
"Fog",
"Frost",
"Gates",
"Heaven",
"Home",
"Ice",
"Justice",
"Life",
"Light",
"Lightning",
"Love",
"Nature",
"Night",
"Pain",
"Snow",
"Springs",
"Summer",
"Thunder",
"Time",
"Victory",
"War",
"Winter"
],
theGenitive: [
"Abyss",
"Blood",
"Dawn",
"Earth",
"East",
"Eclipse",
"Fall",
"Harvest",
"Moon",
"North",
"Peak",
"Rainbow",
"Sea",
"Sky",
"South",
"Stars",
"Storm",
"Sun",
"Tree",
"Underworld",
"West",
"Wild",
"Word",
"World"
],
color: [
"Amber",
"Black",
"Blue",
"Bright",
"Bronze",
"Brown",
"Coral",
"Crimson",
"Dark",
"Emerald",
"Golden",
"Green",
"Grey",
"Indigo",
"Lavender",
"Light",
"Magenta",
"Maroon",
"Orange",
"Pink",
"Plum",
"Purple",
"Red",
"Ruby",
"Sapphire",
"Teal",
"Turquoise",
"White",
"Yellow"
]
};
const forms = {
Folk: {
Shamanism: 4,
Animism: 4,
Polytheism: 4,
"Ancestor Worship": 2,
"Nature Worship": 1,
Totemism: 1
},
Organized: {
Polytheism: 7,
Monotheism: 7,
Dualism: 3,
Pantheism: 2,
"Non-theism": 2
},
Cult: {
Cult: 5,
"Dark Cult": 5,
Sect: 1
},
Heresy: {
Heresy: 1
}
};
const namingMethods = {
Folk: {
"Culture + type": 1
},
Organized: {
"Random + type": 3,
"Random + ism": 1,
"Supreme + ism": 5,
"Faith of + Supreme": 5,
"Place + ism": 1,
"Culture + ism": 2,
"Place + ian + type": 6,
"Culture + type": 4
},
Cult: {
"Burg + ian + type": 2,
"Random + ian + type": 1,
"Type + of the + meaning": 2
},
Heresy: {
"Burg + ian + type": 3,
"Random + ism": 3,
"Random + ian + type": 2,
"Type + of the + meaning": 1
}
};
const types = {
Shamanism: {Beliefs: 3, Shamanism: 2, Druidism: 1, Spirits: 1},
Animism: {Spirits: 3, Beliefs: 1},
Polytheism: {Deities: 3, Faith: 1, Gods: 1, Pantheon: 1},
"Ancestor Worship": {Beliefs: 1, Forefathers: 2, Ancestors: 2},
"Nature Worship": {Beliefs: 3, Druids: 1},
Totemism: {Beliefs: 2, Totems: 2, Idols: 1},
Monotheism: {Religion: 2, Church: 3, Faith: 1},
Dualism: {Religion: 3, Faith: 1, Cult: 1},
Pantheism: {Religion: 1, Faith: 1},
"Non-theism": {Beliefs: 3, Spirits: 1},
Cult: {Cult: 4, Sect: 2, Arcanum: 1, Order: 1, Worship: 1},
"Dark Cult": {Cult: 2, Blasphemy: 1, Circle: 1, Coven: 1, Idols: 1, Occultism: 1},
Sect: {Sect: 3, Society: 1},
Heresy: {
Heresy: 3,
Sect: 2,
Apostates: 1,
Brotherhood: 1,
Circle: 1,
Dissent: 1,
Dissenters: 1,
Iconoclasm: 1,
Schism: 1,
Society: 1
}
};
const expansionismMap = {
Folk: () => 0,
Organized: () => gauss(5, 3, 0, 10, 1),
Cult: () => gauss(0.5, 0.5, 0, 5, 1),
Heresy: () => gauss(1, 0.5, 0, 5, 1)
};
function generate() {
TIME && console.time("generateReligions");
const lockedReligions = pack.religions?.filter(r => r.i && r.lock && !r.removed) || [];
const folkReligions = generateFolkReligions();
const organizedReligions = generateOrganizedReligions(+religionsNumber.value, lockedReligions);
const namedReligions = specifyReligions([...folkReligions, ...organizedReligions]);
const indexedReligions = combineReligions(namedReligions, lockedReligions);
const religionIds = expandReligions(indexedReligions);
const religions = defineOrigins(religionIds, indexedReligions);
pack.religions = religions;
pack.cells.religion = religionIds;
checkCenters();
TIME && console.timeEnd("generateReligions");
}
function generateFolkReligions() {
return pack.cultures
.filter(c => c.i && !c.removed)
.map(culture => ({type: "Folk", form: rw(forms.Folk), culture: culture.i, center: culture.center}));
}
function generateOrganizedReligions(desiredReligionNumber, lockedReligions) {
const cells = pack.cells;
const lockedReligionCount = lockedReligions.filter(({type}) => type !== "Folk").length || 0;
const requiredReligionsNumber = desiredReligionNumber - lockedReligionCount;
if (requiredReligionsNumber < 1) return [];
const candidateCells = getCandidateCells();
const religionCores = placeReligions();
const cultsCount = Math.floor((rand(1, 4) / 10) * religionCores.length); // 10-40%
const heresiesCount = Math.floor((rand(0, 3) / 10) * religionCores.length); // 0-30%
const organizedCount = religionCores.length - cultsCount - heresiesCount;
const getType = index => {
if (index < organizedCount) return "Organized";
if (index < organizedCount + cultsCount) return "Cult";
return "Heresy";
};
return religionCores.map((cellId, index) => {
const type = getType(index);
const form = rw(forms[type]);
const cultureId = cells.culture[cellId];
return {type, form, culture: cultureId, center: cellId};
});
function placeReligions() {
const religionCells = [];
const religionsTree = d3.quadtree();
// pre-populate with locked centers
lockedReligions.forEach(({center}) => religionsTree.add(cells.p[center]));
// min distance between religion inceptions
const spacing = (graphWidth + graphHeight) / 2 / desiredReligionNumber;
for (const cellId of candidateCells) {
const [x, y] = cells.p[cellId];
if (religionsTree.find(x, y, spacing) === undefined) {
religionCells.push(cellId);
religionsTree.add([x, y]);
if (religionCells.length === requiredReligionsNumber) return religionCells;
}
}
WARN && console.warn(`Placed only ${religionCells.length} of ${requiredReligionsNumber} religions`);
return religionCells;
}
function getCandidateCells() {
const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
if (validBurgs.length >= requiredReligionsNumber)
return validBurgs.sort((a, b) => b.population - a.population).map(burg => burg.cell);
return cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]);
}
}
function specifyReligions(newReligions) {
const {cells, cultures} = pack;
const rawReligions = newReligions.map(({type, form, culture: cultureId, center}) => {
const supreme = getDeityName(cultureId);
const deity = form === "Non-theism" || form === "Animism" ? null : supreme;
const stateId = cells.state[center];
let [name, expansion] = generateReligionName(type, form, supreme, center);
if (expansion === "state" && !stateId) expansion = "global";
const expansionism = expansionismMap[type]();
const color = getReligionColor(cultures[cultureId], type);
return {name, type, form, culture: cultureId, center, deity, expansion, expansionism, color};
});
return rawReligions;
function getReligionColor(culture, type) {
if (!culture.i) return getRandomColor();
if (type === "Folk") return culture.color;
if (type === "Heresy") return getMixedColor(culture.color, 0.35, 0.2);
if (type === "Cult") return getMixedColor(culture.color, 0.5, 0);
return getMixedColor(culture.color, 0.25, 0.4);
}
}
// indexes, conditionally renames, and abbreviates religions
function combineReligions(namedReligions, lockedReligions) {
const indexedReligions = [{name: "No religion", i: 0}];
const {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk} = parseLockedReligions();
const maxIndex = Math.max(
highestLockedIndex,
namedReligions.length + lockedReligions.length + 1 - numberLockedFolk
);
for (let index = 1, progress = 0; index < maxIndex; index = indexedReligions.length) {
// place locked religion back at its old index
if (index === lockedReligionQueue[0]?.i) {
const nextReligion = lockedReligionQueue.shift();
indexedReligions.push(nextReligion);
continue;
}
// slot the new religions
if (progress < namedReligions.length) {
const nextReligion = namedReligions[progress];
progress++;
if (
nextReligion.type === "Folk" &&
lockedReligions.some(({type, culture}) => type === "Folk" && culture === nextReligion.culture)
)
continue; // when there is a locked Folk religion for this culture discard duplicate
const newName = renameOld(nextReligion);
const code = abbreviate(newName, codes);
codes.push(code);
indexedReligions.push({...nextReligion, i: index, name: newName, code});
continue;
}
indexedReligions.push({i: index, type: "Folk", culture: 0, name: "Removed religion", removed: true});
}
return indexedReligions;
function parseLockedReligions() {
// copy and sort the locked religions list
const lockedReligionQueue = lockedReligions
.map(religion => {
// and filter their origins to locked religions
let newOrigin = religion.origins.filter(n => lockedReligions.some(({i: index}) => index === n));
if (newOrigin === []) newOrigin = [0];
return {...religion, origins: newOrigin};
})
.sort((a, b) => a.i - b.i);
const highestLockedIndex = Math.max(...lockedReligions.map(r => r.i));
const codes = lockedReligions.length > 0 ? lockedReligions.map(r => r.code) : [];
const numberLockedFolk = lockedReligions.filter(({type}) => type === "Folk").length;
return {lockedReligionQueue, highestLockedIndex, codes, numberLockedFolk};
}
// prepend 'Old' to names of folk religions which have organized competitors
function renameOld({name, type, culture: cultureId}) {
if (type !== "Folk") return name;
const haveOrganized =
namedReligions.some(
({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
) ||
lockedReligions.some(
({type, culture, expansion}) => culture === cultureId && type === "Organized" && expansion === "culture"
);
if (haveOrganized && name.slice(0, 3) !== "Old") return `Old ${name}`;
return name;
}
}
// finally generate and stores origins trees
function defineOrigins(religionIds, indexedReligions) {
const religionOriginsParamsMap = {
Organized: {clusterSize: 100, maxReligions: 2},
Cult: {clusterSize: 50, maxReligions: 3},
Heresy: {clusterSize: 50, maxReligions: 4}
};
const origins = indexedReligions.map(({i, type, culture: cultureId, expansion, center}) => {
if (i === 0) return null; // no religion
if (type === "Folk") return [0]; // folk religions originate from its parent culture only
const folkReligion = indexedReligions.find(({culture, type}) => type === "Folk" && culture === cultureId);
const isFolkBased = folkReligion && cultureId && expansion === "culture" && each(2)(center);
if (isFolkBased) return [folkReligion.i];
const {clusterSize, maxReligions} = religionOriginsParamsMap[type];
const fallbackOrigin = folkReligion?.i || 0;
return getReligionsInRadius(pack.cells.c, center, religionIds, i, clusterSize, maxReligions, fallbackOrigin);
});
return indexedReligions.map((religion, index) => ({...religion, origins: origins[index]}));
}
function getReligionsInRadius(neighbors, center, religionIds, religionId, clusterSize, maxReligions, fallbackOrigin) {
const foundReligions = new Set();
const queue = [center];
const checked = {};
for (let size = 0; queue.length && size < clusterSize; size++) {
const cellId = queue.shift();
checked[cellId] = true;
for (const neibId of neighbors[cellId]) {
if (checked[neibId]) continue;
checked[neibId] = true;
const neibReligion = religionIds[neibId];
if (neibReligion && neibReligion < religionId) foundReligions.add(neibReligion);
if (foundReligions.size >= maxReligions) return [...foundReligions];
queue.push(neibId);
}
}
return foundReligions.size ? [...foundReligions] : [fallbackOrigin];
}
// growth algorithm to assign cells to religions
function expandReligions(religions) {
const {cells, routes} = pack;
const religionIds = spreadFolkReligions(religions);
const queue = new FlatQueue();
const cost = [];
// limit cost for organized religions growth
const maxExpansionCost = (cells.i.length / 20) * byId("growthRate").valueAsNumber;
religions
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
.forEach(r => {
religionIds[r.center] = r.i;
queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0);
cost[r.center] = 1;
});
const religionsMap = new Map(religions.map(r => [r.i, r]));
while (queue.length) {
const {e: cellId, p, r, s: state} = queue.pop();
const {culture, expansion, expansionism} = religionsMap.get(r);
cells.c[cellId].forEach(nextCell => {
if (expansion === "culture" && culture !== cells.culture[nextCell]) return;
if (expansion === "state" && state !== cells.state[nextCell]) return;
if (religionsMap.get(religionIds[nextCell])?.lock) return;
const cultureCost = culture !== cells.culture[nextCell] ? 10 : 0;
const stateCost = state !== cells.state[nextCell] ? 10 : 0;
const passageCost = getPassageCost(cellId, nextCell);
const cellCost = cultureCost + stateCost + passageCost;
const totalCost = p + 10 + cellCost / expansionism;
if (totalCost > maxExpansionCost) return;
if (!cost[nextCell] || totalCost < cost[nextCell]) {
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
cost[nextCell] = totalCost;
queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
}
});
}
return religionIds;
function getPassageCost(cellId, nextCellId) {
const route = Routes.getRoute(cellId, nextCellId);
if (isWater(cellId)) return route ? 50 : 500;
const biomePassageCost = biomesData.cost[cells.biome[nextCellId]];
if (route) {
if (route.group === "roads") return 1;
return biomePassageCost / 3; // trails and other routes
}
return biomePassageCost;
}
}
// folk religions initially get all cells of their culture, and locked religions are retained
function spreadFolkReligions(religions) {
const cells = pack.cells;
const hasPrior = cells.religion && true;
const religionIds = new Uint16Array(cells.i.length);
const folkReligions = religions.filter(religion => religion.type === "Folk" && !religion.removed);
const cultureToReligionMap = new Map(folkReligions.map(({i, culture}) => [culture, i]));
for (const cellId of cells.i) {
const oldId = (hasPrior && cells.religion[cellId]) || 0;
if (oldId && religions[oldId]?.lock && !religions[oldId]?.removed) {
religionIds[cellId] = oldId;
continue;
}
const cultureId = cells.culture[cellId];
religionIds[cellId] = cultureToReligionMap.get(cultureId) || 0;
}
return religionIds;
}
function checkCenters() {
const cells = pack.cells;
pack.religions.forEach(r => {
if (!r.i) return;
// move religion center if it's not within religion area after expansion
if (cells.religion[r.center] === r.i) return; // in area
const firstCell = cells.i.find(i => cells.religion[i] === r.i);
const cultureHome = pack.cultures[r.culture]?.center;
if (firstCell) r.center = firstCell; // move center, othervise it's an extinct religion
else if (r.type === "Folk" && cultureHome) r.center = cultureHome; // reset extinct culture centers
});
}
function recalculate() {
const newReligionIds = expandReligions(pack.religions);
pack.cells.religion = newReligionIds;
checkCenters();
}
const add = function (center) {
const {cells, cultures, religions} = pack;
const religionId = cells.religion[center];
const i = religions.length;
const cultureId = cells.culture[center];
const missingFolk =
cultureId !== 0 &&
!religions.some(({type, culture, removed}) => type === "Folk" && culture === cultureId && !removed);
const color = missingFolk ? cultures[cultureId].color : getMixedColor(religions[religionId].color, 0.3, 0);
const type = missingFolk
? "Folk"
: religions[religionId].type === "Organized"
? rw({Organized: 4, Cult: 1, Heresy: 2})
: rw({Organized: 5, Cult: 2});
const form = rw(forms[type]);
const deity =
type === "Heresy"
? religions[religionId].deity
: form === "Non-theism" || form === "Animism"
? null
: getDeityName(cultureId);
const [name, expansion] = generateReligionName(type, form, deity, center);
const formName = type === "Heresy" ? religions[religionId].form : form;
const code = abbreviate(
name,
religions.map(r => r.code)
);
const influences = getReligionsInRadius(cells.c, center, cells.religion, i, 25, 3, 0);
const origins = type === "Folk" ? [0] : influences;
religions.push({
i,
name,
color,
culture: cultureId,
type,
form: formName,
deity,
expansion,
expansionism: expansionismMap[type](),
center,
cells: 0,
area: 0,
rural: 0,
urban: 0,
origins,
code
});
cells.religion[center] = i;
};
// 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)}`;
ERROR && console.error("Unkown generation approach");
}
function generateReligionName(variety, form, deity, center) {
const {cells, cultures, burgs, states} = pack;
const random = () => Names.getCulture(cells.culture[center], null, null, "", 0);
const type = rw(types[form]);
const supreme = deity.split(/[ ,]+/)[0];
const culture = cultures[cells.culture[center]].name;
const place = adj => {
const burgId = cells.burg[center];
const stateId = cells.state[center];
const base = burgId ? burgs[burgId].name : states[stateId].name;
let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name;
};
const m = rw(namingMethods[variety]);
if (m === "Random + type") return [random() + " " + type, "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme) + "ism", "global"];
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"];
if (m === "Burg + ian + type") return [`${place("adj")} ${type}`, "global"];
if (m === "Random + ian + type") return [`${getAdjective(random())} ${type}`, "global"];
if (m === "Type + of the + meaning") return [`${type} of the ${generateMeaning()}`, "global"];
return [trimVowels(random()) + "ism", "global"]; // else
}
return {generate, add, getDeityName, recalculate};
})();

View file

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

View file

@ -1,106 +0,0 @@
"use strict";
function drawBurgIcons() {
TIME && console.time("drawBurgIcons");
createIconGroups();
for (const {name} of options.burgs.groups) {
const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
if (!burgsInGroup.length) continue;
const iconsGroup = document.querySelector("#burgIcons > g#" + name);
if (!iconsGroup) continue;
const icon = iconsGroup.dataset.icon || "#icon-circle";
iconsGroup.innerHTML = burgsInGroup
.map(b => `<use id="burg${b.i}" data-id="${b.i}" href="${icon}" x="${b.x}" y="${b.y}"></use>`)
.join("");
const portsInGroup = burgsInGroup.filter(b => b.port);
if (!portsInGroup.length) continue;
const portGroup = document.querySelector("#anchors > g#" + name);
if (!portGroup) continue;
portGroup.innerHTML = portsInGroup
.map(b => `<use id="anchor${b.i}" data-id="${b.i}" href="#icon-anchor" x="${b.x}" y="${b.y}"></use>`)
.join("");
}
TIME && console.timeEnd("drawBurgIcons");
}
function drawBurgIcon(burg) {
removeBurgIcon(burg.i);
const iconGroup = burgIcons.select("#" + burg.group);
if (iconGroup.empty()) return;
const icon = iconGroup.attr("data-icon") || "#icon-circle";
burgIcons
.select("#" + burg.group)
.append("use")
.attr("href", icon)
.attr("id", "burg" + burg.i)
.attr("data-id", burg.i)
.attr("x", burg.x)
.attr("y", burg.y);
if (burg.port) {
anchors
.select("#" + burg.group)
.append("use")
.attr("href", "#icon-anchor")
.attr("id", "anchor" + burg.i)
.attr("data-id", burg.i)
.attr("x", burg.x)
.attr("y", burg.y);
}
}
function removeBurgIcon(burgId) {
const existingIcon = document.getElementById("burg" + burgId);
if (existingIcon) existingIcon.remove();
const existingAnchor = document.getElementById("anchor" + burgId);
if (existingAnchor) existingAnchor.remove();
}
function createIconGroups() {
// save existing styles and remove all groups
document.querySelectorAll("g#burgIcons > g").forEach(group => {
style.burgIcons[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
acc[attribute.name] = attribute.value;
return acc;
}, {});
group.remove();
});
document.querySelectorAll("g#anchors > g").forEach(group => {
style.anchors[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
acc[attribute.name] = attribute.value;
return acc;
}, {});
group.remove();
});
// create groups for each burg group and apply stored or default style
const defaultIconStyle = style.burgIcons.town || Object.values(style.burgIcons)[0] || {};
const defaultAnchorStyle = style.anchors.town || Object.values(style.anchors)[0] || {};
const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order);
for (const {name} of sortedGroups) {
const burgGroup = burgIcons.append("g");
const iconStyles = style.burgIcons[name] || defaultIconStyle;
Object.entries(iconStyles).forEach(([key, value]) => {
burgGroup.attr(key, value);
});
burgGroup.attr("id", name);
const anchorGroup = anchors.append("g");
const anchorStyles = style.anchors[name] || defaultAnchorStyle;
Object.entries(anchorStyles).forEach(([key, value]) => {
anchorGroup.attr(key, value);
});
anchorGroup.attr("id", name);
}
}

View file

@ -1,81 +0,0 @@
"use strict";
function drawBurgLabels() {
TIME && console.time("drawBurgLabels");
createLabelGroups();
for (const {name} of options.burgs.groups) {
const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
if (!burgsInGroup.length) continue;
const labelGroup = burgLabels.select("#" + name);
if (labelGroup.empty()) continue;
const dx = labelGroup.attr("data-dx") || 0;
const dy = labelGroup.attr("data-dy") || 0;
labelGroup
.selectAll("text")
.data(burgsInGroup)
.enter()
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", d => "burgLabel" + d.i)
.attr("data-id", d => d.i)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dx", dx + "em")
.attr("dy", dy + "em")
.text(d => d.name);
}
TIME && console.timeEnd("drawBurgLabels");
}
function drawBurgLabel(burg) {
removeBurgLabel(burg.i);
const labelGroup = burgLabels.select("#" + burg.group);
if (labelGroup.empty()) return;
const dx = labelGroup.attr("data-dx") || 0;
const dy = labelGroup.attr("data-dy") || 0;
labelGroup
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "burgLabel" + burg.i)
.attr("data-id", burg.i)
.attr("x", burg.x)
.attr("y", burg.y)
.attr("dx", dx + "em")
.attr("dy", dy + "em")
.text(burg.name);
}
function removeBurgLabel(burgId) {
const existingLabel = document.getElementById("burgLabel" + burgId);
if (existingLabel) existingLabel.remove();
}
function createLabelGroups() {
// save existing styles and remove all groups
document.querySelectorAll("g#burgLabels > g").forEach(group => {
style.burgLabels[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
acc[attribute.name] = attribute.value;
return acc;
}, {});
group.remove();
});
// create groups for each burg group and apply stored or default style
const defaultStyle = style.burgLabels.town || Object.values(style.burgLabels)[0] || {};
const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order);
for (const {name} of sortedGroups) {
const group = burgLabels.append("g");
const styles = style.burgLabels[name] || defaultStyle;
Object.entries(styles).forEach(([key, value]) => {
group.attr(key, value);
});
group.attr("id", name);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,312 +0,0 @@
"use strict";
// list - an optional array of stateIds to regenerate
function drawStateLabels(list) {
TIME && console.time("drawStateLabels");
// temporary make the labels visible
const layerDisplay = labels.style("display");
labels.style("display", null);
const {cells, states, features} = pack;
const stateIds = cells.state;
// increase step to 15 or 30 to make it faster and more horyzontal
// decrease step to 5 to improve accuracy
const ANGLE_STEP = 9;
const angles = precalculateAngles(ANGLE_STEP);
const LENGTH_START = 5;
const LENGTH_STEP = 5;
const LENGTH_MAX = 300;
const labelPaths = getLabelPaths();
const letterLength = checkExampleLetterLength();
drawLabelPath(letterLength);
// restore labels visibility
labels.style("display", layerDisplay);
function getLabelPaths() {
const labelPaths = [];
for (const state of states) {
if (!state.i || state.removed || state.lock) continue;
if (list && !list.includes(state.i)) continue;
const offset = getOffsetWidth(state.cells);
const maxLakeSize = state.cells / 20;
const [x0, y0] = state.pole;
const rays = angles.map(({angle, dx, dy}) => {
const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
return {angle, length, x, y};
});
const [ray1, ray2] = findBestRayPair(rays);
const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
if (ray1.x > ray2.x) pathPoints.reverse();
if (DEBUG.stateLabels) {
drawPoint(state.pole, {color: "black", radius: 1});
drawPath(pathPoints, {color: "black", width: 0.2});
}
labelPaths.push([state.i, pathPoints]);
}
return labelPaths;
}
function checkExampleLetterLength() {
const textGroup = d3.select("g#labels > g#states");
const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example");
const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
testLabel.remove();
return letterLength;
}
function drawLabelPath(letterLength) {
const mode = options.stateLabelsMode || "auto";
const lineGen = d3.line().curve(d3.curveNatural);
const textGroup = d3.select("g#labels > g#states");
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
for (const [stateId, pathPoints] of labelPaths) {
const state = states[stateId];
if (!state.i || state.removed) throw new Error("State must not be neutral or removed");
if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points");
textGroup.select("#stateLabel" + stateId).remove();
pathGroup.select("#textPath_stateLabel" + stateId).remove();
const textPath = pathGroup
.append("path")
.attr("d", round(lineGen(pathPoints)))
.attr("id", "textPath_stateLabel" + stateId);
const pathLength = textPath.node().getTotalLength() / letterLength; // path length in letters
const [lines, ratio] = getLinesAndRatio(mode, state.name, state.fullName, pathLength);
// prolongate path if it's too short
const longestLineLength = d3.max(lines.map(({length}) => length));
if (pathLength && pathLength < longestLineLength) {
const [x1, y1] = pathPoints.at(0);
const [x2, y2] = pathPoints.at(-1);
const [dx, dy] = [(x2 - x1) / 2, (y2 - y1) / 2];
const mod = longestLineLength / pathLength;
pathPoints[0] = [x1 + dx - dx * mod, y1 + dy - dy * mod];
pathPoints[pathPoints.length - 1] = [x2 - dx + dx * mod, y2 - dy + dy * mod];
textPath.attr("d", round(lineGen(pathPoints)));
}
const textElement = textGroup
.append("text")
.attr("text-rendering", "optimizeSpeed")
.attr("id", "stateLabel" + stateId)
.append("textPath")
.attr("startOffset", "50%")
.attr("font-size", ratio + "%")
.node();
const top = (lines.length - 1) / -2; // y offset
const spans = lines.map((line, index) => `<tspan x="0" dy="${index ? 1 : top}em">${line}</tspan>`);
textElement.insertAdjacentHTML("afterbegin", spans.join(""));
const {width, height} = textElement.getBBox();
textElement.setAttribute("href", "#textPath_stateLabel" + stateId);
if (mode === "full" || lines.length === 1) continue;
// check if label fits state boundaries. If no, replace it with short name
const [[x1, y1], [x2, y2]] = [pathPoints.at(0), pathPoints.at(-1)];
const angleRad = Math.atan2(y2 - y1, x2 - x1);
const isInsideState = checkIfInsideState(textElement, angleRad, width / 2, height / 2, stateIds, stateId);
if (isInsideState) continue;
// replace name to one-liner
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130);
textElement.setAttribute("font-size", correctedRatio + "%");
}
}
function getOffsetWidth(cellsNumber) {
if (cellsNumber < 40) return 0;
if (cellsNumber < 200) return 5;
return 10;
}
function precalculateAngles(step) {
const angles = [];
const RAD = Math.PI / 180;
for (let angle = 0; angle < 360; angle += step) {
const dx = Math.cos(angle * RAD);
const dy = Math.sin(angle * RAD);
angles.push({angle, dx, dy});
}
return angles;
}
function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) {
let ray = {length: 0, x: x0, y: y0};
for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) {
const [x, y] = [x0 + length * dx, y0 + length * dy];
// offset points are perpendicular to the ray
const offset1 = [x + -dy * offset, y + dx * offset];
const offset2 = [x + dy * offset, y + -dx * offset];
if (DEBUG.stateLabels) {
drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
}
const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
if (!inState) break;
ray = {length, x, y};
}
return ray;
function isInsideState(x, y) {
if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false;
const cellId = findCell(x, y);
const feature = features[cells.f[cellId]];
if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature);
return stateIds[cellId] === stateId;
}
function isInnerLake(feature) {
return feature.shoreline.every(cellId => stateIds[cellId] === stateId);
}
function isSmallLake(feature) {
return feature.cells <= maxLakeSize;
}
}
function findBestRayPair(rays) {
let bestPair = null;
let bestScore = -Infinity;
for (let i = 0; i < rays.length; i++) {
const score1 = rays[i].length * scoreRayAngle(rays[i].angle);
for (let j = i + 1; j < rays.length; j++) {
const score2 = rays[j].length * scoreRayAngle(rays[j].angle);
const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle);
if (pairScore > bestScore) {
bestScore = pairScore;
bestPair = [rays[i], rays[j]];
}
}
}
return bestPair;
}
function scoreRayAngle(angle) {
const normalizedAngle = Math.abs(angle % 180); // [0, 180]
const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1]
if (horizontality === 1) return 1; // Best: horizontal
if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted
if (horizontality >= 0.5) return 0.6; // Good: moderate slant
if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted
if (horizontality >= 0.15) return 0.2; // Poor: almost vertical
return 0.1; // Very poor: almost vertical
}
function scoreCurvature(angle1, angle2) {
const delta = getAngleDelta(angle1, angle2);
const similarity = evaluateArc(angle1, angle2);
if (delta === 180) return 1; // straight line: best
if (delta < 90) return 0; // acute: not allowed
if (delta < 120) return 0.6 * similarity;
if (delta < 140) return 0.7 * similarity;
if (delta < 160) return 0.8 * similarity;
return similarity;
}
function getAngleDelta(angle1, angle2) {
let delta = Math.abs(angle1 - angle2) % 360;
if (delta > 180) delta = 360 - delta; // [0, 180]
return delta;
}
// compute arc similarity towards x-axis
function evaluateArc(angle1, angle2) {
const proximity1 = Math.abs((angle1 % 180) - 90);
const proximity2 = Math.abs((angle2 % 180) - 90);
return 1 - Math.abs(proximity1 - proximity2) / 90;
}
function getLinesAndRatio(mode, name, fullName, pathLength) {
if (mode === "short") return getShortOneLine();
if (pathLength > fullName.length * 2) return getFullOneLine();
return getFullTwoLines();
function getShortOneLine() {
const ratio = pathLength / name.length;
return [[name], minmax(rn(ratio * 60), 50, 150)];
}
function getFullOneLine() {
const ratio = pathLength / fullName.length;
return [[fullName], minmax(rn(ratio * 70), 70, 170)];
}
function getFullTwoLines() {
const lines = splitInTwo(fullName);
const longestLineLength = d3.max(lines.map(({length}) => length));
const ratio = pathLength / longestLineLength;
return [lines, minmax(rn(ratio * 60), 70, 150)];
}
}
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) {
const bbox = textElement.getBBox();
const [cx, cy] = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
const points = [
[-halfwidth, -halfheight],
[+halfwidth, -halfheight],
[+halfwidth, halfheight],
[-halfwidth, halfheight],
[0, halfheight],
[0, -halfheight]
];
const sin = Math.sin(angleRad);
const cos = Math.cos(angleRad);
const rotatedPoints = points.map(([x, y]) => [cx + x * cos - y * sin, cy + x * sin + y * cos]);
let pointsInside = 0;
for (const [x, y] of rotatedPoints) {
const isInside = stateIds[findCell(x, y)] === stateId;
if (isInside) pointsInside++;
if (pointsInside > 4) return true;
}
return false;
}
TIME && console.timeEnd("drawStateLabels");
}

View file

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

View file

@ -1,677 +0,0 @@
const ROUTES_SHARP_ANGLE = 135;
const ROUTES_VERY_SHARP_ANGLE = 115;
const MIN_PASSABLE_SEA_TEMP = -4;
const ROUTE_TYPE_MODIFIERS = {
"-1": 1, // coastline
"-2": 1.8, // sea
"-3": 4, // open sea
"-4": 6, // ocean
default: 8 // far ocean
};
window.Routes = (function () {
function generate(lockedRoutes = []) {
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
const connections = new Map();
lockedRoutes.forEach(route => addConnections(route.points.map(p => p[2])));
const mainRoads = generateMainRoads();
const trails = generateTrails();
const seaRoutes = generateSeaRoutes();
pack.routes = createRoutesData(lockedRoutes);
pack.cells.routes = buildLinks(pack.routes);
function sortBurgsByFeature(burgs) {
const burgsByFeature = {};
const capitalsByFeature = {};
const portsByFeature = {};
const addBurg = (collection, feature, burg) => {
if (!collection[feature]) collection[feature] = [];
collection[feature].push(burg);
};
for (const burg of burgs) {
if (burg.i && !burg.removed) {
const {feature, capital, port} = burg;
addBurg(burgsByFeature, feature, burg);
if (capital) addBurg(capitalsByFeature, feature, burg);
if (port) addBurg(portsByFeature, port, burg);
}
}
return {burgsByFeature, capitalsByFeature, portsByFeature};
}
function generateMainRoads() {
TIME && console.time("generateMainRoads");
const mainRoads = [];
for (const [key, featureCapitals] of Object.entries(capitalsByFeature)) {
const points = featureCapitals.map(burg => [burg.x, burg.y]);
const urquhartEdges = calculateUrquhartEdges(points);
urquhartEdges.forEach(([fromId, toId]) => {
const start = featureCapitals[fromId].cell;
const exit = featureCapitals[toId].cell;
const segments = findPathSegments({isWater: false, connections, start, exit});
for (const segment of segments) {
addConnections(segment);
mainRoads.push({feature: Number(key), cells: segment});
}
});
}
TIME && console.timeEnd("generateMainRoads");
return mainRoads;
}
function generateTrails() {
TIME && console.time("generateTrails");
const trails = [];
for (const [key, featureBurgs] of Object.entries(burgsByFeature)) {
const points = featureBurgs.map(burg => [burg.x, burg.y]);
const urquhartEdges = calculateUrquhartEdges(points);
urquhartEdges.forEach(([fromId, toId]) => {
const start = featureBurgs[fromId].cell;
const exit = featureBurgs[toId].cell;
const segments = findPathSegments({isWater: false, connections, start, exit});
for (const segment of segments) {
addConnections(segment);
trails.push({feature: Number(key), cells: segment});
}
});
}
TIME && console.timeEnd("generateTrails");
return trails;
}
function generateSeaRoutes() {
TIME && console.time("generateSeaRoutes");
const seaRoutes = [];
for (const [featureId, featurePorts] of Object.entries(portsByFeature)) {
const points = featurePorts.map(burg => [burg.x, burg.y]);
const urquhartEdges = calculateUrquhartEdges(points);
urquhartEdges.forEach(([fromId, toId]) => {
const start = featurePorts[fromId].cell;
const exit = featurePorts[toId].cell;
const segments = findPathSegments({isWater: true, connections, start, exit});
for (const segment of segments) {
addConnections(segment);
seaRoutes.push({feature: Number(featureId), cells: segment});
}
});
}
TIME && console.timeEnd("generateSeaRoutes");
return seaRoutes;
}
function addConnections(segment) {
for (let i = 0; i < segment.length; i++) {
const cellId = segment[i];
const nextCellId = segment[i + 1];
if (nextCellId) {
connections.set(`${cellId}-${nextCellId}`, true);
connections.set(`${nextCellId}-${cellId}`, true);
}
}
}
function findPathSegments({isWater, connections, start, exit}) {
const getCost = createCostEvaluator({isWater, connections});
const pathCells = findPath(start, current => current === exit, getCost);
if (!pathCells) return [];
const segments = getRouteSegments(pathCells, connections);
return segments;
}
function createRoutesData(routes) {
const pointsArray = preparePointsArray();
for (const {feature, cells, merged} of mergeRoutes(mainRoads)) {
if (merged) continue;
const points = getPoints("roads", cells, pointsArray);
routes.push({i: routes.length, group: "roads", feature, points});
}
for (const {feature, cells, merged} of mergeRoutes(trails)) {
if (merged) continue;
const points = getPoints("trails", cells, pointsArray);
routes.push({i: routes.length, group: "trails", feature, points});
}
for (const {feature, cells, merged} of mergeRoutes(seaRoutes)) {
if (merged) continue;
const points = getPoints("searoutes", cells, pointsArray);
routes.push({i: routes.length, group: "searoutes", feature, points});
}
return routes;
}
// merge routes so that the last cell of one route is the first cell of the next route
function mergeRoutes(routes) {
let routesMerged = 0;
for (let i = 0; i < routes.length; i++) {
const thisRoute = routes[i];
if (thisRoute.merged) continue;
for (let j = i + 1; j < routes.length; j++) {
const nextRoute = routes[j];
if (nextRoute.merged) continue;
if (nextRoute.cells.at(0) === thisRoute.cells.at(-1)) {
routesMerged++;
thisRoute.cells = thisRoute.cells.concat(nextRoute.cells.slice(1));
nextRoute.merged = true;
}
}
}
return routesMerged > 1 ? mergeRoutes(routes) : routes;
}
}
function createCostEvaluator({isWater, connections}) {
return isWater ? getWaterPathCost : getLandPathCost;
function getLandPathCost(current, next) {
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
const habitability = biomesData.habitability[pack.cells.biome[next]];
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
const burgModifier = pack.cells.burg[next] ? 1 : 3;
const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
return pathCost;
}
function getWaterPathCost(current, next) {
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
const pathCost = distanceCost * typeModifier * connectionModifier;
return pathCost;
}
}
function buildLinks(routes) {
const links = {};
for (const {points, i: routeId} of routes) {
const cells = points.map(p => p[2]);
for (let i = 0; i < cells.length - 1; i++) {
const cellId = cells[i];
const nextCellId = cells[i + 1];
if (cellId !== nextCellId) {
if (!links[cellId]) links[cellId] = {};
links[cellId][nextCellId] = routeId;
if (!links[nextCellId]) links[nextCellId] = {};
links[nextCellId][cellId] = routeId;
}
}
}
return links;
}
function preparePointsArray() {
const {cells, burgs} = pack;
return cells.p.map(([x, y], cellId) => {
const burgId = cells.burg[cellId];
if (burgId) return [burgs[burgId].x, burgs[burgId].y];
return [x, y];
});
}
function getPoints(group, cells, points) {
const data = cells.map(cellId => [...points[cellId], cellId]);
// resolve sharp angles
if (group !== "searoutes") {
for (let i = 1; i < cells.length - 1; i++) {
const cellId = cells[i];
if (pack.cells.burg[cellId]) continue;
const [prevX, prevY] = data[i - 1];
const [currX, currY] = data[i];
const [nextX, nextY] = data[i + 1];
const dAx = prevX - currX;
const dAy = prevY - currY;
const dBx = nextX - currX;
const dBy = nextY - currY;
const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI);
if (angle < ROUTES_SHARP_ANGLE) {
const middleX = (prevX + nextX) / 2;
const middleY = (prevY + nextY) / 2;
let newX, newY;
if (angle < ROUTES_VERY_SHARP_ANGLE) {
newX = rn((currX + middleX * 2) / 3, 2);
newY = rn((currY + middleY * 2) / 3, 2);
} else {
newX = rn((currX + middleX) / 2, 2);
newY = rn((currY + middleY) / 2, 2);
}
if (findCell(newX, newY) === cellId) {
data[i] = [newX, newY, cellId];
points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes
}
}
}
}
return data; // [[x, y, cell], [x, y, cell]];
}
function getRouteSegments(pathCells, connections) {
const segments = [];
let segment = [];
for (let i = 0; i < pathCells.length; i++) {
const cellId = pathCells[i];
const nextCellId = pathCells[i + 1];
const isConnected = connections.has(`${cellId}-${nextCellId}`) || connections.has(`${nextCellId}-${cellId}`);
if (isConnected) {
if (segment.length) {
// segment stepped into existing segment
segment.push(pathCells[i]);
segments.push(segment);
segment = [];
}
continue;
}
segment.push(pathCells[i]);
}
if (segment.length > 1) segments.push(segment);
return segments;
}
// Urquhart graph is obtained by removing the longest edge from each triangle in the Delaunay triangulation
// this gives us an aproximation of a desired road network, i.e. connections between burgs
// code from https://observablehq.com/@mbostock/urquhart-graph
function calculateUrquhartEdges(points) {
const score = (p0, p1) => dist2(points[p0], points[p1]);
const {halfedges, triangles} = Delaunator.from(points);
const n = triangles.length;
const removed = new Uint8Array(n);
const edges = [];
for (let e = 0; e < n; e += 3) {
const p0 = triangles[e],
p1 = triangles[e + 1],
p2 = triangles[e + 2];
const p01 = score(p0, p1),
p12 = score(p1, p2),
p20 = score(p2, p0);
removed[
p20 > p01 && p20 > p12
? Math.max(e + 2, halfedges[e + 2])
: p12 > p01 && p12 > p20
? Math.max(e + 1, halfedges[e + 1])
: Math.max(e, halfedges[e])
] = 1;
}
for (let e = 0; e < n; ++e) {
if (e > halfedges[e] && !removed[e]) {
const t0 = triangles[e];
const t1 = triangles[e % 3 === 2 ? e - 2 : e + 1];
edges.push([t0, t1]);
}
}
return edges;
}
// connect cell with routes system by land
function connect(cellId) {
const getCost = createCostEvaluator({isWater: false, connections: new Map()});
const isExit = c => isLand(c) && isConnected(c);
const pathCells = findPath(cellId, isExit, getCost);
if (!pathCells) return;
const pointsArray = preparePointsArray();
const points = getPoints("trails", pathCells, pointsArray);
const feature = pack.cells.f[cellId];
const routeId = getNextId();
const newRoute = {i: routeId, group: "trails", feature, points};
pack.routes.push(newRoute);
for (let i = 0; i < pathCells.length; i++) {
const currentCell = pathCells[i];
const nextCellId = pathCells[i + 1];
if (nextCellId) addConnection(currentCell, nextCellId, routeId);
}
return newRoute;
function addConnection(from, to, routeId) {
const routes = pack.cells.routes;
if (!routes[from]) routes[from] = {};
routes[from][to] = routeId;
if (!routes[to]) routes[to] = {};
routes[to][from] = routeId;
}
}
// utility functions
function isConnected(cellId) {
const routes = pack.cells.routes;
return routes[cellId] && Object.keys(routes[cellId]).length > 0;
}
function areConnected(from, to) {
const routeId = pack.cells.routes[from]?.[to];
return routeId !== undefined;
}
function getRoute(from, to) {
const routeId = pack.cells.routes[from]?.[to];
if (routeId === undefined) return null;
const route = pack.routes.find(route => route.i === routeId);
if (!route) return null;
return route;
}
function hasRoad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
return Object.values(connections).some(routeId => {
const route = pack.routes.find(route => route.i === routeId);
if (!route) return false;
return route.group === "roads";
});
}
function isCrossroad(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return false;
if (Object.keys(connections).length > 3) return true;
const roadConnections = Object.values(connections).filter(routeId => {
const route = pack.routes.find(route => route.i === routeId);
return route?.group === "roads";
});
return roadConnections.length > 2;
}
const connectivityRateMap = {
roads: 0.2,
trails: 0.1,
searoutes: 0.2,
default: 0.1
};
function getConnectivityRate(cellId) {
const connections = pack.cells.routes[cellId];
if (!connections) return 0;
const connectivity = Object.values(connections).reduce((acc, routeId) => {
const route = pack.routes.find(route => route.i === routeId);
if (!route) return acc;
const rate = connectivityRateMap[route.group] || connectivityRateMap.default;
return acc + rate;
}, 0.8);
return connectivity;
}
// name generator data
const models = {
roads: {burg_suffix: 3, prefix_suffix: 6, the_descriptor_prefix_suffix: 2, the_descriptor_burg_suffix: 1},
trails: {burg_suffix: 8, prefix_suffix: 1, the_descriptor_burg_suffix: 1},
searoutes: {burg_suffix: 4, prefix_suffix: 2, the_descriptor_prefix_suffix: 1}
};
const prefixes = [
"King",
"Queen",
"Military",
"Old",
"New",
"Ancient",
"Royal",
"Imperial",
"Great",
"Grand",
"High",
"Silver",
"Dragon",
"Shadow",
"Star",
"Mystic",
"Whisper",
"Eagle",
"Golden",
"Crystal",
"Enchanted",
"Frost",
"Moon",
"Sun",
"Thunder",
"Phoenix",
"Sapphire",
"Celestial",
"Wandering",
"Echo",
"Twilight",
"Crimson",
"Serpent",
"Iron",
"Forest",
"Flower",
"Whispering",
"Eternal",
"Frozen",
"Rain",
"Luminous",
"Stardust",
"Arcane",
"Glimmering",
"Jade",
"Ember",
"Azure",
"Gilded",
"Divine",
"Shadowed",
"Cursed",
"Moonlit",
"Sable",
"Everlasting",
"Amber",
"Nightshade",
"Wraith",
"Scarlet",
"Platinum",
"Whirlwind",
"Obsidian",
"Ethereal",
"Ghost",
"Spike",
"Dusk",
"Raven",
"Spectral",
"Burning",
"Verdant",
"Copper",
"Velvet",
"Falcon",
"Enigma",
"Glowing",
"Silvered",
"Molten",
"Radiant",
"Astral",
"Wild",
"Flame",
"Amethyst",
"Aurora",
"Shadowy",
"Solar",
"Lunar",
"Whisperwind",
"Fading",
"Titan",
"Dawn",
"Crystalline",
"Jeweled",
"Sylvan",
"Twisted",
"Ebon",
"Thorn",
"Cerulean",
"Halcyon",
"Infernal",
"Storm",
"Eldritch",
"Sapphire",
"Crimson",
"Tranquil",
"Paved"
];
const descriptors = [
"Great",
"Shrouded",
"Sacred",
"Fabled",
"Frosty",
"Winding",
"Echoing",
"Serpentine",
"Breezy",
"Misty",
"Rustic",
"Silent",
"Cobbled",
"Cracked",
"Shaky",
"Obscure"
];
const suffixes = {
roads: {road: 7, route: 3, way: 2, highway: 1},
trails: {trail: 4, path: 1, track: 1, pass: 1},
searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1}
};
function generateName({group, points}) {
if (points.length < 4) return "Unnamed route segment";
const model = rw(models[group]);
const suffix = rw(suffixes[group]);
const burgName = getBurgName();
if (model === "burg_suffix" && burgName) return `${burgName} ${suffix}`;
if (model === "prefix_suffix") return `${ra(prefixes)} ${suffix}`;
if (model === "the_descriptor_prefix_suffix") return `The ${ra(descriptors)} ${ra(prefixes)} ${suffix}`;
if (model === "the_descriptor_burg_suffix" && burgName) return `The ${ra(descriptors)} ${burgName} ${suffix}`;
return "Unnamed route";
function getBurgName() {
const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()];
for (const [_x, _y, cellId] of priority) {
const burgId = pack.cells.burg[cellId];
if (burgId) return getAdjective(pack.burgs[burgId].name);
}
return null;
}
}
const ROUTE_CURVES = {
roads: d3.curveCatmullRom.alpha(0.1),
trails: d3.curveCatmullRom.alpha(0.1),
searoutes: d3.curveCatmullRom.alpha(0.5),
default: d3.curveCatmullRom.alpha(0.1)
};
function getPath({group, points}) {
const lineGen = d3.line();
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
const path = round(lineGen(points.map(p => [p[0], p[1]])), 1);
return path;
}
function getLength(routeId) {
const path = routes.select("#route" + routeId).node();
return path.getTotalLength();
}
function getNextId() {
return pack.routes.length ? Math.max(...pack.routes.map(r => r.i)) + 1 : 0;
}
function remove(route) {
const routes = pack.cells.routes;
for (const point of route.points) {
const from = point[2];
if (!routes[from]) continue;
for (const [to, routeId] of Object.entries(routes[from])) {
if (routeId === route.i) {
delete routes[from][to];
delete routes[to][from];
}
}
}
pack.routes = pack.routes.filter(r => r.i !== route.i);
viewbox.select("#route" + route.i).remove();
}
return {
generate,
buildLinks,
connect,
isConnected,
areConnected,
getRoute,
hasRoad,
isCrossroad,
getConnectivityRate,
generateName,
getPath,
getLength,
getNextId,
remove
};
})();

View file

@ -1,640 +0,0 @@
"use strict";
window.States = (() => {
const generate = () => {
TIME && console.time("generateStates");
pack.states = createStates();
expandStates();
normalize();
getPoles();
findNeighbors();
assignColors();
generateCampaigns();
generateDiplomacy();
TIME && console.timeEnd("generateStates");
// for each capital create a state
function createStates() {
const states = [{i: 0, name: "Neutrals"}];
const each5th = each(5);
const sizeVariety = byId("sizeVariety").valueAsNumber;
pack.burgs.forEach(burg => {
if (!burg.i || !burg.capital) return;
const expansionism = rn(Math.random() * sizeVariety + 1, 1);
const basename = burg.name.length < 9 && each5th(burg.cell) ? burg.name : Names.getCultureShort(burg.culture);
const name = Names.getState(basename, burg.culture);
const type = pack.cultures[burg.culture].type;
const coa = COA.generate(null, null, null, type);
coa.shield = COA.getShield(burg.culture, null);
states.push({
i: burg.i,
name,
expansionism,
capital: burg.i,
type,
center: burg.cell,
culture: burg.culture,
coa
});
});
return states;
}
};
// expand cultures across the map (Dijkstra-like algorithm)
const expandStates = () => {
TIME && console.time("expandStates");
const {cells, states, cultures, burgs} = pack;
cells.state = cells.state || new Uint16Array(cells.i.length);
const queue = new FlatQueue();
const cost = [];
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
const statesGrowthRate = byId("statesGrowthRate")?.valueAsNumber || 1;
const growthRate = (cells.i.length / 2) * globalGrowthRate * statesGrowthRate; // limit cost for state growth
// remove state from all cells except of locked
for (const cellId of cells.i) {
const state = states[cells.state[cellId]];
if (state.lock) continue;
cells.state[cellId] = 0;
}
for (const state of states) {
if (!state.i || state.removed) continue;
const capitalCell = burgs[state.capital].cell;
cells.state[capitalCell] = state.i;
const cultureCenter = cultures[state.culture].center;
const b = cells.biome[cultureCenter]; // state native biome
queue.push({e: state.center, p: 0, s: state.i, b}, 0);
cost[state.center] = 1;
}
while (queue.length) {
const next = queue.pop();
const {e, p, s, b} = next;
const {type, culture} = states[s];
cells.c[e].forEach(e => {
const state = states[cells.state[e]];
if (state.lock) return; // do not overwrite cell of locked states
if (cells.state[e] && e === state.center) return; // do not overwrite capital cells
const cultureCost = culture === cells.culture[e] ? -9 : 100;
const populationCost = cells.h[e] < 20 ? 0 : cells.s[e] ? Math.max(20 - cells.s[e], 0) : 5000;
const biomeCost = getBiomeCost(b, cells.biome[e], type);
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
const cellCost = Math.max(cultureCost + populationCost + biomeCost + heightCost + riverCost + typeCost, 0);
const totalCost = p + 10 + cellCost / states[s].expansionism;
if (totalCost > growthRate) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost;
queue.push({e, p: totalCost, s, b}, totalCost);
}
});
}
burgs.filter(b => b.i && !b.removed).forEach(b => (b.state = cells.state[b.cell])); // assign state to burgs
function getBiomeCost(b, biome, type) {
if (b === biome) return 10; // tiny penalty for native biome
if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
return biomesData.cost[biome]; // general non-native biome penalty
}
function getHeightCost(f, h, type) {
if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
if (h < 20) return 1000; // general sea crossing penalty
if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
if (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 67) return 2200; // general mountains crossing penalty
if (h >= 44) return 300; // general hills crossing penalty
return 0;
}
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
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
return 0;
}
TIME && console.timeEnd("expandStates");
};
const normalize = () => {
TIME && console.time("normalizeStates");
const {cells, burgs} = pack;
for (const i of cells.i) {
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
if (pack.states[cells.state[i]]?.lock) continue; // do not overwrite cells of locks states
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
const adversaries = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] !== cells.state[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => !pack.states[cells.state[c]]?.lock && cells.state[c] === cells.state[i]);
if (buddies.length > 2) continue;
if (adversaries.length <= buddies.length) continue;
cells.state[i] = cells.state[adversaries[0]];
}
TIME && console.timeEnd("normalizeStates");
};
// calculate pole of inaccessibility for each state
const getPoles = () => {
const getType = cellId => pack.cells.state[cellId];
const poles = getPolesOfInaccessibility(pack, getType);
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.pole = poles[s.i] || [0, 0];
});
};
const findNeighbors = () => {
const {cells, states} = pack;
states.forEach(s => {
if (s.removed) return;
s.neighbors = new Set();
});
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
cells.c[i]
.filter(c => cells.h[c] >= 20 && cells.state[c] !== s)
.forEach(c => states[s].neighbors.add(cells.state[c]));
}
// convert neighbors Set object into array
states.forEach(s => {
if (!s.neighbors || s.removed) return;
s.neighbors = Array.from(s.neighbors);
});
};
const assignColors = () => {
TIME && console.time("assignColors");
const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
const states = pack.states;
// assign basic color using greedy coloring algorithm
states.forEach(state => {
if (!state.i || state.removed || state.lock) return;
state.color = colors.find(color => state.neighbors.every(neibStateId => states[neibStateId].color !== color));
if (!state.color) state.color = getRandomColor();
colors.push(colors.shift());
});
// randomize each already used color a bit
colors.forEach(c => {
const sameColored = states.filter(state => state.color === c && state.i && !state.lock);
sameColored.forEach((state, index) => {
if (!index) return;
state.color = getMixedColor(state.color);
});
});
TIME && console.timeEnd("assignColors");
};
// calculate states data like area, population etc.
const collectStatistics = () => {
TIME && console.time("collectStatistics");
const {cells, states} = pack;
states.forEach(s => {
if (s.removed) return;
s.cells = s.area = s.burgs = s.rural = s.urban = 0;
});
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
// collect stats
states[s].cells += 1;
states[s].area += cells.area[i];
states[s].rural += cells.pop[i];
if (cells.burg[i]) {
states[s].urban += pack.burgs[cells.burg[i]].population;
states[s].burgs++;
}
}
TIME && console.timeEnd("collectStatistics");
};
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];
return neighbors
.map(i => {
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 = () => {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
s.campaigns = generateCampaign(s);
});
};
// generate Diplomatic Relationships
const generateDiplomacy = () => {
TIME && console.time("generateDiplomacy");
const {cells, states} = pack;
const chronicle = (states[0].diplomacy = []);
const valid = states.filter(s => s.i && !states.removed);
const neibs = {Ally: 1, Friendly: 2, Neutral: 1, Suspicion: 10, Rival: 9}; // relations to neighbors
const neibsOfNeibs = {Ally: 10, Friendly: 8, Neutral: 5, Suspicion: 1}; // relations to neighbors of neighbors
const far = {Friendly: 1, Neutral: 12, Suspicion: 2, Unknown: 6}; // relations to other
const navals = {Neutral: 1, Suspicion: 2, Rival: 1, Unknown: 1}; // relations of naval powers
valid.forEach(s => (s.diplomacy = new Array(states.length).fill("x"))); // clear all relationships
if (valid.length < 2) return; // no states to renerate relations with
const areaMean = d3.mean(valid.map(s => s.area)); // average state area
// generic relations
for (let f = 1; f < states.length; f++) {
if (states[f].removed) continue;
if (states[f].diplomacy.includes("Vassal")) {
// Vassals copy relations from their Suzerains
const suzerain = states[f].diplomacy.indexOf("Vassal");
for (let i = 1; i < states.length; i++) {
if (i === f || i === suzerain) continue;
states[f].diplomacy[i] = states[suzerain].diplomacy[i];
if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally";
for (let e = 1; e < states.length; e++) {
if (e === f || e === suzerain) continue;
if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue;
states[e].diplomacy[f] = states[e].diplomacy[suzerain];
}
}
continue;
}
for (let t = f + 1; t < states.length; t++) {
if (states[t].removed) continue;
if (states[t].diplomacy.includes("Vassal")) {
const suzerain = states[t].diplomacy.indexOf("Vassal");
states[f].diplomacy[t] = states[f].diplomacy[suzerain];
continue;
}
const naval =
states[f].type === "Naval" &&
states[t].type === "Naval" &&
cells.f[states[f].center] !== cells.f[states[t].center];
const neib = naval ? false : states[f].neighbors.includes(t);
const neibOfNeib =
naval || neib
? false
: states[f].neighbors
.map(n => states[n].neighbors)
.join("")
.includes(t);
let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
// add Vassal
if (
neib &&
P(0.8) &&
states[f].area > areaMean &&
states[t].area < areaMean &&
states[f].area / states[t].area > 2
)
status = "Vassal";
states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
states[t].diplomacy[f] = status;
}
}
// declare wars
for (let attacker = 1; attacker < states.length; attacker++) {
const ad = states[attacker].diplomacy; // attacker relations;
if (states[attacker].removed) continue;
if (!ad.includes("Rival")) continue; // no rivals to attack
if (ad.includes("Vassal")) continue; // not independent
if (ad.includes("Enemy")) continue; // already at war
// random independent rival
const defender = ra(
ad.map((r, d) => (r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0)).filter(d => d)
);
let ap = states[attacker].area * states[attacker].expansionism;
let dp = states[defender].area * states[defender].expansionism;
if (ap < dp * gauss(1.6, 0.8, 0, 10, 2)) continue; // defender is too strong
const an = states[attacker].name;
const dn = states[defender].name; // names
const attackers = [attacker];
const defenders = [defender]; // attackers and defenders array
const dd = states[defender].diplomacy; // defender relations;
// start an ongoing war
const name = `${an}-${trimVowels(dn)}ian War`;
const start = options.year - gauss(2, 3, 0, 10);
const war = [name, `${an} declared a war on its rival ${dn}`];
const campaign = {name, start, attacker, defender};
states[attacker].campaigns.push(campaign);
states[defender].campaigns.push(campaign);
// attacker vassals join the war
ad.forEach((r, d) => {
if (r === "Suzerain") {
attackers.push(d);
war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`);
}
});
// defender vassals join the war
dd.forEach((r, d) => {
if (r === "Suzerain") {
defenders.push(d);
war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`);
}
});
ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power
dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
// defender allies join
dd.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return;
if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > 2 * gauss(1.6, 0.8, 0, 10, 2)) {
const reason = states[d].diplomacy.includes("Enemy") ? "Being already at war," : `Frightened by ${an},`;
war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
dd[d] = states[d].diplomacy[defender] = "Suspicion";
return;
}
defenders.push(d);
dp += states[d].area * states[d].expansionism;
war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`);
// ally vassals join
states[d].diplomacy
.map((r, d) => (r === "Suzerain" ? d : 0))
.filter(d => d)
.forEach(v => {
defenders.push(v);
dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`);
});
});
// attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
ad.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
const name = states[d].name;
if (states[d].diplomacy[defender] !== "Rival" && (P(0.2) || ap <= dp * 1.2)) {
war.push(`${an}'s ally ${name} avoided entering the war`);
return;
}
const allies = states[d].diplomacy.map((r, d) => (r === "Ally" ? d : 0)).filter(d => d);
if (allies.some(ally => defenders.includes(ally))) {
war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`);
return;
}
attackers.push(d);
ap += states[d].area * states[d].expansionism;
war.push(`${an}'s ally ${name} joined the war on attackers side`);
// ally vassals join
states[d].diplomacy
.map((r, d) => (r === "Suzerain" ? d : 0))
.filter(d => d)
.forEach(v => {
attackers.push(v);
dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`);
});
});
// change relations to Enemy for all participants
attackers.forEach(a => defenders.forEach(d => (states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy")));
chronicle.push(war); // add a record to diplomatical history
}
TIME && console.timeEnd("generateDiplomacy");
};
// select a forms for listed or all valid states
const defineStateForms = list => {
TIME && console.time("defineStateForms");
const states = pack.states.filter(s => s.i && !s.removed && !s.lock);
if (states.length < 1) return;
const generic = {Monarchy: 25, Republic: 2, Union: 1};
const naval = {Monarchy: 25, Republic: 8, Union: 3};
const median = d3.median(pack.states.map(s => s.area));
const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** 0.4) - 2, 0)];
const expTiers = pack.states.map(s => {
let tier = Math.min(Math.floor((s.area / median) * 2.6), 4);
if (tier === 4 && s.area < empireMin) tier = 3;
return tier;
});
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
const republic = {
Republic: 75,
Federation: 4,
"Trade Company": 4,
"Most Serene Republic": 2,
Oligarchy: 2,
Tetrarchy: 1,
Triumvirate: 1,
Diarchy: 1,
Junta: 1
}; // weighted random
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};
for (const s of states) {
if (list && !list.includes(s.i)) continue;
const tier = expTiers[s.i];
const religion = pack.cells.religion[s.center];
const isTheocracy =
(religion && pack.religions[religion].expansion === "state") ||
(P(0.1) && ["Organized", "Cult"].includes(pack.religions[religion].type));
const isAnarchy = P(0.01 - tier / 500);
if (isTheocracy) s.form = "Theocracy";
else if (isAnarchy) s.form = "Anarchy";
else s.form = s.type === "Naval" ? rw(naval) : rw(generic);
s.formName = selectForm(s, tier);
s.fullName = getFullName(s);
}
function selectForm(s, tier) {
const base = pack.cultures[s.culture].base;
if (s.form === "Monarchy") {
const form = monarchy[tier];
// 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 duchies 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
}
if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khanate"; // Mongolian
if (base === 16 && form === "Principality") return "Beylik"; // Turkic
if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Turkic
if (base === 12 && (form === "Kingdom" || form === "Grand Duchy")) return "Shogunate"; // Japanese
if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic
if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek
if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian
if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Horde"; // Turkic
if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian
return form;
}
if (s.form === "Republic") {
// Default name is from weighted array, special case for small states with only 1 burg
if (tier < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name;
return "Free City";
}
if (P(0.3)) return "City-state";
}
return rw(republic);
}
if (s.form === "Union") return rw(union);
if (s.form === "Anarchy") return rw(anarchy);
if (s.form === "Theocracy") {
// European
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) {
if (P(0.1)) return "Divine " + monarchy[tier];
if (tier < 2 && P(0.5)) return "Diocese";
if (tier < 2 && P(0.5)) return "Bishopric";
}
if (P(0.9) && [7, 5].includes(base)) {
// Greek, Ruthenian
if (tier < 2) return "Eparchy";
if (tier === 2) return "Exarchate";
if (tier > 2) return "Patriarchate";
}
if (P(0.9) && [21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if (tier > 2 && P(0.8) && [18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
return rw(theocracy);
}
}
TIME && console.timeEnd("defineStateForms");
};
// state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
const adjForms = [
"Empire",
"Sultanate",
"Khaganate",
"Shogunate",
"Caliphate",
"Despotate",
"Theocracy",
"Oligarchy",
"Union",
"Confederation",
"Trade Company",
"League",
"Tetrarchy",
"Triumvirate",
"Diarchy",
"Horde",
"Marches"
];
const getFullName = state => {
if (!state.formName) return state.name;
if (!state.name && state.formName) return "The " + state.formName;
const adjName = adjForms.includes(state.formName) && !/-| /.test(state.name);
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
};
return {
generate,
expandStates,
normalize,
getPoles,
findNeighbors,
assignColors,
collectStatistics,
generateCampaign,
generateCampaigns,
generateDiplomacy,
defineStateForms,
getFullName
};
})();

View file

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

9
netlify.toml Normal file
View file

@ -0,0 +1,9 @@
[build]
command = "npm run build"
publish = "dist"
environment = { NODE_VERSION = "24" }
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

2667
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

49
package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "fantasy-map-generator",
"version": "1.110.0",
"description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.",
"homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme",
"bugs": {
"url": "https://github.com/Azgaar/Fantasy-Map-Generator/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Azgaar/Fantasy-Map-Generator.git"
},
"license": "MIT",
"author": "Azgaar",
"main": "main.js",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:browser": "vitest --config=vitest.browser.config.ts",
"test:e2e": "playwright test",
"lint": "biome check --write",
"format": "biome format --write"
},
"devDependencies": {
"@biomejs/biome": "2.3.13",
"@playwright/test": "^1.57.0",
"@types/d3": "^7.4.3",
"@types/delaunator": "^5.0.3",
"@types/node": "^25.0.10",
"@types/polylabel": "^1.1.3",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.57.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
},
"dependencies": {
"alea": "^1.0.1",
"d3": "^7.9.0",
"delaunator": "^5.0.1",
"polylabel": "^2.0.1"
},
"engines": {
"node": ">=24.0.0"
}
}

34
playwright.config.ts Normal file
View file

@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test'
const isCI = !!process.env.CI
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: 'html',
// Use OS-independent snapshot names (HTML content is the same across platforms)
snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}',
use: {
baseURL: isCI ? 'http://localhost:4173' : 'http://localhost:5173',
trace: 'on-first-retry',
// Fixed viewport to ensure consistent map rendering
viewport: { width: 1280, height: 720 },
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
// In CI: build and preview for production-like testing
// In dev: use vite dev server (faster, no rebuild needed)
command: isCI ? 'npm run build && npm run preview' : 'npm run dev',
url: isCI ? 'http://localhost:4173' : 'http://localhost:5173',
reuseExistingServer: !isCI,
timeout: 120000,
},
})

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 931 B

After

Width:  |  Height:  |  Size: 931 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 898 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

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